COMの基礎

COMとは?

COM(コンポーネント・オブジェクト・モデル)という言葉を聞いたことがあるでしょうか?

COMとは、プログラムをソフトウェアコンポーネント(独立した情報処理を行なう構成要素のこと。単にコンポーネントとも言う。)で構成する、オブジェクト指向のプログラミングモデルの1つです。ActiveXコントロールやOLEといったものも、このオブジェクトモデルが基本的な土台となっています。

COMは、オブジェクトの公開方法や、オブジェクト自身の寿命の管理方法などを定義しています。また、COMはプロセスやネットワークを越えた動作方法も定義しています。COMが定めるこれらの定義に基づいて、オブジェクトの機能が他のコンポーネントやアプリケーションに公開されるのです。

まあ、簡単に言えば、コンポーネントを作成・使用してプログラムをするときの仕様、あるいは取り決めみたいなものです。つまり、

「新しく独自のCOMコンポーネントを実装するプログラマはここに示す仕様にしたがってコンポーネントを作成してね。」

そして、

「既存の(自分あるいは他の誰かが作った)COMコンポーネントを使用するときにはこの仕様にしたがってね。」

ということだと思ってください。

このようにCOMによる取り決めに従うことによって、オブジェクト側とそれを利用する側のつながりが統一され、必要ならプロセスやネットワークを越えてですら、オブジェクトの機能を使用することができるようになるのです。

オブジェクト

COMコンポーネントは、通常、DLL(拡張子が“.OCX”などの場合もある)やEXEの形で提供されますが、このコンポーネント自体は、ある機能を提供するための単なるプログラムコードに過ぎません。コンポーネントは、プログラム実行時にあるAPI関数を呼び出すことによって、メモリ上にそのインスタンス(実体)が作成されます。このインスタンスのことをオブジェクトといいます。オブジェクトは、自身の内部的なデータを持ち、さらにはインターフェースという形でオブジェクトが持つ機能を提供しています。(インターフェースについては後述)

つまり、コンポーネントというのは、作成されるオブジェクトのプログラムコード(関数)やオブジェクトがどのようなデータ(変数)を持つのかということを記録した設計図のようなものなのです。そして、実行時にその設計図を元にして作られるのがオブジェクトというわけです。実際に機能を提供するのはこのオブジェクトです。

インターフェース

インターフェースとは

COMオブジェクトは仕様によって決められているインターフェースと呼ばれるものを持っています。インターフェースとは、COMオブジェクトとそれを使うアプリケーションの間の通信のメカニズムのことで、あるオブジェクトがその機能を外部に公開するために使われます。

例えば、IFooという名前のインターフェースを持つオブジェクトがある場合、そのオブジェクトは図のように表されます。オブジェクトの脇から、キャンディのように突き出ているものがインターフェースになります。(右上に突き出ているのはIUnknownインターフェースと呼ばれるものです。これについては後で述べます。) ここで描かれている2つのインターフェースは、オブジェクトと外部をつなぐ唯一の接点となっています。つまり、インターフェースを使うことでのみ、COMオブジェクトにアクセスすることができるのです。

インターフェースのメカニズム

COMのインターフェースの正体は、オブジェクトによって実装された関数(メソッドと呼びます)へのポインタのテーブルです。また、通常、COMオブジェクトはインターフェースポインタによって識別されます。このインターフェースポインタとは、この関数ポインタテーブルのあるアドレスが格納された変数のアドレスのことです。

ちょっと分かりづらいので、もう少し詳しく説明しましょう。

オブジェクトは、通常、複数のインターフェースを持っていて、それぞれのインターフェースごとに1つのまとまった機能が提供されます。そして、その1つの機能を提供するために、インターフェースは複数のメソッド(すなわち関数)を持っています。この複数のメソッドは、それぞれ関数のプログラムコードとしてメモリ内に格納されているわけですが、それぞれの関数はその関数コードの先頭のアドレスによって識別されます。(これは他のWin32 APIなどでも同じです。) COMでは、そのインターフェースの提供している複数ある関数のそれぞれのアドレスを1つのポインタテーブル(関数のアドレスを格納する配列変数のことで、vtableとも呼ばれる)に格納し、さらにその配列変数のアドレス(vtableポインタと呼ばれ、“lpVtbl”と表記されることがある)を格納した変数領域が作成されます。そしてその変数領域のアドレスをインターフェースポインタとして返すわけです。

