読者です 読者をやめる 読者になる 読者になる

アドレス指定 P/Invoke と COM メソッド呼び出し

.NET DirectX XNA

いつものように IRC でだべっていると,サークルの後輩が Managed DirectX 1.1 の Lock 処理で毎回マネージ配列確保が行われることにご立腹だったので*1C# から 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 の互換性という概念が厄介になるわけですな.id:Kazzz:20061109:p1#c1163071567)

[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 のようなバックドアを公開してくれないようですけど.

*1:GC 回数を切り詰めていくと,動的頂点バッファ Lock 処理での配列確保が目立つようになってきます

*2:id:akiramei:20040418