optional 属性がうまく動かない

SPECIAL


optional 属性

COM インターフェイスでは [optional] という IDL 属性が利用できます。これを使用したメソッドやプロパティを実装するとすると、その引数が指定されていなくてもそれらを呼び出すことができます。

Visual C++ 6.0 までは何の問題もなく利用していたのですけど、Visual Studio .NET に開発環境を変えたところで、この [optional] 属性が正常に動いていないのではないか、という問題にぶつかってしまったのでした。

 

Visual C++ 6.0 から Visual C++ .NET ( 7.0 / 7.1 ) になった時点でいろいろと大きく仕様が変更されていました。最初は Visual C++ 5.0 から 6.0 にバージョンアップしたときのように気楽に構えていたのですけど…。

スタティックライブラリは .NET で再コンパイルしないと互換性がないため組み込めない。そのためせっせと、必要な自作ライブラリをコンパイルしなおして…。ATL COM の作成方法が大きく変更となっていたため、実装部分こそコピーするものの基本的には作り直し。USES_CONVERSION の廃止に伴うコードの修正など、いろいろと面倒な作業に追われてしまいました。

そのころの記録が残っていたのでリンクしておきますね。 >> EZ-NET : VC6 と VC7 の互換性

 

さて、そしてようやくビルドが完成したと思ったら、省略可能引数が省略できない…。

インターフェイス周りの書き方自体も変更されていて断定するほどの資料がそろってはいなかったものの、個人的な直感では動くはずのものが動かない。なぜか省略すると引数が足りないか不正だといわれてしまうのでした。

インターネットを探してみるも、まだ .NET が出たてだったせいか、それともそれ以前の情報が多すぎるせいか、有用な情報も得られず…。どこが間違っているかも見当がつかず…。精神的にもちょっとやられていた時期だったので、そのまま放り出したまま、数年が過ぎてしまっていたのでした。

 

そんなこんなながら、ようやく趣味で楽しむ分には意欲が戻りつつあるこのごろ。ずいぶんと時がたったのに、なぜかこの問題について調べても明確な解答が得られないというのはどういうことなのでしょう…。

やっとこさ重い腰を上げて、再び自力で調べてみることにしたのでした。今回はそのお話です。

 

実験用 COM を作成する

投げ出した当時は Visual Studio .NET (2002) だったのですけど、今はいつのまにか MSDN で手に入れた Visual Studio .NET 2003 です。

もし仮にバグだったとしたならば、こっそり修正が入って、普通に使えるようになっているはず…。そんな淡い期待とともに、次のような TestCom という COM コンポーネントを Visual C++ で作成してみます。

// ITestInterface

[

object,

uuid("FD88D459-81C4-4B88-8D6D-43C4ED797247"),

dual, helpstring("ITestInterface インターフェイス"),

pointer_default(unique)

]

__interface ITestInterface : IDispatch

{

[id(1), helpstring("メソッドGetString")] HRESULT GetString([in, defaultvalue("default")] BSTR message, [out,retval] BSTR* pResult);

[id(2), helpstring("メソッドGetString2")] HRESULT GetString2([in, optional] VARIANT message, [out,retval] VARIANT* pResult);

};

 

// CTestInterface

[

coclass,

threading("apartment"),

support_error_info("ITestInterface"),

vi_progid("TestCom.TestInterface"),

progid("TestCom.TestInterface.1"),

version(1.0),

uuid("3F22F89F-8408-4082-8F03-C21E4E4ED2DC"),

helpstring("TestInterface Class")

]

class ATL_NO_VTABLE CTestInterface :

public ITestInterface

{

public:

CTestInterface()

{

}

 

DECLARE_PROTECT_FINAL_CONSTRUCT()

 

HRESULT FinalConstruct()

{

return S_OK;

}

 

void FinalRelease()

{

}

 

public:

STDMETHOD(GetString)(BSTR message, BSTR* pResult);

STDMETHOD(GetString2)(VARIANT message, VARIANT* pResult);

};

上記が "TestCom.TestInterface.1" のヘッダー部分です。

// TestInterface.cpp : CTestInterface の実装

#include "stdafx.h"

#include "TestInterface.h"

#include ".\testinterface.h"
 

// CTestInterface

STDMETHODIMP CTestInterface::GetString(BSTR message, BSTR* pResult)

{

*pResult = ::SysAllocString(message);

return S_OK;

}

 

STDMETHODIMP CTestInterface::GetString2(VARIANT message, VARIANT* pResult)