インターフェースポインタはオブジェクトを識別するためのものであるといいましたが、そのオブジェクトを指しているはそのインターフェースポインタだけではありません。COMオブジェクトは、通常、複数のインターフェースを提供していますが、それらのインターフェースポインタはすべて同一のオブジェクトを表しているのです。

インターフェースのインデックス

ところで、COMのインターフェースがこのような仕様になっている以上、インターフェースポインタからメソッド呼び出しをするときには、メソッド名(関数名)よりもむしろ、ポインタテーブル上でのインデックス(つまり、呼び出したい関数のアドレスがvtableの何番目に格納されているのか)が重要であることが分かります。

実際に、LOLLIPOPモジュールを使ってHSP上でインターフェースのメソッドを呼び出すときには、そのメソッドのインデックスを調べて、それを指定する必要があります。(これがなかなか面倒な作業です。) C++などのプログラミング言語では、その言語の仕様上、メソッド名で指定することができるため簡単なのですが、通常はHSPではメソッド名での指定ができません。HSPでのメソッドの呼び出しは、COMをサポートするほかのプログラミング言語に含まれるインターフェースの定義(例えば、C++用のヘッダファイルなど)や、COMコンポーネントの作成者が公開しているインターフェースの定義を参照して調べる必要があります。

このページで使われるインターフェースについては、メソッドのインデックスも含めて説明をしていくつもりですが、このページで扱っていないインターフェースを使う場合、そのメソッドのインデックスは、各自で調べなければなりません。

インターフェース名とメソッド名

それぞれのインターフェースは名前がつけられています。ほとんどの場合、インターフェースの名前はアルファベット大文字の“I”から始まる名前がつけられます。代表的なインターフェースとして、IUnknownインターフェースがあります。

メソッドの名前が同じで機能やパラメータがまったく違うメソッドが複数の異なるインターフェースで使用されることがあります。これらが区別できるようにするため、インターフェースの持つメソッドの名前は、便宜上、C++のクラスのメンバ関数の表記を使用します。すなわち、「インターフェース名::メソッド名」というように、間に2つのコロン『::』を入れた表記をします。例えば、IUnknownインターフェースの持つQueryInterfaceメソッドは、「IUnknown::QueryInterface」と表記します。(メソッド名のみで表記することもあります。)

また、あるインターフェースポインタからそのインタフェースのメソッドを呼び出すときには、C++の表記では 「インターフェースポインタ->メソッド名(引数...)」のように、アロー演算子『 -> 』を使って記述されますが、このページでもその表記法を用いるところがあります。たとえば、インターフェースポインタを格納した変数pFooの持つFunc1メソッドの呼び出しは、『pFoo->Func1()』と表記されます。

インターフェースの派生

COMコンポーネントのバージョンアップによって、コンポーネントに新しい機能を実装する場合、その機能を新しいインターフェースとして加えることもできますが、もともとある機能を拡張するような場合(新しいメソッドを追加したい場合)には、既存のインターフェースを元にしたインターフェースが作成される場合があります。これをインターフェースの派生といいます。

バージョンアップされたコンポーネントは、古いインターフェースと新しいインターフェースの両方を提供することになります。

一般的には、既存のインターフェース名の末尾に“2”,“3”というように数字をつけたものが新しいインターフェース名になります。このとき、新しいインターフェースは、既存のインターフェースのメソッドをすべて持っていて、それぞれのメソッドのインデックスは同じであることが保証されています。

