Component Object Model(COM)とは、Windowsを形成する開発体系である。
COMはC++ファーストで記述されている。 このことは、非常に厄介である。 例として、VulkanとDirect3D12のFenceの作成を比較する。
VulkanのAPIは主に、オブジェクトのハンドルと関数だけである。 Fenceを作成するには、vkCreateFence関数の第一引数に論理デバイスVkDeviceを渡す。
vkCreateFence(device, &fence_create_info, NULL, &fence);
一方で、COMのAPIは、オブジェクト指向なインターフェースである。 Fenceを作成するには、ID3D12Device::CreateFenceを呼び出す必要がある。 関数ではなく、メソッドなのである。
しかし、Windows APIはC言語でも扱えるようにされている。 というか、恐らくC++の登場より、COMの登場の方が早いので当然C言語で扱える。 当然ながらC言語はオブジェクト指向をサポートしていない。 そこでCOMは、関数テーブルによってメソッドを実現している。 関数ポインタを大量に持った構造体を持った構造体をインターフェースとしているのである。
このことは、とても有名なので、COMについて少し調べたり、C言語でCOMを扱ったことがあれば、知っているはずである。 C言語でID3D12Device::CreateFenceを呼び出すには以下のように書く。
device->lpVtbl->CreateFence( device, 0, D3D12_FENCE_FLAG_NONE, __uuidof(ID3D12Fence), (void **)&fence );
で、問題はこれをどうやってRustで実装するのか、ということである。 Rustはオブジェクト指向をサポートしていないため、C言語でのCOMを愚直にFFIしなければならない。 このためには、ヘッダーを読むのが手っ取り早い。
例としてID3D12Deviceを取り上げる。 C言語向けのID3D12Deviceは、d3d12.hに以下のように定義されている。
typedef struct ID3D12DeviceVtbl { BEGIN_INTERFACE DECLSPEC_XFGVIRT(IUnknown, QueryInterface) HRESULT ( STDMETHODCALLTYPE *QueryInterface )( ID3D12Device *This, REFIID riid, _COM_Outptr_ void **ppvObject); DECLSPEC_XFGVIRT(IUnknown, AddRef) ULONG ( STDMETHODCALLTYPE *AddRef )( ID3D12Device *This); DECLSPEC_XFGVIRT(IUnknown, Release) ULONG ( STDMETHODCALLTYPE *Release )( ID3D12Device *This); DECLSPEC_XFGVIRT(ID3D12Object, GetPrivateData) HRESULT ( STDMETHODCALLTYPE *GetPrivateData )( ID3D12Device *This, _In_ REFGUID guid, _Inout_ UINT *pDataSize, _Out_writes_bytes_opt_( *pDataSize ) void *pData); /* 中略 */ DECLSPEC_XFGVIRT(ID3D12Device, GetAdapterLuid) LUID *( STDMETHODCALLTYPE *GetAdapterLuid )( ID3D12Device *This, LUID *RetVal); END_INTERFACE } ID3D12DeviceVtbl; interface ID3D12Device { CONST_VTBL struct ID3D12DeviceVtbl *lpVtbl; };
非常に目障りなコードをしている。 およそ、人間向けには書かれていない。 これを、C言語向けにマクロ展開すると以下のようになる。
typedef struct ID3D12DeviceVtbl { HRESULT (__stdcall *QueryInterface)( ID3D12Device *This, const IID *const riid, void **ppvObject); ULONG (__stdcall *AddRef)(ID3D12Device *This); ULONG (__stdcall *Release)(ID3D12Device *This); HRESULT (__stdcall *GetPrivateData)( ID3D12Device *This, const GUID *const guid, UINT **pDataSize, void *pData); /* 中略 */ LUID *(__stdcall *GetAdapterLuid)(ID3D12Device *This, LUID *RetVal); } ID3D12DeviceVtbl; struct ID3D12Device { struct ID3D12DeviceVtbl *lpVtbl; };
関数テーブルを持っているだけである。 インスタンスを生成する際、関数がこのポインタに設定されるわけだから、確かに仮想関数的と言える。 そして、呼び出し規約がstdcallである関数ポインタを構造体に詰められれば、FFIできそうだ。
ちなみに、Direct3D9のCOMの定義はもっといかつい見た目をしているが、流石に今更DX9を使うのはナンセンスなので、書かなくて良いかなあと。
愚直にすべてのメソッドを定義するのは億劫であるため、プログラムに使わないメソッドにはダミー型を割り当てることにする。 例えば、絶対にありえないことではあるが、ID3D12Device::CreateFenceだけを用いるプログラムの場合、以下のようにID3D12Deviceを定義すればいい。
type DummyFunction = *const c_void; #[repr(C)] struct ID3D12DeviceVtbl { QueryInterface: DummyFunction, AddRef: DummyFunction, Release: DummyFunction, GetPrivateData: DummyFunction, /* 中略 */ CreateFence: extern "stdcall" fn( This: *const ID3D12Device, InitialValue: UINT64, Flags: D3D12_FENCE_FLAGS, riid: *const IID, ppFence: *mut *const c_void, ) -> HRESULT, /* 中略 */ GetAdapterLuid: DummyFunction, } #[repr(C)] struct ID3D12Device { lpVtbl: *const ID3D12DeviceVtbl, }
以下のように呼び出す。
let mut fence = std::ptr::null(); ((*(*device).lpVtbl).CreateFence)(device, 0, D3D12_FENCE_FLAG_NONE, &iid, &mut fence);
■