前回
その2 本当に言語非依存なのか?
前回は、様々な言語でCOMクライアントを実装しました。
今回は、呼ぶ側ではなく呼ばれる側、COMコンポーネントを実装してみたいと思います。
実装言語
C++を使うのが王道のようですが、今回はC#を使います。
理由は、筆者がC#好きだからです。
実装の流れ
プロジェクト作成
普通にC#の.Net Framework クラスライブラリのプロジェクトを作成します。
HelloクラスとIHelloインターフェースを実装
呼び出し側がCreateInstanceでオブジェクトを生成するには、
生成対象であるインターフェースと、その実体となるクラスが必要になります。
namespace ComComponentTest
{
public interface IHello
{
string GetHelloMessage();
}
public class Hello : IHello
{
public string GetHelloMessage()
{
return "Hello!";
}
}
}
GetHelloMessage()メソッドで"Hello!"文字列を返すだけのHelloクラスと、そのメソッドを宣言しているだけのIHelloインターフェースを実装します。
…これだとただのC#のライブラリであって、COMにはなっていませんね。
COMとして機能させるためには、もう少し細工が必要です。
クラスとインターフェースに属性を設定
まずクラスとインターフェースに、いくつかの属性を設定してやる必要があります。
それらを足したものがこちら。
namespace ComComponentTest
{
using System.Runtime.InteropServices;
[Guid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")]
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IHello
{
string GetHelloMessage();
}
[Guid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")]
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComDefaultInterface(typeof(IHello))]
[ProgId("ComComponentTest.Hello")]
public class Hello : IHello
{
public string GetHelloMessage()
{
return "Hello!";
}
}
}
System.Runtime.InteropServices名前空間にCOM関連のメソッド等があるのでusingしておきます。
Guid
まずクラス、インターフェース両方にGuidの設定が必要です。
これは、COMがレジストリで管理される際にクラスやインターフェースの一意なキーとして扱われるようです。
例では便宜上、「XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX」としていますが、
実装時は実際のGUIDを格納するようにしてください。
当然、値を使いまわしてはいけません。
Visual Studioの機能などを使って毎回生成するようにしましょう。
ComVisible
その名の通り、COMとして公開するかどうか。
公開するもの、しないものをここで切り替えることができるようです。
プロジェクト生成時にデフォルトで生成されるAssembleInfo.csの中にもComVisible属性の記述があります。
これは、アセンブリ全体に対して適用される、ということでしょうか。
ここをtrueにすると個別のインターフェースやクラスに大してComVisible属性をつけなくてよくなるのかもしれません(未調査)。
InterfaceType
ここで指定できるインターフェースタイプは以下4つ。
メンバー名 | 意味 |
---|---|
InterfaceIsIUnknown | 事前バインディングのみを有効化 |
InterfaceIsIDispatch | 遅延バインディングのみを有効化 |
InterfaceIsDual | 事前、遅延両方のバインディングを有効化 |
InterfaceIsIInspectable | Windows ランタイム インターフェイス |
事前バインディング、遅延バインディングは前回の各言語の実装であった2つのパターンのことです。
事前バインディング=型宣言された変数に値を割り当てる(前回の例におけるC#, VB.NET, C++)
遅延バインディング=型宣言されていない変数に値を割り当てる(前回の例におけるPHP, Python)
InterfaceIsDualとしておけば、両方の使い方ができる、といったところでしょうか。
ひとまず両方できるように今回はDualを設定しています。
InterfaceIsIInspectableについてはここでは触れません(理解していません。。。)。
ClassInterface
ここで指定できるインターフェースタイプは以下3つ。
メンバー名 | 意味 |
---|---|
AutoDispatch | 事前バインディングのみを有効化 |
AutoDual | デュアルインターフェースを有効化 |
None | クラスのインターフェースを生成しない |
おそらくですが、事前バインディングやデュアルインターフェースを実現するためにはそのためのインターフェースの実装(IDispatch等)
が必要で、それをするかどうか、ということだと思います。
この属性によってC#で明示的にインターフェースを実装せずともビルド時に自動生成してくれる、といったところでしょうか。
インターフェース側をDualにしたので、ここもひとまず合わせてDualにしておきます。
(Noneでも動作的には問題ないようです)
ComDefaultInterface
これはCreateInstanceを実行した際にデフォルトで返されるインターフェースを指定しているようですね。
今回はインターフェースがひとつしかないのであまり意味がないかもしれませんが、複数のインターフェースを実装している場合だと、
ここで指定したインターフェース型が戻されるようになるのだと思います。
ProgId
これは、CreateIntance時に指定する文字列です。
COM公開設定
属性を指定しただけでは、まだただのライブラリです。
COMとして使用できるようにするためには、レジストリに登録する必要があります。
C#(というか.NET?)のプロジェクトでは、以下のように「COMの相互運用機能の登録」というチェックボックスがあります。
これをチェックすることで、ビルド時にレジストリの登録までやってくれるようです。
これですべての設定が完了しましたので、ビルドを行います。
※ビルド時にレジストリを編集するので、管理者権限がないとビルドエラーになるようです
テスト実行
Dim test
Set test = CreateObject("ComComponent.Hello")
WScript.echo(test.GetHelloMessage())
これで、Hello!の文字列が表示されるようになりました。
補足
今回のテストはWindows 7で行いましたが、Windows 10では動きませんでした。
(CreateObjectでエラーとなる)
いろいろ調べましたが、この点についてはわかりませんでした。
まとめ
C#を使ってCOMコンポーネントが実装できることが確認できました。
あとはここに様々な機能を実装していけば、それが言語非依存のAPIとして機能すると思います。
次回
今回はクラスやインターフェースに属性を付与したり、プロジェクトのプロパティでチェックボックスをオンにしたりしました。次回は、一体それによって何が行われているのかという内部動作的なところについて少し掘り下げてみたいと思います。