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

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

January 17, 2000

サンプルファイル (Zip形式 combasics3.zip 21.4KB)

はじめに

新年になりました。さて、みなさんにとって今年はどんな年になるのでしょうか。私たちデベロッパーにとっては Windows 2000 の登場が今年最初のイベントであり、それに対応していくための新しい技術を身に付けていくことが今年の難事業になりそうです。Windows 2000 が登場することで、COM の世界にも少し変化が出てきます。Windows 2000 では今まで以上に COM が重要な位置を占めるようになります。その変化に立ち向かっていくためにも、今ここで基本をおさえておくことが重要です。変わるといっても COM がインターフェイスをベースにすることは変わるはずもありませんから、ここさえおさえておけば Windows 2000 恐るるに足らず、です。

前回のおさらい

前回はインターフェイスを定義するための専用言語として IDL をご紹介しました。そして、その IDL を開発環境に取り込んでインターフェイスベースのプログラミングを行い、そのメリットを確認しました。開発環境に入る前にまず IDL でインターフェイスを定義することにより、インターフェイスベースの考え方を厳密に表すことができるようになります。COM はインターフェイスベースの考え方で作られていますので、「真の COM プログラマは IDL から作業を開始する」わけです。

ところで、前回の最後に患者の「バージョンアップ」の話が出てきました。これまでは医師は病状を聞いたら有無を言わさず処置を行っていたのですが、患者によってはそれでは満足してくれない場合が出てきたのです。システム開発をしていれば「バージョンアップ」はよくある話ですからそんなに深く考えることはない...のでしょうか。

COM コンポーネントのバージョンアップ

前回のまとめによると、まず、医師はこんな具合にバージョンアップしたと考えられます。

STDMETHODIMP CSmallClinic::Treat(IPatient* patient, long* ret)
{
BSTR Where;
patient->TellHowIFeelBad(&Where);

if (wcscmp(Where, L"I have a head ache") == 0)
	MessageBoxA(0, "Try aspirin.", "SmallClinicVC", MB_OK);
else if (wcscmp(Where, L"I have a stomach ache") == 0)
	MessageBoxA(0, "Try 胃腸薬.", "SmallClinicVC", MB_OK);
else if (wcscmp(Where, L"I have a bad cold") == 0)
{
	MessageBoxA(0, "I'll give you a shot.", "SmallClinicVC", MB_OK);
	Inject(300, patient);     //InjectはCSmallClinicのメンバ関数とする
}
else
	MessageBoxA(0, "You are going to die soon.  Sorry.", "SmallClinicVC", MB_OK);

	SysFreeString(Where);
	*ret = 0;
	return S_OK;
}

患者が「風邪を引いた」といった場合、「注射します」と言ってから薬を 300ml ほど注射しています。これまでは、患者が「風邪を引いた」と言った場合は、医師がその処置方法を知らなかったので、「もうすぐ死ぬ」なんて物騒なことを言われていた患者も、医師が注射を覚えてくれたおかげで一安心です。連載の初回で説明したとおり、インターフェイスと実装を分離しているので、IDoctor インターフェイスさえ変えなければ、実装コードはいくら変更しても受け付け(IClerk)のコードや患者(IPatient)のコードを同時に修正する必要はないわけです。

インターフェイスの不変性とは

「IDoctor インターフェイスさえ変えなければ」と書きましたが、何を変えたら「インターフェイスが変わった」と言うのでしょうか。また、連載初回では「インターフェイスベースのプログラミングでは、インターフェイスは絶対不変のものとして扱う」というようなことを書きましたが、絶対不変とはどういう状態を指すのでしょうか。実はここまで、「インターフェイスとは何か」という厳密な定義をせずに進んできているため、上のような疑問が生まれてきます。ここで厳密な定義をしてしまいましょう。

