前回までで単純なメソッドを DLL から呼び出すことができるようになりました。
今回はメソッドに文字列型や構造体を含む場合について考えましょう。
C/C++ で次のような文字列を扱う関数を用意してみましょう。
1 2 3 4 5 6 |
void __stdcall Sample02(int a, char* str) { printf("--<SampleDll:Sample02>---------------\r\n"); printf("[%d] %s\r\n", a, str); printf("-------------------------------------\r\n"); } |
与えられた文字列 str を標準出力するだけの関数です。
これを C# から使えるようにするには次のようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/// <summary> /// 文字列を引数に持つ関数のインポート例 /// </summary> /// <param name="a">4 バイト符号付き整数を指定します。</param> /// <param name="str">文字列を指定します。</param> [DllImport("Tips_Win32DLL.dll")] private static extern void Sample02(int a, string str); static void Main(string[] args) { #region Sample02 var sample02_a = "string 型で文字列を渡すことができます。"; Sample02(2, sample02_a); #endregion Sample02 Console.ReadKey(); } |
C/C++ 側ではポインタ型でしたが、C# のほうではそのまま string 型のままで OK です。
実行結果は次のようになります。
このように文字列を渡すだけだと簡単です。
ただし、C/C++ 側の文字コードが Unicode の場合は
明示的に指定する必要がある場合があります。
1 2 |
[DllImport("Tips_Win32DLL.dll", CharSet = CharSet.Unicode)] private static extern void Sample02(int a, string str); |
ところで、文字列を C/C++ 側からもらうときは string 型ではなく、StringBuilder クラスを使う必要があります。
例えば次のような関数を DLL で用意します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <stdio.h> #include <string.h> #include "SampleDll.h" namespace Tips_Win32DLL { void __stdcall Sample03(int a, char* str) { printf("--<SampleDll:Sample03>---------------\r\n"); printf("[%d] %s\r\n", a, str); sprintf_s(str, 256, "DLL 側から文字列を返す場合は StringBuilder クラスを使用します。"); printf("-------------------------------------\r\n"); } } |
与えられた文字列バッファ str に対して sprintf_s() 関数で文字列をセットしています。これを C# から呼び出すときは、入力引数を StringBuilder クラスにする必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
namespace Tips_Win32DllImport { using System; using System.Runtime.InteropServices; using System.Text; class Program { /// DLL 側から文字列を受け取る関数のインポート例 /// </summary> /// <param name="a">4 バイト符号付き整数を指定します。</param> /// <param name="str">文字列を受け渡すバッファを指定します。</param> [DllImport("Tips_Win32DLL.dll")] private static extern void Sample03(int a, StringBuilder str); static void Main(string[] args) { #region Sample03 var sample03_a = new System.Text.StringBuilder(256); sample03_a.Append("文字列のバッファを渡す場合は StringBuilder クラスで受け渡します。"); Sample03(3, sample03_a); Console.WriteLine(sample03_a); #endregion Sample03 Console.ReadKey(); } } } |
実行結果は次のようになります。
ここでは C# 側からも StringBuilder クラスで文字列を渡しています。
受け渡しどちらでもできるので、文字列型は StringBuilder クラスを使うようにしたほうがいいのかもしれません。
次に、構造体を扱う関数を考えてみましょう。
DLL 側では次のような関数を公開するようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <stdio.h> #include <string.h> #include "SampleDll.h" typedef struct _SampleStruct { int index; char name[128]; int data[50]; } SampleStruct, *PSampleStruct; namespace Tips_Win32DLL { void __stdcall Sample04(SampleStruct st) { printf("--<SampleDll:Sample04>---------------\r\n"); printf("index = %d\r\n", st.index); printf("name = %s\r\n", st.name); printf("data[0] = %d, data[1] = %d, data[2] = %d, data[3] = %d\r\n", st.data[0], st.data[1], st.data[2], st.data[3]); printf("-------------------------------------\r\n"); } } |
C++ では構造体のサイズはコンパイル時に決定されますが、C# では実行時に決定されます。したがって、C# 側で構造体のサイズをあらかじめ指定しておく必要があります。この場合、構造体は固定長サイズとなるため、例えば配列などを定義する場合は異なるサイズの配列を後からインスタンス化するようなことができなくなります。C# 側のコードは次のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
namespace Tips_Win32DllImport { using System; using System.Runtime.InteropServices; class Program { /// <summary> /// 構造体を引数に持つ関数のインポート例 /// </summary> /// <param name="st">DLL 側に渡す構造体を指定します</param> [DllImport("Tips_Win32DLL.dll")] private static extern void Sample04(SampleStruct st); /// <summary> /// DLL との取り合いのために定義する構造体です。 /// LayoutKind.Sequential を指定することで、 /// C/C++ 同様、変数の宣言順通りにメモリに配置されるようにされます。 /// </summary> [StructLayout(LayoutKind.Sequential)] private struct SampleStruct { /// <summary> /// 4 バイト符号付整数 /// </summary> [MarshalAs(UnmanagedType.I4)] public int index; /// <summary> /// 固定長文字配列 (SizeConst は配列のサイズを示す) /// </summary> [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string name; /// <summary> /// 固定長配列 (SizeConst は配列の要素数を示す) /// </summary> [MarshalAs(UnmanagedType.ByValArray, SizeConst = 50)] public int[] data; } static void Main(string[] args) { #region Sample04 var sample04_a = new SampleStruct() { index = 4, name = "構造体サンプル", data = new int[50], }; sample04_a.data[0] = 11; sample04_a.data[1] = 22; sample04_a.data[2] = 33; Sample04(sample04_a); #endregion Sample04 Console.ReadKey(); } } } |
構造体を定義するとき、MarshalAs 属性を付加することで各フィールドのサイズをコンパイル時に決定させることができます。この場合、配列の長さは指定した長さでしかインスタンス化できません。
実行結果は次のようになります。
確かに構造体の値が DLL 側に渡されている様子がわかります。
今度は逆に構造体を返してもらいましょう。
DLL 側では次のような関数を公開するようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <stdio.h> #include <string.h> #include "SampleDll.h" typedef struct _SampleStruct { int index; char name[128]; int data[50]; } SampleStruct, *PSampleStruct; namespace Tips_Win32DLL { void __stdcall Sample05(SampleStruct* st) { printf("--<SampleDll:Sample05>---------------\r\n"); //memset(st, 0, sizeof(SampleStruct)); (*st).index = 5; sprintf_s((*st).name, 128, "構造体ポインタサンプル"); (*st).data[0] = 11; (*st).data[1] = 22; (*st).data[2] = 33; printf("-------------------------------------\r\n"); } } |
C# 側では先ほどと同様に、構造体を定義するときに MarshalAs 属性を付けてサイズを固定化することに加え、関数の入力引数は SampleStruct 構造体ではなく IntPtr 構造体によるポインタを与えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
namespace Tips_Win32DllImport { using System; using System.Runtime.InteropServices; class Program { /// <summary> /// DLL 側から構造体を受け取る関数のインポート例 /// </summary> /// <param name="st">受け渡す構造体の先頭アドレスを示すポインタを指定します。</param> [DllImport("Tips_Win32DLL.dll")] private static extern void Sample05(IntPtr st); /// <summary> /// DLL との取り合いのために定義する構造体です。 /// LayoutKind.Sequential を指定することで、 /// C/C++ 同様、変数の宣言順通りにメモリに配置されるようにされます。 /// </summary> [StructLayout(LayoutKind.Sequential)] private struct SampleStruct { /// <summary> /// 4 バイト符号付整数 /// </summary> [MarshalAs(UnmanagedType.I4)] public int index; /// <summary> /// 固定長文字配列 (SizeConst は配列のサイズを示す) /// </summary> [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string name; /// <summary> /// 固定長配列 (SizeConst は配列の要素数を示す) /// </summary> [MarshalAs(UnmanagedType.ByValArray, SizeConst = 50)] public int[] data; } static void Main(string[] args) { #region Sample05 var sample05_a = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(SampleStruct))); try { Sample05(sample05_a); var sample05_b = (SampleStruct)Marshal.PtrToStructure(sample05_a, typeof(SampleStruct)); Console.WriteLine("index = " + sample05_b.index); Console.WriteLine("name = " + sample05_b.name); Console.WriteLine("data[0] = {0}, data[1] = {1}, data[2] = {2}, data[3] = {3}", sample05_b.data[0], sample05_b.data[1], sample05_b.data[2], sample05_b.data[3]); Console.WriteLine("DLL 側できちんと初期化していないと data[3] に値が現れることに注意する必要がある。"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } finally { // 必ずメモリを解放するようにする Marshal.FreeHGlobal(sample05_a); } #endregion Sample05 Console.ReadKey(); } } } |
Marshal.SizeOf() メソッドで SampleStruct 構造体のサイズを取得し、Marshal.AllocHGlobal() メソッドでそのサイズ分だけメモリ領域を確保し、その先頭アドレスをポインタ変数 sample05_a に格納しています。このとき、変数 sample05_a が用済みになった段階で必ず Marshal.FreeHGlobal() メソッドでメモリ領域を解放しないとメモリリークしてしまうので注意してください。
受け取ったポインタから SampleStruct 構造体の情報に構築し直すために、Marshal.PtrToStructure() メソッドを使用して変数 sample05_b に格納しています。
実行結果は次のようになります。
実行結果を見てわかるように、
構造体メンバの配列の要素 data[3] に謎の数値が代入されています。
これは、DLL 側の関数の中で渡されたポインタが示すメモリ領域を memset() 関数などでゼロクリアしていないことから起こる現象です。DLL 内で初期化処理をしないとこのようなことが起こり得るので、memset() 関数などによるゼロクリアは必ずするようにしたほうが良いでしょう。
だいぶ長くなってしまいました。
次回はおそらく最終回。メンバにポインタを含む構造体の受け渡しに関して紹介します。