思うだけで学ばない日記 2.0

思うだけで学ばない日記から移転しました☆!よろしくお願いします。

C#事始め

C#初心者が最初に学ぶべきことはマネージコード-アンマネージコード間のデータの受け渡しであることは論をまたない。
これを押さえれば、使い慣れた言語で生成した“Hello World!”文字列をC#に持ってきて表示させれは“Hello World!”のできあがりだからだ(何
これはIntPtrとかいったunsafeな言語機能を一切使わずとも実現できる。

C++からC#に文字列を渡す例

文字列のような多バイトデータのやりとりをする場合、次の点が常に問題になる。

  • 誰がメモリ上にバッファを用意するのか
  • 誰がそのバッファを解放するのか

これについて2通りのやり方を示す。

C++側で独自にメモリを確保・管理し、C#側で解放にタッチしないタイプ

呼び出し側(C#)。

using System.Runtime.InteropServices;
...
// 宣言
[DllImport("HelloWorldDll.dll", CharSet = CharSet.Ansi, EntryPoint = "GetHelloWorld1")]
public static extern void GetHelloWorld1(
    [Out, MarshalAs(UnmanagedType.LPStr)]
    out string msg);
...
// 呼び出し
string str;
GetHelloWorld1(out str);
Console.WriteLine("str1={0}", str);

呼び出される側(C++)

extern "C" __declspec(dllexport) void GetHelloWorld1(LPSTR* msg);  // 宣言。Cリンケージにしとくとカソタソ
__declspec(dllexport) void GetHelloWorld1(LPSTR* p_msg) {     // 引数がポインタのポインタなことに注意!
    static CHAR msg_buf[1000];
    sprintf(msg_buf, "Hello World!");
    *p_msg = msg_buf;
}

このように、呼び出される側では、DLL生存の間永続するメモリ領域を適当に確保しておき、それへのポインタを*p_msgに代入して戻ればいい。
string<-->LPSTR変換はマーシャラがやってくれる。マルチバイト文字列でなくてユニコードを使いたいときは、CharSet.UnicodeとLPWSTRを併用でもするといい*1

メモリの確保・解放をC#側にお任せするタイプ

呼び出し側(C#)。

using System.Runtime.InteropServices;
...
// 宣言
[DllImport("HelloWorldDll.dll", CharSet = CharSet.Ansi, EntryPoint = "GetHelloWorld2")]
public static extern void GetHelloWorld2(
    [Out, MarshalAs(UnmanagedType.LPStr, SizeParamIndex = 1)]
    StringBuilder msg, int siz);
...
// 呼び出し
StringBuilder sb = new StringBuilder(1000);
GetHelloWorld2(sb, 1000);
Console.WriteLine("str2={0}", sb.ToString());

呼び出される側(C++)

extern "C" __declspec(dllexport) void WINAPI GetHelloWorld2(LPSTR msg, int siz);  // 宣言。Cリンケージにしとくとカソタソ
extern __declspec(dllexport) void WINAPI GetHelloWorld2(LPSTR msg, int siz)       // こっちは普通にLPSTRでいい
{
    sprintf(msg, "This is a pen.");
}

このように、呼び出し側(C#)でStringBuilderを使いそいつにメモリの面倒を見させる。
謎の指定子SizeParamIndexは、バッファサイズを伝える引数を指定する。上の例だと2番目の引数を使っているのでSizeParamIndex = 1としている。

実行結果

str1=Hello World!!
str2=This is a pen.

応用編

C#側から渡した文字列をC++側で編集して戻したり、バイナリデータをやりとりしたり、構造体や共用体のやりとり等も適当にゴニョゴニョしればできる(ry
配列については

[In, Out, MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr, SizeParamIndex = 1)]

みたくしてマーシャラにLPArray指定で配列である旨を知らしめ、さらにArraySubTypeで要素の型を教えてやる。
構造体や共用体についてはStructLayout属性とかFieldOffset属性でググればよろし。あとはこれらの組み合わせ。

*1:CallerとCalleeで揃える必要があるから、こういった場合LPTSTRはあまり使う機会がない。