【C#】構造体配列のポインタを持つ構造体のマーシャリング

C#の相互運用(C#からネイティブDLLの呼び出しの場合)で構造体の中に構造体配列のポインタを持つ関数のマーシャリングのやり方です。

ネイティブDLL側の宣言

ネイティブのDLL側の宣言は以下のようになっているとします。

// Sample.h

// 外部に公開する関数
extern "C" __declspec(dllexport) int WINAPI foo(MyStructTop* top)
{
    top->len = 3;
    top->childs = (MyStructChild*)malloc(sizeof(MyStructChild) * top->len);

    for (int i = 0; i < top->len; i++)
    {
        top->childs[i].a = i;
        top->childs[i].b = i + 1;
        top->childs[i].c = i + 2;
    }

    return 0x10;
}

// メモリ開放用の関数
extern "C" __declspec(dllexport) void WINAPI freeObj(void* p)
{
    free(p);
}

中身でmallocしているので、最後にメモリを解放するためにfree関数をラップ下freeObjも併せて公開しています。

内容は動作確認のためのサンプルとして適当に構造体を3つ確保しています。

上記の引数の構造体は以下のようになっています。

// 子要素の構造体
typedef struct
{
    int a;
    int b;
    int c;
} MyStructChild;

// 親要素の構造体
typedef struct
{
    // 割り当てた構造体のサイズ
    int len;
    // 動的に確保した子要素の配列の先頭ポインタ
    MyStructChild* childs;
} MyStructTop;

C#側の実装方法

C#側は上記の関数と同じ名前の関数をDllImportで宣言します。

// // 外部に公開する関数
// extern "C" __declspec(dllexport) int WINAPI foo(MyStructTop* top)
[DllImport("NativeDLL.dll", EntryPoint = "foo")]
public static extern int Foo(ref MyStructTop top); // refつけないとポインタ扱いにならない

// // メモリ開放用の関数
// extern "C" __declspec(dllexport) void WINAPI free(void* p)
[DllImport("NativeDLL.dll", EntryPoint = "freeObj")]
public static extern int FreeObj(IntPtr target);

foo(MyStructTop* top) は IntPtr ではなく ref を付けて構造体をそのまま渡すように宣言するのがポイントです。

次に、ネイティブ側の構造体と同じサイズの構造体をC#側でも宣言します。

[StructLayout(LayoutKind.Sequential)]
public struct MyStructTop
{
    // // 親要素の構造体
    // typedef struct
    // {
    //     // 割り当てた構造体のサイズ
    //     int len;
    //     // 動的に確保した子要素の配列の先頭ポインタ
    //     MyStructChild* childs;
    // } MyStructTop;

    public int Length;

    public IntPtr Childs;
}

[StructLayout(LayoutKind.Sequential)]
public struct MyStructChild
{
    // // 子要素の構造体
    // typedef struct
    // {
    //     int a;
    //     int b;
    //     int c;
    // } MyStructChild;

    public int A;
    public int B;
    public int C;
}

サイズさえ合っていれば大丈夫です。ネイティブ側で「MyStructChild* childs;」と宣言していた個所は、「IntPtr Childs」でポインタを受け取るようにしています。

次に「IntPtr Childs」の構造体ポインタを.NETの構造体への変換の仕方です。

public static void Main(string[] args)
{
    // まずはref付きで普通に呼び出す
    var top = new MyStructTop();
    Foo(ref top);

    // ポインタの子要素の.NET側の入れ物を同じサイズで用意する
    var childs = new MyStructChild[top.Length];

    // 構造体のネイティブのサイズを取得
    int size = Marshal.SizeOf(typeof(MyStructChild));

    for (int i = 0; i < top.Length; i++)
    {
        // サイズと位置があっていればOKの精神でポインタを加算
        IntPtr address = top.Childs + size * i;
        childs[i] = Marshal.PtrToStructure<MyStructChild>(address);
    }

    // 最後にメモリを開放する
    FreeObj(top.Childs);

    Console.WriteLine(top.Length);
    for(int i = 0; i < top.Length; i++)
    {
        Console.WriteLine($"A = {childs[i].A}, B = {childs[i].B}, C ={childs[i].C}");
    }
    > 実行結果
    > 3
    > A = 0, B = 1, C =2
    > A = 1, B = 2, C =3
    > A = 2, B = 3, C =4
}

これで、出力結果もあっているので無事変換できました。

MyStructChildのサイズをtop.Childsのアドレスに.NET上で足し算で加算します。コメントにも書きましたが、ここら辺はサイズさえ合っていればOKの精神です。

また、.NETの構造体に移し終えたらChildsのポインタは解放しておきます。