{

VariantInit(pResult);

 

if (message.vt == VT_ERROR)

{

pResult->vt = VT_BSTR;

pResult->bstrVal = ::SysAllocString(L"optional");

}

else

{

VariantCopy(pResult, &message);

}

 

return S_OK;

}

そしてこちらが実装部分です。

実装されているメソッドは "str = GetString(str)" と "str = GetString2(str)" の二つだけ。やっていることも非常に単純で、引数に受け取った文字列をそのまま返すだけというものです。

GetString の方の引数は BSTR 型で、[defaultvalue] 属性によって初期値を与える、すなわち引数が省略可能なメソッドとなっています。省略した場合、"default" という文字列が返されます。

GetString2 の方は [optional] 属性で省略可能となっています。[optional] は VARIANT 型か VARIANT* 型でないといけないため、やや複雑なコードになっていますけど、こちらの方は、省略したら "optional" という文字列を返すようなコーディングがしてあります。

なお、引数省略時は、今までの経験上、VT_ERROR という VARIANT 値が手に入るはずなので、それを見て省略されたかどうかの判定を行っています。

 

WSH で実験してみる

Debug モードでコンパイルしたら、さっそく、Windows Server 2003 上の WSH で動作実験を行ってみます。実験の仕方は単純で、下記のスクリプトを保存した test.vbs ファイルを実行するだけです。

まずは、そもそも TestCom が正常に動くかどうかのテストです。

' ---- TestCom.TestInterface の作成

Set test = WScript.CreateObject("TestCom.TestInterface.1")

 

' ---- まずは引数を省略せずに呼び出します。

WScript.Echo test.GetString("test1")

WScript.Echo test.GetString2("test2")

 

' ---- あとしまつ

Set test = Nothing

これを実験してみると、正常に、"test1" と "test2" という文字列が表示されました。

 

では、それぞれの引数を省略してみると…、どちらとも "引数の数が一致していません。または不正なプロパティを指定しています。" というエラーになってしまうのでした。

以前にも、そして今回も、COM が呼び出された後にエラーとなっているのでは…、とも疑ったのですけど、各メソッドが呼び出されたすぐで MessageBox を表示してみても、省略しなければダイアログが表示されるけれども省略するとそれ以前にエラーが出る、そんな状態でした。

 

Visual Basic で実験してみる

Visual Basic .NET で動かしてみれば何か分かるかもしれない…、ということで、今度は Visual Basic にて実験です。コンソールアプリケーションを1つ作成して、その中で先ほどと同じようなコードを実験してみます。

Dim test As TestCom.CTestInterface

 

Sub Main()

 

test = New TestCom.CTestInterface

 

MsgBox(test.GetString())

MsgBox(test.GetString2())

 

test = Nothing

 

End Sub

組んでいて…、そういえば 「プロジェクト」 の 「参照の追加」 を調整しなくては行けないのだっけ…、ということで、参照設定にて "TestCom 1.0 タイプライブラリ" を追加しておきました。

これで実験してみたところ、なんと正常に省略可能でした。

もちろん省略しないときも正常に動作します。ついでに念のため、下記のようにインターフェイスでの実験も行ってみましたけど、こちらも問題なしです。

Dim test As TestCom.ITestInterface

 

Sub Main()

 

test = CreateObject("TestCom.TestInterface.1")

 

MsgBox(test.GetString())

MsgBox(test.GetString2())

 

test = Nothing

 

End Sub

 

違いは…、タイプライブラリ…?

いや、そもそもぜんぜん環境がことなるのですけど、先ほどの WSH と Visual Basic とでの目立った違いというと、「参照」 としてタイプライブラリを組み込んでいるかどうかというのがあると思います。

もっとも、そうだとすると、ADO を利用するときなどにも省略できなくて困るはずなのですけどね。

 

何はともあれこれが重要か否かを、ひとまず実験で確認しておこうと思います。

とはいえ WSH でタイプライブラリを組み込むにはどうしたらいいのでしょう…。よく分からなかったので、似た環境ともいえる (?) Microsoft Word 2003 の VBA にて実験を行ってみようと思います。

 

Word 2003 で文書をひとつ作成してそこへボタンを配置…、配置したボタンが押されたときのコードを次のように記述します。その際、参照設定にて "TestCom 1.0 タイプライブラリ" も参照されるように設定しておきます。

Private Sub CommandButton1_Click()

 

Dim test

 

Set test = CreateObject("TestCom.TestInterface.1")

 

MsgBox(test.GetString())

MsgBox(test.GetString2())

 