COM のインターフェイスは2つのものを定義します。1つは構文です。IDL で書かれたインターフェイス定義がこれにあたります。インターフェイスを IDL で定義した場合、そこにはメソッドの名前、引数の数、順番、データ型、入出力の方向、戻り値の型、メソッドの数や並び順など、OS がプログラムを実行するにあたって必要な情報が網羅されています。これがインターフェイスの定義その1です。インターフェイスは絶対不変なのですから、一度公開したら(そのインターフェイスを利用するクライアントアプリケーションの開発が始まったら)絶対に変更できません。IDL は読み取り専用にしなければいけないのです。これが絶対不変の意味です。メソッドの名前や順番、引数の数やデータ型は変更してはいけません。メソッドを追加することもできません[1]

COM インターフェイスが定義するものの2つ目は、そのインターフェイスの意味です。つまり「IDoctor インターフェイスの Treat メソッドは、1つ目の引数として与えられた IPatient に対して処置を施す。2つ目の引数として与えられた long* は将来のために予約されているもので、現在は0を返す」、というように、そのインターフェイスと、各メソッドの機能を定義します。これは IDL では書けません(コメントにすればもちろん書けますが)。したがって、COM インターフェイスのこの部分はインターフェイスに対応するドキュメントを整備して、その中にきちんと書いてやる必要あります。コードに対してドキュメントを書くのはシステム開発の世界では当たり前の話ですから(とは言ってもなかなか実行できないことでもありますが)、この点は特に問題はないでしょう。いずれにしろ、IDoctor::Treat メソッドを呼んだら、医学的処置をしてくれず、かわりに玄関先で飴玉をくれた [2]、なんてことになったら、クライアントアプリケーションは何を信じて開発してよいのかわからなくなってしまいます。IDoctor は医師であり、その Treat メソッドは患者の病状に合わせて何らかの処置を施す、という「意味」は変えてはいけないのはおわかりいただけるでしょう。COM インターフェイスは以上の2つを定義し、これら両方を絶対不変のものとして扱わなければならないのです。COM インターフェイスの定義には実装コードは含まれませんから、上記の CSmallClinic のコードのように、実装部はいつでも変更することができ、しかもそれは IDoctor を扱う他のコードには一切影響を与えません。

インターフェイスのバージョンアップ

インターフェイスが不変だからといって、人間がすることに完全なものなどありませんから、当然インターフェイス定義もあとから変更したくなることがあります。たとえば今回の病院を例にとると、すべての患者が医師からの IPatient::TellHowIFeelBad メソッド呼び出しに対して、「頭がいたい」とか「風邪を引いた」とかきちんと答えられるようになっています。医師は返事によって治療をしているわけですが、今のところ、医師は患者の様子をまったく考慮していません。相手が大人ならこんなぶっきらぼうな医師でもなんとかなるでしょうが、子供の場合、医師が何を聞いても泣き叫ぶばかりでしょう。なんとかがんばって「風邪を引いた」と医師に伝えようものなら、有無を言わさず注射されることになります。なにしろ

	MessageBoxA(0, "I'll give you a shot.", "SmallClinicVC", MB_OK);

このコードを実行したときに表示されるメッセージボックスには OK ボタンしかついていないのですから。医師のほうも、IPatient インターフェイスが今のままでは、相手が子供であってもそれを認識できません。医師としては、相手が子供かどうかを見極め、子供なら注射する前に飴玉でもあげるとか、「痛くないよ~」などと慰めつつ注射したいものです。つまり、IPatient では表現できなかった新しい機能が必要になるわけです。IPatient を変更することはできません。ではどうしたら良いのでしょう。