例えば、IFooというインターフェースがあったとします。このインターフェースには2つのメソッドFunc1Func2があり、Func1メソッドのインデックスが0で、Func2メソッドのインデックスが1であるとしましょう。今、IFooを拡張して、Func3メソッドを追加した新しいインターフェースを作るとします。このとき、新しいインターフェース名はIFoo2であり、このインターフェースは3つのメソッドFunc1Func2Func3を持つことになりますが、ここではIFooインターフェースと同じくFunc1メソッドのインデックスは0になり、Func2メソッドのインデックスは1になるのです。そして新しく追加されたメソッドはそれらの後ろに追加されるのでインデックスは2になります。

IUnknown インターフェース

IUnknownインターフェースは特別なインターフェースです。

まず1つは、すべてのオブジェクトがこのIUnknownインターフェースをサポートしているということがあります。このインターフェースは、オブジェクトが提供している他のインターフェースを取得する機能と、オブジェクトの寿命の管理(オブジェクトの解放など)の機能を持っています。つまり、はじめにオブジェクトを作成するときにIUnknownインターフェースを取得しておけば(コンポーネントが正常にインストールされていれば、必ず取得できる)、そこからオブジェクトの提供するほかのインターフェースが取得してその機能を使うことができるということです。

もう1つの特徴として、他のすべてのインターフェースはこのインターフェースから派生しているのです。IUnknownインターフェースは3つのメソッドQueryInterfaceAddRefReleaseを持ちます。すなわち、すべてのインターフェースはこれらの3つのメソッドを持っていて、オブジェクトが提供するほかのインターフェースの取得と、オブジェクトの管理が行なえるわけです。

GUIDとクラスID・インターフェースID

COMでは、さまざまなものの識別のためにGUID(Globally Unique Identifiers)と呼ばれる識別子を使用します。GUIDは、16バイト(128ビット)の値によって識別するものです。GUIDはオブジェクトやインターフェースの識別にも使われ、特にCOMオブジェクトを識別するGUIDをクラスID(CLSID)、インターフェースを識別するGUIDをインターフェースID(IID)といいます。

GUIDは、一般に、文字列形式で表される場合と、構造体で表される場合があります。

文字列で表されるGUIDは、

{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}

のように表されます。ここで、xxxxの部分には16進数で表された数値が記述されます。例えば、IUnknownインターフェースを表すインターフェースIDは

{00000000-0000-0000-C000-000000000046}

のように表されます。識別子である以上、このGUIDはIUnknownインターフェースだけを表しているのであり、それ以外の何物も表しません。

他の例として、Windowsシステムが提供するシェルリンクオブジェクトを識別するクラスIDは

{00021401-0000-0000-C000-000000000046}

のように表されます。このGUIDもまたシェルリンクオブジェクトのみを表すもので、他の何物も表しません。

構造体で表されるGUIDは、識別子である16バイト値が以下のように定義されるGUID構造体に格納されたものを言います。

typedef struct _GUID {
    DWORD  Data1;      // 4バイト
    WORD   Data2;      // 2バイト
    WORD   Data3;      // 2バイト
    BYTE   Data4[8];   // 1バイト×8
} GUID;

また、クラスIDを格納するCLSID構造体やインターフェースIDを格納するIID構造体は、GUID構造体の別名として定義されています。(以下の typedef は、型の別名を定義するものです。)

typedef GUID  CLSID;
typedef GUID  IID;

ところで、通常クラスIDやインターフェースIDは、それぞれの名前の前にCLSID_IID_をつけた定数名で定義されています。また、一般にそれ以外のGUIDもGUID_から始まる定数名で定義されています。例えば、シェルリンクオブジェクトのクラスIDはCLSID_ShellLinkという名前で、IUnknownインターフェースのインターフェースIDはIID_IUnknownという名前で定義されています。実際にはこれらが指定されたときはGUIDが格納されたGUID構造体になります。

関数や構造体のパラメータとしてGUIDを渡す場合、実際には GUID(またはCLSIDIID)構造体へのポインタを渡すことになります。