Set test = Nothing

 

End Sub

こうした上で、ボタンを押してみると…。WSH 同様のエラーとなってしまいました。

 

バイナリレベルの互換性…?

そういえば、Visual C++ .NET から、ライブラリですけどバイナリレベルで互換性がないため再コンパイルが必要とのお話がありました。

COM がそのあたりで互換性を欠くとは思いがたいのですけど、とりあえずコンパイラのオプションになにかそれらしいものがないか調べてみることにしました。

 

まったく関係ないであろう "ATL で CRT をできるだけ使用しない" を "はい" にしてみたり、"関数レベルでリンクする" を "はい" にしてみたりしましたが、当然のように改善されませんでした。

他にも何かそれらしいものはないか…、といろいろ実験してみようと思いましたけど、ちょっといろいろとありすぎるのでとりあえずはやめておくことにします。ついでにダメもとでリリースビルドでコンパイルしなおしてみましたけど、それでも特に改善は見られませんでした。

 

手探りで探してみる…

何か理由があるのではないか…、ともう一度、インターネットで調べてみることにします。

今回は Visual Basic では正常に動作していることを確認できているので少しは探しやすくなるはず…、と思ったのですけど、やっぱり "COM" とか "optional" とか "VBScript" とか "省略" とか、ありきたりなキーワードしか思い当たらないため、なかなか目的の情報が見つかりません。

 

そんななか、ではエラーのときに表示される "引数の数が一致していません。または不正なプロパティを指定しています。" というのをそのまま検索したらどうなるだろう…。

そう思って探してみたところ、とりあえずその理由として気になるものに 「読み取り専用のプロパティに値を代入しようとしている」 というものがありました。別にこれがずばりという訳ではないですけど、一応気にしておいてもよさそうな気がするので、ここにメモしておきました。ちなみにこれは、Dynamic HTML でのお話みたいです。

また、Microsoft さまのサイトにて、VBScript にてこのエラーが出る状況が書かれていたので一応列挙しておきます。

  • プロシージャ名が間違っている。
  • プロシージャに指定している引数の数が間違っている。
  • 引数の型が間違っている。

あと、ぜんぜん関係ないのですけど、検索に、本当にそのエラーが出ているページが載っていて笑えました^^;

 

それにしても、明確な答えはさっぱり見当たりませんでした。COM って ASP とかの VBScript でよく使うと思うのですけど、誰もなんとも思わないということなのでしょうか。すなわちただの単純な勘違いなのでしょうか…。

 

引数を LPVARIANT 型にしてみる…

そういえば以前に、Visual C++ 6.0 のころに VARIANT について調べたことがあったのです。そのとき、VBScript と Visual Basic とでは、もらえ方が違うということが判明しました。

その時の調査が EZ-NET 研究室: VARIANT 型を受け取る際の注意 なのですけど、素直に値を取り扱いたい場合は、VBScript ならば LPVARIANT 型で、Visual Basic では VARIANT 型で受け取るのが良いようでした。

…とすると、変な話ではありますけど、LPVARIANT すなわち VARIANT 型のポインタを受け取るメソッドを [optional] 指定で作ってみたらどうなるだろう…。

 

そう思って、新たに次のメソッドを追加しました。

[id(3), helpstring("メソッドGetString3")] HRESULT GetString3([in, optional] VARIANT* message, [out,retval] VARIANT* pResult);

 

STDMETHOD(GetString3)(VARIANT* message, VARIANT* pResult);

STDMETHODIMP CTestInterface::GetString3(VARIANT* message, VARIANT* pResult)

{

VariantInit(pResult);

 

if (message->vt == VT_ERROR)

{

pResult->vt = VT_BSTR;

pResult->bstrVal = ::SysAllocString(L"lpvariant");

}

else

{

VariantCopy(pResult, message);

}

 

return S_OK;

}

コードを挿入した部分は省略しておきます。

こうしてこれを VBScript にて実行してみたのですけど、やはり省略時は同じエラーが出てしまいました。しかも、省略しないときには 「型が一致しない」 とのエラーになってしまいます。Visual Basic だと、省略時も指定時も、まったく問題なく動くんですけどね…。

 

どこが悪いのかさっぱりです。

ASP なら動くのではないかと薄い期待をかけてみましたけど、やはり VBScript (WSH) で実行したときと同じ結果となってしまいました。

 

Visual Studio .NET の最新版は…?

これはただのバグなのでしょうか…。