COM を利用しない、これまでの Windows 上でのコンポーネントベースの開発(たとえば DLL の利用)ではこの種のバージョンアップに関するルールが何もありません。ですからデベロッパーの一存で引数が増えたり減ったりし、データ型が変わり、バージョン1では存在したメソッドがバージョン2には存在しないなんてことが往々にしてありました。あるコードをバージョンアップするたびにクライアント側もコードの書き直しが必要になっていたのです。コードが書き直せればまだ良いのですが、誰が書いたのかわからない、ドキュメントもないコードを追いかけて、どこを修正したらよいのかを探り当てなければならなかったりして、結局スクラップアンドビルドしたほうが早かった、というような経験はシステム開発をされていれば多かれ少なかれ誰にでもあると思います。さらにCOMでは、あるコンポーネントの開発を地球の反対側にある会社が行ったのでソースコードは一切手元にない、という状況にすらなりえます。こうなると、そもそもある部品のバージョンアップにあわせてコードを書き直すことができません。 COM ではそのような事態を防ぐため、インターフェイスを不変にしました。ですから、インターフェイスをバージョンアップするということは、すなわち新しいインターフェイスを定義するということです。この場合も、IPatient をバージョンアップするのではなく、むしろ IInfantPatient という新しいインターフェイスを定義することで「バージョンアップ」するのが COM のやり方です。これ以外のやり方はありません。こうすることで、COMコンポーネントのバージョンがあがっても、古いクライアントには何ら影響しませんし、新しいクライアントが古いインターフェイスを使うこともできるようにしているわけです(図1)。

図1
図1

IInfantPatient の定義

基本的に新しいインターフェイスを定義するわけですから、IInfantPatient の定義は IPatient の定義のやり方と変わりません。GUID を生成し、IDL で interface の定義を行います。ただし、引き続き古いインターフェイスもサポートすることを明示するために、通常は新しいインターフェイスは古いインターフェイスから継承して定義します。これまでは IUnknown という正体不明のインターフェイスから3つのインターフェイスを継承させていましたが、IInfantPatient は以下のように IPatient から継承させます。こうすることで、IInfantPatient を実装するコードは IPatient の各メソッドも実装しなければならなくなります。hospitalitf2.idl を以下のように定義します。

import "hospitalitf.idl";

[uuid(e418c156-9956-4d71-b752-aa65162cdb3e), object]
interface IInfantPatient : IPatient
{
	HRESULT ItDoesnotHurt ([in] short numCandies, [out, retval] BSTR *HowIFeelNow);
};

[uuid(4930a4af-9da1-4c72-a6fb-7d2c31d09471)]
coclass SomethingBad2
{
	interface IInfantPatient;
	interface IPatient;
};

IInfantPatient にはメソッドは1つしかありませんが、このインターフェイスを実装するコードは全部で7つ(IInfantPatient x 1 + IPatient x 3 + IUnknown x 3)のメソッドを実装する必要があります。また、この IDL ファイルをコンパイルすると最初の import ディレクティブによって前回の hospitalitf.idl が取り込まれますので、IPatient の定義を書きうつす必要はありません。

新しいインターフェイスの実装

新しいインターフェイスと古いインターフェイスを実装する際には以下の2つのアプローチが考えられます。

  1. 前回と同様の手順で、新インターフェイスを実装するクラスを作成する。
    この場合は、新しいクラスで上記の7つのメソッドをすべて実装することになります。古いインターフェイスのコードが手元にあれば、それを使って足りない分だけ補えばよいのですが、ない場合は少し困難です。本稿ではこの方法を利用します。
  2. 古いクラスをそのまま(ソースコードではなくバイナリのまま)使い、新しいクラスは新しいメソッドのみを実装する。
    この場合は、古いインターフェイスへの呼び出しを古いクラスのほうへ転送する仕組みが必要になります。COM にはこの仕組みが用意されています(包含と集約)が、本稿では説明しません。

VC++ の場合

C++ の場合は、実装の継承を使えば書くコードの量を COM の仕組みを使わなくても減らすことができるのですが、ここでは前回と同じようにクラスをインターフェイスからのみ継承する形にします。IPatient と IInfantPatient を実装するのはクライアントコードですので、手順は:

  1. IDL ファイルをコンパイルして、hospitalitf2.h と hospitalitf2_i.c ファイルを作成する。
  2. 新しいクラス CSomethingBad2 を宣言するヘッダーファイルを作成する。

7つのメソッドがすべて宣言されている点に注意してください。

SomethingBad2.h
#include "..\..\hospitalitf2.h"

