C/C++ による汎用 DLL を作成する その5

前回までで単純なメソッドを DLL から呼び出すことができるようになりました。
今回はメソッドに文字列型や構造体を含む場合について考えましょう。

C/C++ で次のような文字列を扱う関数を用意してみましょう。

与えられた文字列 str を標準出力するだけの関数です。
これを C# から使えるようにするには次のようにします。

C/C++ 側ではポインタ型でしたが、C# のほうではそのまま string 型のままで OK です。
実行結果は次のようになります。

クリップボード01

このように文字列を渡すだけだと簡単です。
ただし、C/C++ 側の文字コードが Unicode の場合は
明示的に指定する必要がある場合があります。


ところで、文字列を C/C++ 側からもらうときは string 型ではなく、StringBuilder クラスを使う必要があります。
例えば次のような関数を DLL で用意します。

与えられた文字列バッファ str に対して sprintf_s() 関数で文字列をセットしています。これを C# から呼び出すときは、入力引数を StringBuilder クラスにする必要があります。

実行結果は次のようになります。

クリップボード03

ここでは C# 側からも StringBuilder クラスで文字列を渡しています。
受け渡しどちらでもできるので、文字列型は StringBuilder クラスを使うようにしたほうがいいのかもしれません。


次に、構造体を扱う関数を考えてみましょう。
DLL 側では次のような関数を公開するようにします。

C++ では構造体のサイズはコンパイル時に決定されますが、C# では実行時に決定されます。したがって、C# 側で構造体のサイズをあらかじめ指定しておく必要があります。この場合、構造体は固定長サイズとなるため、例えば配列などを定義する場合は異なるサイズの配列を後からインスタンス化するようなことができなくなります。C# 側のコードは次のようになります。

構造体を定義するとき、MarshalAs 属性を付加することで各フィールドのサイズをコンパイル時に決定させることができます。この場合、配列の長さは指定した長さでしかインスタンス化できません。

実行結果は次のようになります。

クリップボード04

確かに構造体の値が DLL 側に渡されている様子がわかります。


今度は逆に構造体を返してもらいましょう。
DLL 側では次のような関数を公開するようにします。

C# 側では先ほどと同様に、構造体を定義するときに MarshalAs 属性を付けてサイズを固定化することに加え、関数の入力引数は SampleStruct 構造体ではなく IntPtr 構造体によるポインタを与えます。

Marshal.SizeOf() メソッドで SampleStruct 構造体のサイズを取得し、Marshal.AllocHGlobal() メソッドでそのサイズ分だけメモリ領域を確保し、その先頭アドレスをポインタ変数 sample05_a に格納しています。このとき、変数 sample05_a が用済みになった段階で必ず Marshal.FreeHGlobal() メソッドでメモリ領域を解放しないとメモリリークしてしまうので注意してください。

受け取ったポインタから SampleStruct 構造体の情報に構築し直すために、Marshal.PtrToStructure() メソッドを使用して変数 sample05_b に格納しています。

実行結果は次のようになります。

クリップボード05

実行結果を見てわかるように、
構造体メンバの配列の要素 data[3] に謎の数値が代入されています。
これは、DLL 側の関数の中で渡されたポインタが示すメモリ領域を memset() 関数などでゼロクリアしていないことから起こる現象です。DLL 内で初期化処理をしないとこのようなことが起こり得るので、memset() 関数などによるゼロクリアは必ずするようにしたほうが良いでしょう。

だいぶ長くなってしまいました。
次回はおそらく最終回。メンバにポインタを含む構造体の受け渡しに関して紹介します。