次の方法で共有


COM プログラミングの基本 (中)

Fumiaki Yoshimatsu (吉松 史彰)
株式会社アスキーNT
ソリューション事業部 教育部

December 1, 1999

サンプルファイル (Zip形式 combasics2.zip 20.0KB)

 

はじめに

前回「COM プログラミングの基本」というタイトルで連載をはじめておきながら「COM とは少し離れて...」なんて話になって、「結局COMではどうなんだ!」とお怒りの方、ご安心ください。今回と次回は COM にどっぷりつかっていただきましょう。この連載が終わる頃にはみなさんごいっしょに「COM is Love!」と叫べるようになっている...はずです。

前回のおさらい

前回は、COM を理解する上で最初に必要な考え方である「インターフェイス」と「インターフェイスベースプログラミング」について説明しました。システムの保守性、拡張 性を向上させるこれらの考え方は Windows 上での開発に限らず、あらゆる局面で応用できます。ですが、前回は問題点を積み残したまま終わっていました。内容としては、

  1. ソースコードを最後に1プロジェクトに集めてコンパイルしている
  2. インターフェイス定義を各プログラミング言語で行っている

ということを挙げておきました。

問題点その1では、

  • 医者機能を修正すると、患者も含めた全体の再ビルドが必要
  • 患者と病院という本来違う場所にあるはずのものを同じマシン上でしか実行できない
  • 患者と病院を違う言語で書けない

ということが現象として出てきます。また、問題点その2では、

  • インターフェイス作成時に開発環境が決められてしまう
  • 患者と病院を違う言語で書けない

ということが現象として出てくることになるでしょう。インターフェイスベースプログラミングではこれらの問題を克服できないのでしょうか?

インターフェイスと実装の分離 Again

そもそもなぜ上記のような問題が起こるのかを考えれば解決の糸口も見えてきます。まず問題点その1ですが、これはソースコードを1プロジェクトに集 めているのが悪いので、SmallClinic クラスと SomethingBad クラスを別々にビルドすれば解決します。ただし、インターフェイス定義を参照して2つのクラス間の通信を可能にする通信レイヤを別途実装する必要があるだ けです(といってもこれが難しいのですが)。根が深いのは問題点その2のほうです。その1とも絡んでくるのですが、プロジェクトを別にするのなら、1つ1 つのプロジェクトが利用する開発環境はなんでも良いはずです。最後に1つのプロジェクトに集めてしまったのは、実は問題点その 2、インターフェイスを定義した時点で開発環境が限定されていることからなのです。

ちょっと頭を切り替えてみましょう。そもそもインターフェイスの作成を各プログラミング言語で行うということは、前回の「インターフェイスの一般的な定義」で説明した インターフェイスと実装の分離を 達成できていないことになります。インターフェイスには How は書かないと言いましたが、インターフェイスをたとえば C++ で定義した時点で1つ のHow、つまり「どの言語で実装するか = C++」を定義してしまったことになるからです。本当にインターフェイスと実装を分離したければインターフェイスを定義する言語と、実装する言語をも分離 しなければなりません。それには、世間に出回っているプログラミング言語ではない、インターフェイス定義専用の言語を使う必要があります。

なぜインターフェイス定義専用言語か?

読者の中には、「うちは全部 C++ で開発してるんだから何も問題はないよ」と思われる方もいらっしゃることでしょう。しかし、インターフェイス定義に専用言語を使えばインターフェイスベー スプログラミングの本質をより表現しやすくなります。専用言語でかかれたインターフェイスは、実装に使われるプログラミング言語を想定しません。逆に、イ ンターフェイス定義専用言語で書かれたとおりのインターフェイスを、プログラミング言語の側が公開することを、強制します。実装言語を想定しないで、でき あがったプログラムが公開するインターフェイスの形は強制するということは、ソースコードレベルではなく、実行可能コードになったバイナリモジュールの形 を決めていることになります。

バイナリモジュールの形を決めるとどういうメリットがあるのでしょうか。ちょっとハードウェアの世界を思い浮かべてください。たとえば PC と Ethernet ネットワークを結ぶケーブルがあります。10BASE-T のネットワークなら、ケーブルの先には電話のモジュラージャックに似た、RJ-45 タイプのインターフェイスがついているはずです。これはベンダーが違っても共通に使えるものですね? C 社のハブには接続できるが、B 社のハブには接続できないなんてことはありません(図1)。