class CSomethingBad2 : public IInfantPatient
{
protected:
	ULONG	m_cRef;

public:
	CSomethingBad2();
	~CSomethingBad2();

	//IUnknown...
	STDMETHOD(QueryInterface)(REFIID, void**);
	STDMETHOD_(ULONG, AddRef)();
	STDMETHOD_(ULONG, Release)();
	
	//IPatient...
	STDMETHOD(GetHealthInsurance)(BSTR *ret);
	STDMETHOD(Pay)(long HowMuch, long *ret);
	STDMETHOD(TellHowIFeelBad)(BSTR *ret);

	//IInfantPatient...
	STDMETHOD(ItDoesnotHurt)(short numCandies, BSTR *ret);
};

  1. CSomethingBad2 の7つのメソッドを実装する実装ファイルを作成する。
    前回のコードはそのまま使えます。新しいメソッドのみ新しく実装コードを書きます。
SomethingBad2.cpp (抜粋)
STDMETHODIMP CSomethingBad2::ItDoesnotHurt(short numCandies, BSTR *ret)
{
	if (numCandies > 2)
	{
		*ret = ::SysAllocString(L"I feel better now.");
		return S_OK;
	}
	return E_FAIL;
}

  1. 前回のコードの wmain 関数を一部修正し、CSomethingGood ではなく CSomethingGood2 がインスタンス化されるようにする。
  2. ビルドして完成。

VJ++ の場合

Java の場合も実装の継承を使うことができます。手順としては:

  1. IDL ファイルに Library キーワードを追加し、コンパイルしてタイプライブラリを作成する。
  2. タイプライブラリを取り込む。

    前回は Jactivex.exe コマンドを使用する方法を紹介しましたが、VJ++ ではメニューの[プロジェクト] - [COMラッパーの追加] を実行すれば同じことを行うことができます。この場合図のようなリストが出てきますが、これはレジストリに登録してあるタイプライブラリの一覧なので、まだ登録されていない hospitalitf2.tlb は表示されません。[参照] ボタンを押して選択してください(図2)。

図2
図2
  1. 前回のプロジェクトを VJ++ で開き、Class3 を新しく追加する。
    Class3 は前回の Class2 を継承 (extends) し、新しく IInfantPatient を実装 (implements) します。
Class3.java
 
import hospitalitf2.*;
public class Class3 extends Class2 implements IInfantPatient
{
	public String ItDoesnotHurt(short numCandies)
	{
		if (numCandies > 2)
			return new String("I feel better now");
		else
			throw new com.ms.com.ComFailException(0x80004005);
	}
}

  1. Class1 のコードを少し修正し、Class2 ではなく Class3 をインスタンス化するようにする。
  2. ビルドして完成。
    VJ++ ではデベロッパーが IUnknown の3つのメソッドを実装する必要がないことにも注意してください。

VB の場合

前回の説明どおり、VB では IDL を若干修正して coclass のデフォルトインターフェイスを IUnknown にしてから再度タイプライブラリを生成します。これであとは前回のコードにクラスを追加して IInfantPatient を Implements し、新しいメソッドを実装すれば万事 OK...とはいきません。VB の Implements ステートメントは、IInfantPatient インターフェイスのように IUnknown または IDispatch から直接継承していないインターフェイスは受け付けてくれないのです。これはヘルプにも書いてある挙動です。困りました。

どうしようもないので、ここでは以下のようにhospitalitf2.idlを定義しなおしました。

hospitalitf2.idl(VB用)
interface IInfantPatient : IUnknown
{
	HRESULT GetHealthInsurance([out, retval] BSTR *ret);
	HRESULT Pay([in] long HowMuch, [out, retval] long *ret);
	HRESULT TellHowIFeelBad([out, retval] BSTR *ret);
	HRESULT ItDoesnotHurt([in] short numCandies, [out, retval] BSTR *HowIFeelNow);
};