発売されてからこれだけ時間が経っているにもかかわらずぜんぜん騒ぎ立てられている様子がなくてなんともいえないのですけど、とりあえずは Visual Studio .NET のサービスパックがないか調べてみることにします。

Microsoft Visual Studio ホームページ へ接続していろいろと探してみるも、それらしいダウンロードはありませんでした。"よく寄せられる質問" という項目もあったのですが載っているはずもなく…。

 

なんなんでしょうね。

公式にバグとでも書いてあれば、じゃあコーディング自体は問題ないのね…、とか納得きかせてそのまましばらく我慢するか VC6 にするか選べるでしょうに、まったく触れられていないというのも困りものです。

って、そのまま VC7 で行くとすると、省略可能引数はあえて値を渡して我慢するとしても、LPVARIANT 型の値を渡すメソッドは VBScript から利用できないんですね…。COM は VC6 で書け、すなわちそこで使用する予定の静的ライブラリも VC6 で…、って…、全部 VC6 で書けということなのでしょうか…。

ただの VBScript の使い方を間違えているだけなのか、それともコンパイルオプションや、コードに少し追記をしてあげればいいのか、なんともよくわからないのでした。

 

属性を使用しないでみる

一日明けて、朝ごはんの最中にふと、「IDL 属性の取り扱いが変更されたのが影響しているとしたら、従来通りの IDL 表記方法に変えてみたらどうだろう…」 と思ったのでした。

そう思ったのも、以前に EZ-NET 研究室: ATL でサービスプログラムを作ってみる (成功版) で教えていただいた Noppi さまのメールの中に、IDL の属性にチェックを入れると Release ビルド時にエラーが出るというバグっぽいものがあるという情報が記載されていたからなのでした。

バグはべつとして、とりあえずそういえば属性サポートは任意で選択することができる…。

 

さっそく、「ATL プロジェクトウィザード」 で新たに "TestCom2" なる COM を作成してみることにします。注意点とすれば、ウィザードのところで 「アプリケーションの設定」 から 「属性」 のチェックをはずすだけ…。

そしてまったく同じ要領で、TestCom2.TestInterface インターフェイスを属性なし (ディフォルト) で作成し、GetString, GetString2, GetString3 をまったく同じコードで記載しました。

 

そして Debug ビルドでコンパイルして、まずは Visual Basic で実行してみます。

名前空間などの取り扱いがことなっているようで、COM インスタンスの生成の際に、若干違う書き方となりましたので、念のためもう一度ここに記載しておきます。

' ---- New による作成の場合

Dim test As TestCom2Lib.TestInterfaceClass

 

Sub Main()

 

test = New TestCom.TestInterfaceClass

 

MsgBox(test.GetString())

MsgBox(test.GetString2())

MsgBox(test.GetString3())

 

test = Nothing

 

End Sub

' ---- CreateObject による作成の場合

Dim test As TestCom2Lib.ITestInterface

 

Sub Main()

 

test = CreateObject("TestCom2.TestInterface.1")

 

MsgBox(test.GetString())

MsgBox(test.GetString2())

MsgBox(test.GetString3())

 

test = Nothing

 

End Sub

これでしっかりと動作することを確認しました。

 

では肝心の WSH での実験です。

' ---- TestCom.TestInterface の作成

Set test = WScript.CreateObject("TestCom2.TestInterface.1")

 

' ---- 引数を省略して呼び出します。

WScript.Echo test.GetString()

WScript.Echo test.GetString2()

WScript.Echo test.GetString3()

 

' ---- あとしまつ

Set test = Nothing

このようなスクリプトを実行してみると…、なんと、何事もなかったようにしっかりと、3つともが正常に動作するのでした。

これならば、今まで移行してしまったものをまた同じ要領で直す必要こそあれ、静的ライブラリも、COM コンポーネントも、開発ツールを Visual Studio .NET 2003 に統一できそうな感じですね。

これでようやく、何もかもがうまく行かなかった状況から突破できそうです。Noppi さま、ほんとうにどうもありがとうございました。

 

っと、そうでした。

Release ビルドだと C3861 コンパイルエラーが起きるバグっぽいのがあるらしい、とのことでしたね。確認すべく、さっそく TestCom2 を Release ビルドしてみます…。

…、あら、出ませんでした。

もしこのエラーがでるようでしたら、stdafx.h かプログラム名.cpp に #include <cstdio> (もしくは #include <stdio.h>) の一行を加えれば回避できるみたいです。

とのことですので、もしエラーがでたらこのあたりをちょちょっといじってあげれば安心そうですね。