図1

ではこの文章の「ケーブルの先の RJ-45 インターフェイス」を「共通の専用言語でかかれたインターフェイス」に、「ハブ」と「PC」を「プログラム」に、「ベンダー」を「プログラミング言語」 に、それぞれ置き換えてみてください。あら不思議、違う言語で書かれたプログラムであっても、共通インターフェイスを介して通信できるということになりま す。つまり、バイナリの形が決まっていて、世界中にそれが公開されているのなら、世界中にある無数のプログラム間で、そのインターフェイスを介した通信が できることになります。また、そのバイナリ構造は当然 OS のベンダーも利用できることになります。だとしたら、先ほどの「ケーブル」は「OS」に置き換えてみたらどうでしょう? OS がバイナリになったプログラム間の通信を仲立ちしてくれる「ケーブル」になってくれるなら、デベロッパーがやらなければならないのは、まさに「プログラ ム」を書くことだけになります(図2)。


図2

まとめると、インターフェイス定義に専用言語を使ってインターフェイスベースプログラミングを行うということは、

  • 自分以外の人が、違うプログラミング言語で作った、別々のプログラムであっても、相互に連携できる
  • (物理的な接続さえあれば)どこで動いているプログラムであっても、相互に連携できる
  • (OS がサポートしてくれれば)上の「連携」の部分は自前で開発しなくても良い

ということを表します。ここでグッドニュースをお知らせしましょう。Windowsは「連携」の部分をサポートしています。どうやってサポートするかはもうみなさんお気づきですね?そう、COM を利用して、です。

COM が使うインターフェイス定義専用言語:IDL

COM が、インターフェイス定義専用言語を使うことで、プログラム間の連携を可能にしていることは説明しましたが、ではインターフェイス定義専用言語として何を 使えば良いのでしょうか?これは決め事なので、覚えていただくしかありません。COM では OSF(Open Software Foundation)が DCE(Distributed Computing Environment)で使用する RPC(Remote Procedure Call)のために規定した、IDL(Interface Definition Language)を COM 用に拡張したもの(単に IDL と呼ばれる)をインターフェイス定義専用言語として利用します。IDL は文字どおりインターフェイス定義言語として作られたものなので、この役割にうってつけなのです。

IDL 超入門

それでは IDL を実際に書いていくことにしましょう。前回のシナリオをそのまま、今度は IDL を使って追ってみます。IDL開発ツールとして用意していただくのは、メモ帳です。IDL はただのテキストファイルですので。

インターフェイス

シナリオどおり、まずはインターフェイスの定義からいきましょう。IDLでは次のような構文でインターフェイスを定義します。

[attr1, attr2, ...] interface InterfaceName : ParentInterfaceName
{
  Method-Declaration...
}

ずばり、interface キーワードでインターフェイスを定義します。InterfaceName にはそのインターフェイスの名前を指定します。通常、インターフェイスの名前はIではじめます。ParentInterfaceName にはそのインターフェイスが継承する親インターフェイスを指定します。新しいインターフェイスを定義している場合は通常、親インターフェイスは IUnknown になります。このインターフェイスについては次回説明します。

[] で囲まれた部分には、インターフェイスの属性を設定します。[] の中に設定された属性がインターフェイスの性格を決めます。設定できる属性はたくさんありますが、最低でも [object] 属性と [uuid] 属性は設定しなければなりません。

[object] 属性はこのインターフェイスが OSF DCE RPC インターフェイスではなく、COM インターフェイスであることをあらわす IDL の拡張属性です。これを指定しないと COM インターフェイスを定義したことになりません。

[uuid] 属性には、このインターフェイスの ID を GUID(Globally Unique IDentifier) で指定します。これは全世界にただ一つしかない値です。インターフェイスの名前は重複する可能性がありますが、この ID は適切な手順で作成すれば重複しません。これによって、全世界のデベロッパーが別々に定義したインターフェイスを相互に識別することができます。GUID を生成するには、Visual Studio や Platform SDK に付属している、GUIDGEN.EXE (GUI)、UUIDGEN.EXE (コマンド) を使用します。どちらも適切な GUID を生成してくれます。開発マシンと同じである必要はありません。

Method-Declaration 部には、そのインターフェイスが公開する機能(What)を定義します。細かく書くと以下のようになります。

