Dr. GUI、コンポーネント、COM、および ATL を使う

第 1 部 : 1998 年 2 月 2 日
第 2 部 : 1998 年 2 月 9 日
第 3 部 : 1998 年 2 月 23 日
第 4 部 : 1998 年 3 月 2 日
第 5 部 : 1998 年 3 月 30 日
第 6 部 : 1998 年 4 月 27 日
第 7 部 : 1998 年 5 月 29 日
第 8 部 : 1998 年 7 月 30 日

目次

第 1 部 : COM を取り上げるって?まだ取り上げていなかったの?
第 2 部 : COM の基礎知識
第 3 部 : オブジェクトおよびインターフェイスの取得
第 4 部 : オブジェクト クラスとオブジェクト ライブラリ
第 5 部 : オブジェクトの実装
第 6 部 : Visual Basic と Visual J++ で COM オブジェクトを使用する
第 7 部 : Visual C++ からオブジェクトを使用する
第 8 部 : スマートに行こう!スマート ポインタ で COM オブジェクトを使おう

第 1 部 : COM を取り上げるって?まだ取り上げていなかったの?

読者のこんな声が聞こえてきます。「これから何をしようというのですか?COM について説明するって?それについては本が出ているじゃないですか」。

確かにいろいろな本が発行されています。Dr.GUI は優れた資料をいくつも推薦できます。Dale Rogerson の 『Inside COM』 (Microsoft Press、1997) や Don Box の 『Essential COM』 (Addison-Wesley、1998) は特にお薦めです。Adam Denning の 『ActiveX Controls Inside Out』 (Microsoft Press、1996) および Sing Li と Panos Economopoulos による 『Professional Visual C++ 5 ActiveX/COM Control Programming』 (WROX Press、1997) もお薦めできます。そしてもちろん、紙に印刷されたものとしては究極の OLE の参考書として Kraig Brockschmidt の 『Inside OLE, 2nd Edition』 (Microsoft Press、1995) があります (もちろん究極の参考資料は MSDN であることに異論はないでしょう)。また、コンポーネント ソフトウェアについての正式かつ公正な説明が必要であれば、Clemens Szyperski の新著、『Component Software: Beyond Object-Oriented Programming』 (Addison-Wesley、1998) を読むことができます。

読者は、「それで、Dr.GUI はこれに何か付け加えることはあるのですか?」と思うことでしょう。

 「ありません」というのが名医の答えです。これらの著書 (そして他の著書) や文書ですべて言い尽くされています。つまり、これらを読む時間がありさえすればですが。

読者はここで、「そうか!そういうことか! COM と ATL の概要を簡単に紹介してくれるわけですね。毎週、数画面分ずつ勉強すればいいということですね」と膝を打つのです。

そこで Dr.GUI としては、「優秀な生徒が来たな。思ったより簡単にいきそうだ」と思うわけです。

コンポーネント?ステレオのプログラミングをするの?

Visual Basic を頻繁に使用するユーザーは、コンポーネントを使用したプログラミングに慣れているはずです。ActiveX のビジュアルなコントロール (スピン ボタンなど) と、ビジュアルではない ActiveX コンポーネント (データベース アクセス オブジェクトなど) の両方を使用していることでしょう。実践的な Visual Basic プログラムで、事前に作成された、再利用可能なコンポーネントを多用しないプログラムはほとんどありません。しかし、数多くのコンポーネントを再利用している割には、ユーザー自身が再利用可能なたくさんのコンポーネントを作成するにはいたっていません。

C++ のプログラミングではこれとは違った経験をしたかもしれません。C++ やオブジェクト指向のプログラミングでは再利用が容易だと言われていますが、実際に経験してどうですか?再利用可能なオブジェクトのライブラリは作成できましたか?もちろん、できた人もいるでしょうが、ほとんどのユーザーはできていないでしょう。そしてそのようなライブラリを作成できたとして、そのライブラリを日常的に活用していますか?コードの再利用をしない理由は、単に心構えの問題だけではありません。本当のところ、コードを再利用するのが難しく (何かしら必要な機能が足りないのが常です)、また再利用可能なコードを作成するのはさらに困難なのです (実際に使用できるものにしながら、なおかつ汎用的にするのは非常に困難です)。

また、C++ を使用することにより容易になるのは、再利用可能なバイナリ コンポーネントの作成ではありません。C++ を使用することにより相対的に容易になるのは、ソース コードの再利用なのです。C++ の主なライブラリはコンパイル済みの形式ではなく、ソース形式で提供されます。そして、オブジェクトから正しく継承をするために、ソースを調べなくてはならない機会が多すぎるのです。また、オリジナルのライブラリを再利用するときに、その実装の詳細に簡単に依存できてしまうのです (依存しなくてはならないこともあります)。さらに悪いことに、しばしばオリジナルのソースを修正して私用のライブラリを構築するという誘惑にも駆られます (MFC の私用ビルドが世の中にどれだけ存在するかは神のみぞ知るです)。

ソース コードではなく、バイナリ オブジェクトを再利用することしよう

では、バイナリ オブジェクトはどうしたら再利用できるのでしょうか。Windows のプログラマが最初に考えるのは単純にダイナミック リンク ライブラリ (DLL) を使用するということでしょう。DLL を使用することは可能です。Windows 自身、実際のところ、ほとんどは DLL のかたまりなのですから。しかし、問題もいくつかあります。

最初に、DLL は必ずしもプログラミング言語から独立してはいません。 C 言語で作成された DLL の場合であっても、呼び出し規則 (パラメータをどの順番でプッシュするかなど) を、C 言語のプログラムからしか使用できない形に簡単に変更できてしまいます。Windows で使用されている呼び出し規則は Windows システムのための標準としての地位を確立していますが、名医は呼び出し規則の不一致のために DLL が失敗するケースを数多く見てきました。

自分の DLL のインターフェイスを C 言語のスタイルで作成すると、いくつかの大きな制約が生じます。最初に、C++ のオブジェクト指向機能では関数名の名前の修飾が必要であるため、オブジェクト指向のプログラミングができなくなります。名前の修飾には標準形式はありません。同じコンパイラであっても、バージョンが異なれば名前の修飾方法が異なることがあります。2 番目に、ポリモーフィズムを実現するのが困難です。両方の側でラッパー クラスを作成すればこれらの問題に対処できますが、この方法は痛みを伴います。Dr.GUI は痛い方法はとりません (ほどほどには痛いかもしれませんがね)。

名前の修飾の問題を解決して DLL を正常にリンクできるようになったとしても、オブジェクトを更新するときがくると、別の問題が発生します。

第一に、仮想関数を追加してオブジェクトを更新すると、もはや身動きできなくなります。新しい関数を最後に追加すればいいと思うかもしれませんが、それではうまくいきません。対象オブジェクトから継承をしているほかのすべてのオブジェクトの vtable エントリをずらしてしまうことになるからです。仮想関数の呼び出しでは正しい関数を呼び出すために vtable 内への固定のオフセットが必要なため、vtable に変更を加えるわけにはいきません (少なくとも、対象オブジェクトおよびそこから派生したオブジェクトを使用するすべてのプログラムを再コンパイルしない限り)。オブジェクトを更新するたびにすべてを再コンパイルすることなど考えられません。

2 番目に、オブジェクトを作成するためにクライアントで new を使用している場合、オブジェクトのサイズを変更する (つまりデータを追加する) ためには全体の再コンパイルが必要になります。

最後の、そしてもっとも重大な問題は、にっちもさっちもいかない状態の中で、あまり魅力のない選択肢しか残されていないという点です。DLL の更新は悪夢なのです。DLL を上書きして 「インプレース」で更新するか、あるいは新しいバージョンの名前を変更するしかないのです。インプレースで DLL を更新するのは恐ろしいことです。インターフェイスの整合性を保っておいたとしても、この DLL を使っているほかのユーザーを破壊してしまう可能性はきわめて高いものです。この制約に関連して Microsoft も含めた業界全体が直面したすべての問題についてここで説明する必要もないでしょう。

もう 1 つのオプション、つまり新しい DLL 名を使用する方法では、少なくとも機能中のシステムを機能したままにしておくことはできます。しかし、ディスク スペースの使用というコスト (ハード ディスクはこのところ 3GB 程度あるのが一般的なので、大きな問題ではないかもしれません) と、メモリ使用量の増加という 2 番目のコストが生じてきます。ユーザーが DLL の両方のバージョンを使用している場合には、ユーザーの作業セットの中には非常に似たコードのコピーが 2 つ入っていることになります。ユーザーのメモリの使用状況を調べたら、たとえば Visual Basic のランタイムや Microsoft Foundation Class (MFC) DLL のバージョンが複数見つかるなどということもめずらしいことではありません。ほとんどすべての Windows システムでは一般的に物理メモリの量以上の仮想メモリが使用されるため、作業セットのサイズが大きくなれば、ディスクへの仮想メモリのスワップ回数が増えるため、パフォーマンスに深刻な影響が及びます (遅いシステムにメモリを追加すると速くなるのはこのためです)。

ユーザー (またはアプリケーション) がどのバージョンを使用するかを選択できるようにするのが理想的です。これは DLL を静的にリンクした場合は非常に難しいことですが、DLL を動的にロードする場合はきわめて簡単なことです。

C++ のために弁解しておくとすれば、C++ はこのような問題を解決するように意図されたものではないのです。C++ は 1 つのファイルに含まれている複数のプログラムのコードを再利用するためのものであり、すべてのオブジェクトが一度にコンパイルされます。C++ は、長期間にわたり、異なるバージョンを自由に組み合わせられるような、再利用可能なバイナリ コンポーネントを構築する方法として提供されたものではありません。Java の作成者らはこれらの問題に着目しました。これらの制約は、後に Java となる Oak を開発する大きな要因となったのです。

では Java ではどうなのでしょう ?

Java ではこれらの問題のいくつかは解決されていますが、いくつかの新しい問題が追加されています。最大の問題は、Java コンポーネント (通常は JavaBeans) が、Java で作成したプログラムだけで使用することを想定しているということです。Microsoft 仮想マシン (VM) では JavaBeans を COM オブジェクトとして使用できます。このため、どの言語からでもこれらを使用することができます。また、Sun には Java/ActiveX ブリッジがあります。しかし一般的には、Windows 上で実行していない限り、Java は単一言語システムなのです。Java コンポーネントは Java プログラムでしか使用できません。そして Java を使用するためには、システムをほとんど全部最初から書き直す必要があります (確かにネイティブ コールをするのは可能です。しかし、Java ネイティブ インターフェイス (JNI) を使用してこれを行うのは大変な作業ですし、プログラムの移植性は完全に失われます)。Dr.GUI はこのようなことは受け入れ難いと考えるため、Microsoft 仮想マシン (VM) が少なくとも Windows に関してはこれよりも柔軟なものになっていることをうれしく思っています。C++ にせよ、Visual Basic にせよ、Java にせよ、すべてのプログラマやすべての問題を解決できる絶対の言語はありません。

Java ではまた、プログラムを作成するときに、使用するコンポーネントがローカル (ローカルのマシンにある) かリモート (別のマシンにある) かを決定する必要があります。また、ローカルのコンポーネントを使用する方法と、リモートのコンポーネントを使用する方法はかなり違います。

Java にはその他にもいくつかの問題があるため、コンポーネントのあらゆるニーズに応える理想的な解決策とはなりません。最初に、バージョン管理のための確固とした方法がありません (Microsoft VM のパッケージ マネージャはこの問題の大きな助けになります)。2 番目に、Java は C++ と比較してかなり低速です。Dr.GUI はあるオンライン Java マガジンに掲載された最近の 「C++ と同程度に速い」というベンチマークでは、Java の不得意なテストが省略されていることに気がつきました。すぐにわかる 2 つの例としては文字列と配列操作 (Java はアクセスのたびに境界検査を行う必要があります) と、最初のメソッド呼び出し (Java の最初の呼び出しでは、テーブルの中のシグニチャでメソッドを調べる必要がありますが、それ以降の呼び出しは、マガジンのテストで示されているように高速です) があります。最後に、Java の 「一度に 1 つのクラス」というロードの方法では、すべてのコードを一度にロードするよりも (コードが少ない場合でさえも) 格段に長い時間がかかります。なぜなら、共に多大なオーバーヘッドを伴うファイル トランザクションまたは HTTP トランザクションがはるかに多くなるためです。

パフォーマンスが高くなる方法で Java を使用している場合でも、Java コンポーネントを別の言語から使用する場合には、異なる言語およびオブジェクト モデルをリンクするために必要となる変換層の存在のためにパフォーマンスが影響を受けることになります。

Java の利点は、マシンのプロセッサやオペレーティング システムごとに再コンパイルをしなくても、コンパイル済みコンポーネントを異なるマシン上で使用できる可能性があることです。しかしこれは自動的に可能になるわけではありません。サポートする予定のプラットフォームごとにテストやデバッグが必要になります。

ではどうしたらいいのでしょう?