つまり、IPatient から継承するかわりに IPatient のメソッドをすべてそのまま IInfantPatient にコピーアンドペーストしたわけです。開発環境に合わせて IDL をいじってしまうようでは IDL を書いた意味が半減してしまうのですが、これは VB の制約なので仕方ありません。したがって手順は:

  1. 上記の IDL をコンパイルしてタイプライブラリを生成する。
  2. 前回のプロジェクトを開いて、1で生成したタイプライブラリに参照設定する。
  3. クラスモジュールを1つ追加し、IInfantPatient と IPatient の両方を Implements する。
  4. 3で作成したクラスモジュールに全部で7つのメソッドを実装する。
Class1.cls
 
Option Explicit

Implements IPatient
Implements IInfantPatient

Private Function IInfantPatient_GetHealthInsurance() As String
  Call IPatient_GetHealthInsurance
End Function

Private Function IInfantPatient_ItDoesnotHurt(ByVal numCandies As Integer) As String
  If (numCandies > 2) Then
    IInfantPatient_ItDoesnotHurt = "I feel better now."
  Else
    Err.Raise &H80004005  'E_FAIL
  End If
End Function

Private Function IInfantPatient_Pay(ByVal HowMuch As Long) As Long
  Call IPatient_Pay(HowMuch)
End Function

Private Function IInfantPatient_TellHowIFeelBad() As String
  Call IPatient_TellHowIFeelBad
End Function

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 = Form1.Text1.Text
End Function

IPatient_** の各メソッドは以前と同じものです。IInfantPatient_** メソッドのうち、本来は IPatient インターフェイスに実装されている3つのメソッドは、呼び出しをIPatient_** メソッドに転送します。新しいメソッドのみ、実際に実装しています。このような小手先のテクニックを使わなければならないのも開発ツールの制限事項ということになります。また、VB でも VJ++ と同様に IUnknown の3つのメソッドは実装してなくても良いことにも注意してください。

  1. Command1_Click イベントプロシージャを少し修正して、Me(Form1)ではなく、新しく作成した Class1 をインスタンス化して pa オブジェクト変数に Set する。
  2. コンパイルして完成。

新しいインターフェイスの利用

これで IInfantPatient の実装はできたわけですが、当の医師はまだその存在を知りませんので、相変わらずすべての患者をIPatientとして扱い、「風邪を引いた」と言えばすぐに注射されてしまう状況です。医師に子供の患者もいるということを教えて、子供のときは注射する前に気持ちを落ち着かせるようにします。子供かどうかを判断するには、IInfantPatient を実装しているかどうか調べればよいわけです。

VC++ の場合

VC++ では、IUnknown インターフェイスの QueryInterface というメソッドで、COM オブジェクトがある特定のインターフェイスを実装しているかどうかを調べることができます。COM オブジェクトは例外なくすべて IUnknown インターフェイスを実装していますので、QueryInterface メソッドも全 COM オブジェクトに存在します。VB や VJ++ ではデベロッパーはそれを実際には実装せず、それぞれのランタイムが処理するようになっています。

SmallClinic.cpp(抜粋)
STDMETHODIMP CSmallClinic::Treat(IPatient* patient, long* ret)
{
//同じなので省略...
else if (wcscmp(Where, L"I have a bad cold") == 0)
{
	IInfantPatient *child;
	HRESULT hr = patient->QueryInterface(IID_IInfantPatient, (void**)&child);
	if (SUCCEEDED(hr))
	{
	  BSTR hownow;
	  hr = child->ItDoesnotHurt(2, &hownow);
	  if (FAILED(hr))
		MessageBoxW(0,
		  L"You should be patient for a little while.", L"SmallClinicVC", MB_OK);
	  else
		MessageBoxW(0, hownow, L"SmallClinicVC", MB_OK);
	  child->Release();
	}
	MessageBoxA(0, "I'll give you a shot.", "SmallClinicVC", MB_OK);
	Inject(300, patient);     //InjectはCSmallClinicのメンバ関数とする
}//以下省略...
}