[attr1, attr2...] HRESULT MethodName ([attrs...] Param1, [attrs...] Param2, ...);

[] の中に指定するのはこのメソッドの属性です。特に必須の属性はありません。HRESULT は戻り値の型です。COM インターフェイスのメソッドはほとんどの場合戻り値は HRESULT 型になります。これを別の型にすると面倒な作業が発生しますので、通常は HRESULT にしておきます。MethodName はメソッドの名前です。

() 内は通常の関数と同じ引数リストになります。引数にも属性を指定することができます。引数に設定する属性には、主に [in]、[out]、[in, out] があります。[in] はその引数が入力専用であることを示します。つまり、クライアントからメソッドに対してデータが渡され、メソッド内ではそれが変更されないということで す。逆に [out] は出力専用であることを示します。クライアントからは何も渡されず、メソッド内でデータが生成されてクライアントに返されることを意味します。引数の方向 を明確に定義することで、このインターフェイスを利用するクライアントとサーバーの間を流れるデータの量を最適化することができますので、必ず指定する必 要があります。指定しなかった場合は [in] であるとみなされます。また、[retval] という属性もあります。これは VB などの、HRESULT を直接扱わない環境でメソッドの戻り値になる引数です。

以上の用件を組み合わせると、前回のシナリオで出てきた各インターフェイスの定義は IDL では以下のようになります。

[uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), object]
interface IPatient : IUnknown
{
  HRESULT GetHealthInsurance([out, retval] BSTR *ret);
  HRESULT Pay([in] long HowMuch, [out, retval] long *ret);
  HRESULT TellHowIFeelBad([out, retval] BSTR *ret);
};

[uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), object]
interface IDoctor : IUnknown
{
  HRESULT Treat([in] IPatient *patient, [out, retval] long *ret);
};

[uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), object]
interface IClerk : IUnknown
{
  HRESULT DispatchPatient([in] IPatient *patient, [out, retval] long *ret);
};

GUID の部分は訳あって xxx を羅列してあります。皆さんの環境で、コマンドプロンプトから

C:>uuidgen.exe -i[ENTER]

と入力してください。この IDL の雛型が作成されます。

COM クラス

前回のシナリオでは、インターフェイスを作成したら、次にどのインターフェイスをどのクラスに実装するかを決めていました。この部分も IDL で行うことができます。以下のような構文でクラスを定義します。

[attr1, attr2...] coclass COMClassName
{
  [attr1, attr2...] interface InterfaceName;
  ...
};

IDL では COM クラスを coclass キーワードで定義します。coclass にも属性を設定できます。[uuid] 属性は必須です。coclass に設定された GUID は COM クラスを識別します。クライアントはこの GUID を利用して COM クラスの実装を起動します。

interface 部にはこの COM クラスがまとめるインターフェイスを指定します。このCOMクラスを実装するときは、ここに書かれたインターフェイスで定義されているメソッドをすべて実 装する必要があります。interface 部の属性としては [default] などがあります。COM クラスはいくつかのインターフェイスをまとめることができますが、クライアントの中にはその中の1つのインターフェイスにしかアクセスできないものもあり ます(スクリプト言語など)。その場合は coclass の中で [default] として定義されたインターフェイスにアクセスすることになります。VB、VC++、VJ++ ではすべてのインターフェイスにアクセスできます。以上の用件を組み合わせると、前回のクラスは以下のようになります。

[uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)]
coclass SmallClinic
{
  interface IDoctor;
  interface IClerk;
}

[uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)]
coclass SomethingBad
{
  interface IPatient;
}

実装の準備

前回はクラスを定義したら実装に入りました。でも前回と違って、IDL はそのままでは開発環境に取り込めません。IDL を開発環境で利用するにはどうしたらよいでしょうか。

IDL を開発環境に取り込むには、Visual Studio や Platform SDK に付属する MIDL.exe を使います。MIDL.exe は IDL のコンパイラです。さっそく上記の 3 つのインターフェイスと 2 つの COM クラスをコンパイルしてみましょう。上のコードをすべてメモ帳にコピーし、GUID を生成して置き換えたら、ファイルを hospitalitf.idl という名前で保存します。次にコマンドプロンプトで以下のコマンドを入力します。

C:>midl /Oicf hospitalitf.idl[ENTER]

