yohhoyの日記

技術的メモをしていきたい日記

文字列を含む構造体のP/Invoke

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;...}」など。