渡された IPatient インターフェイスの QueryInterface を呼び出します。IInfantPatient の IID を指定して、COM オブジェクトがそれを実装しているかどうかを調べています。実装していれば IInfantPatient::ItDoesnotHurt メソッドを呼び出して、飴玉を2つ与えています。これで気持ちが落ち着けば(hownow 引数に落ち着いたという返事が返ってきます)注射をします。それでもだめならエラーが返されますので、「我慢しなさい」といってやっぱり注射をします。あまり変わってないですね...。もし COM オブジェクトが IInfantPatient を実装していなければ、今までどおり注射をします。その判定は QueryInterface の戻り値の HRESULT が成功状態かどうかを判定することで行うわけです。

VJ++ の場合

VJ++ の場合は、instanceof 演算子を利用すれば、渡された COM オブジェクトが IInfantPatient を実装しているかどうかがわかります。VC++ で利用した IUnknown::QueryInterface メソッドは、VJ++ では指定したインターフェイスへのキャストに相当するので、単純にキャストしてみて try...catch ブロックで例外を捕まえることで判定することも可能です。

SmallClinicVJ.java(抜粋)
public int Treat(IPatient patient){
//同じなので省略...
}else if (where.compareTo("I have a bad cold") == 0){
if (patient instanceof IInfantPatient)
{
	IInfantPatient child;
	child = (IInfantPatient)patient;
	try
	{
	  String hownow;
	  hownow = child.ItDoesnotHurt((short)3);
	  com.ms.win32.User32.MessageBox(0, hownow, "HospitalVJ", 0);
	}
	catch(com.ms.com.ComException er)
	{
	  // 気持ちが落ち着いていない...
	  com.ms.win32.User32.MessageBox(0, 
		"You should be patient for a little while.", "HospitalVJ", 0);
	}
	com.ms.com.ComLib.release(child);
}
// とにかく注射する!
com.ms.win32.User32.MessageBox(0, "I'll give you a shot.", "HospitalVJ", 0);
inject(300, patient);     //InjectはSmallClinicVJのメンバ関数とする
//以下省略...
}

VJ++ では、HRESULT は例外として throw されます。子供が飴玉の数に満足しない場合は、E_FAIL が返ります。VC++ ではこれを SUCCEEDED/FAILED マクロで処理していましたが、VJ++ では try...catch ブロックで捕まえます。

VB の場合

VB の場合は、TypeOf 演算子を利用すれば、渡された COM オブジェクトが IInfantPatient を実装しているかどうかがわかります。VC++ で利用した IUnknown::QueryInterface メソッドは、VB では Set ステートメントで実行できますので、単純に Set してみて On Error ステートメントでエラーを捕まえることで判定することも可能です。

SmallClinicVB.cls (抜粋)
Private Function IDoctor_Treat(ByVal patient As HospitalLib.IPatient) As Long
'同じなので省略...
  ElseIf (Where = "I have a bad cold") Then
    If (TypeOf patient Is IInfantPatient) Then
      Dim child As IInfantPatient
      Set child = patient
      On Error Resume Next
      Dim hownow As String
      hownow = child.ItDoesnotHurt(1)
      If (Err.Number = 0) Then
        MsgBox hownow, vbOKOnly, "SmallClinicVB"
      ElseIf (Err.Number = &H80004005) Then
          '気持ちが落ち着いていない...
          MsgBox "You should be patient for a little while.", vbOKOnly, "SmallClinicVB"
      Else
        ''...
      End If
      Err.Clear
      On Error GoTo 0
      Set child = Nothing
    End If
    'とにかく注射する!
    MsgBox "I'll give you a shot.", vbOKOnly, "SmallClinicVB"
    Call Inject(300, patient)     //InjectはSmallClinicVBのメンバ関数とする
'以下省略...
End Function

VB ではエラーは VB のエラーとして返ります。VB の On Error ステートメントでランタイムエラーを捕まえて、Number プロパティを参照すれば戻された HRESULT 値がわかります。

あなたは子供ですか?