エラーが出ましたか? HRESULT が定義されていないというエラーになったと思います。IDL はプログラミング言語によってはあいまいなデータ型に依存せずに、独自の基本データ型を持っています。たとえば int は IDL では 32 ビットの整数、short は 16 ビットの整数という具合です。基本データ型には HRESULT という型は定義されていません。そのため、コンパイルエラーが発生します。

IDL ファイルに毎度毎度 Windows で使う全データ型の IDL バージョンを書くのは大変です。そこで、MIDL コンパイラは、C++ や Java などと同じくファイルのインクルードという概念をサポートします。現在の hospitalitf.idl では、IUnknown、HRESULT、BSTR という型/識別子が MIDL には理解されません。その定義が書いてあるファイルを IDL ファイルの先頭で import することでこの問題を解決します。hospitalitf.idl の先頭に

import "unknwn.idl";

という行を追加してもう一度コンパイルすれば今度はコンパイルできたはずです。このファイルは %INCLUDE% フォルダにあり、主に IUnknown インターフェイスの定義が書かれています。さらにこのファイルで wtypes.idl というファイルを import しており、このファイルに Windows のデータ型とIDLのデータ型との対応が書かれています。VB、VC++、VJ++ のデータ型と、IDL のデータ型との対応関係(抜粋)をまとめておきます。詳しくは Platform SDK などのドキュメントを参照してください。

IDL VC++ VJ++ VB
(VARIANT_BOOL) VARIANT_BOOL boolean Boolean
double double double Double
float float float Single
hyper __int64 long -
long long int Long
short short short Integer
small char byte -
(BSTR) BSTR Java.lang.String String
wchar_t wchar_t char (Integer)

実装

さて、IDLをコンパイルするとファイルが4つ作成されたと思います。それぞれ以下のような内容が含まれています。

dlldata.c proxy/stub DLL用C++ソース
hospitalitf.h C++インターフェイス、クラス定義
hospitalitf_i.c C++GUID定義
hospitalitf_p.c proxy/stub C++ソース

さしあたって VC++ で病院/患者プログラムを実装するのに必要なのは hospitalitf.h と hospitalitf_i.c だけです。この2つのファイルを #include して、作成したいクラス(今回は SmallClinic)を宣言するヘッダーファイルを作ります。今回は、前回の問題点を克服するため、病院プログラムと患者プログラムをわけて作ること にします。最初は病院側からです。

SmallClinic.h (抜粋)

#include "hospitalitf.h"
class CSmallClinic : public IDoctor, IClerk
{
protected:
    ULONG        m_cRef;
public:
    CSmallClinic();
    ~CSmallClinic();
    //IUnknown...
    STDMETHODIMP        QueryInterface(REFIID, void**);
    STDMETHODIMP_(ULONG)    AddRef();
    STDMETHODIMP_(ULONG)    Release();
    //IDoctor...
    STDMETHODIMP        Treat(IPatient*, long*);
    //IClerk...
    STDMETHODIMP        DispatchPatient(IPatient*, long*);
};

前回同様、クラスは継承するインターフェイスのすべてのメソッドを実装しなければなりません。あとはこのクラスを実装する.cppファイルを作成し てビルドすれば立派な COM DLL のできあがりです。完全なコードをダウンロードできますのでお試しください。VC++ ではこのように、IDL からストレートに実装へと進むことができます。

C++ じゃないときは?

これで「次回までさようなら」してしまうと、VB と VJ++ ユーザーに怒られるでしょう。IDL は何も C++ 専用ではありません。VB や VJ++ でも利用することができます。ただし、VB や VJ++ では、IDL にさらに内容を追加してもう一度コンパイルする必要があります。

タイプライブラリの生成

IDL に interface と coclass しか書いていない場合は、MIDL は上に挙げた 4 つのファイルを生成します。しかし、IDL に library キーワードを設定するともう1つのファイル、タイプライブラリを生成することができます。library キーワードの構文は以下のとおりです。

[attr1, attr2,...] library TypeLibraryName
{
  Any statement you want to be in this library...
}

library にも属性を設定します。やはり [uuid] 属性が必須です。その他に [version]、[helpfile] などの属性を指定することもできます。library の中({}の中) には interface や coclass などを含めることができます。library の中に含まれる IDL のキーワードは、コンパイル後のタイプライブラリに含まれ、VB や VJ++ で参照できるようになります。タイプライブラリは IDL をバイナリトークンにしたものです。ただし、IDL に書いてもタイプライブラリに含まれない属性もあるので注意してください。

