EternalWindows
COMサーバー / IDLファイルの作成

COMオブジェクトが実装できるインターフェースは、IPersistFileのような既存のインターフェースだけでなく、 開発者が用意したカスタムインターフェースも可能です。 カスタムインターフェースを使用する利点は、インターフェースに含まれるメソッドを自由に決定できることであり、 これによってオブジェクトを操作できる方法は大きく増えることになります。 一方、カスタムインターフェースの欠点は、それが独自に定義されたインターフェースであるという関係上、 インターフェースの情報を予めクライアント側に渡さなければならないという点です。 オブジェクトにアクセスするクライアントがC/C++アプリケーションである場合は、 インターフェースの情報を定義したIDLファイルをクライアントに渡すのが一般的です。

カスタムインターフェースの設計は、IDLファイルを作成するところから始まります。 IDL(Interface Definition Language)はインターフェース定義言語とも呼ばれ、 クライアントがサーバーのアクセスに使用するインターフェースの情報を記述するために使用されます。 IDLで記述されたファイルはIDLファイルと呼ばれ、これをIDLコンパイラでコンパイルすれば、 実際にインターフェースを使用するためのファイル群が作成されることになります。 たとえば、Visual Studioに付属しているMIDLというコンパイラは、 インターフェースの型を定義したヘッダーファイルを作成しますから、 クライアントはこれをインクルードすることで、カスタムインターフェースを使用できるようになります。 この他、リモート間の呼び出しを可能にするためのプロキシやスタブを含んだソースファイルも作成されます。

IDLは言語ですが、その文法はC/C++と非常に近いものとなっています。 次にIDLファイルの例を示します。

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

[
	object,
	uuid(A09AFC29-31EA-462a-9DF0-40A7D2D16E76)
]
interface IFileControl : IUnknown
{
	typedef [v1_enum] enum tagFILEMODE {
		FM_READ = 0,
		FM_WRITE = 1
	} FILEMODE;

	HRESULT CreateFile([in] LPWSTR lpszFileName, [in] FILEMODE filemode); 
	HRESULT ReadFile([in] DWORD dwLength, [out] LPWSTR *lplp);
	HRESULT WriteFile([in] LPWSTR lpszData, [in] DWORD dwLength);
	HRESULT CloseFile();
	HRESULT FilePos([in] DWORD dwPos);
}

[
	uuid(79BDE8FF-CEE2-4c6d-A7B2-BED85A67A708)
]
coclass MyServer
{
	[default] interface IFileControl;
}

cpp_quote("DEFINE_GUID(CLSID_MyServer, 0x79bde8ff, 0xcee2, 0x4c6d, 0xa7, 0xb2, 0xbe, 0xd8, 0x5a, 0x67, 0xa7, 0x8);")

Visual Studioのバージョンによっては、「プロジェクト」の「新しい項目の追加」からIDLファイルを追加することができます。 このとき、作成されたIDLファイルの先頭には、oaidl.idlとocidl.idlのインポートが自動で記述されますが、 この部分は削除せずそのままにして構いません。 理由は、これらのIDLファイルが様々な型やインターフェースを定義しているからであり、 これをインポートすることでコンパイルが成功するからです。 IDLファイルにおける1つの情報は、[](ヘッダ)と{}(ボディ)に分けて記述することになっています。 何の情報に関するものかはボディの直前の属性から分かるようになっており、 上記の例であればinterfaceになっています。 この場合は、ボディにインターフェースのIIDなどを記述し、 ヘッダにはインターフェースに含まれるメソッドを記述します。 それぞれの部分を詳細に見ていきます。

[
	object,
	uuid(A09AFC29-31EA-462a-9DF0-40A7D2D16E76)
]

object属性は、これから定義するインターフェースがCOMインターフェースであり、 RPCのインターフェースでないことを意味します。 uuid属性は、GUIDを指定します。 今回はinterfaceを使用していたため、このGUIDはインターフェースのIIDを識別しています。 GUIDはguidgen.exeなどで作成したものを指定するようにします。 上記には記述されていませんが、ヘッダにはlocal属性を指定することがあります。 この属性、このインターフェースのためのネットワークコードを生成しないことを意味します。 続いて、ボディの部分を確認します。

interface IFileControl : IUnknown
{
	typedef [v1_enum] enum tagFILEMODE {
		FM_READ = 0,
		FM_WRITE = 1
	} FILEMODE;

	HRESULT CreateFile([in] LPWSTR lpszFileName, [in] FILEMODE filemode); 
	HRESULT ReadFile([in] DWORD dwLength, [out] LPWSTR *lplp);
	HRESULT WriteFile([in] LPWSTR lpszData, [in] DWORD dwLength);
	HRESULT CloseFile();
	HRESULT FilePos([in] DWORD dwPos);
}

interface IFileControlと記述することにより、IFileControlを定義できます。 カスタムインターフェースといえど、それはCOMインターフェースには変わりありませんから、 COMの規則通りIUnknownを継承するようにします。 enumによって列挙子を定義しているのは、 ファイルのモードを専用の定数で表したいからです。 v1_enum属性は、列挙子のサイズを既定の16ビットではなく32ビットに設定します。 CreateFileというメソッドは、ファイルの作成またはオープンを行います。 第1引数はファイル名であり、第2引数は読み取りか書き込みを表すモードを指定します。 このようにデータを渡すことを目的とした引数にはin属性を指定します。 ReadFileは、ファイルから第1引数の数だけデータを読み取り、それを第2引数に返します。 この場合は、データを渡すのではなく受け取ることを目的としているため、out属性を指定するようにします。 WriteFileは、第2引数の数だけ第1引数のデータを書き込みます。 CloseFileは、ファイルを閉じる場合に呼び出します。 FilePosは、ファイルポインタの位置を調整するために呼び出します。