結論から言うと、C++ を使用して DLL やその他の再利用可能なバイナリ コンポーネントを作成することができます。Dale Rogerson の著書 『Inside COM』および Don Box の著書 『Essential COM』 では共に、再利用したい C++ クラスの説明から始めて、前記の問題 (および、それ以外のいくつかの問題) を解決する賢いテクニックを解説しています。そして両方の著書の結論は COM になっていますが、これは驚くことでもありません。言い換えるならば、バイナリの再利用の問題の解決方法がつまり、COM の重要な特性なのです (この発展の過程を調べたいのであれば、Markus Horstmann による記事、"From CPP to COM" を参照してください。

COM の 「ネイティブ言語」は C++ ですが、C から COM を使用するのは比較的簡単です。ヘッダーはこれをサポートさえしています。そして、言語により多少の違いはあるものの、Visual Basic、Java、Delphi など、どの言語からでも双方向の COM サポートが可能です (双方向の COM サポートとは、その言語から COM オブジェクトを使用することと、その言語で COM オブジェクトを作成することの両方が可能であるという意味です)。使用言語のランタイムコードで COM 互換性を実現するのは簡単な作業ではありませんが、この利点は大きなものになります。これを実現すれば、作成済みでデバッグ済みの COM オブジェクトのすべてを使用できるようになります。また、作成した COM コンポーネントには大きな市場が開けます。Giga Information Group の見積りによれば、現在の市場規模は年間 4 億ドルであり、今後 3 年間で 30 億ドルに達します (COM コンポーネントの市場は、Microsoft よりも速いペースで成長するのです)。これらの市場予測はサード パーティの COM オブジェクトのものであり、Microsoft が提供する COM コンポーネントは含まれていません。

COM のもう 1 つの重要な特性は、インプロセス (DLL) 、ローカル (同じマシン上の別のプロセスの中の EXE)、そしてリモート (分散 COM、すなわち DCOM を通じて、別マシン上の DLL または EXE) という 3 種類のオブジェクトがサポートされることです。COM コンポーネントを使用するコードを作成するときには、インプロセス、ローカル、またはリモートのオブジェクトを使用するためにまったく同じコードが使用されるため、どの種類の COM オブジェクトを使用することになるかを考える必要は (あるいは知る必要すら) ありません。COM はどのようにして正しいオブジェクトを使用するのでしょう。COM はレジストリの中でオブジェクトのクラス ID を調べます。レジストリのエントリは、どの種類のオブジェクトが使用可能かを示します。COM はプロセスの起動やネットワークを介した通信を含む残りの作業をしてくれます (注意 : COM オブジェクトの種類によってパフォーマンスが異なることは考慮する必要があります。しかし、どの種類のオブジェクトを使用することになっても、少なくともオブジェクトに接続し、使用するためのコードはまったく同じです)。

しかし COM は世界中の問題を解決するわけではありません。たとえば、コンポーネントを更新すると、そのコンポーネントを使用しているプログラムが壊れる可能性は依然としてあります (COM により、コンポーネントは実装の詳細を知ることのできない 「ブラック ボックス」として扱わなければならないため、プログラムが壊れる可能性は少なくなってきてはいますが、それでも可能性は残ります)。このため、インプレースでコンポーネントを更新して破壊のリスクを犯すか、新しいコンポーネントで新しいクラス ID を使用するかの選択が依然として必要になります。しかし COM により、ユーザー (またはアプリケーション) が再コンパイルを行わずに、使用するバージョンを選択できるようにするためのコードの作成がいくらか容易になります。

COM コンポーネントはほとんどすべての言語で作成し、ほとんどすべての言語から使用できること、そしてどのマシンにでも置けることを思い出してください。これは便利です。しかし、プラットフォーム間のサポートはどうなっているのでしょう。

プラットフォーム間のサポートについては、よい知らせと悪い知らせがあります。悪い知らせというのは、現在のところ、Win32 を除いては、どのプラットフォームについてもあまり多くの COM は提供されていないというものです。COM は Windows 以外のプラットフォームへも移植されていますが、数は多くありません。しかしニュースはこれだけではありません。

よい知らせというのは、さらに多くの移植が近く利用できるようになるというものです。広く使われているバージョンの UNIX や MVS が対象ということです。また、Microsoft 自身も移植作業をしています。したがって、あなたが使用しているメインフレームや UNIX マシンで COM および DCOM を利用できるようになるのも遠いことではありません。UNIX 対応版の入手可能時期は、2 月に発表される予定です。自分のマシン上からどの言語 (Visual Basic、Java、Delphi、C++) を使用してでも、高速のメインフレーム上で任意の言語で作成されているリモート COM オブジェクトを実行するためにアクセスできたらどんなに便利になるか想像してみてください。

さて、Windows 向けに開発をしているのなら、Visual Basic、Java、C++、Delphi またはその他のどの COM 互換言語を使用するとしても、COM オブジェクトの作成は検討に値するはずです。作成したオブジェクトは、COM および DCOM のおかげで、コンポーネント、またはコンポーネントのクライアントを再構築することなしに、ローカル マシンで、またはリモートから使用できるようになります。また、作成したソリューションを Windows 以外のプラットフォーム上で実行する必要がある場合でも、COM の魅力は日に日に増しているので、真剣に検討するべきなのは確かです。

覚えておく必要のある COM の基本概念

来週は、COM の基礎知識、つまりオブジェクト、インターフェイス、およびモジュールについて説明します。また、時間があれば、単純な COM オブジェクトの C++ コードを見てみることにします (時間がなければ、翌週に回します)。

第 2 部 : COM の基礎知識

今までの復習、これからの予定

第 1 部では、C++ のような言語を使用しても、バイナリ コンポーネントを組み合わせてソフトウェアを構築することができない理由を説明しました。その理由の核心は、C++ がそのような問題を解決するためのものではなく、単一の実行可能プログラムのソース コードを簡単に再利用できるようにするためのものだからというものでした。C++ はその目的を比較的よく達成しています。しかし、私たちの目標は、コンポーネントを変更するたびにシステムの一部分 (または全体) を再構築することなしに、さまざまなベンダーから提供されるコンポーネントを組み合わせて使えるようにすることです。C++ モデルがこのシナリオには適さない理由は十分に説明しました。

ではどうしたらよいのでしょう。C++ はあきらめるべきなのでしょうか。いいえ、その必要はありません。しかし、今までの使用方法とは多少異なる方法で使用する必要があります。これから説明するのはこのこと、つまり、COM を C++ から使用する方法です。

では、C++ プログラマでない人はこの先を呼んでも無駄なのでしょうか。いいえ、そうではありません。どの COM 互換言語 (Visual Basic、Visual J++、Delphi、その他) を使用する場合でも、その裏では (手術室の中だったかな?) これから説明することを実行しているからです。読み進めば、ActiveX コントロールや COM コンポーネントの仕組みについての貴重な知識が得られるはずです。

COM って何なんですか?

コンポーネント オブジェクト モデル (Component Object Model:COM) は、ソフトウェア コンポーネント同士が相互に通信をするための方法です。任意の 2 つのコンポーネントが、実行するマシン (マシン同士が接続されている限り)、マシンのオペレーティング システム (COM がサポートされている限り)、コンポーネントが作成された言語に関係なく、互いに通信できるようにするための、バイナリおよびネットワークの標準です。COM では場所も透過になります。つまり、コンポーネントを作成するときには、他のコンポーネントがインプロセス DLL であるか、ローカル EXE であるか、または別のマシン上にあるかを考慮する必要はありません (もちろんパフォーマンス上の影響はありますが、重要なのは、他のコンポーネントの置き場所に合わせて書き直しを行う必要がないということなのです)。

オブジェクト

COM はオブジェクトに基づいています。しかしこのオブジェクトは、C++ または Visual Basic で使用されているオブジェクトと同じものではありません (ところで、オブジェクトとコンポーネントはほとんど同じものです。 Dr.GUI は、アプリケーション アーキテクチャについて話す場合には 「コンポーネント」という言葉を使い、実装について話す場合には 「オブジェクト」という言葉を使用する傾向があります)。

最初に、COM オブジェクトは完全にカプセル化されています。ユーザーはオブジェクトの内部の実装にアクセスすることはできませんし、オブジェクトで使用されているデータ構造について知ることもできません。実際のところ、オブジェクトは完全にカプセル化されているため、COM オブジェクトは通常は単なるボックスとして描かれます。図 1 は完全にカプセル化されたオブジェクトの絵です。実装の詳細が見えないことに注目してください。

図 1 : 完全にカプセル化された非 COM オブジェクト

カプセル化についてはこれでいいでしょう。しかし、通信をするとなるとどうでしょう。このボックスの中のコンポーネントと通信をする方法がありません。当然ながら、これではうまくいきません。

インターフェイス : オブジェクトとの通信

ここで、インターフェイスが必要になります。COM オブジェクトにアクセスする唯一の方法は、インターフェイスを使用することです。図 2 に示すように、オブジェクトに対する IFoo という名前のインターフェイスを描けます。

図 2 : インターフェイスを持つオブジェクト―依然として非 COM

オブジェクトの脇からキャンディの棒のように突き出しているものがインターフェイスで、この例では IFoo インターフェイスという名前を使用しています。このオブジェクトと通信する唯一の方法がこのインターフェイスです。名医としては、インターフェイスをキャンディとたとえるよりも、電気プラグにたとえるほうが適切に思えます。オブジェクトの機能にプラグを差し込む手段と考えるのです。あるいは、ビデオやテレビのアンテナ入力のようなものと考えてもよいでしょう。

インターフェイスには 2 つの役割があります。まず、インターフェイスは、オブジェクトに何かをさせるために呼び出すことができる、一組の関数です。C++ では、インターフェイスは抽象的な基本クラスとして表現されます。たとえば、次のような IFoo の定義を考えることができます。

class IFoo {
 virtual void Func1(void) = 0;
 virtual void Func2(int nCount) = 0;
};

ここでは戻り値の型と継承については無視することにします。しかし、インターフェイスの中では複数の関数を使用することができ、すべての関数は純粋な仮想関数であることは覚えておいてください。つまり、クラス Ifoo の中には実装部分はないのです。ここでは動作は定義しません。インターフェイスに含まれる関数を定義するだけです (もちろん実際のオブジェクトには実装が必要です。これについてはあとで説明します)。

ここでは戻り値の型と継承については無視することにします。しかし、インターフェイスの中では複数の関数を使用することができ、すべての関数は純粋な仮想関数であることは覚えておいてください。つまり、クラス Ifoo の中には実装部分はないのです。ここでは動作は定義しません。インターフェイスに含まれる関数を定義するだけです (もちろん実際のオブジェクトには実装が必要です。これについてはあとで説明します)。

この具体的な契約は、COM およびコンポーネント ソフトウェアにとって非常に重要なものです。「確固とした」契約がなければ、コンポーネントの交換は不可能です。

インターフェイス契約は、ダイヤモンドと同じように永遠です

COM では、コンポーネントを送り出してインターフェイス契約を 「公開」すると、契約は不変のものになります。いかなる部分も変更してはいけません。追加をしてはいけません。削除をしてもいけません。変更もいけません。なぜでしょう。他のコンポーネントがこの契約に依存しているからです。契約を変更すると、そのソフトウェアが壊れます。契約を守っている限りは、内部実装を改善することができます。

何か忘れていた場合はどうなるのでしょう。要件が変更された場合は?世の中をよくしていく方法はあるのでしょうか?

答えは簡単です。新しい契約を作成するのです。標準 OLE インターフェイス リストには、IClassFactory および IClassFactory2IViewObject および IViewObject2、など多くのものがあります。それらに倣って、IFoo2 を提供することも当然できます (ところで、インターフェイス名が大文字の I で始まるという規則に気付いていることでしょうね)。

では、新しい契約を作成したら、古い契約しか認識していないソフトウェアはどうすれば、その新しいコンポーネントを使用できるようになるのでしょう。これによって古いコンポーネントが影響を受けることはないのでしょうか?

COM オブジェクトは複数のインターフェイスをサポートできます - 複数の契約を実装できます

いいえ、古いコンポーネントが影響を受けることはありません。理由は簡単です。COM では、1 つのオブジェクトが複数のインターフェイスをサポートできます。実際のところ、実用的な COM オブジェクトはすべて、少なくとも 2 つのインターフェイスをサポートしています (少なくとも標準の Iunknown インターフェイス (これについてはあとで説明) と、ユーザーがオブジェクトに実行させたいことを行うインターフェイスです)。ビジュアルな ActiveX コントロールは 1 ダースほどのインターフェイスをサポートしており、そのほとんどは標準インターフェイスです。コンポーネントがインターフェイスをサポートするためには、そのインターフェイスに含まれているすべてのメソッドを実装する必要があるため、これは大変な作業になります。Active Template Library (ATL) などのツールが広く使用されているのはこのためなのです。これらのツールは、すべてのインターフェイスの実装を提供しています。

そこで、新しい IFoo2 の機能をサポートするために、このオブジェクトに IFoo2 も追加することにします。

図 3 : IFoo と IFoo2 の両方をサポートするバージョン 2.0 - 依然として非 COM オブジェクト

プラグの例を覚えていたら、IFoo はテレビのアンテナ入力、IFoo2 はコンポジット ビデオ入力だと考えてみてください。アンテナのケーブルをコンポジット ビデオ入力に差し込むことはできませんし、その逆もできません。つまり、各インターフェイスは論理的に固有のものだということです。

一方、これらのインターフェイスには共通の部分もあります。古いインターフェイスとほとんど同じ新しいインターフェイスを追加するために、実装全体を書き直す必要があるのでしょうか。いいえ、COM はインターフェイスの継承をサポートするため、その必要はありません。IFoo の中にすでに存在する関数を変更するのでない限り、IFoo2 を次のように定義できます。

class IFoo2 : public IFoo {
 // Inherited Func1, Func2
 virtual void Func2Ex(double nCount) = 0;
};

インターフェイスの復習

今までのところを復習してみましょう。まず、COM は、ソフトウェア オブジェクトの対話のためのバイナリ標準です。バイナリ標準であるため、COM は使用するオブジェクトの実装の詳細を知らず、知ることもできません。つまり、オブジェクトはブラック ボックスなのです。

これらのブラック ボックス オブジェクトは、オブジェクトが公開するインターフェイスを通じてのみ操作できます。最後に、オブジェクトは必要なだけの数のインターフェイスを公開できます。

簡単ですよね。

まあ、これまでのところ、詳細についてはほとんど無視してきました。オブジェクトはどのようにして作成するのか。どうすればインターフェイスにアクセスできるのか。これらのインターフェイスのメソッドはどのようにすれば呼び出せるのか。そもそもこれらのオブジェクトの実装はいったいどこにあるのか。また、このオブジェクトは最終的にいつ破棄されるのか。

いい質問です。しかし、Dr.GUI は手術に遅れそうですので、これらの質問には第 3 部で答えることにします。しかしここでは 1 つだけ説明しておきましょう。インターフェイス メソッドの呼び出し方法です。

インターフェイス メソッドの呼び出し

すでにお分かりかもしれませんが、結論は簡単です。COM メソッドの呼び出しは C++ の仮想関数の呼び出しにすぎません。インターフェイスを実装しているオブジェクトへのポインタをある方法で (詳しくは第 3 部で説明します) 取得したら、あとはインターフェイスのメソッドを呼び出すだけです。

まず、IFoo インターフェイスを実装する CFoo という C++ クラスがあるとします。正しいインターフェイスを正しい順序で実装するために、IFoo から継承している点に注目してください。

class CFoo : public IFoo {
 void Func1() { /* ... */ }
 void Func2(int nCount) { /* ... */ }
};

使用するポインタはインターフェイス ポインタと呼ばれます。ポインタが取得できたとすると、コードは次のようになります。

#include <IFOO.H // Don't need CFoo, just the interface
void DoFoo() {
 IFoo *pFoo = Fn_That_Gets_An_IFoo_Pointer_To_A_CFoo_Object();

 // Call the methods.
 pFoo -> Func1();
 pFoo -> Func2(5);
};

たったこれだけです。

しかし、水面下では何が行われているのでしょう。結論を先に言うと、COM バイナリ標準はメソッド呼び出しにも適用されるということです。このため、COM は関数を呼び出すために何を行うかを定義しています。実際には、仮想関数の呼び出しで行われるのと同じことが行われます。

  1. オブジェクトの中の vtable ポインタを見つけるために pFoo の参照先が調べられます。

  2. 呼び出す関数を見つけるために、vtable ポインタの参照先が調べられ、インデックス付けされます。

  3. 関数が呼び出されます。

ステップの順番については図 4 を参照してください。

図 4 : インターフェイス ポインタを介した C++ 仮想関数呼び出し

C++ では、仮想関数があれば、必ずその関数を指す vtable があります。そして、呼び出しは必ず、この vtable を参照することによって行われることを思い出してください。

読者の 「なるほど、わかったぞ。COM は本当に C++ に縛られているんだ!バイナリ標準なんかじゃないんだな」という声が聞こえるようです。

Dr.GUI の返事は 「ナンセンス」です。結局のところ、この呼び出しは、関数ポインタの配列をサポートする言語であればどの言語でも実装できます。たとえば、C 言語ではこれを簡単に実装できます。ポインタ p を使用して Func2 を呼び出すには次のようにします。

(*((*p)+1))(p, 5); // passing 5 to the 2nd function in the array

p は最初のパラメータとして渡す必要があります。これは、C++ の this ポインタに相当します。(*p) が最初の参照であり (ステップ 1)、*((*p) + 1) は vtable からの参照であり (ステップ 2)、最終的には関数を呼び出して p と 5 を渡します (ステップ 3)。簡単ですが、美しくはありません。これを紹介したのはこの方法が可能であることを説明するため (そして C++ に感謝できるようにするため) です。x86 アセンブラ言語では、次のようなものになるはずです。

MOV EAX, [pFoo]  ; step 1
MOV EAX, [EAX + 4] ; step 2, indexed to 2nd pointer
CALL [EAX]   ; step 3

関数のアドレスを EAX の中に入れておく必要がなければ、2 番目と 3 番目の命令は CALL [EAX + 4] としてに統合できることはわかっています。

Dr. はこれをなぜこれほど詳しく説明しているのでしょう。要するに、アセンブリまたは C でこれが可能ならば、ほかのどの言語でも可能だからです。他の言語 (Visual Basic、Visual J++、Delphi) では、これらの呼び出しのためのサポート機能がランタイム コード、または仮想マシンの中に組み込まれています。これには、上記のものに似た、アセンブラまたは C コードがしばしば使用されています。

重要なことは、どのような COM メソッドの呼び出しにおいても、オリジナルの言語の種類、および COM オブジェクトの場所に関係なく、ここで示したデータ構造を使用する必要があるということです。COM において場所の透過性をどのように達成しているかについては、別の機会に説明します。

今までの復習、これからの予定

さて、インターフェイス、オブジェクト、そしてインターフェイス メソッドの呼び出し方法については説明が済みました。

オブジェクトは COM の基本的な単位です。オブジェクトとは、COM が作成するものです。オブジェクトには、いくつかのインターフェイスが組み込まれます。インターフェイスは一組のメソッドと、これらのメソッドが何を行うかについての契約です。インターフェイス メソッドは C++ 仮想関数と同じ方法で呼び出されます。

第 3 部では、これらのオブジェクトの作成方法、インターフェイス ポインタの取得方法、そしてオブジェクトを破棄する方法について説明します。

第 3 部 : オブジェクトおよびインターフェイスの取得

第 2 部では、COM の基本的な概念である、オブジェクトとインターフェイスについて説明しました。また、オブジェクトに複数のインターフェイスを実装する方法についても説明しました。最後に COM メソッドの呼び出し方法について細かく説明し、これが C++ 仮想関数の呼び出し方法と同じであることがわかりました (また、関数ポインタの配列へのポインタをサポートする言語、あるいはそれらをサポートするアセンブラ言語への呼び出しが可能な言語であれば、どの言語でもこれが可能なことも説明しました)。

COM の追加情報 : オブジェクトの作成と破棄、インターフェイス ポインタの取得

しかし、読者はこれでは説明が不十分だと感じているかもしれません。オブジェクトの作成方法については説明していませんし、オブジェクトのインターフェイスでメソッドを呼び出すためにインターフェイス ポインタを取得する方法についても説明していません。また、不必要となったオブジェクトを廃棄する方法や、インターフェイスを切り替える方法についても説明していません。

今週は、これらのトピックについては取り上げていきます。しかし先に片付けておく必要のあるものがあります。何かを説明していて、重要なことを説明し忘れた経験はありませんか?ここでも同じです。われらが名医は、オブジェクトを作成するためには、オブジェクトを参照する必要があり、インターフェイスを明確に定義する方法が必要だということを説明するのを忘れていました。そこで、まずは忘れていた問題を片付けたあとで、もっと面白いことを説明することにします (このコラムが長く、遅れているのはこのためです)。

COM の識別子

理解の早い読者であれば、COM の世界のさまざまなエンティティのために、なんらかの識別子が必要なことに気付いたかもしれません。最初に、オブジェクト型 (つまりクラス) のための識別子が必要です。次に、各インターフェイスの識別子が必要です。しかし、識別子として何を使用したらいいのでしょう。32 ビットの整数でしょうか。64 ビットの整数でしょうか。そうですね、使えるかもしれませんが、問題があります。コンポーネントがどのマシンにインストールされるかを知ることはできないので、識別子は、すべてのマシンを通じて一意でなければなりません。すべてのクライアントがコンポーネントを使用できるようにするために、ユーザーのオブジェクトとインターフェイスは、すべてのマシンで同じ識別子を使用する必要があります。さらに、他のオブジェクトまたはインターフェイスがどこからきたものであっても、その識別子を使用できないようにしなくてはなりません。つまり、識別子は全世界を通じて固有でなければならないのです。

幸運なことに、このような識別子を作成するためのアルゴリズムとデータ形式は存在します。GUIDGEN.EXE と呼ばれるプログラムが、マシンの固有のネットワーク カード ID、現在時刻、そしてその他のデータを使用することにより、GUID (Globally Unique Identifiers) と呼ばれる識別子を作成します。GUID は 16 バイト (128 ビット) の構造に記憶され、2128 個の GUID が可能です。GUID が不足しないかなどと心配することはありません。名医は宇宙全体の原子の正確な数を調べることはできませんでしたが、Web の検索を行った結果、いずれにしても 2128 よりは大幅に少ないことが確認できました。COM 開発者らの内輪の冗談にもかかわらず、GUID が不足することはないため、GUID を節約する必要はありません。

C++ には、GUID、CLSID (クラス識別子 GUID)、IID (インターフェイス識別子 GUID) のためのデータ型が COM ヘッダー ファイルで定義されています。これらの 16 バイトの構造は値として受け渡すには大きいため、通常は GUID を渡すときにはパラメータのデータ型として REFCLSID と REFIID を使用します。作成するオブジェクト型ごとに CLSID を作成し、作成するカスタム インターフェイスごとに IID を作成する必要があります。

標準インターフェイス

COM は、多数の標準インターフェイスと、これに関連する IID を定義しています。たとえば、すべてのインターフェイスの親である IUnknown の IID は "00000000-0000-0000-c000-000000000046" (ハイフンは、GUID を表記する標準方法の一部です) です。この IID は COM によって定義されるものであり、これを直接参照する必要が生じることはありません。この代わりに、ヘッダーの中に定義されているシンボル IID_IUnknown を使用します。

IUnknown インターフェイスには次のような 3 つの関数があります。

HRESULT QueryInterface(REFIID riid, void **ppvObject);
ULONG AddRef();
ULONG Release();

これらの関数の機能については、あとで詳しく説明します。

COM プログラミングの大部分は、標準インターフェイスを使用して行います。そしてその大部分の作業は、他の COM クライアントやオブジェクトがそのオブジェクトを使用できるように、標準インターフェイスの実装を作成するというものです。

関数の戻り値の型と呼び出し規則を記述するために COM で使用されるマクロがいくつかあります。COM メソッドはほとんどすべて HRESULT を戻すため、マクロ STDMETHODIMP ではこれが前提となっています。マクロ STDMETHODIMP_() はメソッドの戻り値の型をパラメータとして受け取ります (STDMETHOD マクロは、純粋な仮想関数を持つインターフェイスを定義するときにだけ使用します。この場合、IDL コンパイラがコードを生成します。これについてはあとで詳しく説明します)。

これらのマクロを使用して上記の宣言を行うと、次のようになります。

STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG) Release();