実装の準備 Again

VB や VJ++ でも使えるように hospitalitf.idl に library キーワードを追加しましょう(太字が追加/変更部分)。

[uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx),version(1.0)]
library HospitalLib
{
  importlib("stdole2.tlb");
  //import "unknwn.idl";
   [uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), object]
  interface IPatient : IUnknown
  {
    HRESULT GetHealthInsurance([out, retval] BSTR *ret);
    HRESULT Pay([in] long HowMuch, [out, retval] long *ret);
    HRESULT TellHowIFeelBad([out, retval] BSTR *ret);
  };
  [uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), object]
  interface IDoctor : IUnknown
  {
    HRESULT Treat([in] IPatient *patient, [out, retval] long *ret);
  };
  [uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), object]
  interface IClerk : IUnknown
  {
    HRESULT DispatchPatient([in] IPatient *patient, [out, retval] long *ret);
  };
  [uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)]
  coclass SmallClinic
  {
    interface IDoctor;
    interface IClerk;
  };
  [uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)]
  coclass SomethingBad
  {
    interface IPatient;
  };
};

先ほどの IDL 全体を library{} でくくっただけです。これをコンパイルしてみます。コマンドは先ほどと同じです。すると今度は hospitalitf.tlb というファイルが作成されます。これがタイプライブラリです。この中に IDL の定義がバイナリ化されて詰め込まれています。

タイプライブラリができたので、開発環境へ取り込んで実装へ進みましょう。やはり病院側から実装していきます。

VB の場合

タイプライブラリの取り込み

VB では参照設定でタイプライブラリを取り込みます。まず ActiveX DLL プロジェクトを新規作成し、プロジェクト名を HospitalVB に、クラス名を SmallClinicVB に設定します。これで VB 版病院を呼び出すときは ProgID(CreateObjectの引数)として "HospitalVB.SmallClinicVB" を指定することを決定したことになります。次に参照設定で今作成したタイプライブラリを参照します。レジストリに登録されていないので、一覧には出てきま せんから、[参照]ボタンをクリックして、hospitalitf.tlb を選択してください(図3)。


図3

デフォルトのクラスモジュールに Implements ステートメントを入力します。IntelliSense が働いて、リストが出てくるはずです。HospitalLib. と入力すると絞込みが行われます(図4)。


図4

このクラスは、IClerk と IDoctor を実装するクラスですので、両者を選択します...が、IDoctor が見えません。これはどうしたことでしょう?

実は VB は、タイプライブラリに定義されている coclass の中の [default] インターフェイス(または最初に書いてあるインターフェイス)をインターフェイス名ではなく COM クラス名で表示してしまうという悪い癖があります。これでは見た目にも何を実装しているのかわかりにくくなってしまうので、VB 用に IDL を以下のとおり少し手直しして、再度 MIDL でコンパイルします(太字が修正部分)。VB が参照していると、タイプライブラリファイルを書き込めなくなってしまうので、コンパイルする前に参照設定をいったん解除してください。

[uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx),version(1.0)]
library HospitalLib
{
  importlib("stdole2.tlb");
  //import "unknwn.idl";
   [uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), object]
  interface IPatient : IUnknown
  {
    HRESULT GetHealthInsurance([out, retval] BSTR *ret);
    HRESULT Pay([in] long HowMuch, [out, retval] long *ret);
    HRESULT TellHowIFeelBad([out, retval] BSTR *ret);
  };
  [uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), object]
  interface IDoctor : IUnknown
  {
    HRESULT Treat([in] IPatient *patient, [out, retval] long *ret);
  };
  [uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), object]
  interface IClerk : IUnknown
  {
    HRESULT DispatchPatient([in] IPatient *patient, [out, retval] long *ret);
  };
  [uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)]
  coclass SmallClinic
  {
    [default] interface IUnknown;
  interface IDoctor;
    interface IClerk;
  };
  [uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)]
  coclass SomethingBad
  {
    [default] interface IUnknown;
    interface IPatient;
  };
};

VB ではもともと IUnknown インターフェイスは扱えないので、IUnknown を見えなくしても問題はありません。

実装

