アドレス指定 P/Invoke と COM メソッド呼び出し
いつものように IRC でだべっていると,サークルの後輩が Managed DirectX 1.1 の Lock 処理で毎回マネージ配列確保が行われることにご立腹だったので*1,C# から Unmanaged DirectX の COM メソッド呼び出しコードを書いてみました.これはそのメモ書きです.
まずは基本から.
.NET で任意アドレスを関数の先頭アドレスと思って call する方法は,DynamicModule や Lightweight Code Generation による IL Emit などが有名です*2 .この方法の欠点は,標準マーシャラによるマーシャリングが行われないため自分でマーシャリングを行う必要があるところでしょう.
一方 .NET 2.0 で追加されたMarshal.GetDelegateForFunctionPointer を使用すると,標準マーシャラが適用される .NET delegate を関数アドレスから動的に作成することができます.
使い方はこんな感じ.
まず DllImport と同じ感覚で,属性によってマーシャリング方法を属性で指定した delegate を定義します.(余談ですが,こういうところで属性を使うから,timeBeginPeriodProc と System.Query.Func
[UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate int timeBeginPeriodProc(uint uPeriod);
ただしここでは『DllImport 属性』ではなく『UnmanagedFunctionPointer 属性』を使用します.
delegate が定義できたら,後は Marshal.GetDelegateForFunctionPointer に関数アドレスと delegete 型を渡すだけです.
timeBeginPeriodProc timeBeginPeriod = Marshal.GetDelegateForFunctionPointer( funcptr, typeof(timeBeginPeriodProc)) as timeBeginPeriodProc;
これで IntPtr からデリゲートの作成については問題ないでしょう.『Dynamically Writing and Executing Native Assembly in C#』で紹介されているように,C++/CLI を使わずとも,x86 コードを動的生成して call なんてことも可能です.
さて,COM の呼び出し規約は,COM オブジェクトのアドレスの指す番地に,ある interface の仮想関数テーブルのアドレスが書かれていることを期待しています.
言うならばこんな感じ.
[StructLayout(LayoutKind.Explicit)] unsafe struct ComObject { [FieldOffset(0x00)] public VTable* vptr; }
んで,VTable の構造はどうなっているかというと,例えば IDirect3DBaseTexture9 の VTable ならこんな感じですかねと.
(追記)IUnknown 定義メンバの順序が間違っていたのを修正.
// IDirect3DBaseTexture9 [StructLayout(LayoutKind.Explicit)] struct VTable { [FieldOffset(0)] public IntPtr QueryInterface; [FieldOffset(1*4)] public IntPtr AddRef; [FieldOffset(2*4)] public IntPtr Release; // 中略 [FieldOffset(19*4)] public IntPtr LockRect; [FieldOffset(20*4)] public IntPtr UnlockRect; }
また,COM の呼び出し規約は this ポインタを先頭に積む StdCall です.
[UnmanagedFunctionPointer(CallingConvention.StdCall)] delegate int LockRectProc( IntPtr thisPtr, // this call uint level, [Out] out D3DLOCKED_RECT lockedRect, [In] IntPtr zero, // NULL を渡すために IntPtr を使用している LockFlags flag);
Managed DirectX では,UnmanagedComPointer という名前のフィールドで COM の生ポインタが公開されていますので,ここから VTable 経由でメソッドをたぐります.
律儀にポインタ計算を行っても良いですし,以下のようにキャストで代用するという方法もあります.
unsafe { ComObject* obj = (ComObject*) texture.UnmanagedComPointer; LockRectProc LockRect = Marshal.GetDelegateForFunctionPointer( obj->vptr->LockRect, typeof(LockRectProc)) as LockRectProc; // this を最初に積む // これ以降一切 texture オブジェクトが参照されない場合は texture オブジェクトの GC に注意 int hr = LockRect( texture.UnmanagedComPointer, .... ) // texture オブジェクトの GC を明示的に避けたければ // この辺にでも GC.KeepAlive( texture ) を書く }
このように,目的のメソッドの VTable インデックスさえわかれば,COM ポインタから任意のアンマネージ COM メソッドを呼ぶのはそれ程難しくはありません.この作業は,C++/CLI などを使う必要もなく,C# のコードのみで完結します.
残念ながら,XNA のラッパクラスは UnmanagedComPointer のようなバックドアを公開してくれないようですけど.