P/Invokeにおいて “メンバ変数として文字列データを含む構造体” をマネージドコード(C#)からアンマネージド(native)関数へ渡す方法メモ。
注意:本記事中のC#コードでは例外安全を考慮しておらず、確保したメモリブロックのリークが発生しうる。
メモ:アンマネージド側はマルチバイト文字セット(MBCS)を仮定している。また本記事中ではメモリブロック管理にCoTaskMem系メソッドを利用しているが、確保/解放共にマネージドコード(C#)から行うため、正しく対になってさえいればHGlobal系メソッドで代替可能。(マネージド/アンマネージド境界を越えてメモリ確保/解放を行う場合のみ留意する)
前提補足
本記事に示すアンマネージド関数であれば、自前のマーシャリング処理を記述する必要は無く、externメソッドのパラメータとして直接 “C#構造体への参照” または “C#クラス” を渡すだけでよい。下記コードではアンマネージド側に必要となるデータ全体が自動的にマーシャリングされ、同時にメモリブロック管理も自動的に行われる。
// caller0.cs struct Data { ... } [DllImport(...)] extern void NativeFunc([In] ref Data data); Data data; NativeFunc(ref data); // OK
後述の各サンプルコードのように構造体も自前メモリブロックで管理する方式は、該当構造体のポインタ型が他の構造体メンバとして含まれるような複雑なケースで必要とされる。*1
文字列へのポインタ型を含む場合
アンマネージド(native)構造体が “文字列へのポインタ型(char*
など)” を含む場合、マネージド側(C#)ではMarshalAs
属性=UnmanagedType.LPStr
としたstring
型メンバを対応させる。このData1
は参照型をメンバとして含むため、StructureToPtr
メソッドでメモリブロックへマーシャリングした後に、DestroyStructure
メソッドを呼び出さないとメモリリークとなる。
// native.dll struct Data1 { DWORD m1; char* str; // 文字列へのポインタ型 void* m2; }; void WINAPI NativeFunc1(const Data1 *data);
// caller1.cs [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)] struct Data1 { public int m1; [MarshalAs(UnmanagedType.LPStr)] public string str; public IntPtr m2; } [DllImport("native.dll")] extern void NativeFunc1(IntPtr data); void CallNativeFunc1(ref Data1 data) { IntPtr p = Marshal.AllocCoTaskMem(Marshal.SizeOf(data)) Marshal.StructureToPtr(data, p, false); NativeFunc1(p); Marshal.DestroyStructure(p, typeof(data)); // ★必須 Marshal.FreeCoTaskMem(p); }
文字列へのポインタ型を含む場合(手動マーシャリング)
メンバへのMarshalAs
属性を指定する代わりに、IntPtr
メンバ+StringToCoTaskMemXxx
メソッドを用いて、アンマネージド(native)側が要求するデータ構造を構築することも出来る。プログラマが手動管理すべきメモリブロックが増えるため、よほど特殊な事情がない限りは避けること。
// caller1'.cs [StructLayout(LayoutKind.Sequential)] struct Data1 { public int m1; public IntPtr pStr; public IntPtr m2; } void CallNativeFunc1(ref Data1 data, string str) { IntPtr p = Marshal.AllocCoTaskMem(Marshal.SizeOf(data)) data.pStr = Marshal.StringToCoTaskMemAnsi(str); Marshal.StructureToPtr(data, p, false); NativeFunc1(p); Marshal.DestroyStructure(p, typeof(data)); Marshal.FreeCoTaskMem(data.pStr); Marshal.FreeCoTaskMem(p); }
文字型配列を含む場合
アンマネージド(native)構造体が “文字型の配列(char[N]
など)” を含む場合、マネージド側(C#)ではMarshalAs
属性=UnmanagedType.ByValTStr
としたstring型メンバを対応させ、同時にアンマネージド側での配列要素数も指定する。Data2
のメンバには参照型を含まないためDestroyStructure
メソッドは実際には何も行わないが、記述の一貫性やコード拡張を考慮すると常に呼び出した方がよい。
// native.dll #define BUFSIZ 64 struct Data2 { DWORD m1; char str[BUFSIZ]; // 文字型配列 void* m2; }; void WINAPI NativeFunc2(const Data2 *data);
// caller2.cs const int BUFSIZE = 64; [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)] struct Data2 { public int m1; [MarshalAs(UnmanagedType.ByValTStr, SizeConst=BUFSIZE)] public string str; public IntPtr m2; } [DllImport("native.dll")] extern void NativeFunc2(IntPtr data); void CallNativeFunc2(ref Data2 data) { IntPtr p = Marshal.AllocCoTaskMem(Marshal.SizeOf(data)) Marshal.StructureToPtr(data, p, false); NativeFunc2(p); Marshal.DestroyStructure(p, typeof(data)); // (省略可能) Marshal.FreeCoTaskMem(p); }
関連URL
*1:アンマネージド側:「struct Data {...}; struct ParentData { struct Data* pData;... };」に対して、マネージド側:「struct Data {...} struct ParentData { IntPtr pData;...}」など。