IDLファイルにインターフェースの情報を記述することにより、 作成されるヘッダーファイルにはインターフェースの定義やIIDが含まれるようになります。 しかし、ヘッダーファイルには、インターフェースだけではなくCOMオブジェクトの情報も含まれてほしいはずです。 たとえば、クライアントがCoCreateInstanceを呼び出す際にはオブジェクトのCLSIDが必要になりますから、 こうしたCLISDも定義される必要があります。

[
	uuid(79BDE8FF-CEE2-4c6d-A7B2-BED85A67A708)
]
coclass MyServer
{
	[default] interface IFileControl;
}

cpp_quote("DEFINE_GUID(CLSID_MyServer, 0x79bde8ff, 0xcee2, 0x4c6d, 0xa7, 0xb2, 0xbe, 0xd8, 0x5a, 0x67, 0xa7, 0x8);")

既に述べたように、IDLファイルの1つの情報は[]と{}の組み合わせによって記述されます。 上記の場合の情報というのはcoclassであり、これはCOMオブジェクトを識別しています。 よって、uuidに指定したGUIDはオブジェクトのCLSIDになります。 default属性は、オブジェクトを既定で識別するインターフェース指定します。 Visual BasicやJAVAではオブジェクトをnewというキーワードで作成することになりますが、 その作成したオブジェクトを何で識別できるかというと、defaultが指定されたインターフェースになります。 cpp_quoteは、ヘッダーファイルにC++コードを挿入するために使用できます。 CLSIDの実体はソースファイルに含まれることになりますが、 ヘッダーファイルには定義が含まれないため、 上記のように定義を含めようとしています。 ただし、coclassをLibrary属性の中に含める場合は、これは不要です。

IDLファイルを作成したら、それをコンパイルすることになります。 Visual StudioのプロジェクトにIDLファイルを追加している場合は、 プロジェクトをビルドするだけでIDLファイルがMIDLによってコンパイルされ、 次に示すファイルが作成されることになります。 IDLファイルの名前はsample.idlであると仮定します。

ファイル 説明
sample_h.h インターフェースが定義されている。
sample_p.c インターフェースのリモートアクセスを可能にするためのマーシャリングコードを含み、プロキシ/スタブの役割を果たす。
sample_i.c IIDやCLSIDの実体が定義されている。
sample.tlb IDLファイルにLibrary属性を指定した場合は、このようなタイプライブラリも作成される。
dlldata.c プロキシ/スタブDLLを作成するために必要なデータを含む。

sample_h.hとsample_i.cは、クライアントとサーバーの両方が使用することになります。 一方、sample_p.cとdlldata.cは、プロキシ/スタブDLLを作成する場合に使用します。 プロキシ/スタブDLLは、サーバーのアパートと異なるアパートに属するスレッドからのアクセスに対応したい場合や、 アウトプロセスサーバーを作成する際に必要となります。 各種ファイルを使用してコンパイルする際には、構成プロパティ/C++/詳細の「コンパイル言語の選択」が既定置になっていなければ失敗することがあります。

IDLファイルのコンパイルによって、sample_h.hにはIFileControlが定義されるようになりました。 よって、これを継承したクラスを定義します。

class CMyServer : public IFileControl
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();

	STDMETHODIMP CreateFile(LPWSTR lpszFileName, FILEMODE filemode); 
	STDMETHODIMP ReadFile(DWORD dwLength, LPWSTR *lplp);
	STDMETHODIMP WriteFile(LPWSTR lpszData, DWORD dwLength);
	STDMETHODIMP CloseFile();
	STDMETHODIMP FilePos(DWORD dwPos);
	
	CMyServer();
	~CMyServer();
	
private:
	LONG   m_cRef;
	HANDLE m_hFile;
};

クラスの名前はこれまでと同じようにCMyServerとなっています。 IFileControlにはCreateFileからFilePosまでのメソッドがありましたから クラスはこれらをオーバーライドすることになります。 これで、クライアントはIFileControlでオブジェクトへアクセスできるようになります。 各メソッドの実装内容は名前が示す通りですが、 CreateFileについては少し補足しておきます。

STDMETHODIMP CMyServer::CreateFile(LPWSTR lpszFileName, FILEMODE filemode)
{
	DWORD dwDesiredAccess;
	DWORD dwCreationDisposition;

	if (filemode == FM_READ) {
		dwDesiredAccess = GENERIC_READ;
		dwCreationDisposition = OPEN_EXISTING;
	}
	else if (filemode == FM_WRITE) {
		dwDesiredAccess = GENERIC_WRITE;
		dwCreationDisposition = CREATE_ALWAYS;
	}
	else
		return S_FALSE;

	m_hFile = ::CreateFileW(lpszFileName, dwDesiredAccess, 0, NULL, dwCreationDisposition, FILE_ATTRIBUTE_NORMAL, NULL);

	return HRESULT_FROM_WIN32(GetLastError());
}

FILEMODEの値がFM_READである場合は、ファイルをオープンしてデータを読み取るようにしたいため、 dwDesiredAccessとdwCreationDispositionにそのような定数を指定しています。 一方、FM_WRITEの場合は新しくファイルを作成してそこに書き込みたいため、 そのような定数を指定しています。 関数の呼び出しに::を付けているのは、メソッドと名前が重複しているのを区別するためです。


戻る