« Trenz社 UltraScale+ SOM新シリーズ「AM0010」が登場 | トップページ | ADCのプリアンプとしてオペアンプ4種類を評価してみた »

2023.04.09

C++で作ったDLLから配列をC#で安全に受け取る方法

コアのライブラリはC++で作ってDLL化し、上位層をC#で作りたいということがよくあります。

その場合のやり方を調べたのですが、まぁ、なんというかなかなか本質的な情報にたどり着かないですね。フリーランス向けのWeb塾みたいなページで広告が出てきてクリックしないと消せず、最後まで読んでも結局やりたいことが書いていないとか・・

 

さて、C++でこんな関数を作りたいと思います。

void get_hogehoge(int maxch, double* volts, double* phases)

maxch個の要素を持つvolts[]とphases[]に値をセットして返す関数だと思ってください。

 

プロトタイプの宣言は

CSZAPI void WINAPI get_hogehoge(int maxch, double* volts, double* phases);

として、頭にCSZAPIを、戻り値と関数名の間にWINAPIを付けておきます。

このCSZAPIは自分のライブラリ用に作った適当なマクロの名前で、 __declspec(dllexport) に解釈されます。

 

さて、C++の関数の中では、

for(int i=0;i<maxch;i++) volts[i] = ・・・;

などと書いて値をセットして戻ります。

 

C#のプログラムではDllImportとMarshalを使うために

using System.Runtime.InteropServices;

が必要です。

クラス内の宣言で、DLLの関数を呼び出すために

[DllImport("libほげほげ.dll")]
protected static extern void get_hogehoge(int maxch, IntPtr volts, IntPtr phases);

として、受け取る型はIntPtrにします。最初、out double[]でやとうとしたらクラッシュしました。

プログラムのメインの部分では、

double[] Volts = new double[32];
double[] Phases = new double[32];

として値が格納される配列を作っておき、

IntPtr pVolts = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(double)) * 32);
IntPtr pPhases = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(double)) * 32);

でIntPtr型の変数を作ります。

そして、

get_hogehoge(32, pVolts, pPhases);
Marshal.Copy(pVolts, Volts, 0, 32);
Marshal.Copy(pPhases, Phases, 0, 32);

とします。C#からC++を呼び出すときに、Marshalで作ったIntPtrのメモリにデータを格納して、それを普通に作った配列にコピーするというしくみです。C#のバッファって、たぶん、突然アドレスが変わったりすることが変わるのでしょう。newで普通に確保した配列にDLLの中から値セットしてはいけません。(unsafedとfixedを使えばできなくはない)

使い終わったら、

Marshal.FreeCoTaskMem(pVolts);
Marshal.FreeCoTaskMem(pPhases);

で解放します。

高速性を求めるならunsafeなコードでfixedを使うというやりかたもありますが、そこまで必要でなければMarshalを使うのが安全だと思います。

★補足説明

CSZAPIの定義は

#if defined (__cplusplus)
#ifdef CSZWIN_EXPORTS
#define CSZAPI extern "C" __declspec(dllexport)
#else
#define CSZAPI extern "C" __declspec(dllimport)
#endif
#else
#ifdef CSZWIN_EXPORTS
#define CSZAPI __declspec(dllexport)
#else
#define CSZAPI __declspec(dllimport)
#endif
#endif
#endif

です。

関数のプロトタイプを集めたヘッダファイルの先頭で宣言しておきます。このヘッダファイルをincludeする前にCSZWIN_EXPORTSをあらかじめdefineしておけばdllexportになり、CSZWIN_EXPORTをdefineしていなければdllimportになります。

これで、DLLのソースか、DLLを使うプログラムかで使い分けられるようになっています。

このマクロは、

  • C++なら、extern "C" __declspec(dllexport)になり
  • Cなら、__declspec(dllexport)になります

これで関数名の先頭に_(アンダーバー)が付いたりすることなく、引数に@数字とか付かず、CとC++で名前が一致するようになります。

 

 

 

|

« Trenz社 UltraScale+ SOM新シリーズ「AM0010」が登場 | トップページ | ADCのプリアンプとしてオペアンプ4種類を評価してみた »

コメント

初心者の私の場合は、マーシャル関係は、C++/CLI で記述しますかね。
.NET 側では、そういった細々としたのは考えさせないようにします。

個人内部で完結するならともかく、水平展開する場合はとくに。


投稿: yh | 2023.04.30 13:21

連投すみません。少しめんどいのは、VC++で参照渡しのAPIを使った場合ですね。
.NET(C#)側でメモリー解放とかを考えさせるのはよくないですので、

そうさせないようにVC++側で解放の実装とか要りますね。

初心者の私はポインタを学習中ですが、 VC++ DLL ← → .NET DLL の実装から出発
すると、比較的スムーズに意味がわかるようになりました・・

投稿: yh | 2023.04.30 13:25

コメントを書く



(ウェブ上には掲載しません)




« Trenz社 UltraScale+ SOM新シリーズ「AM0010」が登場 | トップページ | ADCのプリアンプとしてオペアンプ4種類を評価してみた »