これで新しいインターフェイスにアクセスする新しいコードを作成することができました。これまでの古い医師も、新しい医師も IPatient インターフェイスを介して患者を治療することはできます。しかし、新しい医師は、患者の種類にあわせてよりきめ細かいコミュニケーションを行えるようになったわけです。新しい医師が子供でない患者(前回ビルドした CSomethingBad クラス)にアクセスした場合も問題はありません。患者が、医師からの QueryInterface(またはそれに相当する呼び出し)に反応しないので、子供でないことが判断できます。子供でなければエイヤで注射してしまえば良いのです。

さて、ここでみなさんが以前に行ったことのある普通の病院を思い出してください。医師に「○○さんどうぞ」と呼ばれてカーテンの向こうに入ります。目の前の医師が「どうしました?」と聞きます。「風邪を引いた」と答えました。ここで医師はおもむろに「ところであなたは子供ですか?」と聞きます...。そんなことがかつて一度でもありましたか?私にはそんな経験はありません。難しい国家試験をパスして医師の資格をとるくらいの人ですから、ぱっと患者を診ればその人が子供かどうかの区別はつくのです。

そんな当たり前のことが上記のコードではできていません。医師は注射をする前に必ず患者が子供かどうか、つまり引数として渡された patient は IInfantPatient インターフェイスも実装しているのかどうかを問い合わせます。医師オブジェクト(IDoctorを実装している)と患者オブジェクト(IPatient を実装している)が同じプロセス内でインスタンス化されていれば気にならないかもしれません。でも WAN やインターネットの向こう側だったら...?このやり取りが完結するまでに何秒かかるでしょうか。

患者オブジェクトは、自分が IInfantPatient も実装しているかどうかを知っていますから、医師に会ったときに自分から「自分は IInfantPatient を実装しています/いません」と通知できれば、ネットワークをまたがる処理の往復回数を1回減らすことができます。どうしたらこれを実現できるでしょうか。

COM の問題は IDL が解決する

VC++ の例で紹介した IUnknown::QueryInterface メソッドでは、IInfantPatient を要求するためにそのインターフェイスの IID を指定しています。

HRESULT hr = patient->QueryInterface(IID_IInfantPatient, (void**)&child);

また、前回は記事中では触れませんでしたがサンプルコードとして提供したVC++ 用の QueryInterface メソッドの実装では要求されたインターフェイスを以下のように識別していました。

if (IsEqualIID(riid, IID_IPatient)||IsEqualIID(riid, IID_IUnknown)) 

これと同じ方法を使えば、患者の側から医師に対して、自分がただの IPatient なのか、それとも IInfantPatient なのかを能動的に通知することができます。このようなやり方をとるには、IDoctor::Treat メソッドを以下のように定義すればよかったのです。

HRESULT Treat([in] REFIID riid, [in, iid_is(riid)]IUnknown *patient, [out, retval] long *ret); 

前回 IDL の書き方を説明した中で、インターフェイスのメソッドの各引数にも属性がつけられる、という話がありました。このように [iid_is(parameter_iid)] 属性を引数につけてやると、総称的な IUnknown というインターフェイス(COM オブジェクトなら必ず IUnknown インターフェイスを実装しています)をとるように見える引数が実はその前のriid引数で指定された IID のインターフェイスを表す引数であることを明示的に伝えることができます。この場合、Treat メソッド呼び出しは:

dr->Treat(patient, &howmuch);

ではなく:

dr->Treat(IID_IInfantPatient, (IUnknown*)patient, &howmuch); 

と呼び出せるようになります。Treatメソッドの実装コードでは:

HRESULT hr = patient->QueryInterface(IID_IInfantPatient, (void**)&child); 

ではなく:

if (IsEqualIID(riid, IID_IInfantPatient)) 

という形で渡された COM オブジェクトが IInfantPatient を実装しているかどうかを判断できます。これはサーバー クライアント間の往復を必要としませんから、こちらのほうが高速なのは容易に想像がつくと思います。これによって似た機能を持つ複数のインターフェイスをサポートするメソッドの実行が高速になるのですが、残念ながらこの方法は VB では使えません。VB では上のように宣言されたメソッドを呼び出すことも Implements することもできないので注意してください。

まとめ

今回はシステムのバージョンアップに伴う問題を COM ではどう解決するかをご紹介しました。COM ではインターフェイスをベースに開発を進めますので、バージョンアップの単位もインターフェイスになります。インターフェイスは絶対に変更できないので、バージョンアップするにはインターフェイスを新しく定義することになります。ただし新しいインターフェイスを古いインターフェイスから継承させることで新旧両方のインターフェイスを実装させることができますので、COM オブジェクト側が新しくなったからといって、古いインターフェイスを前提に開発されているクライアントアプリケーションに手を入れる必要はありません。

また、あるインターフェイスをサポートするかどうかを確認する QueryInterface というメソッドをすべての COM オブジェクトが実装する(しなければならない)ので、新しいインターフェイスを前提に作られているクライアントでも、相手となるオブジェクトが新しいインターフェイスをサポートしているかどうかを問い合わせて、返答にあわせて挙動を変えることができるようにしています。個々のメソッドを実装するかどうかを個別に問い合わせて1つ 1つ呼び出す必要がないので、バージョンチェックも容易に行えるようにしています。これもインターフェイスベースプログラミングがもたらす恩恵の1つと言えるでしょう。

最後に

3回シリーズでお送りした COM プログラミングの基本でしたがいかがでしたでしょうか。中には「やはり小難しい」という感想をお持ちの方もいらっしゃることと思いますが、この連載でみなさんにお伝えしたかったことはたった1つだけです。それは「インターフェイスベースプログラミングとはどんなことで、どんなメリットがあるのか」ということです。COM がインターフェイスベースの開発を前提に作られている環境なので、COM の基本というタイトルで説明をしてきましたが、本来インターフェイスベースプログラミングは COM に依存する考え方ではありません。COM を使わなくても(使えない環境でも)インターフェイスベースの考え方を実践することはできます。Windows 2000 とともに Windows を利用したシステムも、より大規模化していくことでしょう。大規模分散システム開発では、従来のような「原則のない」開発手法は通用しません。デベロッパーのみなさんがそんな壁に突き当たったとき、インターフェイスベースという考え方を思い出していただけるようならば、この連載の目標は達せられたと思います。そうは言っても、Windows デベロッパーにとって COM の理解が今後ますます重要になっていくことも間違いありません。本稿は COM プログラミングの基本ということですから、「インターフェイスとは」というような観念的な議論だけで終わらせないように、COM プログラミングの実例を使って解説するように心がけました。COM はプログラミング言語に依存しないフレームワークなので、あえて3つの開発環境に合わせてそれぞれ実装コードの説明を入れましたが、3つの言語をマスターしなければ COM は理解できない、ということでは決してありません。今必要な、あるいは自分のもっとも得意な開発環境に関する部分だけをご覧いただくだけで、他のコードまで詳細に理解する必要はありません(筆者自身、3つともマスターしているわけではないことはコードをご覧いただければおわかりいただけるでしょう)。

それではまたお会いする日まで。Happy COM programming!

なお、MSDN Online Japan - Column This Week では、皆さんからのご意見・ご要望・ご感想などをお待ちしております。kksbnreq@microsoft.com までメールでお送りください。皆さんのご意見を反映した記事にしていければと考えております。お待ちしております。

[1] VB ではメソッドを追加しても違反になりませんが、これは VB がわざとチェックを甘くするために COM の裏技を使っているからです。COM 的には好ましくありません。

[2] Halloween の日、子供たちが仮装して家々を回り「Trick or TREAT」(何かくれないといたずらしちゃうぞ)と言ってお菓子をもらうのにひっかけてみました。

著者近影Fumiaki Yoshimatsu: 株式会社アスキーNT Non-MS Link に勤務し、主として 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 によるディレクトリ対応アプリケーションの開発」のセッションスピーカーを務めました。

© 2000 Microsoft Corporation. All rights reserved.


表示: