COM
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しなければならない。 このためには、ヘッダーを読むのが手っ取り早い。
COMインターフェースの定義
例として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を使うのはナンセンスなので、書かなくて良いかなあと。
Rustでの表現
愚直にすべてのメソッドを定義するのは億劫であるため、プログラムに使わないメソッドにはダミー型を割り当てることにする。 例えば、絶対にありえないことではあるが、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);
■