今後はこれらのマクロを使用することにします。これにより、異なる COM プラットフォーム (Macintosh や Solaris など) へのコードの移植が容易になります。

カスタム インターフェイス

カスタム インターフェイスはユーザーが作成するインターフェイスです。これらのインターフェイスについてはユーザー自身で IID を作成し、ユーザー独自の関数のリストを定義します。IFoo インターフェイスはカスタム インターフェイスです。私は自分のマシンで GUID ジェネレータを実行し、IID_IFoo (値は "13C0205C-A753-11d1-A52D-0000F8751BA7") という名前の IID を定義しました。

クラス宣言は元々は次のようなものであったことを思い出してください。

class IFoo {
 virtual void Func1(void) = 0;
 virtual void Func2(int nCount) = 0;
};

これを多少変更して、COM 互換になるようにします。

interface IFoo : IUnknown {
virtual HRESULT STDMETHODCALLTYPE Func1(void) = 0;
virtual HRESULT STDMETHODCALLTYPE Func2(int nCount) = 0;
};

前述のマクロを使用すると、次のようになります。

interface IFoo : IUnknown {
STDMETHOD Func1(void) PURE;
STDMETHOD Func2(int nCount) PURE;
};

"interface" という語は C++ の予約語ではなく、該当する COM ヘッダーの中で "struct" として #defined されます (C++ では、構造体が標準でプライベートではなくパブリックの継承とアクセスを使用することを除けば、クラスと構造体は同じであることを思い出してください)。STDMETHOD は STDMETHODCALLTYPE を使用しますが、これは __stdcall として定義されているため、これらの関数については、コンパイラが標準の関数呼び出しシーケンスを生成します。これらのマクロを使用する理由は、これらの定義は、コードを別のプラットフォームに移植するときに変更されるからです。

COM 関数はすべて (ほとんど例外なく)、HRESULT エラー コードを戻します。この HRESULT は 32 ビットの数値であり、符号ビットが成功か失敗かを示します。残りの 31 ビットの中のフィールドは 「ファシリティ」とエラー コード (ファシリティにより異なる) を示し、予約ビットもいくつかあります。通常は S_OK という成功コードを戻すことにするでしょうが、メソッドの中で問題に遭遇した場合には、標準のエラー コードまたは独自に作成したエラー コードを戻すこともできます。

最後に、IFoo は標準の COM インターフェイス IUnknown から派生させていることに注目してください。これは、IFoo を実装するクラスはどれも、AddRefRelease、および QueryInterface という 3 つの関数も実装する必要があることを意味します。また、IFoo の vtable には、Func1 および Func2 へのポインタの前に、これら 3 つの関数へのポインタが入ります。これで、vtable の中の関数は合計 5 つになり、実装する必要のある関数も 5 つとなります。COM インターフェイスはすべて IUnknown から派生します。したがって、どの COM インターフェイスにも、他の関数に加えて、これら 3 つの関数が含まれています。

MIDL には何がはいっているのか ?

上記の宣言をユーザー自身が実際に作成することはありません。これは MIDL コンパイラによって生成されます。どうしてなのでしょう。結論から言うと、C++ ではインターフェイスの中で表現する必要のあるすべてのことを表現できないからです。COM オブジェクトはインプロセスで使用される DLL である場合も考えられることを思い出してください。これは、同じアドレス空間内にあるということです。このため、インプロセスのサーバーにデータへのポインタを渡した場合、サーバーはポインタの先を直接参照できます。

しかし、COM オブジェクトは別の EXE アドレス空間の中のローカル (プロセス外) のサーバーであることもあれば、リモートにアクセスされる場合さえもあります。このようなオブジェクトの中の COM メソッドにポインタを渡そうとすると、問題が発生します。ポインタは別のアドレス空間では意味を持ちません。意味があるのは、ポインタが指し示しているデータです。このデータを、他方のアドレス空間にコピーする必要があります。そしておそらくはコピーし戻す必要もあるでしょう。このように、正しいデータをコピーする処理を 「マーシャリング」と言います。ありがたいことに、COM はほとんどの場合は、ユーザーに代わってマーシャリングの処理をしてくれます。しかし、これを可能にするためには、ポインタが指し示しているデータの型だけではなく、ポインタがどのように使用されるかを COM に伝える必要があります。たとえば、ポインタは配列を指しているのかどうか、文字列を指しているのか、パラメータは入力パラメータだけなのか、出力パラメータなのか、それとも両方なのかなどを伝えなければなりません。C++ ではこれを表現する方法がないことはおわかりでしょう。

このため、インターフェイスを定義するためには、IDL (インターフェイス定義言語) と呼ばれる別の言語が必要になります。IDL は C++ に似ていますが、C++ のように見えるコードに、大かっこで囲まれた 「属性」が追加されています。MIDL.EXE はユーザーが作成した IDL ファイル (または Visual Studio) をコンパイルし、さまざまな出力を作成します。今のところは、出力についてはインターフェイスのヘッダー ファイルについてのみ考えることにし、これをコードに入れておきます。

例の中では、値渡しをしているため、あまり大きな違いはなく、IDL コードは似たようなものになります。最大の相違点は "virtual" という語がなくなっていることです。しかし、他の 2 つのメソッドにメソッド Func3(int *) を追加する新しいインターフェイス IFoo2 を作成すると、IDL は次のようなものになります。

[ uuid(E312522F-A7B7-11D1-A52E-0000F8751BA7) ]
interface IFoo2 : IUnknown
{
 HRESULT Func1();
 HRESULT Func2(int in_only);
 HRESULT Func3([in, out] int *inout);
};

ここでいくつか注意することがあります。まず、IDL の中には、大かっこで囲まれたさまざまな属性があります。属性は必ず、その直後に定義されているものに適用されます。つまり、上記の UUID 属性はインターフェイスに適用され、インターフェイスの IID になります (UUID:Universally Globarl IDentifier は GUID と同義です)。[in, out] 属性はポインタに適用され、Func3 を呼び出すときに、in および out の両方の属性を持つ単独の int をマーシャルする必要があることを COM に指示します (マーシャリングが必要な場合)。Int ポインタが配列を参照するとしたら、別の属性 (size_is と、附随するパラメータ) が追加されます。オブジェクトを定義するための IDL コードもあります。たとえば、例として紹介しているオブジェクトを定義する場合のコードは次のようになります。

[ uuid(E312522E-A7B7-11D1-A52E-0000F8751BA7) ]
coclass Foo
{
 [default] interface IFoo;
};

これが、CLSID をクラスに関連付ける方法です。また、クラスに実装される一組のインターフェイスを定義する方法です。これは、属性をいくつか追加しただけの C++ によく似ていますが、インターフェイス定義の場合とは異なり、C++ コードに完全に対応しているわけではありません。

オブジェクトの作成

CLSID をオブジェクト型 (これについてはあとで説明します) に関連付けたら、オブジェクトを作成することができます。これは非常に簡単で、関数を 1 つ呼び出すだけで済みます。

IFoo *pFoo = NULL;
HRESULT hr = CoCreateInstance(CLSID_Foo, NULL, CLSCTX_ALL,
    IID_IFoo, (void **)&pFoo);

CoCreateInstance が成功すると、CLSID GUID、CLSID_Foo で識別されるオブジェクトのインスタンスが作成されます。「オブジェクトへのポインタ」などというものはありません。その代わりに、インターフェイス ポインタを使用してオブジェクトを参照します。したがって、必要なインターフェイス (IID_IFoo) を指定し、CoCreateInstance がインターフェイス ポインタを記憶するべき場所を指すポインタを渡す必要があります。

まだ説明していない 2 つのパラメータは、現在のところはあまり重要ではありません。

呼び出しを行い、呼び出しが成功したかどうかを確認したら、オブジェクトを使用できるようになります。

if (SUCCEEDED(hr)) {
 pFoo->Func1(); // Call methods.
 pFoo->Func2(5);
 pFoo->Release(); // MUST release interface when done.
}
else // Creation failed...

CoCreateInstance は、成功か失敗かを示すために HRESULT を戻します。負数ではない値は成功を意味するため、必ず SUCCEEDED マクロを使用して結果を調べてください。実際、もっとも一般的な成功コード、S_OK の値はゼロであるため、"if (hr) // Success" のような検査はうまくいきません。オブジェクトが正常に作成されたら、上記のように、インターフェイス ポインタを使用してインターフェイスのメソッドを呼び出すことができます。

インターフェイスを使い終わったら、Release を呼び出してインターフェイス ポインタを解放しておくことは特に重要です。すべてのインターフェイスは IUnknown から派生するため、すべてのインターフェイスが Release をサポートしています。COM オブジェクトは指示された場合には自分自身を解放する責任がありますが、終了したかどうかについてはユーザーからの指示を受ける必要があります。Release を呼び出すことを忘れると、オブジェクトは漏れとして残ります (そして、少なくともアプリケーションが終了するか、システムが再起動されるまで、メモリの中にロックされます)。オブジェクトの寿命が混乱した状態になるのは COM プログラミングでよくある問題であり、しばしば検出するのは困難です。ですから、今から、気を付けるように心掛けてください。インターフェイスを解放するのは、インターフェイスを実際に作成できた場合だけである点に注意してください。

これは新しく作成したオブジェクトの図です。規則として、IUnknown にはラベルはなく、必ずオブジェクトの右上隅に描かれます。その他のインターフェイスはすべて左側に描かれます。

図 5 : ラベルなしの IUnknown のある、最初の単純な COM オブジェクト

これで IUnknown が実装されたので、本当の COM オブジェクトができました (コネクタを書くのと同じぐらい簡単ならよいのですが)。

オブジェクトに IFoo2 インターフェイスを追加すると、このように、合計で 3 つのインターフェイスがあることになります。

図 6 : IFoo と Foo2 の両方をサポートする、理論上のバージョン 2.0

GUID とレジストリ

それでは、COM はオブジェクトのインスタンスを作成するためにオブジェクトのコードをどうやって見つけたのでしょう。簡単です。レジストリを調べたのです。COM コンポーネントをインストールする場合には、レジストリの中にエントリを作成する必要があります。私たちの Foo クラスの場合のエントリは次のようになります。

HKEY_CLASSES_ROOT
 CLSID
 {E312522E-A7B7-11D1-A52E-0000F8751BA7}="Foo Class"
  InprocServer32="D:\\ATL Examples\Foo\\Debug\\Foo.dll"

ほとんどのオブジェクトにはこの他にもエントリがありますが、今のところは無視しておきましょう。

HKEY_CLASSES_ROOT\CLSID には、私たちのクラスの CLSID のエントリが入っています。CoCreateInstance はこれを使ってコンポーネントの DLL 名を調べるのです。CoCreateInstance に CLSID を渡すと、DLL 名を見つけ、DLL をロードし、コンポーネントを作成します (これについてはあとで詳しく説明します)。

レジストリのエントリは、サーバーがプロセス外の場合、またはリモートの場合には多少異なりますが、重要なのは、ここに情報が入っているため、COM がサーバーを起動し、オブジェクトを作成できるということです。

オブジェクトの名前 (ProgID) は知っているが、CLSID がわからない場合には、レジストリで CLSID を調べることができます。私たちのオブジェクトの場合は、次の場所にエントリがあります。

HKEY_CLASSES_ROOT
 Foo.Foo="Foo Class"
 CURVER="Foo.Foo.1"
 CLSID="{E312522E-A7B7-11D1-A52E-0000F8751BA7}"
 Foo.Foo.1="Foo Class"
 CLSID="{E312522E-A7B7-11D1-A52E-0000F8751BA7}"

"Foo.Foo" はバージョンに依存しない ProgID であり、Foo.Foo.1 は ProgID です。Visual Basic で Foo オブジェクトを作成した場合には、これらの ProgID の 1 つを使用して CLSID が検索されます (ATL ウィザードは現行バージョンではレジストリ エントリを正確に作成しないので注意してください。上記の 2 つの CLSID キーのうち最初のものが省略されます。バージョンに依存しない ProgID の CLSID をコピーしておくことを忘れないようにしてください)。

モジュール、コンポーネント クラス、およびインターフェイス

1 つのモジュール (DLL または EXE) で、複数の COM コンポーネント クラスを実装することは可能であり、実際によく行われることです。その場合、同じモジュールを参照する複数の CLSID エントリが存在することになります。

さてこれで、モジュール、クラス、およびインターフェイスの間の関係を定義することができます。1 つのモジュール (構築およびインストールできる基本単位) で、1 つまたは複数のコンポーネントを実装できます。各コンポーネントはレジストリの中に独自の CLSID と、モジュールのファイル名を示すエントリを持ちます。また、各コンポーネントには少なくとも 2 つのインターフェイス、すなわち IUnknown と、コンポーネントの機能を公開するインターフェイスが実装されます。図 7 はこれを示しています。

図 7 : モジュール Oo.DLL は、3 つのオブジェクト Foo、Goo、および Hoo の実装を含む。各オブジェクトには、IUnknown と、1 つまたは複数の追加のインターフェイスが実装されている

QueryInterface を使用して別のインターフェイスを取得する

2 つのカスタム インターフェイス、IFooIFoo2 が実装された、新しい、改良された Foo2 オブジェクトがあるとします。CoCreateInstance を使用してこのようなオブジェクトを作成する方法と、その 3 つのインターフェイス (IUnknown を忘れてはいけません) の 1 つへのポインタを取得する方法はすでにわかっています。

このインターフェイス ポインタを取得したあとで、オブジェクトのその他のインターフェイス ポインタをどうやって取得することができるでしょうか。ここで再び CoCreateInstance を呼び出すことはできません。これでは新しいオブジェクトが作成されてしまいます。新しいオブジェクトを作成するのではなく、既存のオブジェクトの別のインターフェイスが必要なだけなのです。

IUnknown::QueryInterface がこの問題を解決します。すべてのインターフェイスは IUnknown から継承されるため、どのインターフェイスにも QueryInterface が実装されています。このため、単に最初のインターフェイス ポインタを使用して QueryInterface を呼び出すだけで、2 番目のインターフェイス ポインタを取得できます。

IFoo *pFoo = NULL;
HRESULT hr = CoCreateInstance(CLSID_Foo2, NULL, CLSCTX_ALL,
    IID_IFoo, (void **)&pFoo);
if (SUCCEEDED(hr)) {
 pFoo->Func1(); // call IFoo::Func1
 IFoo2 *pFoo2 = NULL;
 hr = pFoo->QueryInterface(IID_IFoo2, (void **)&pFoo2);
 if (SUCCEEDED(hr)) {
  int inoutval = 5;
  pFoo2->Func3(&inoutval); // IFoo2::Func3
  pFoo2->Release();
 }
 pFoo->Release();
}

QueryInterface には、要求するインターフェイスの IID と、QueryInterface が新しいインターフェイス ポインタを記憶するべき場所へのポインタを渡します。QueryInterface が正常に終了したら、そのインターフェイス ポインタを使用してインターフェイスの関数を呼び出せます。

インターフェイスの作業が終了したら、インターフェイス ポインタを 「両方とも」解放することを忘れないでください。いずれかのインターフェイスの解放を忘れると、オブジェクトが漏れとして残ります。オブジェクトへの参照は必ずインターフェイス ポインタによって行われるため、オブジェクト全体を解放するためには、すべてのインターフェイス ポインタを解放しておく必要があります。

IUnknown のその他の関数

IUnknown にはこの他に、AddRef および Release という 2 つの関数があります。Release については、インターフェイス ポインタの使用が終了したことをオブジェクトに通知するために使用することをすでに説明しました。では AddRef はいつ使用するのでしょう。

参照カウンタ、およびオブジェクトを解放できるタイミング

ほとんどの COM オブジェクトは 「参照カウンタ」を使用しています。これらは、オブジェクトに対するインターフェイス ポインタがいくつ使用中であるかを追跡するために必要となります。 オブジェクトのすべてのインターフェイスで参照カウンタがゼロになったら、オブジェクトを解放することができます。オブジェクトの解放は明示的に行うわけではありません。オブジェクトのインターフェイス ポインタをすべて解放しておけば、オブジェクトは適切なタイミングで自分自身を解放します。

AddRef は参照カウンタをインクリメントし、Release はこれをデクリメントします。では、AddRef を呼び出さなかったのに、Release を呼び出す必要があるのはなぜでしょう。

QueryInterface はオブジェクトに新しいポインタを渡すとき、AddRef を呼び出す責任を負います。取得したポインタについて AddRef を呼び出す必要がなかったのはこのためです。QueryInterface がユーザーに代わってこれを行ってくれていたのです (CoCreateInstance QueryInterface を呼び出し、結果として AddRef が呼び出されるので、オブジェクトへの最初のインターフェイス ポインタを取得するときも同様です)。

AddRef を呼び出したのと同じインターフェイス ポインタを対象に Release を呼び出す必要があるので注意してください。オブジェクトは必要であれば、インターフェイスごとに参照回数を追跡することができます。上記のコードでは、暗黙の AddRef 呼び出しを適切な Release 呼び出しと正しく組み合わせることにより (インターフェイス ポインタごとに 1 つの Release 呼び出し)、注意深くこれを行っています。

インターフェイス ポインタのコピーを作成している場合には、インターフェイスの参照カウンタを正しい状態にするために、AddRef を呼び出す必要があります。これを行う必要がある場合とない場合の説明は少し込み入っていますが、COM に関するさまざまな文献で詳しく解説されています。詳しくはそれらの文献を参照してください。

さまざまな高度なポインタ クラスを使用すれば、IUnknown の処理がかなり簡単に (実際は自動的に) なります。ATL および Visual C++ Version 5.0 の中にはこのようなクラスがいくつかあります。Visual Basic または Java など、その他の言語を使用する場合には、その言語での COM の実装が、ユーザーの代わりに IUnknown メソッドを正しく処理します。

今までの復習、これからの予定

さて、これまではオブジェクトの作成の方法と、破棄の方法 (実際は破棄はしていません。オブジェクトに対するインターフェイス ポインタをすべて解放するだけです) について説明してきました。また、インターフェイス メソッドの呼び出し方法と、インターフェイスの切り替え方法についても説明しました。その過程で、オブジェクトやインターフェイスを識別するために使用されるさまざまな GUID の概念や、COM がオブジェクトを作成するために調べるレジストリ エントリについても紹介しました。

第 4 部では、インプロセス オブジェクトの作成方法と、作成をより効率的に行う方法について、さらに詳しく説明します。時間が許せば、オブジェクトを作成するのに必要なコードと IUnknown を含め、オブジェクトを実装する方法の基本的事項についても説明します。

第 4 部 : クラス オブジェクトとクラス ファクトリ

第 3 部では、オブジェクトの作成の方法と、破棄の方法 (実際は破棄はしていません。オブジェクトに対するインターフェイス ポインタをすべて解放するだけです) について説明してきました。また、インターフェイス メソッドの呼び出し方法と、インターフェイスの切り替え方法についても説明しました。その過程で、オブジェクトやインターフェイスを識別するために使用されるさまざまな GUID の概念や、COM がオブジェクトを作成するために調べるレジストリ エントリについても紹介しました。

ここでは、インプロセス オブジェクトの作成方法と、作成を効率的に行う方法について詳しく説明します。また、クラス オブジェクト (クラス ファクトリとも呼ばれます) について説明し、これを実装する方法も紹介します。ユーザー自身のオブジェクトを実装する時間は今週はありませんが、これは来週の最優先事項とします。

CoCreateInstance を呼び出すと何が起こるか

CoCreateInstance を呼び出すと、COM がオブジェクトを実装する DLL (または EXE) を見つけるために、レジストリで CLSID の検索をどのように行うかについてはすでに説明しました。しかし、具体的に何が行われるかについては詳しく説明していませんでした。CoCreateInstance は次の機能をカプセル化しています。

IClassFactory *pCF;
CoGetClassObject(rclsid, dwClsContext, NULL,
  IID_IClassFactory, (void **)&pCF);
hresult = pCF->CreateInstance(pUnkOuter, riid, ppvObj)
pCF->Release();

これでわかるように、3 つのステップがあります。最初のステップは、その IID_IclassFactory インターフェイスを使用して、クラス オブジェクトを取得することです。次に、このクラス オブジェクトで IClassFactory::CreateInstance を呼び出します (この呼び出しのパラメータは、CoCreateInstance の呼び出しから渡されます)。パラメータ、pUnkOuter は、集約と呼ばれる再利用メソッドで使用されますが、これについてはあとで説明します。今のところは NULL であるものと考えておいてください。これで、*ppvObj の中には、私たちのオブジェクトのインスタンスへのポインタが入りました。最後に、クラス オブジェクトを解放します。

では、このクラス オブジェクトとは何でしょう。どうしてこのようなものが必要なのでしょう。

クラス オブジェクト

クラス オブジェクトは特殊な COM オブジェクトであり、その主な目的は IClassFactory インターフェイスを実装することです (このオブジェクトは 「クラス ファクトリ」、または 「クラス ファクトリ オブジェクト」と呼ばれることさえありますが、クラス オブジェクトというのが正確な呼び方です)。

Java に習熟している場合は、COM クラス オブジェクトを、"Class" クラスの Java オブジェクトとほぼ同じものと考えることができます。また、Java の Class.newInstance は IClassFactory::CreateInstance と同義である (COM の CoGetClassObjectClass.forName 静的メソッドと同義であるのと同じように) ことに気が付くはずです。

このオブジェクトは、ほとんどの COM オブジェクトと違って、CoCreateInstance または IClassFactory::CreateInstance を呼び出すことによって作成されるのではない点が特殊です。このオブジェクトは常に、CoGetClassObject を呼び出すことによって作成されます。この記事の終わりに、その他の特殊な COM オブジェクトの例を紹介します (CoGetClassObject は常にクラス オブジェクトを作成するわけではありません。COM に、使用可能な正しいクラスのクラス オブジェクトがあれば、単にこれに対するインターフェイス ポインタを戻すことができます)。

コードでは CoGetClassObject を呼び出したあと、どのような種類のオブジェクトが作成されるかを考慮する必要がありません。たとえば、インプロセスでもローカル サーバーでも関係ありません。クラス オブジェクトがこれらの相違をすべて処理します。しかし CoGetClassObject は、要求された CLSID のクラス オブジェクトの作成方法、または検索方法を確認するために、レジストリ (および登録されている既存のクラス オブジェクトのリスト) を調べるという作業を行う必要があります。

クラス オブジェクトは、ポリモーフィズムの力を示すたいへん優れた例です。オブジェクトを取得するためには COM API を呼び出します。しかしオブジェクトをいったん取得したら、必要な標準インターフェイス (IClassFactory) がサポートされていることを確認し、そのインターフェイスのメソッド、この場合には IClassFactory::CreateInstance を呼び出すことができます。クラス オブジェクトの CreateInstance の仕組みについては私たちは知りません。私たちが知っているのは、成功した場合には、オブジェクトを参照するインターフェイス ポインタが戻されるということだけです。これ以外のことは知る必要もありませんし、知りたいとも思いません (これがカプセル化です)。そして、同じ関数呼び出しを行っても、それぞれのクラス オブジェクトに対応する適切な処理が行われます (これがポリモーフィズムです)。実際の動作は、クラス オブジェクトのアイデンティティによって決定されるのです。

各クラス オブジェクトのインスタンスは、特定の CLSID に関連付けられます。IClassFactory::CreateInstance はパラメータとして CLSID を持っていない ことに注意してください。クラス オブジェクトはその代わりに、どの CLSID を作成するかを知っています。これは、作成できるようにしたい CLSID ごとに、少なくとも 1 つのクラス オブジェクトが必要になることを意味します。

クラス オブジェクトには IClassFactory に加えて、任意のインターフェイスを実装できます。たとえば、特定のクラス オブジェクトから作成されるオブジェクト インスタンスに対して既定値を設定できるようにするためのインターフェイスを定義することができます。しかし、特定の CLSID に対してクラス オブジェクトが 1 つだけであるという保証はありませんので、CoGetClassObject を何回か呼び出した場合には、別のクラス オブジェクトに対するインターフェイス ポインタを取得する可能性があります (クラス オブジェクトの作成はユーザーが管理するため、ユーザーの実装の中でこれを定義しておくことができます)。

クラス オブジェクトを使用する理由

すでに説明したように、COM でクラス オブジェクトの実装が必要になるもっとも大きな理由は、クライアントが作成の詳細について正確に知らなくても、COM があらゆるタイプのオブジェクトを作成できるようにする、ポリモーフィズムを実現する標準の方法を提供するためです。クラス オブジェクトはクライアントが知る必要がないように、この知識をカプセル化します。これは、クラス オブジェクトと実際のオブジェクトとが緊密な関係 (そしてしばしば他方についての多量の知識) を持つことを意味します。

しかし、どうしてもっと単純な方法を使用しないのでしょう。たとえば、COM DLL の中で、CLSID を受け入れて、新しいインスタンスを作成する、たとえば DLLCreateInstance という名前の関数を使用したらどうでしょう。このような関数を使用すれば、COM オブジェクトと IClassFactory を使用するよりも簡単なはずです。

しかしこれは EXE には通用しません。EXE で単純に関数をエクスポートするわけにはいきません。また、リモート オブジェクトに対してもうまく働かないのは確かです。そこで、クラス オブジェクトを COM オブジェクトにすれば、COM がプロセス内、およびプロセス外のすべての問題を私たちに代わって処理してくれます。これはいい取り決めです。

クラス オブジェクトは対象オブジェクトのインスタンスを作成する 「正しい方法」を知っている COM オブジェクトであるため、クラス オブジェクトを作成してしまえば、COM はインスタンスの作成については、関知しないようになります。したがって、作成される特定の型のオブジェクトの最初のものについては、COM はたくさんの作業を行う必要があります。最初に、登録されているクラス オブジェクトのリストの中 (またはクラス オブジェクトが存在しない場合にはレジストリの中) で、CLSID を探す必要があります。クラス オブジェクトを作成する必要がある場合には、COM はこれを作成し、おそらくは DLL のロードや、EXE の起動も行います。最後に、COM は正しいクラス オブジェクトで IClassFactory::CreateInstance を呼び出して、ユーザーのインスタンスを作成します。やれやれ。

しかし、クラス オブジェクトをこのまま生かしておけば、これ以降のインスタンスについてはほとんどの作業を割愛できます。IClassFactory::CreateInstance を呼び出すだけで追加のオブジェクトを作成できます。これは、演算子 new を直接に呼び出すのと同じくらい高速に行われますし、COM にオブジェクトを作成させるよりもはるかに迅速に行われます。

重要 クラス オブジェクトをこのまま生かしておく場合には、サーバーをメモリの中に入れておくように COM に指示するために、IClassFactory::LockServer を呼び出す必要があります。クラス オブジェクトを参照しても、サーバーがメモリの中に自動的に維持されるわけではありません。この動作は、通常の COM 動作の例外です。サーバーをロックしなかった場合には、サーバーがアンロードされたあとでクラス オブジェクトにアクセスしようとすると、保護違反が発生する可能性があります。クラス オブジェクトの使用が終了したら、サーバーをアンロックすることを忘れないでください。

最後に、クラス オブジェクトは、オブジェクトを作成する追加の方法をサポートすることができます (ライセンス コントロールを作成するために、IClassFactory の代わりに使用される IClassFactory2 インターフェイスなど)。ライセンス コントロールとは、コントロールを作成するためには、ユーザーが正しいライセンス ID を所持している必要のあるコントロールです。

オブジェクトを作成するもう 1 つの方法、そしてこれを使用する状況

オブジェクトのインスタンスを 1 つだけ作成する場合に、オブジェクトを作成するために IClassFactory を使用できる場合には、CoCreateInstance (またはリモート オブジェクトを作成するための CoCreateInstanceEx) を使用することもできます。しかし、オブジェクトのインスタンスを複数作成する場合、またはオブジェクトを作成するために IClassFactory 以外のインターフェイスを使用する必要がある場合には、クラス オブジェクトを取得する (そしておそらくは維持する) 必要があります。

クラス オブジェクトは簡単に取得できます。CoCreateInstance が行うのと同じことをするだけです。つまり、CoGetClassObject を呼び出します。クラス オブジェクトに対するインターフェイス ポインタを取得したら、IClassFactory::LockServer(TRUE) を呼び出して、サーバーをメモリ内にロックします。これで、クラス オブジェクトに対するインターフェイス ポインタを確保したまま、新しいオブジェクトが必要になるたびに IClassFactory::CreateInstance を呼び出すことができます。最後に、オブジェクトの作成が終了したら、IClassFactory::LockServer(FALSE) を呼び出してサーバーを解放し、Release を呼び出してそのインターフェイス ポインタを解放します。インターフェイスに対する最後の作業は、インターフェイスの解放であることを忘れないでください。

クラス オブジェクトの実装

ではこのクラス オブジェクトは実際にはどのようなものでしょう。これは、単純な COM オブジェクトです。これは、少なくとも 1 つのインターフェイス、IUnknown が実装されていることを意味します。ほとんどすべてのクラス オブジェクトには、インスタンスを作成できるようにするために、IClassFactory も実装されています。

クラス オブジェクトは次のように宣言できます。

class CMyClassObject : public IClassFactory
{
protected:
 ULONG m_cRef;
public:
 CMyClassObject() : m_cRef(0) { };
  //IUnknown members
  STDMETHODIMP QueryInterface(REFIID, void **);
  STDMETHODIMP_(ULONG) AddRef(void);
  STDMETHODIMP_(ULONG) Release(void);

  //IClassFactory members
  STDMETHODIMP CreateInstance(IUnknown *, REFIID iid, void **ppv);
  STDMETHODIMP LockServer(BOOL);
};

もちろんこのクラスには、IclassFactory::CreateInstance および LockServer の中の各関数の宣言が含まれています。さらに、IUnknown 関数もあります (IClassFactory は、すべての COM インターフェイスと同じように、IUnknown から派生することを思い出してください)。このオブジェクトの参照カウンタを保持するメンバーがあること、そしてこのカウンタはコンストラクタの中でゼロに初期化しています。また、メソッドの実装を宣言するために、COM の公式マクロを使用していることにも注目してください。

このクラス オブジェクトの作成された方法

クラス オブジェクトを作成するには多くの方法がありますが、どの方法でも CoCreateInstance は使用されません。このオブジェクトのインスタンスは 1 つしか必要なく、このオブジェクトはコンストラクタを持たない小さいオブジェクトであるため、コードの中ではグローバル オブジェクトだけを宣言することにしました。

CMyClassObject g_cfMyClassObject;

これは、このオブジェクトが、DLL がロードされている間は常に存在することを意味します。

IClassFactory::LockServer を実装するためには、非クラス オブジェクトのすべてのインスタンスと、LockServer が呼び出された回数についての、グローバル カウンタも必要になります。

LONG g_cObjectsAndLocks = 0;

CoGetClassObject がクラス オブジェクトを取得する方法

インプロセス DLL サーバーの場合は簡単です。COM はユーザーの DLL の中で、DllGetClassObject という名前の関数を呼び出します。ユーザーの DLL の中に、COM で作成可能な COM オブジェクトが含まれている場合には、この関数をエクスポートする必要があります。DllGetClassObject には次のプロトタイプがあります。

STDAPI DllGetClassObject(const CLSID &rclsid, const IID &riid,
void ** ppv);

COM は CLSID と IID を渡します。DllGetClassObject は、要求されたインターフェイスに対するポインタを *ppv の中に返します。クラス オブジェクトを作成できない場合、または要求されたインターフェイスが存在しない場合には、HRESULT 戻り値の中にエラーが返されます (STDAPI は HRESULT を戻すように #defined されています)。

EXE サーバーの場合の手順は異なります。クラス オブジェクトごとに CoRegisterClassObject を呼び出すことにより、COM が作成できるクラスごとにクラス オブジェクトを登録します。これにより、クラス オブジェクトは登録済みクラス オブジェクトのリストに入れられます。EXE の処理が終了すると、登録済みリストからオブジェクトを除去するために、クラス オブジェクトごとに一度ずつ CoRevokeClassObject が呼び出されます。この詳細を知りたい場合は、COM のドキュメントを調べるか、COM のさまざまな文献を調べてください。ここでは、インプロセス (DLL) サーバーを中心に説明します。

CoGetClassObject を呼び出したときに COM が実際にクラス オブジェクトをどのように取得するかは、オブジェクトが DLL によって実現されているか、EXE によって実現されているかによって異なりますので注意してください。DLL の場合には、DLL がロードされ (まだロードされていない場合)、DllGetClassObject が呼び出されます。EXE の場合には、EXE がロードされ (まだロードされていない場合)、検索対象のクラス オブジェクトが EXE によって登録されるか、タイムアウトが発生するのを待つことになります。

DllGetClassObject は次のようなものになります。

STDAPI DllGetClassObject(REFCLSID clsid, REFIID iid, void **ppv) {
 if (clsid != CLSID_MyObject) // Right CLSID?
  return CLASS_E_CLASSNOTAVAILABLE;

 // Get the interface from the global object.
 HRESULT hr = g_cfMyClassObject.QueryInterface(iid, ppv);
 if (FAILED(hr))
  *ppv = NULL;
 return hr;
}

要求された CLSID が、サポートされているものかどうかを確認する必要があります。サポートされていない場合には、E_FAIL が戻されます。次に、要求されたインターフェイスに対して QueryInterface を実行します。これが失敗した場合には、出力ポインタは NULL に設定され、E_NOINTERFACE が戻されます。成功した場合には、S_OK と、インターフェイス ポインタが戻されます。

クラス オブジェクトのメソッドの実装

IUnknown::AddRef および IUnknown::Release

私たちのクラス オブジェクトはグローバルなものです。これは常に存在し、破棄することはできません (少なくとも DLL がアンロードされるまでは)。このオブジェクトは決して削除することがなく、クラス オブジェクトへの参照によってサーバーがロードされたままになることはないため、参照カウンタを実装する必要はほとんど ありません。しかし、参照カウンタはデバッグに便利なため、いずれにしてもこのオブジェクトにも参照カウンタを実装します。

AddRef Release はオブジェクトの参照カウンタを維持する役割を果たします。ゼロに初期化されているインスタンス変数 m_cRef があるのに注目してください。AddRef Release は参照カウンタのインクリメントとデクリメントを行い、参照カウンタの新しい値を戻すだけのものです。

オブジェクトが動的に作成されたものである場合には、参照カウンタがゼロになったときにオブジェクトを削除するのは Release の役目になります。私たちのオブジェクトはグローバルに割り当てられているため、それはできません。

STDMETHODIMP_(ULONG) CMyClassObject::AddRef() {
 return InterlockedIncrement(&m_cRef);
}

STDMETHODIMP_(ULONG) CMyClassObject::Release() {
 return InterlockedDecrement(&m_cRef);
}

ここでは、単に ++m_cRef および --m_cRef を使用するのではなく、マルチスレッド処理を考慮して、スレッドセーフなインクリメントとデクリメント用の関数を使用しています。

AddRefRelease を本当に単純なものにしたい場合には、ゼロ以外の値を戻すようにすることができます。また、クラス オブジェクトの参照カウンタのメンバー変数をなくすこともできます (ただし、オブジェクトとロックのカウンタとなるグローバル変数はなくしてはなりません)。

IUnknown::QueryInterface

QueryInterface の実装は、このオブジェクトの場合は 100 パーセント標準どおりとなっています。クラス オブジェクトなので、特別なことは何もしていません。要求されたインターフェイスが、サポートされている 2 つのインターフェイス (IUnknownIClassFactory) のいずれかであるかどうかを確認するだけです。そうである場合には、オブジェクトに対して、適切な型に変換したインターフェイス ポインタを戻します。そして、そのポインタを対象に AddRef を呼び出して、参照カウンタの処理をします。そうでない場合には、適切なエラー コード、E_NOINTERFACE を戻します。

STDMETHODIMP CMyClassObject::QueryInterface(REFIID iid, void ** ppv) {
 *ppv = NULL;
 if (iid == IID_IUnknown==iid || iid == IID_IClassFactory) {
  *ppv = static_castthis;
  (static_cast*ppv)->AddRef();
  return S_OK;
 else {
  *ppv = NULL; // COM Spec requires NULL if failure
  return E_NOINTERFACE;
 }
}

新しい static_cast 演算子に注目してください。ANSI C++ では、異なる演算子を使用することにより、型変換の 3 つの異なる使い方をそれぞれ区別することができます。static_cast 演算子は、異なる種類のクラスを指すポインタ同士を適切に型変換し、必要であればポインタの値を変更します (上記の場合は多重継承をしていないため、あてはまりません)。

IClassFactory::CreateInstance

これは、クラス オブジェクトの中核部分です。インスタンスを作成するための関数です。

STDMETHODIMP CMyClassObject::CreateInstance (IUnknown *pUnkOuter,
 REFIID iid, void ** ppv)
{
 *ppv=NULL;

// Just say no to aggregation.
 if (pUnkOuter != NULL)
  return CLASS_E_NOAGGREGATION;

 //Create the object.
 CMyObject *pObj = new CMyObject();
 if (pObj == NULL)
  return E_OUTOFMEMORY;

 //Obtain the first interface pointer (which does an AddRef).
 HRESULT hr = pObj->QueryInterface(iid, ppv);

 // Delete the object if the interface is not available.
 //Assume the initial reference count was zero.
 if (FAILED(hr))
  delete pObj;

 return hr;
}

最初に、集約はサポートしません。このため、ポインタが NULL 以外の場合には、集約をサポートするように要求されるため、オブジェクトを作成することはできません。次に、オブジェクトの割り当てを行い、それができない場合には、E_OUTOFMEMORY を戻します。

次に、新しく作成したオブジェクトを対象 QueryInterface を呼び出し、戻すべきインターフェイス ポインタを取得します。これが失敗した場合には、オブジェクトを削除し、エラー コードを戻します。これが成功した場合には、QueryInterface から成功コードが戻されます。成功の場合には QueryInterfaceAddRef を呼び出して、オブジェクトの正しい参照カウンタをユーザーに提供します。

オブジェクトおよびロック カウンタ、g_cObjectsAndLocks はインクリメントしていないことに注意してください。作成が成功した場合にはこれを行っておくこともできますが、インスタンス オブジェクトの Release か、そのデストラクタの中でこれをデクリメントしなければならなくなります。第 5 部では、これをオブジェクト自体のデストラクタの中に入れます。しかし、デストラクタの中でデクリメントを行うのであれば、インクリメントはここではなく、コンストラクタの中で行うべきです。

オブジェクトに対して QueryInterface を行う方法は、オブジェクト自体が最初の参照カウンタをどのように処理するかによって、いくつかの異なるパターンがあります。1 つ問題になるのは、場合によっては、オブジェクトが QueryInterface の実施中に何かを行ったため、AddRef 呼び出しと Release 呼び出しの対が実行されることがあることです。オブジェクトの初期の参照カウンタがゼロである場合には、Release への呼び出しによって、CreateInstance が戻る前であっても、オブジェクト自体が解放されることになります。これは困ります。

一般的な技法として、オブジェクトの初期参照カウンタをゼロ以外の値に設定することができます。オブジェクトのコンストラクタの中では簡単にこれを行うことができます (第 5 部参照)。しかし、これを行う場合には、参照カウンタが正しく設定されるように、CreateInstance を修正して、ReleasQueryInterface を呼び出したあとで Release を呼び出すようにしておく必要があります。

そうした場合、オブジェクトの削除は省略されます。QueryInterface が失敗した場合には、AddRef は呼び出されません。このため、オブジェクトの参照カウンタは 2 ではなく、1 になります。この時点で Release を呼び出すと、オブジェクトの参照カウンタはゼロになり、オブジェクトは自分自身を削除します。QueryInterface は成功すると、参照カウンタを 2 にインクリメントし、その後 Release が参照カウンタを 1 にデクリメントします。これが健全なオブジェクトのあるべき姿です。

初期の参照カウンタが 1 であると想定すると、次のような QueryInterfaceCreateInstance コードが考えられます。

// ...

//Obtain the first interface pointer (which does an AddRef).
 HRESULT hr = pObj->QueryInterface(iid, ppv);

 // Delete object if interface not available.
 // Assume the initial referece count was one, not zero.
 pObj->Release(); // Back to one if QI OK, deletes if not

 return hr;
}

第 5 部では、オブジェクトの中でこのコードを使用します。これは単純であり、いつでもうまくいきます。CreateInstance はオブジェクトの実装の詳細を知っている必要がありますが、私はこれは欠点だとは思いません。結局、CreateInstance はこのために、つまりクライアントが心配しなくてもいいように、このような詳細をカプセル化するために存在しているのですから。

IClassFactory::LockServer

LockServer はグローバル ロックとオブジェクト カウンタをインクリメント、およびデクリメントさせるだけのものです。これは、カウンタがゼロになったときに DLL の解放を試みることはありません (これが EXE サーバーであれば、対話しているユーザーがいない限り、カウンタがゼロになるとサーバーがシャットダウンされます)。

STDMETHODIMP CMyClassObject::LockServer(BOOL fLock) {
 if (fLock)
  InterlockedIncrement(&g_cObjectsAndLocks);
 else
  InterlockedDecrement(&g_cObjectsAndLocks);
 return NOERROR;
}

ここでも、コードをスレッド セーフにしています。カウンタがゼロになったら、オブジェクトを削除することができます。

DllCanUnloadNow

COM は、DLL をアンロードするかどうかを決定するために、DllCanUnloadNow を呼び出します。アンロードしてもかまわない場合には単純に S_OK を戻し、アンロードしてはいけない場合には S_FALSE を戻します。サーバー上にオブジェクト、またはロックがない場合には、アンロードしてもかまいません。

STDAPI DllCanUnloadNow() {
 if (g_cObjectsAndLocks == 0)
  return S_OK;
 else
  return S_FALSE;
}

今までの復習、これからの予定

以上で、インプロセス オブジェクトの作成方法の一部と、作成をより効率的に行う方法について説明してきました。また、クラス オブジェクト (クラス ファクトリとも呼ばれる) についても説明し、これを実装する方法も説明しました。しかし、オブジェクトを実際に実装する作業にはまだとりかかっていません。

次回は、IUnknown で必要となるコードや、ユーザー自身のカスタム インターフェイスを含め、インスタンス オブジェクトを実装する方法について説明します。また、COM を使用せずに作成を行う、効率の高い、特殊な COM オブジェクトについても説明するかもしれません。

注 : 実装には C++ を使用していますが、C を使用することもできます。名医には、使用する理由が何もありません (C と C++ の混在したプログラムは問題ありません)。しかし、なんらかの理由で本当に必要がある場合には、『Inside OLE 』 の第 2 章の "RectEnumerator in C: ENUMC.C," も含め、MSDN に例が掲載されています。

第 5 部:オブジェクトの実装

第 4 部では、インプロセス オブジェクトの作成方法と、作成を効率的に行う方法について説明しました。また、クラス オブジェクト(クラス ファクトリとも言います)の詳細と、その実装方法も説明しました。今度は、実際にオブジェクトを実装する方法を説明します。また第 6 部では、ここで書いたこれらの COM オブジェクトの作成方法と使用方法を説明します。

Dr. GUI の広がり続ける本棚

最近 Dr.GUI は、ATL について書かれた優れた本を 2 冊購入しました。名医は ATL を勉強している自分の患者に、この 2 冊の本を処方します。

1 冊目は Wrox Press から出版されている本で、Beginning ATL COM Programming というタイトルが付けられています (http://www.worldofatl.com/BegATLCOM/begAtlCom.htm の World of ATL Web サイトをご覧ください)。これは Grimes、Stockton、Reilly、Templeman の 4 者による共著です。この 4 人の名前を並べると、どこかの法律事務所の名前みたいですが、著者の写真の載っている表紙を一目見ただけで、その思い込みはなくなるでしょう。同書は、ATL プログラミングを深く掘り下げて紹介する非常に優れた本です。また、World of ATL Web サイト (http://www.worldofatl.com) には、ATL についての情報がたくさんあります。

2 冊目の本は、MIT Press から出版されている本で、タイトルは Active Template Library: A Developer's Guide です(http://www.idgbooks.com/cgi-bin/db/fill_out_template.pl?idgbook:1-5585-1580-1:book-idg::uidg2896 の IDG Books の WWW サイトをご覧ください)。著者は Tom Armstrong。この名前から法律事務所は連想しませんね。彼の写真が手に入らないので、彼が弁護士のように見えるかどうか、お伝えできないのが残念です。同書は、Wrox の本ほど詳しくないところもありますが、広い範囲の題材を取り上げているので、どちらも手に入れる価値はあります。

オブジェクトそのものの実装

第 4 部で、IClassFactory を実装するクラス オブジェクトをお見せしました。CreateInstance メソッドの中で、実際のインスタンスを生成するために new を呼び出したのを憶えていると思います。

CMyObject *pObj = new CMyObject();

ですが、このオブジェクトが何をするものなのか、どのように生成されるかについては、まだ説明していません。これを次に説明します。

CmyObject の設計

私たちのオブジェクトは 4 つのインターフェイスを実装します。すなわち、IFooIFoo2IGoo、そして、当然ですが、私たちの良き友 IUnknown です(IUnknown があらゆるオブジェクトに属するという考え方には、何か宗教上の意味があるのかもしれませんね)。

IFoo2 IFoo の拡張です(IFoo に関数が 1 つ追加されたもの)。これに対して IGoo は、全く独立のインターフェイスです。したがって CMyObject の図は、次のようになります。

図 8:4 つのインターフェイスをサポートする CMyObject

図を見てわかるように、COM(つまり、QueryInterface)は、IFoo IFoo2 がお互いに関係していることを全く考慮しません。その意味で言えば、インターフェイスが IUnknown に関係あるということも考慮しません。IUnkown を要求すると、IUnknown が返されます。そして、別のインターフェイスを要求すれば、そのインターフェイスが返されます。インターフェイスは、クライアントがオブジェクトを扱うための唯一方法だということを忘れないでください。

COM と ATL についての、このシリーズの第 2 部でも紹介しましたが、IFoo をマクロ化したものが次のようなものでした。

interface IFoo : IUnknown {
STDMETHOD Func1(void) PURE;
STDMETHOD Func2([in] int nCount) PURE;
};

ですが、IDL を使ってインターフェイスを定義する習慣をつけましょう。このインターフェイスの IDL は、次のようになります(注意:GUID は自分のものに変更してくださいね…)。

 [
  uuid(7BA998D0-C34F-11D1-A54D-0000F8751BA7)
 ]
 interface IFoo : IUnknown
 {
  HRESULT Func1();
  HRESULT Func2(int inonly);
 };

2 番目の、インターフェイス独立型インターフェイスである IGoo は、IDL で表すと次のようになります。

[
uuid(0E02B134-C350-11d1-A54D-0000F8751BA7),
]
interface IGoo : IUnknown
{
HRESULT Gunc();
};

IFoo2 は変わった形のインターフェイスです。これは IFoo を拡張するので、IFoo から IFoo2 を派生させます。

 [
  uuid(62F890DA-C361-11d1-A54D-0000F8751BA7)
 ]
 interface IFoo2 : IFoo
 {
  HRESULT Func3([out, retval] int *pout);
 };

注意 IFoo2 には 6 のメソッドで構成されます。3 つの IUnknown メソッドと 2 つの IFoo メソッド、それに Func3 です(同様に、IFoo IGoo はそれぞれ 5 つと 4 つのメソッドで構成されます。IUnknown はどこにでも出てくるのでお忘れなく!)。

さて、これらのメソッドは何をするのでしょう。インターフェイスは文書化しておくべきだということなので、そうしましょう。

オブジェクトが生成されたとき、そのオブジェクトは 5 という値を内部の値として持っています。Func1 はその内部値をインクリメントし、新しい値が 3 の倍数になった場合にはビープ音を鳴らします。Func2 はパラメータとして渡された値を内部値として設定します。

内部値を読み取る手段を提供するのを忘れているのに気付いたでしょうか。ドジな話です!しかし、既存のインターフェイスに別の機能を追加しなければならないというのはよくあることです。そのために、IFoo から 「派生」させた IFoo2 という新しいインターフェイスを定義します。IFoo2 は、現在の内部値を取得するための Func3 を追加します。“out”属性と“retval”属性を使用したので、“retval”を認識する言語(少なくとも Visual Basic と Visual J++)は、関数からの戻り値としてこの値を使用することになります。つまり、Visual Baisc では、“Foo2.Func3(Text2)”と書く代わりに、“Text2 = Foo2.Func3”と書いて、この関数を呼び出します。

また、オブジェクトには要求に応じてビープ音を鳴らす機能を追加したかったので、ビープ音を鳴らす Gunc メソッドを備えた IGoo インターフェイスも加えました。IGoo は内部値に全く依存しないように設計されている点に注目してください。内部値を扱うのは IFoo IFoo2 だけです。つまり、どんなオブジェクトでも IFoo インターフェイスなしで IGoo を実装すること、そしてその逆が可能だということです。要するに、これらのインターフェイスはお互いに独立だということです。

多重継承による多重インターフェイスの実装

私たちのオブジェクトは、IFooIFoo2IGoo、そして IUnknown の合計 4 つのインターフェイスを実装します。さて、QueryInterface の実装がこれらのインターフェイスの有効なインターフェイス ポインタを返す限り、実装のコードそのものの具体的な書き方は重要ではありません。たとえば、C で COM オブジェクトを書いたとすると、関数ポインタの配列(vtable と同じ記憶形式)を作成し、QueryInterface は vtable を指すポインタのアドレス(オフセット 0 に vtable ポインタを持つオブジェクトと、同じ記憶形式)を返せばいいわけです。しかしこの方法は複雑なので、Dr.GUI としては COM を書く患者には C を処方していません。

C++ でも様々な方法があります。たとえば、各インターフェイスを個別のオブジェクトで実装するという方法が考えられます。そうすれば、QueryInterface は適切なオブジェクトに対するポインタを返します。この方法の変形として、これらの個別のオブジェクトを、メイン オブジェクト内にあるクラスを使用して、メイン オブジェクトのメンバーとして作成するという方法もあります。MFC の内部ではこの方法が使われています。しかし、一番すっきりとしている方法は、オブジェクトが実装する各インターフェイスを単純に継承するという方法です。ATL およびここで紹介するコードは、この方法を採用しています。QueryInterface は、ポインタを適切な基本クラスの型に変換してから、そのポインタを返すだけでいいのです。

したがって、私たちが実装したクラスの宣言は以下のようになります。

class CMyObject :
 public IFoo2,
 public IGoo
{
private:
 int m_iInternalValue;
 ULONG m_refCnt;

public:
 CMyObject();
 Virtual ~CMyObject();

 // IUnknown
 STDMETHODIMP QueryInterface(REFIID, void **);
 STDMETHODIMP_(ULONG) AddRef(void);
 STDMETHODIMP_(ULONG) Release(void);
 // IFoo
 STDMETHODIMP Func1();
 STDMETHODIMP Func2(/*[in]*/ int inonly);
 // IFoo2
 STDMETHODIMP Func3(/*[out, retval]*/ int *pout);
 // IGoo
 STDMETHODIMP Gunc();
};

IUnknown IFoo から明示的に継承していないことに注意してください。IFoo2 IFoo から継承しているので、ここで IFoo を継承する必要がありません。同様に、IFoo2 IGoo はどちらも IUnknown から(直接的、あるいは間接的に)継承するため、ここで IUnknown から継承する必要がありません。

仮に IFoo IUnknown から直接継承すると、曖昧さが発生してしまい、それを示すコンパイラ エラーを受け取ることになります。したがって、もっとも末端の派生クラスからのみ継承することを原則とします。オブジェクトで実装する別のクラスが、あなたが実装する基本クラスから継承している場合は、基本クラスから継承をしてはいけません。ただし、基本クラスのメソッドはすべて、派生クラスで実装できます(実際、すべてのメソッドを実装する必要があります)。

IUnknown メソッドで多重継承が可能な理由

私たちのオブジェクトが継承をする様子を、図で見てみましょう。

図 9:オブジェクトの継承

注意 IUnknown は 2 つ経路を通じて継承されています。どうしてこれで正しく動作するのでしょう。参照カウントが 1 つ以上になるのでしょうか?これで何も混乱は起こらないのでしょうか?

IUnknown の実装を継承していたとしたら、たいへん困ったことになっていたでしょう。同一のオブジェクトに IUnknown の実装が 2 つ存在することになるからです(さらに多くのインターフェイスから継承すると、実装される IUnknown の数も多くなります)。しかし、ここでは実装は継承していません。IUnknown にはデータも関数もないので、継承するのはインターフェイスだけなのです。

クラスのインターフェイスだけを継承する場合、普通はそれらの関数の実装を 1 つだけ、もっとも末端の派生クラスに書くきます。したがって、QueryInterfaceAddRefRelease の実装をそれぞれ 1 つだけ、CMyObject に書きます。

IUnknown の vtable ポインタはすべて、同じ QueryInterfaceAddRef、および Release 関数を指します。これは、C++ の仮想関数の動作とまったく同じです。仮想関数を呼び出すときは、常に派生のもっとも末端にある実装を呼び出すことになります。このため、CMyObject の vtable は、おおよそ次のようになります。

図 10:CMyObject に対する vtable の実装例(コンパイラは任意です)

注意 vtable ポインタは、IGoo IUnknown セクション内のものも含めて、すべて CMyObject 関数を指します。これにより、派生のもっとも末端にある実装を呼び出すことが保証されます。

つまり、QueryInterfaceAddRef、および Release の実装がそれぞれ 1 つあれば、私たちが継承したすべてインターフェイスを満たせるということです。これは IUnknown にとって大変便利ですが、たまたま同じシグネチャを持つメソッドが、2 つの無関係のインターフェイスにある場合には、正しい方法ではありません。多重継承機能を利用して COM オブジェクトを実装した場合、あなたの書いたメソッドが、両方のインターフェイスに呼び出されることになります。これが、多重継承を使用して COM オブジェクトを実装した場合の大きな弱点の 1 つです。ありがたいことに、これはめったにあることではありません。またあったとしたら、ネストされたクラスを使って一方のインターフェイスを実装し、オブジェクトの残りの部分では多重継承を使用することで、問題は解決できます。

図 11 に、CMyObject オブジェクトを図解します。

図 11:CMyObject オブジェクト

this ポインタを IUnknown *IFoo *IFoo2 *、あるいは CMyObject * にキャストしても値は変わりません。値が変わるのは IGoo * にキャストした場合だけです。したがって、IUnknownIFooIFoo2 に対する QueryInterface を行う場合には、IFoo2 へキャストした this ポインタを戻します。このポインタは vtable の先頭を指す vtable ポインタを指します。IGoo に対して QueryInterface を呼び出すとき、this ポインタを IGoo * ポインタへキャストします。これによって、this ポインタの複製が、オブジェクト中の 2 番目の vtable ポインタを指すようになります。

QueryInterface の実装

さて、QureryInterface が何を行い、なぜうまく機能するのかを理解できたと思います。コードを見てみましょう。

STDMETHODIMP CMyObject::QueryInterface(REFIID iid, void **ppv)
{
 *ppv = NULL;
 if (iid == IID_IUnknown ||
  iid == IID_IFoo ||
  iid == IID_IFoo2)
 {
  *ppv = static_cast<IFoo2 *>(this);
 }
 else if (iid == IID_IGoo)
 {
  *ppv = static_cast<IGoo *>(this);
 }
 if (*ppv) {
  AddRef();
  return S_OK;
 }
 else return E_NOINTERFACE;
}

QueryInterface が成功したら AddRef を呼び出します。インターフェイス ポインタを返すときには、必ず AddRef を呼び出さなければなりません(そして、クライアントはポインタが必要なくなった時点で Release を呼び出さなければなりません)。

クライアントが IID_IGoo を要求した場合、AddRef の呼び出しには、クライアントに返すポインタとは別のポインタを渡している点に注意してください。インターフェイスごとの参照カウントをサポートするために、正しいインターフェイス ポインタを使って AddRef Release を必ず呼び出す必要があります。当該オブジェクトにおいて参照カウント機能がどのように実装されているか知っているので(結局、私たちが実装を行うわけですから)、QueryInterface の要件を緩和することができます。this->AddRef() を呼び出すだけで参照が正しくカウントされることを知っているからです。なお、一時オブジェクトによって実装された切り離し型のインターフェイスや、ネストされたクラスによって実装されたインターフェイスなど、より複雑な状況の場合は、このように簡単には済まないかもしれません。

AddRef と Release の実装

結局のところ、AddRef Release は簡単に実装できるのです。

STDMETHODIMP_(ULONG) CMyObject::AddRef(void)
{
 return ++m_refCnt; // NOT thread-safe
}

STDMETHODIMP_(ULONG) CMyObject::Release(void)
{
 --m_refCnt; // NOT thread-safe
 if (m_refCnt == 0) {
  delete this;
  return 0; // Can't return the member of a deleted object.
 }
 else return m_refCnt;
}

マルチスレッド処理をサポートするのであれば、InterlockedIncrement InterlockedDecrement を使用しなければなりません。しかし、インクリメント演算子とデクリメント演算子だけを使うほうが効率的なので、今のところはそれらを使うことにします(使用するスレッディング モデルを指定すれば、ATL は自動的にもっとも効率のよい方法を選択します)。

Release には、参照カウントのデクリメントのほか、参照カウンタがゼロになったときにオブジェクトを削除するという役目があります。

コンストラクタとデストラクタ

以下はコンストラクタとデストラクタのコードです。

CMyObject::CMyObject() : m_iInternalValue(5), m_refCnt(1)
{
 g_cObjectsAndLocks++; // NOT thread-safe
}

CMyObject::~CMyObject()
{
 g_cObjectsAndLocks--; // NOT thread-safe
}

予想に違わず、コンストラクタはメンバー変数の内部値を初期化します。この場合は m_iInternalValue を 5 に設定しています。

さらに、コンストラクタとデストラクタでは、2 つのちょっとしたテクニックが使われています。その 1 つは、コンストラクタが参照カウンタをゼロではなく 1 に初期化していることです。これに対応するために、CMyClassObject::CreateInstance の中で、最初の QueryInterface を実行した後、当該オブジェクトを対象に Release を呼び出しています。最初の QueryInterface が成功すると CreateInstance AddRef を呼び出します。AddRef によって参照カウンタがインクリメントされ、値は 2 となります。失敗した場合は AddRef が呼び出されないので、参照カウンタの値は 1 のままです。いずれの場合も、CreateInstance はカウンタをデクリメントする Release を呼び出します。QueryInterface が成功していれば、これで参照カウンタの値が 1 になります。失敗していれば参照カウンタの値がゼロになり、Release によってオブジェクトが削除されます。IClassFactory::CreateInstance の実装を思い出してください。

STDMETHODIMP CMyClassObject::CreateInstance(IUnknown *punkOuter,
REFIID iid, void **ppv)
{
 *ppv=NULL;

 if (punkOuter != NULL) // Just say no to aggregation.
  return CLASS_E_NOAGGREGATION;

 //Create the object.
 CMyObject *pObj = new CMyObject();
 if (pObj == NULL)
  return E_OUTOFMEMORY;

 //Obtain the first interface pointer (which does an AddRef).
 HRESULT hr = pObj->QueryInterface(iid, ppv);

 // Delete the object if the interface is not available.
 // Assume that theinitial reference count was one, not zero.
 pObj->Release(); // back to one if QI OK, deletes if not

 return hr;
}

2 つ目のちょっとしたテクニックは、コンストラクタとデストラクタがグローバルなオブジェクト カウントとロック カウントをインクリメントしたりデクリメントしたりすることです。このカウントは、DLL がロードされるときにゼロに初期化され、オブジェクト(クラス オブジェクトを除く)の作成と IClassFactory::LockServer(TRUE) の呼び出しのたびに、インクリメントされます。カウントは、オブジェクトの破棄と LockServer(FALSE) の呼び出しのたびにデクリメントされます。コンストラクタとデストラクタでは逆の動作をすると考えるのが普通なので、名医としては、コンストラクタの中でインクリメントを行い、デストラクタの中でデクリメントを行うほうが(精神)衛生上よいとは思います。もちろん、オブジェクトを Release で削除するなら、CreateInstance でインクリメントとデクリメントを行うことはできます。しかし、互いに関連のある関数を別々のオブジェクトで実行するのは、エレガントとは言えません。

インプロセス サーバーの場合、この参照カウンタは、グローバル関数 DllCanUnloadNow だけが使用します。

STDAPI DllCanUnloadNow() {
 if (g_cObjectsAndLocks == 0)
  return S_OK;
 else
  return S_FALSE;
}

あなたが直接的に DllCanUnloadNow を呼び出すことはありません。むしろ COM が、使用されていないサーバーをアンロードするときにこれを呼び出します。しかし、CoFreeUnusedLibraries を呼び出すことによって、使用されていないサーバーをアンロードするよう COM に要求することができます。たとえば、インプロセス サーバーが解放されたことが分かっている場合になどに、これを行います。

プロセス外サーバーでは状況が異なります。これらのサーバーは、プロセスを終了することによって自身を解放するよう要求されます。

カスタム インターフェイスの実装

さて、COM のオーバーヘッドは片付いたので、メソッドは簡単に実装できます。

STDMETHODIMP CMyObject::Func1()
{
 m_iInternalValue++;
 if (m_iInternalValue % 3 == 0) MessageBeep((UINT)-1);
return S_OK;
}

STDMETHODIMP CMyObject::Func2(/* [in] */ int inonly)
{
 m_iInternalValue = inonly;
 return S_OK;
}

// IFoo2
STDMETHODIMP CMyObject::Func3(/* [out, retval] */ int * pout)
{
 MessageBeep((UINT)-1);
 *pout = m_iInternalValue;
 return S_OK;
}

// IGoo
STDMETHODIMP CMyObject::Gunc()
{
 MessageBeep((UINT)-1);
 return S_OK;
}

これらの関数には、驚くようなところは何もありません。単に、私たちが言った通りのことを行うだけです。IDL が宣言されている方法を思い出してもらえるように、Func3 に [out, retval] のコメントを加えておきました。注意してほしいのは、どの COM メソッドにおいても、本当の戻り値は必ず HRESULT であるということです。

ビルドしてみましょう

結論から先に言うと、このアプリケーションをビルドするのは比較的簡単です。この簡単なオブジェクトでは、名医は単純に両方のクラスの宣言を共通のヘッダーに入れ、2 つのクラスのコードを同じ .CPP ファイルに入れました。これ以外に書く必要のあるのは、IDL ファイルとリンカ用の .DEF ファイルだけです。

すでに IDL ファイルの大部分は書き上げていますが、先頭にコードを数行追加する必要があります。

import "oaidl.idl";
import "ocidl.idl";

また、インターフェイスの定義の後にも、いくつかのコードが必要です。

[
 uuid(7BA998C3-C34F-11D1-A54D-0000F8751BA7)
]
library NONATLOBJECTLib
{
 importlib("stdole32.tlb");
 importlib("stdole2.tlb");
 [
  uuid(2E98593E-C34A-11D1-A54D-0000F8751BA7)
 ]
 coclass MyObject
 {
  [default] interface IFoo;
  interface IFoo2;
  interface IGoo;
 };
};

最初の GUID(先頭が 7BA9)は、タイプ ライブラリの GUID です。次の GUID(先頭が 2E98)は、このオブジェクトの CLSID です。

このインターフェイス用のヘッダー ファイルを作成していない点に注目してください。この点については、MIDL をコマンドラインから実行すると、MIDL が面倒を見てくれます。

midl /Oicf /h "NonATLObject.h" /iid "NonATLObject_i.c"
"NonATLObject.idl"

この処理を、IDL ファイルのためのカスタムのビルド手順にすることができます。MIDL はいくつかのファイルを作成してくれます。

  • クライアントが使用できるタイプ ライブラリ:NonATLObject.TLB

  • プロキシ/スタブ ファイル(これについては、また別の機会に)

  • ヘッダー ファイル:NonATLObject.h

  • GUID(IID と CLSID)を定義するために必要なコードを含むファイル:NonATLObject_i.

その後、.CPP ファイルに NonATLObject.h をインクルードしてから、ヘッダーをインクルードします。このヘッダーは、必要になりそうなすべての Windows と COM のヘッダーを自動的にインクルードします。私たちのヘッダーの前にこれをインクルードすることで、私たちのヘッダーの中にこれをインクルードする必要がなくなります。

さらに、私たちの .CPP ファイルのどれか 1 つに NonATLObject_i.c をインクルードする必要があります。これには私たちのすべての GUID の定義が含まれています。そして、グローバル クラス オブジェクト、オブジェクト、ロック カウンタを定義します。インクルードと定義はこのようになります。

#include "NonATLObject.h"
#include "MyObject.h"
#include "NonATLObject_i.c"

// The global class object.
CMyClassObject g_cfMyClassObject;

// The count of locks and objects.
ULONG g_cObjectsAndLocks = 0;

// Class members and Dll* functions next

最後に、両方のクラスのメンバー関数と、DllGetClassObject(第 4 部で紹介しました)および DllCanUnloadNow を定義します。

サーバーをビルドしたら、第 3 部で説明したように適切な GUID を指定して登録をします。

使ってみましょう

今回、私たちは完全な COM オブジェクトを実装しました。第 6 部では、Visual Basic、Visual J++ からこのオブジェクトを使用する方法について説明します。また、Visual C++ からこのオブジェクトを使用する方法を 2 つ紹介します。ちょっとだけ予告しておくと、新しく作成したオブジェクトにアクセスする方法を Visual Basic や Visual J++、さらには Visual C++ 5.0 に知ってもらうためには、MIDL によって作成されたタイプ ライブラリ(.TLB ファイル)がいかに重要であるかを学びます。

第 6 部:Visual Basic と Visual J++ で COM オブジェクトを使用する

第 5 部で、私たちは完全な COM オブジェクトを実装しました。今度は、Visual Basic と Visual J++ から、そのオブジェクトを使用する方法について説明しましょう(C と C++ については次回に説明します)。みなさんからの手紙の中に、「COM オブジェクトの実装方法なんて気にしていない。使い方を教えてくれ」と書かれた方もいました。今回は、その願いがかなえられることになりました。

しかし、まずは最新情報を

Brent Rector さんは、私たちのオブジェクトの QueryInterface 関数において、私があることを見落としていると指摘しました。私が先走って、ppv が NULL であるかどうかを確認せずに *ppv を NULL に設定していると彼は気付いたのです。

STDMETHODIMP CMyObject::QueryInterface(REFIID iid, void **ppv)
{
 *ppv = NULL;
 // ...

名医は、これでかまわないことを最初に話しました。もし ppv が NULL だったら、結果として一般保護(GP)違反になるはずです。テストの際には、この問題が検出されるのは確実です。そして、単に無視されてしまうようなエラーを返すよりは、このほうがよいのかもしれないのです。

オブジェクトが単なるインプロセス サーバー(つまり DLL)だとしたら、それでもかまわないかもしれません。結局、GP 違反の結果として動かなくなるのはクライアント プロセスだからです。そもそも QueryInterface に NULL ポインタを渡すという無謀なことをするようなクライアントは動かなくなって当然なのです。まったく無礼なことです!当然の報いです(それに、Dr. GUI は、人の間違いを調べるためにコードを実行するのは、時間の無駄だと思っています)。

問題は、オブジェクトがプロセス外(EXE の中で)実装されたときに起きます。この場合、動かなくなるのはサーバー プロセスであって、クライアントではありません。これは、みっともありません。特にサーバーが他のプロセスに対して他のオブジェクトを提供しながら、スタンド アローンとしても動作している場合はなおさらです(バックグラウンド プロセスによって QueryInterface に NULL ポインタが渡されたために Microsoft Word がクラッシュしたとしたら、楽しくもなんともありませんよね)。サーバーがリモート マシン上にあったとしたら、問題はさらに深刻です。

そこで、長い話を短くするために、Dr. GUI は追加のエラー チェックが必要だという Brent さんの意見に同意します。その結果として、これまでの QueryInterface メソッドに次のように1行追加します。

STDMETHODIMP CMyObject::QueryInterface(REFIID iid, void **ppv)
{
 if (ppv == NULL) return E_INVALIDARG; // Don't crash!
 *ppv = NULL;
 // ...

Visual Basic からオブジェクトを使用する

Visual Basic から私たちのオブジェクトを使用するのは、きわめて簡単です。

最初に、Visual Basic プロジェクトに、私たちのオブジェクトへの 「参照」を追加しなければなりません。図 12 に示すように、[Projects]メニューの[References...]コマンドを選択して、リスト ボックスから私たちの COM オブジェクトを探し出して、それにチェック印を付けることで参照を追加します。

図 12:COM オブジェクトに対する参照の追加

次に、図 13 に示すように、各メソッドを呼びだすボタンと、現在の値を示すテキスト ボックスを持つ簡単なフォームを作成します。

図 13:各メソッドを呼びだすフォーム

最後に、オブジェクトにアクセスするためのコードを書きます。まず、General 宣言セクションで、オブジェクトに対する参照を宣言します。

Private obj As MyObject

それから、フォームの Load メソッドで実際のオブジェクトを作成します。

Private Sub Form_Load()
 Set obj = New MyObject
End Sub

これによって、Visual Basic は CoCreateInstance を呼び出してオブジェクトを作成します。

オブジェクトが作成されたので、ボタン ハンドラを追加して、デフォルトの IFoo インターフェイスの Func1 Func2 メソッドを呼び出せるようにします。

Private Sub Func1IncBeep3_Click()
 obj.Func1
End Sub

Private Sub Func2Set_Click()
 If IsNumeric(Text1) Then obj.Func2 (Text1)
End Sub

ほかのインターフェイスのメソッドを呼び出せるように、それらのインターフェイスにアクセスできなければなりません。そのために、インターフェイスに対応する正しい型の変数を作成し、それがオブジェクトを指すように設定します(これを行うと、Visual Basic はオブジェクトを対象に実際に QueryInterface を呼び出します)。インターフェイスを切り換えるために、次のようなコードを使用します。

 Dim Foo2 As IFoo2 ' Switch interfaces
 Set Foo2 = obj

こうしておけば、Foo2 オブジェクト参照を使用して、IFoo2 メソッドを呼び出すことができます。

これが理解できれば、オブジェクトのほかのメソッドにアクセスするコードも理解できるでしょう。

Private Sub Func3BeepGet_Click()
 Dim Foo2 As IFoo2 ' Switch interfaces
 Set Foo2 = obj
 Text1 = Foo2.Func3
End Sub

Private Sub GuncBeep_Click()
 Dim Goo As IGoo ' Switch interfaces
 Set Goo = obj
 Goo.Gunc
End Sub

最後に、Increment Text Box メソッド用のボタン ハンドラを追加します。

Private Sub IncTextBox_Click()
 If IsNumeric(Text1) Then Text1 = Text1 + 1
End Sub

これほど簡単なことがあるでしょうか。

実行時バインディングのファンの方は次の点に注目です。このオブジェクトにアクセスするために、実行時バインディングを使用することはできますが、デフォルトの(IFoo)インターフェイスにアクセスできるだけです。実行時バインディングは、多重インターフェイスを使用するオブジェクトではうまく動きません。それに、必ず速度も落ちます(ところで、少なくとも IFoo2 をデフォルトのインターフェイスにするのが賢明かもしれません。IFoo2 は、IFoo のすべての機能のほか、Func3 も実装するからです)。

Visual J++ からオブジェクトを使用する

Visual J++ の場合も、Visual Basic の同じくらい簡単にオブジェクトを使用できます。まず、COM オブジェクトを表す専用の .class ファイルを作成しなければなりません。それには、Microsoft Visual Studio の[Tools]メニューにある Java Type Library Wizard を使用します。この場合も、図 14 に示すように、オブジェクトを選択して、チェック印を付けます。

図 14:Java Type Library 内の COM オブジェクトの選択

この結果として、.class ファイルが trustlib ディレクトリ内(他の Java クラスと同じサブディレクトリ)に作成されます。結果ウィンドウには、コピーして .java ファイルに貼り付けるインポート文があります。ウィンドウには、クラス ライブラリをテキストで表したものを含む summary.txt への参照もあります。このファイルをコンパイルしてはいけません。.class ファイルは、すでに作成されているのですから。

私たちの場合、インポート文は“import nonatlobject.*;”となります。これを .java ファイルの先頭に挿入します。

このようにインポートしておけば、オブジェクトを作成して、使用することができます。

オブジェクトの作成は、次のように new を呼び出すだけで簡単に行えます。

IFoo myObj = new MyObject();

私たちは MyObject オブジェクトを作成しましたが、これを IFoo オブジェクト参照に割り当てた点に注目してください。このオブジェクトは、デフォルト インターフェイスの概念を認識せず、サポートもしないので、そのままでは Visual J++ で MyObject として使用することができません。

この時点で、IFoo メソッドを呼び出すことができます。

myObj.Func2(5); // Sets
myObj.Func1(); // Increments and beeps
myObj.Func1(); // Increments
myObj.Func1(); // Increments

IFoo2 または IGoo メソッドを呼び出したい場合は、新しいオブジェクト参照を作成して、それらにオブジェクトを割り当てなければなりません。

IFoo2 myFoo2 = (IFoo2)myObj;
System.out.println("Value is " + myFoo2.Func3() + " (8?)");

IGoo myGoo = (IGoo)myFoo2;
myGoo.Gunc(); // Beeps

ご覧のように、Visual J++ から COM オブジェクトを使用するのは、Visual Basic から使用するのと同じくらい簡単です。

今までの復習、これからの予定

今回、私たちは Visual Basic と Visual J++ から COM オブジェクトを使用する方法を学びました。第 7 部では、C++ と、おそらくは C からこれを使用する方法を学ぶことになるでしょう。

第 7 部:Visual C++ からオブジェクトを使用する

第 6 部では、COM オブジェクトを Visual Basic と Visual J++ から使用する方法を説明しました。今回は、C++ と古き良き C から、これを使用する方法を説明します。なお次回は、Visual Basic の場合と同じくらい簡単に COM オブジェクトを使用させてくれる新しい Visual C++ 5.0 の洗練された COM ポインタ クラスの使用法を解説する予定です。

Visual C++ からオブジェクトを使用する:従来の方法

定義を正しく取得する

結論から先に言うと、C++ から直接オブジェクトを使用するのは難しくありません。しかし、いくつかの注意すべき点があります。

まず、オブジェクトをビルドしたとき、MIDL コンパイラが、IDL 入力ファイルと同じファイル名で拡張子が .h のヘッダー ファイルを生成したのを思い出してください(たとえば、MyObject.IDL を対象に MIDL を実行すると、MyObject.h という名前のヘッダファイルが生成されます)。このファイルは、コントロールを構築したときにも使用しましたが、クライアントを構築するためにも、これを使用します。さらに MIDL は、私たちの GUID 用のデータ定義ファイルも作成しました。このファイルは、MIDL ファイルのファイル名に“_i.c”を追加した名前になります。

ヘッダー ファイルは、次のようにインクルードします。

#include "MyObject.h"

MyObject_i.c ファイルをインクルードする代わりに、これをソース ファイルとしてプロジェクトに追加します。これにより、このソース ファイルもコンパイルされ、クライアント プロジェクトにリンクされます。

C++ における COM の初期化と非初期化

COM オブジェクトを使用するには、事前に CoInitialize を呼び出して、COM ライブラリをスレッド用に初期化する必要があります。また、プログラム終了の前に CoUninitialize を呼び出さなければなりません。

引数の NULL は、ダミーの引数として必要です。以下のように、CoInitialize の戻り値を確認する点に注意してください。

HRESULT hr = CoInitialize(NULL);
if (FAILED(hr)) {
 cout << "CoInitialize Failed: " << hr << "\n\n";
 exit(1);
}
else {
 cout << "CoInitialize succeeded\n";
}

exit を呼び出すのは、比較的過激ですが(これはコマンドライン プログラムであり、Windows アプリケーションではない点に注意してください)、Dr. GUI としては、COM ライブラリを初期化できないのだったら、プログラムを実行する意味がないと考えます。

名医の意見には賛成しかねるという方もいるかもしれません。そこで、CoUninitialize の呼び出しにラベルを付けて、オブジェクトの生成に失敗したときに、goto を使ってそこにジャンプできるようにしました。これを、ネストされた if 文を使って行うこともできます。

// ... rest of program...

Uninit:
 CoUninitialize();
} // end main()

あなたのアプリケーションがマルチスレッド対応で、フリースレッド モデルをサポートするオブジェクトを使用したい場合は、CoInitializeEx を呼び出す必要があるかもしれません(スレッドのモデルについては別の機会に詳しく説明します)。CoInitializeEx は COM のマルチスレッド機能がサポートされている場合にのみサポートされます(Windows NT 4.0 以降と DCOM アドインを追加した Windows 95)。しかし、私たちはマルチスレッド機能は必要ないので、CoInitialize をそのまま使うことにします。

COM のヘッダーは、MIDL が生成するヘッダー ファイルが自動的にインクルードしてくれるので、明示的にインクルードする必要はありません。

C++ におけるオブジェクトの作成

COM ライブラリが初期化されたら、オブジェクトを作成することができます。

 IFoo * pIFoo;
 hr = CoCreateInstance(CLSID_MyObject, NULL, CLSCTX_ALL,
  IID_IFoo, reinterpret_cast<void **>(&pIFoo));
 if (FAILED(hr)) {
  cout << "CoCreateInstance Failed: " << hr << "\n\n";
  goto Uninit;
 }
 else {
  cout << "CoCreateInstance succeeded\n";
 }

MIDL が生成したヘッダーがすべての宣言を処理してくれます。IFoo インターフェイス ポインタの型やクラス ID 用の GUID、それにインターフェイス ID などです。実に便利なファイルです!

次に、CoCreateInstance を呼び出します。パラメータについては、少し説明の必要があるでしょう。

CLSID_MyObject はこのオブジェクトのクラス ID です。CoCreateInstance のパラメータにある NULL ポインタは、このオブジェクトを別のオブジェクトに 「集成する」場合、NULL 以外になります(集成については、要求があれば別の機会に説明します)。CLSCTX_ALL は、オブジェクトがインプロセスであるか、プロセス外であるかを気にしないという意味です。特定のタイプのサーバーを使用するのであれば、CLSCTX の列挙から別の値を使用します。

COM には、オブジェクトへのポインタがありません。その代わり、オブジェクトのいずれかのインターフェイスに対するポインタがあります。このため、オブジェクトを生成する際には、必要となるインターフェイスを指定しなければなりません。この例の場合、最後から 2 番目のパラメータとして IID_IFoo を渡して、IFoo インターフェイスを要求します。

最後のパラメータは、CoCreateInstance によって返されるインターフェイス ポインタを格納する領域へのポインタです。生成が失敗した場合(オブジェクトが生成できなかったか、要求されたインターフェイスが得られなかった場合)、このポインタに NULL が書き込まれます。新しい reinterpret_cast 演算子は、インターフェイス ポインタのアドレスを void ** ポインタにキャストする一番よい方法です。reinterpret_cast を使用することで、ポインタの値をいかなる方法でも変えずに、単純にポインタのビットを調べるのが目的であることを明示的に示します。

HRESULT をチェックして、生成が成功したかどうかを確認します。成功しなかった場合は、CoUnitialize の呼び出しまでジャンプします。

C++ におけるインターフェイスのメソッドの呼び出し

インターフェイスのメソッドを呼び出すのは簡単です。以下のように、インターフェイス ポインタを使ってメソッドを呼び出せばよいのです。

 pIFoo->Func1();

これ以上簡単なことはありません!ここでは HRESULT を確認していない点に注意してください。本来は、確認しなければなりませんが、インプロセス サーバーでは、これは致命的というほどではありません。なぜなら、呼び出しが失敗するのはメソッドがエラーを明示的に返した場合だけだからです。このメソッドがエラーを明示的に返さないことは分かっているので、私たちはこれでOKということになります。

しかし一般的なケースでは、メソッドがエラーコードを返さなくても、呼び出しが失敗する理由はほかにもあります。たとえば、サーバーがリモート サーバーの場合は、ネットワークが故障するかもしれません。その場合、(クライアントとしての)あなたは、その故障の事実について、メソッドを呼び出したときに HRESULT に返される値によって知ることができます。同じマシン上のプロセス外サーバーでも、クラッシュした場合には呼び出しに失敗することがあります。HRESULT は、これも反映できます。

そこで、堅牢性を保つために、以下のようなコードを使って、すべての呼び出しの HRESULT を確認しなければまりません。

hr = pIFoo->Func1();
if (FAILED(hr)) DoSomethingAboutIt();
// Etc.

C++ でインターフェイスを変更する

私たちのコンポーネントは、3 つのインターフェイス(IUnknown を含めて 4 つ)を通じて機能を公開します。そこで、コンポーネントをフルに活用するには、別のインターフェイスに対するインターフェイス ポインタを取得する必要があります。これこそが、QueryInterface の役割です。

IGoo *pIGoo;
hr = pIFoo->QueryInterface(IID_IGoo,
 reinterpret_cast<void **>(&pIGoo));

 if (FAILED(hr)) {
  cout << "QI Failed: " << hr << "\n\n";
  goto ReleaseIFoo;
 }
 else {
  cout << "QI succeeded\n";
 }

新しいインターフェイスの ID と、新しいインターフェイス ポインタを格納するためのポインタへのポインタを、QueryInterface に渡します。これが失敗した場合には、IFoo ポインタを解放して CoInitialize を呼び出すコードへジャンプします(以下を参照)。

QueryInterface を呼び出した後、IGoo インターフェイスを呼び出すことができます。

pIGoo->Gunc();

ここでも、先に説明したように、エラーの確認をしなければなりません。

後始末:C++ におけるインターフェイスの解放と CoUninitialize の呼び出し

オブジェクトを使い終わったので、終了の準備をしましょう。しかしその前に、現在保持している両方のインターフェイス ポインタを解放して、私たち自身の後始末をしなければなりません。

 cout << "All done!\n";
 pIGoo->Release();

ReleaseIFoo:
 pIFoo->Release();

Uninit:
 CoUninitialize();
} // end main()

ラベルと goto があるのは、エラーが生じた場合に必要な後始末だけを行うためです。どの時点でも、エラーが生じたとき、すでに行ったことしか後始末できません。未だやっていないことを後始末することはできません。

C からオブジェクトを使用する:昔ながらの方法

むしろ難しくなってしまうのですが、C から COM コンポーネントを使用することは可能です。ただし、これを説明するのは、Dr. GUI がこの方法を推薦する(あるいは、ユーザーがこの方法を使用すると期待する)からではありません。むしろ C、C++、COM などの理解を深めるための 1 手段として、これを説明しようと思います。

C から COM コンポーネントを使用するには、C++ にある vtable と仮想関数をシミュレートする必要があります。C では、これは簡単ですが煩雑でもあります。ありがたいことに、この問題の大部分は、MIDL によって生成されるヘッダー ファイルが扱ってくれます。

コードを C に戻す

当然ですが、cout << "Output"; 文をすべて printf 呼び出しに変えなければなりません。そして、C では reinterpret_cast は使用できないので、昔ながらの (void **) キャスト演算子に戻らなければなりません。大変面倒ではありますが、命に関わるほどではありません。

上記のサンプルを C に変換する際のもっとも難しい部分は、C の制約を我慢しなければならないという点です。変数宣言はすべて、ブロックの先頭に移動しなければなりません。そして、C には参照型がないので、関数呼び出しのすべての GUID の先頭にアンパサンド (&) を付け加えなければなりません。また、C++ では単に“IFoo”と言えばよかったのですが、C では“struct IFoo”と言わなければなりません。とは言え、コードは比較的簡単に理解できると思います。

C++ と同一のヘッダー ファイルを使用し、同一の定義ファイルをリンクする点に注目してください。なぜ同じヘッダーが使用できるのでしょう。MIDL によって生成されたヘッダー ファイルをざっと調べると、次のような形式の条件付きコンパイル ブロックでいっぱいであることがわかります。

#if defined(__cplusplus) && !defined(CINTERFACE)
// C++ code
#else
// C code
#endif

__cplusplus シンボルは、C++ コードをコンパイルする場合に自動的に定義されます。それ以外の場合は定義されません。つまり、C モジュールにこのファイルをインクルードすることによって、適切なコードをコンパイルすることになるのです。条件の後半部分は、C++ のプログラムをコンパイルする場合でも、C の定義を使用する手段を提供します。ヘッダーをインクルードする前に、#define CINTERFACE を指定するだけでいいのです。

C において C++ の vtable とオブジェクトをシミュレートするデータ型

この 「適切なコード」は、vtable を含め、C++ のクラスと同等の C のデータ構造体を作らなくてはならないので、一見奇妙に見えます。たとえば、以下は、IFoo の vtable を表すために MIDL が生成した C のコードです。

typedef struct IFooVtbl
{
 BEGIN_INTERFACE

 HRESULT (STDMETHODCALLTYPE __RPC_FAR *QueryInterface)(
  IFoo __RPC_FAR * This,
  /* [in] */ REFIID riid,
  /* [iid_is][out] */ void __RPC_FAR *__RPC_FAR *ppvObject);

 ULONG (STDMETHODCALLTYPE __RPC_FAR *AddRef)(
  IFoo __RPC_FAR * This);

 ULONG (STDMETHODCALLTYPE __RPC_FAR *Release)(
  IFoo __RPC_FAR * This);

 HRESULT (STDMETHODCALLTYPE __RPC_FAR *Func1)(
  IFoo __RPC_FAR * This);

 HRESULT (STDMETHODCALLTYPE __RPC_FAR *Func2)(
  IFoo __RPC_FAR * This,
  int inonly);

 END_INTERFACE
} IFooVtbl;

BEGIN_INTERFACE と END_INTERFACE は、Windows を含むほとんどのプラットフォームで、空の値として定義されています。そのため、実際に注目する必要があるのは、この構造体のメンバーということになります。

IUnknown メソッドも含め、インターフェイスには各メソッドに対応するメンバーが 1 つずつあります。各メンバーの型は、対応する関数のポインタ型です。また各メンバーの名前は、メソッドの名前になります。メンバーの値が未定義である点に注意してください。この構造体は、オブジェクトによって提供される vtable の上に 「かぶせる」テンプレート(C++ のテンプレートではありません)にすぎません(C++ で COM オブジェクトを実装する場合は、値が埋め込み済みの構造体を vtable として提供しなければなりません)。この構造体は、呼び出しがメソッドのプロトタイプと一致することを保証します。

個々のメソッドには“This”という追加パラメータがあることに注目してください(頭文字に大文字を使用していますが、これは C++ コンパイラで COM を使用するための C スタイルのコードをコンパイルする場合に、C++ のキーワードである this とコンパイラが混同しないようにするためです)。C では This ポインタのパラメータを明示的に渡さなければなりません。C++ では自動的に渡されます。追加パラメータはすべて、上記のように This の後に続きます。

インターフェイスは、次のように vtable へのポインタを格納する構造体として定義されます。

interface IFoo // interface is #defined as struct
 {
  CONST_VTBL struct IFooVtbl __RPC_FAR *lpVtbl;
 };

CONST_VTBL は単に“const”となります。この構造体には、メンバーが 1 つあります。vtable へのポインタです。これは、まさにデータを格納していない C++ のオブジェクトに含まれるもの、すなわち、クラスの vtable へのポインタです。繰り返しますが、データの初期化は行われません。データはオブジェクトによって提供されるものであり、私たちはそのデータの型を C で解釈しているだけなのです。

C における COM の初期化とオブジェクトの生成

C で COM を初期化したり、オブジェクトを作成したりする方法は、C++ の場合とほとんど同じです。変数を先頭で宣言する点に注目してください。さらに、インターフェイス ポインタの宣言に、“struct”キーワードを使用しなければなりません。なぜなら、実質的には構造体に対するポインタを宣言しているからです。

その後は、CoInitialize CoCreateInstance を通常どおりに呼び出します。CoCreateInstance には、GUID 構造体のアドレスを明示的に渡さなければなりません。C には参照型がないからです。

HRESULT hr;
struct IFoo *pIFoo;
struct IGoo *pIGoo;

printf("Hello, world!\n\n");
hr = CoInitialize(NULL);
if (FAILED(hr)) {
 printf("CoInitialize Failed: %x\n\n", hr);
 exit(1);
}
else {
 printf("CoInitialize succeeded\n");
}

hr = CoCreateInstance(&CLSID_MyObject, NULL, CLSCTX_ALL,
   &IID_IFoo, (void **)&pIFoo);
if (FAILED(hr)) {
 printf("CoCreateInstance Failed: %x\n\n", hr);
 goto Uninit;
}
else {
 printf("CoCreateInstance succeeded\n");
}

ここでも、goto を使用してエラー処理をします。そして、この方法が気に入らなければ、代わりにネストされた“if”文を使用することもできます。

C でオブジェクトのメソッドを呼び出す

C を使うことの本当の代償は、メソッドを呼び出すために、みっともないコードを書かなければならないという点です。C++ で仮想関数呼び出しを行う場合、2 重間接参照と this ポインタはコンパイラが処理します。C を使用して仮想関数呼び出しをシミュレートする場合、自分でこれを書かなければなりません。

pIFoo->lpVtbl->Func1(pIFoo);

このコードには少し説明が必要です。まず、piFoo は vtable ポインタを格納するメモリの領域(実際にはオブジェクトの中)を指すインターフェイス ポインタであることを思い出してください。上記の構造体宣言の中では、vtable を指すメンバーの名前は IpVtabl です。

したがって、最初の間接参照で構造体に到達し、2 番目の間接参照で vtable に到達します。vtable を、一連の関数ポインタを格納する構造体として定義している点を思い出してください。そのうちの 1 つが、Func1 という名前です。そこで、このポインタを使用して、vtable の中の関数を呼び出します(C や C++ では、関数ポインタを明示的に逆参照する必要はありません。そのため、コードが比較的簡単になっています)。関数ポインタの型には、パラメータと型と、戻り値の型が含まれているので、関数が正しい型で呼び出されることが保証されます。

ヘッダーをインクルードする前に COBJMACROS というプリプロセッサ シンボルを宣言すると、インターフェイスの各メソッドに対応するマクロが得られます。マクロの形式は次の通りです。

#define IFoo_Func1(This) \
 (This)->lpVtbl -> Func1(This)
こうすれば、先に示したみっともない呼び出しの代わりに、次のような呼び出しができます。
IFoo_Func1(pIFoo);

これは C++ の場合ほどエレガントではありませんが、他の例よりはましです。

C でインターフェイスを変更する

C でインターフェイスを変更するのは、C++ の場合と同じように簡単です。

hr = pIFoo->lpVtbl->QueryInterface(pIFoo,
 &IID_IGoo, (void **)&pIGoo);
if (FAILED(hr)) {
 printf("QI Failed: %x\n\n", hr);
 goto ReleaseIFoo;
}
else {
 printf("QI succeeded\n");
}

明示的に this ポインタを指定することを含め、奇妙な関数呼び出しをしなければならない点に注意してください。COBJMACROS を使用すれば、上記の QueryInterface 呼び出しを、次のように書き換えることができます。

IFoo_QueryInterface(pIFoo, &IID_IGOO, (void **)pIGoo);

後始末:C におけるインターフェイスの解放と CoUninitialize の呼び出し

呼び出しの構文を除けば、プログラム終了時の後始末の方法は、C++ の場合と同じです。

 IGoo_Release(pIGoo);

ReleaseIFoo:
 pIFoo->lpVtbl->Release(pIFoo);

Uninit:
 CoUninitialize();

Release を呼び出す構文を 2 つ示した点に注目してください。どちらの構文も、両方のインターフェイスで使用できます。

今までの復習、これからの予定

今回は、C++ と C から COM オブジェクトを使用する方法を見てきました。第 8 部では、COM オブジェクトに対して使用できる Visual C++ 5.0 の高度なポインタを使用する方法について説明します。そして、Visual Basic の場合と同じくらい簡単に、Visual C++ 5.0 から COM オブジェクトを使用できるようにする方法を紹介する予定です。

第 8 部 : スマートに行こう!スマート ポインタで COM オブジェクトを使おう

ここまで、基本的な COM オブジェクトの書き方と、それを Visual Basic や Visual J++、Visual C++、さらに C から使う方法を学びました。このコラムで、VC++ 5.0 のスマート COM ポインタを通じて COM オブジェクトを使う方法を学び終えたら、次からは小型で高性能な COM オブジェクトを書くための最良の手段である Active Template Library、つまり ATL についての説明を始めます。

スマート ポインタを通じて COM オブジェクトを使用する—#import と typelib のインポート

Visual C++ 5.0 以降を使用しているなら、とてもクールな機能を使うことができます。Visual Basic のように簡単で、Visual J++ より容易に COM オブジェクトが使用できる、COM オブジェクトのためのスマート ポインタです。

これらのスマート ポインタは、タイプ ライブラリを 「インポート」するための、新しい #import ディレクティブを使用したときに生成されます。#import を使用すると、Visual C++ コンパイラは指定されたファイルからタイプ ライブラリを読み込みます。LoadTypeLib API を使って読み込めるタイプ ライブラリを含んでいる任意のファイルを指定できます。通常は、.TBL.ODL.EXE.DLL、または .OCX ファイルですが、LoadTypeLib が読み込めるならどんなファイルでも指定できます。

#import を実際に使用して、それによって作成されたスマート ポインタ型を使うだけでよいのです。

Visual C++ は、コンパイル中に自動的に #include される 2 つのファイルを生成します。これらは出力先ディレクトリに保存され、タイプ ライブラリと同じファイル名を使って、".TLH" と ".TLI" という拡張子を付けた名前が与えられます。たとえば、プログラムに次の #import 文があったとします。

#import "..\NonATLObject.tlb"

そうすると、NonATLObject.TLH NonATLObject.TLI という 2 つのファイルを、出力先ディレクトリ(デバッグ用ビルドの場合は DEBUG)に生成します。

#import が生成するもの

これらのファイルに何が入っているかを知っている必要はありません。しかし、名医は、ファイルが何をしているかを知らずに COM スマート ポインタを使うのはあまり気持ちのいいものではないことはよくわかっています。そこで、本当のことを知りたいという患者の強い要望に応えて、裏で何が行われているのかを説明します。

.TLH ファイルには、標準で次の宣言が含まれています。

  • クラスと各インターフェイスを GUID に関連付けるための declspec(uuid("<GUID をここに指定する >") を含む struct 宣言。これにより、__uuidof(MyClass) という形で、uuidof 演算子を使って、クラスとそれぞれのインターフェイスの GUID を取得することができます。

  • COM_SMARTPTR_TYPEDEF マクロを使用した、各インターフェイス ポインタに対するスマート ポインタの定義。これらのスマート ポインタは、AddRef、Release、そして QueryInterface を自動的に呼び出します。また、CoCreateInstance を明示的に呼び出さなくてもオブジェクトを作成できるようにします。IFoo インタフェース用のスマート ポインタの宜言は、次の通りです。
    _COM_SMARTPTR_TYPEDEF(IFoo, uuidof(IFoo));
 

コンパイラはこれを次のように展開します。

typedef _com_ptr_t<_com_IIID<IFoo, __uuidof(IFoo)> > IFooPtr;

このようにして、com_ptr_t テンプレート クラスに基づく、IFooPtr という名前の新しいスマート ポインタ クラスが生成されます。このポインタ クラスは、CoCreateInstance と、すべての IUnknown の機能を自動的に処理します(このテンプレートの詳細については、オンライン ヘルプで com_ptr_t を調べて下さい)。

  • 各インターフェイスのインターフェイス宣言。これには、インターフェイスのすべてのメソッドとプロパティに対応する、raw ラッパーと、エラー チェック済みのラッパーが含まれます(サンプルにはプロパティはありませんが、プロパティがあれば、プロパティ名そのものを使って、Visual Basic と同じように簡単かつ直接的にプロパティを使用できます。コンパイラは、特別な declspec を使ってプロパティを生成し、適切な get と set メソッドを生成します。また、"pIFoo->Prop1 = 5;" のようなプロパティの使用を適切な GetXxx や SetXxx メソッド呼び出しに変換します)。

    エラー チェック済みラッパーは、HRESULT を検査してエラーを調べ、HRESULT がエラーを示す場合は com_error 例外を発行します。

    結局、私達の IFoo 宣言は、次のようになります。

    struct __declspec(uuid("7ba998d0-c34f-11d1-a54d-0000f8751ba7"))
    IFoo : IUnknown
    {
    // Wrapper methods for error-handling
    
     HRESULT Func1 ();
     HRESULT Func2 (int inonly);
    
    // Raw methods provided by interface
    
     virtual HRESULT __stdcall raw_Func1 () = 0;
     virtual HRESULT __stdcall raw_Func2 (int inonly) = 0;
    };
 

.TLH は、名前の前に "raw_" を付けることで実際のインターフェイス関数を 「リネーム」する点に注目してください。インターフェイスに実際にマップされるのは、唯一の純粋仮想関数である raw 関数です。ラッパーは raw 関数を呼び出します。

これらの宣言はすべて、typlib 内にある LIBRARY 名と同じ名前の C++ 名前空間に属します。これらの名前は、C++ のすべての識別子と同じく、大文字と小文字が区別されます。次のように、typelib 名を使って、識別子の使用を修飾することができます。

NONATLOBJECTLib::IFooPtr *pIFoo;

あるいは、次のように、using namespace ディレクティブを使って、インポートした識別子をアクセス可能にできます。

using namespace NONATLOBJECTLib;

// later, but in same scope...

IFooPtr *pIFoo;

using namespace ディレクティブは通常のスコープのルールに従うので便利です。つまり、名前空間名を何度も指定する必要がなく、ブロック内で使ってスコープを制限することができるのです。複数の typelib を使用したときに名前の衝突が起こる可能性があるので、Dr. GUI としては、#import ステートメントに no_namespace 修飾子を使わないことをお勧めします。

ところで、例外処理、名前空間、ラッパー関数など、固有のデフォルトが多数あることに気づいたでしょうか。これらのデフォルト設定はたいへん便利ですが、好みに合わなければ、#import ディレクティブにある無数の属性を使って、コードの生成方法を簡単に変えることができます。これによって、どのようなコードでも素早く簡単に作成できます。#import の詳細については、オンライン ヘルプを参照してください。

.TLH ファイルに比べると、.TLI ファイルは単純なものです。ラッパー関数の実装を格納されているだけです。たとえば、私達の IFoo インターフェイスに対応する .TLI ファイルには、raw インターフェイス関数を呼び出し、HRESULT がエラーを示す場合には例外を発行するラッパーが含まれています。

inline HRESULT IFoo::Func1 () {
 HRESULT _hr = raw_Func1();
 if (FAILED(_hr))
_com_issue_errorex(_hr, this, __uuidof(this));
 return _hr;
}

inline HRESULT IFoo::Func2 (int inonly) {
 HRESULT _hr = raw_Func2(inonly);
 if (FAILED(_hr))
_com_issue_errorex(_hr, this, __uuidof(this));
 return _hr;
}

COM スマート ポインタを使った COM オブジェクトの作成と使用

これ以上簡単に COM オブジェクトを作成して使用する方法はおそらくないでしょう。手順は以前の説明と似ていますが、すべての手順がより簡単になっており、エラー処理も単独の例外処理ブロックにまとめることができます。

タイプ ライブラリの #import と COM の初期化

まず、#import ディレクティブを指定して、コンパイラがタイプ ライブラリにアクセスできるようにします。

次に、CoInitialize または CoInitializeEx を呼び出して、COM ライブラリを初期化します (Dr. GUI の場合は、MFC を使用してテスト用のアプリケーションを書いているので、AfxOleInit を呼び出しました。COM スマート ポインタは MFC アプリケーションと互換性がある点に注目してください)。

オブジェクトの作成

オブジェクトの作成は、きわめて簡単です。スマートポインタを作成し、作成したいオブジェクトの GUID を、オーバーロード コンストラクタに渡すだけです。GUID をパラメータとして受け取るオーバーロードが、自動的に CoCreateInstance を呼び出します。

IFooPtr pIFoo(__uuidof(MyObject));

名前空間のことを忘れてはなりません。#import によって宣言された型を使用できるように、適切な using the namespace Xxx ディレクティブを使わなくてはなりません。

メソッドの呼び出しとプロパティへのアクセス

オブジェクトが作成されたら、そのメソッドを呼び出すことができます。構文は次の通り、まさに望んでいた通りです。

pIFoo->Func1();
pIFoo->Func2(5);

インターフェイスにプロパティがあれば(これについては、まだ詳しく説明していませんが)、それらも直接操作することができます。

pIFoo->Prop1 = 5;      // calls Set method
int a = pIFoo->Prop2;   // calls Get method

コンパイラは、プロパティの参照を自動的に適切な関数呼び出しに変換します。プロパティは、特殊な __declspec を使って、.TLI ファイル内のインターフェイス構造体で宣言されています。

例外をキャッチする

オブジェクトを対象とする操作は、どれも失敗する可能性があります。オブジェクトを作成するという操作でもです。標準では、何かうまくいかないと、com_error 例外が発行されます。あらゆる例外をキャッチできるように、try/catch ブロックを使用します。

try {
IFooPtr pIFoo(__uuidof(MyObject));

pIFoo->Func1();
}
catch (_com_error e) {
   AfxMessageBox(e.ErrorMessage());
}

例外処理を使用すると、コードは簡潔になり、理解しやすくなりますが、サイズと処理速度の面でいくらかのコストを支払うことになります。一般的に、別の方法で例外処理を書くことに比べれば、このコストは支払う価値があります(以前に示した、if goto の酷いネストを覚えているでしょうか)。優秀なプログラマなら、機能のコストを知っているので、それらを使うべきかどうか合理的な判断を下すことができるでしょう。コストの考えを理解するために、小さなテスト プログラムのアセンブラ コードを生成してみましょう。別の方法でエラー チェックを書いた場合の迷宮と、これを比べてみてください。例外を利用することが、自分でエラー チェックをするのに比べて、それほど悪いものではないことがわかると思います。しかも、はるかに便利です。

異なるインターフェイスを使用する

異なるインターフェイスを使用するのもきわめて簡単です。新しいインターフェイス型の新しいポインタを作成して、別のスマート インターフェイス ポインタで初期化するか、既存のスマート ポインタをこれに代入するだけです。コンストラクタや代入演算子は、自動的に QueryInterface を呼び出します(失敗すると、例外が発行されます)。たとえば、次のように書けます。

IGooPtr pIGoo(__uuidof(MyObject));   // create object
pIGoo->Gunc();            // call method

IFooPtr pIFoo = pIGoo;   // QI different interface, same object
pIFoo->Func1();      // call method on new interface

この場合、パラメータとしてスマート ポインタを受け取る IFooPtr コンストラクタが、QueryInterface を呼び出します。失敗した場合は、例外が発行されます。

COM の初期化解除

Dr. GUI のプログラムは、COM の初期化を明示的に解除する必要がありません。MFC がやってくれるからです。しかし、MFC を使用していない場合は、CoInitialize CoInitializeEx のそれぞれの呼び出しに対応する CoUninitialize 呼び出しを使う必要があります。

これ以上簡単にはできない

これまでに見てきたように、COM スマート ポインタを使用すれば、Visual Basic の場合と同じぐらい簡単に C++ から COM オブジェクトを使用することができます。そして、COM スマート ポインタの実装にかかるコストは、ごくわずかな効率の低下です。しかし、ほとんどの場合、コードが簡潔になり、手間も少なく、一貫性のあるエラー チェックが実現できるので、この比較的小さなコストは問題になりません。

ここまでのまとめと今後の予定

今回は、Visual C++ 5.0 の新しいスマート COM ポインタ クラスを使って、COM オブジェクトを Visual Basic の場合と同じぐらい簡単に使えるように方法を説明しました。次回は、ATL C++ テンプレートの "T" の部分、すなわちテンプレートから、ATL について学び始めます。

表示: