🎃

C#からC++のDLLを呼ぶ

2023/03/03に公開

C++とC#それぞれに資産がある場合は,できれば2言語同時に動かしたい。特にC++をクラスライブラリ・C#をUIなどを含めた呼び出し側とした運用は快適で,高速演算と大規模開発向きのオブジェクト指向プログラミングを両立させることができる。

環境

  • Windows
  • Visual Studio (>= 2019)
    • C++によるデスクトップ開発
    • .NETデスクトップ開発

方法

  • 初めに以下の文言を入れる。
#ifdef __cplusplus
#define DLLEXPORT extern "C" __declspec(dllexport)
#else
#define DLLEXPORT __declspec(dllexport)
#endif

<解説>
C++特有の「名前マングリング」でDLL内の関数名がわからなくなるのを避けるためにC言語の関数としてエクスポートするよ,という宣言でextern "C"をつける。ちなみにマングリングが行われた場合でも「dumpbin」なるコマンドを使えば名前を特定できる。でもそれはキモイし面倒だからオススメしません。

  • 次に関数たちを以下の形で定義する。
DLLEXPORT <type> __stdcall <function name>(<args>)
{
   ...
}

<解説>
__stdcallは呼び出し規約のひとつ。規約のデフォルト値はC++では__cdecl,C#では__stdcallである。 これらは両者で一致している必要があり,どちら側から合わせてもよいが,ここでは__stdcallで統一している。 また,ここでは.cppファイルしか作っていないが,.hと.cppに分けてもよい。

  • DLLの出力先はC#の.exeから参照できる階層にするか,手動でコピーする。
  • 呼び出し側のC#は以下のように記述する。
using System.Runtime.InteropServices;
 
public static class Wrapper
{
    [DllImport("DLL_NAME.dll")]
    public static extern (return value) (function name)(args);
}
  • 気になるのはどんな型なら受け渡しできるのかということだろう。ざっと例を挙げると,
C++ C#
int int
int& ref int
int*(ポインタ) out int
int*(配列) IntPtr
int**(配列のポインタ) out IntPtr
unsigned char byte
char sbyte
char*(文字列) StringBuilder
  • C++のstringは組み込み型じゃないので使えません。注意。

  • 本当はクラスごとエクスポートしたいが,C++とC#間でそれはできない。構造体なら可能だが,それもちょっとめんどくさいのでオススメはしません。

  • 戻り値を構造化することが難しいので,一つの関数でたくさん値を受け渡ししたい場合は引数にポインタ渡しをいっぱい取るのが簡単かなあと思います。

  • 配列をやりとりする場合は何も言わなかったらメモリサイズが伝わらないので,C#側でマーシャリングというのを行う必要がある。以下のように,呼び出しの前後で仰々しくメモリサイズの確保と解放を行う。

void CallCppFunction(int[] array)
{
    IntPtr ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(int)) * array.Length);
    Marshal.Copy(array, 0, ptr, array.Length);
    CppDllFunction(ptr);
    Marshal.FreeCoTaskMem(ptr);
}

混合デバッグ

  • C#側で呼び出すときに,プロジェクトの「プロパティ」->「デバッグ」->「ネイティブコードのデバッグ」を有効にしておくと,C++側にブレークポイントを当てて変数を見たりできる。 これを「混合デバッグ」という。とても強い機能だが,C++側にバグがあったり正常終了できなかったりするとVisual Studioがフリーズすることがあるので注意。
  • うまくいかないときはC++側に問題があることが多い。dumpbinやdependenciesなどのDLL解析系ツールを試してみよう。

Discussion