もう一度 ActiveX DLL プロジェクトに HospitalLib への参照設定を行い、Implements ステートメントを書いてみてください。今度は IDoctor と IClerk が見えるようになっていますので、2つとも Implements します。その他の実装コードは前回とほとんど同じです。異なるのはメソッドの引数の型の表記だけですが、これは VB のコードウィンドウでメソッドをプルダウンして選択すれば自動的に入力されますので問題はないでしょう。コードをすべて入力したら、コンパイルして hospital.dll を作成します。

VJ++ の場合

タイプライブラリの取り込み

VJ++ では JactiveX.exe を利用してタイプライブラリを Java インターフェイス/クラスに変換します。コマンドプロンプトで以下のコマンドを入力すると、タイプライブラリの内容が Java インターフェイス/クラスに変換されます。

C:><VJ98フォルダへのパス>\JactiveX.exe /d "出力フォルダ" /e hospitalitf.tlb[ENTER]

実装

VJ++ を起動し、COM DLL を新規作成します。プロジェクト名は HospitalVJ とします。デフォルトで作成されるクラスファイル名を SmallClinicVJ.java に、クラス名を SmallClinicVJ に変更します。これで VJ++ 版病院を呼び出すときは ProgID として "HospitalVJ.SmallClinicVJ" と指定することを決定したことになります。また、プロジェクトフォルダの下に JactiveX.exe で作成されたフォルダ(hospitalitf)を移動します。さらに、SmallClinicVJ クラスが IDoctor と IClerk を implements するようにします。その他の実装コードは前回とほとんど同じです。ただし、IDL の long 型は VJ++ では int になるので、それを修正します。

最後に、HospitalVJ プロジェクトに先ほど JactiveX ツールで作成したインターフェイスファイルを3つとも追加し、ビルドして HospitalVJ.DLL を作成します。

クライアントプログラムの実装

患者を実装しましょう。すでに病院側は COM コンポーネントなので、クライアントの開発環境は何でもかまいません。VB の場合を説明します。VJ++、VC++についてはサンプルコードを参照してください。

VB の場合

標準 EXE プロジェクトを新規作成し、先ほどと同じ、HospitalLib を参照設定します。次に Form1 にテキストボックスとコマンドボタンを貼り付けて、以下のコードを入力すれば完成です。テキストボックスに "I have a head ache" と入力してみてください。医者の処方が聞けるでしょう。

Form1.frm

Implements IPatient
Private Sub Command1_Click()
  Dim clk As IClerk
'VC病院  Set clk = New SmallClinic
'VB病院  Set clk = CreateObject("HospitalVB.SmallClinicVB")
'VJ病院  Set clk = CreateObject("HospitalVJ.SmallClinicVJ")
  Dim pa As SomethingBad
  Set pa = Me
  clk.DispatchPatient pa
End Sub

Private Function IPatient_GetHealthInsurance() As String
  IPatient_GetHealthInsurance = "VALID"
End Function

Private Function IPatient_Pay(ByVal HowMuch As Long) As Long
  IPatient_Pay = 1
End Function

Private Function IPatient_TellHowIFeelBad() As String
  IPatient_TellHowIFeelBad = Text1.Text
End Function

あれ?

と思われた方もいらっしゃるでしょう。なぜ VC 病院だけは VB の New ステートメントで起動できるのでしょうか?実はこの問題は見かけより重要な意味を含んでいます。もう一度今回の IDL をじっくり眺めてみてください...。そもそも各キーワードの属性として必須である [uuid] とは何なのでしょうか?

interface キーワードのところで触れたとおり、UUID に指定する値は全世界でユニークであり、お互いを識別するための値を指定しています。COMではそれぞれの UUID/GUID に固有の名前を付けています。Interface の属性である UUID は IID (Interface-ID) と呼びます。同様に coclass の UUID は CLSID、library の UUID は LIBID と呼びます。ではクライアントの立場にたって COM コンポーネントを眺めてみましょう。クライアントはこれまでの Windows プログラミングとは違って DLL 名を直接指定して COM を利用することはありません。かわりに CLSID を利用します。CLSID を使えば全世界にあるさまざまな coclass から、今回実装した SmallClinic を特定できるわけです。

しかし、VB と VJ++ では、{ActiveX | COM} DLL を開発する際にデベロッパーが CLSID を作ることを許しません。むしろデベロッパーが GUID なんかにわずらわされず、本来の実装に専念できる環境を提供しているわけです。必要な GUID は VB/VJ++ が必要なタイミングで必要なだけバックグラウンドで生成し、それを割り振っています。したがって、SmallClinicVB と SmallClinicVJ には、IDL で SmallClinic に割り当てたものとは違う、新しい CLSID が振られてしまっているのです。ありがた迷惑なことに、VB と VJ++(デフォルト設定の場合)は、タイプライブラリも新しく作成してくれます。

VB では CreateObject と New という 2 つの方法で COM オブジェクトを利用できるようにします。一見似ているこの2つですが、内部的な動作が以下のように異なっています。

CreateObject("HospitalVC.SmallClinicVC")

  1. ProgID "HospitalVC.SmallClinicVC" を CLSID に(レジストリなどを利用して)変換する。
  2. CLSID を利用して DLL を起動し、COM オブジェクトを作成する。

New SmallClinic

  1. 参照設定してあるタイプライブラリを見て、SmallClinic に対応する CLSID を取得する。
  2. CLSID を利用して DLL を起動し、COM オブジェクトを作成する。

もうおわかりですね。タイプライブラリに書いてある CLSID と一致する実装クラスを持っているのは VC++ で作った DLL だけなのです。VB と VJ++ で作った DLL には新しい CLSID が振ってあるので、それを取得するために ProgID->CLSID 変換を行わなければならないのです。VB/VJ++ で開発した DLL を New するには、VB/VJ++ が生成したタイプライブラリを参照設定しなければなりません。IDL を書いたあと、実装フェーズで、患者プログラムと病院プログラムを別の人が同時に開発しはじめるような場合は CreateObject しか使えないことになるわけです。このあたりは自動生成という便利さと、融通が利かないという不便さとのトレードオフになります。

まとめ

真のインターフェイスベースプログラミングを実現するには、プログラミング言語から独立した専用の言語を使わなければならないこと、COM は真のインターフェイスベースプログラミングを実現する環境であること、COM はそのためにIDLを利用することがおわかりいただけたでしょうか?VB や VJ++ など、IDL をまったく知らなくても COM コンポーネントを開発することができるツールがたくさん出てきています。しかし、IDL を理解していればこれらのツールの動作、メリット、そして限界がよりわかりやすくなります。COM+の時代になってもこれは変わりません。COM の権威、Don Box 氏は言います:

Real COM Programmers start work in IDL[1]

さて、この {VB|VJ++|VC++} 病院の医師は今度新しく「注射」をマスターしました。さっそく IDoctor.Treat のコードに注射機能を追加し、IPatient.TellHowIFeelBad 呼び出しに対して「風邪を引いた」と返す患者たちに注射することにしました。ところが、医師の知らない間にこの病院は有名になり、多種多様な患者がやって くるようになっていました。しかし、風邪を引いたと言ったとたん医師が有無を言わさず注射をするので、小児科のつもりでこの医師に子供を見せた親たちから 苦情が出るようになってきました...。どうやら医師の知らない間に患者が「バージョンアップ」して、より繊細になったようなのです。今までのように単に 病状を聞いて処置するだけではすまなくなってきました。この問題にどう立ち向かったらよいのでしょう...。次回をお楽しみに。

**編集者注:**添付のサンプルファイルは当記事の内容に従って、GUIDの記述部分や、IDLのコンパイルなどを順次行って頂くことにより動作します。従って、サンプルをそのままビルドする事はできませんので、ご注意ください。


[1] Introducing Distributed COM and the New OLE Features in Windows NT 4.0 Box, Don [Microsoft Systems Journal May/1996] より


Fumiaki Yoshimatsu:株式会社アスキーNT に勤務し、主として Windows NT/2000 関連の Microsoft University コースのトレーナーを担当しています。1999年度 MCSP MCT アワードの受賞者です。現在は、Windows DNA、COM、MTS、ADO/OLE DB などのテクノロジにフォーカスした、デベロッパー向けのトレーニング コースの開発および教育を行っています。 Microsoft Tech・Ed 99 Yokohama では「ADOパフォーマンスチューニング」、Microsoft Developer Days '99 では「ADSI によるディレクトリ対応アプリケーションの開発」のセッションスピーカーを務めました。

 

ページのトップへ