ライブラリの基礎 - C++DLL
概要
C# EXEからC++ DLLへ様々なデータ型の変数を渡したいときがある。
サンプルコード
例えば、C++ DLLから次のような関数がエクスポートされているとする。
void WINAPI ConvertToShort(char *pstr, short *pret);
上記の関数において、C# EXEから使用する時は、第1引数のchar*
型は文字列なので、string
型を渡す。
第2引数のshort*
型は、IntPtr
型を渡す。(IntPtr型は汎用ポインタを表す型であり、void*型とほぼ同義)
ただし、C#は厳しい型付け言語なので、曖昧さを解決するために変換メソッドを経由する必要がある。
具体的には、IntPtr
型の変数にMarshal.AllocHGlobal
関数で必要なサイズのメモリを確保して、それをC++ DLLに渡した後、
Marshal.ReadInt16
関数(型によって異なる)等で変換した後、確保したメモリをMarshal.FreeHGlobal
関数で解放するというプロセスを経る必要がある。
以下の例では、C++ DLLを呼ぶC# EXEのソースコードを記述している。
// DllImportを使用するために必要
using System.Runtime.InteropServices;
[DllImport("DrsUtil.dll", EntryPoint = "ConvertToShort")]
extern static void _ConvertToShort(string pstr, IntPtr pret); //呼び出し元の名前変えちゃう
public static short ConvertToShort(string str)
{
IntPtr buffer = new IntPtr();
buffer = Marshal.AllocHGlobal(2); // 2バイトのメモリ確保
_ConvertToShort(str, buffer); // C++/DLLの関数を呼ぶ
short sval = Marshal.ReadInt16(buffer); // 変換
Marshal.FreeHGlobal(buffer); // メモリ開放
return sval;
}
IntPtr
型の変数は様々なものが入る。
ただし、構造体を取得することもできるが、C# EXEで構造体を定義しなければならない。
ネイティブコードと.NET Frameworkでは型の管理方法が違うため、実際には型の相互変換(マーシャリング)が行われる。
なお、Windows APIではBOOL型の実体はLONG型なので、.NET Frameworkではboolの代わりにintを指定することも可能である。
APIの型名 (括弧内は対応するC言語の型) |
対応するC#の型 (括弧内は.NET Frameworkでの型名) |
備考 |
---|---|---|
HANDLE (void *) | System.IntPtr System.UIntPtr |
x86は4バイト x64は8バイト |
BYTE (unsigned char) | byte (System.Byte) | |
SHORT (short) | short (System.Int16) | |
WORD (unsigned short) | ushort (System.UInt16) | |
INT (int) LONG (long) |
int (System.Int32) | |
UINT (unsigned int) DWORD, ULONG (unsigned long) |
uint (System.UInt32) | |
BOOL (long) | bool (System.Boolean) | |
CHAR (char) | 文字を渡すとき char (System.Char) 文字を受け取るとき StringBuilder |
|
WCHAR(wchar_t) | 文字を渡すとき char (System.Char) 文字を受け取るとき StringBuilder |
|
LPSTR (char *, char[]) LPWSTR (wchar_t *, wchar_t[]) |
文字を渡すとき string (System.String) 文字を受け取るとき System.Text.StringBuilder |
|
LPCSTR (const char *, const char[]) LPCWSTR (const wchar_t *, const wchar_t[]) |
文字を渡すとき string (System.String) 文字を受け取るとき System.Text.StringBuilder |
|
FLOAT (float) | float (System.Single) | |
DOUBLE (double) | double (System.Double) |
サンプルコード
C++ DLLの作成方法はライブラリの基礎 - DLLの作成(C/C++/MFC)を参照する。
下記にもC++ DLLを記述する。
SampleDLL.h
double __stdcall SampleFunc01(int a);
void __stdcall SampleFunc02(int a, char *pstr)
void __stdcall SampleFunc03(int a, char *pstr)
void __stdcall SampleFunc04(SampleStruct st)
void __stdcall SampleFunc05(SampleStruct *pStructure)
SampleDll.cpp
#include <stdio.h>
#include <string.h>
#include "SampleDll.h"
typedef struct tagSampleStruct
{
int index;
char name[128];
int data[50];
} SampleStruct, *pSampleStruct;
typedef struct tagSampleStruct2
{
int length;
double *data;
} SampleStruct2, *pSampleStruct2;
double __stdcall SampleFunc01(int a)
{
printf(--<SampleDll:SampleFunc01>--\r\n");
printf("a = %d\r\n", a);
printf("---------------------------\r\n");
return 3.14;
}
void __stdcall SampleFunc02(int a, char *pstr)
{
printf("--<SampleDll:SampleFunc02>--\r\n");
printf("[%d] %s\r\n", a, str);
printf("-----------------------------\r\n");
}
void __stdcall SampleFunc03(int a, char *pstr)
{
printf("--<SampleDll:SampleFunc03>--\r\n");
printf("[%d] %s\r\n", a, str);
sprintf_s(str, 256, "C++ DLL側から文字列を返す場合は、StringBuilderクラスを使用する");
printf("------------------------\r\n");
}
void __stdcall SampleFunc04(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");
}
void __stdcall SampleFunc05(SampleStruct2 *pStructure)
{
dData[256] = {0};
printf("--<SampleDll:Sample05>--\r\n");
memset(pStructure, 0, sizeof(SampleStruct2));
pStructure->length = 10;
pStructure->data = dData;
for(int i = 0; i < pStructure->length; i++)
{
dData[i] = (i + 1) / 10.0;
}
printf("------------------------\r\n");
}
SampleDll.def // モジュール定義ファイル
LIBRARY SampleDll
EXPORTS
; 公開する関数名をリストアップ
SampleFunc01 @1
SampleFunc02 @2
SampleFunc03 @3
SampleFunc04 @4
SampleFunc05 @5
次に、C# EXEからC++ DLLを呼び出す方法を記述する。
文字列をC++ DLL側に渡す場合は、string型を使用する。
文字列をC++ DLL側から返す場合は、string型ではなくStringBuilderクラスを使用する必要がある。
StringBuilderクラスは受け渡しの両方が可能なので、文字列はStringBuilderクラスを使用すべきである。
C++では構造体のサイズはコンパイル時に決定されるが、C#では実行時に決定される。
したがって、C#側で構造体のサイズを予め指定する必要がある。
この場合、構造体は固定長サイズとなるため、配列などを定義する場合は異なるサイズの配列を後からインスタンス化することができなくなる。
構造体をC++ DLL側から返す場合、IntPtr型からdouble型の配列を取得するときは、一度Int64型に変換し、これをBitConverter.Int64BitsToDouble()メソッドでdouble型に変換する。
using System;
using System.Text;
using System.Runtime.InteropServices;
namespace SampleEXE
{
class Program
{
/// <summary>
/// 最も基本的な関数のインポート例
/// </summary>
/// <param name="a">4 バイト符号付き整数を指定します。</param>
/// <returns>倍精度浮動小数を返します。</returns>
[DllImport("SampleDLL.dll")]
private static extern double SampleFunc01(int a);
[DllImport("SampleDLL.dll", CharSet = CharSet.Unicode)]
// C++ DLL側の文字コードがUnicodeの場合は"CharSet = CharSet.Unicode"と明示的に指定する
private static extern void SampleFunc02(int a, string str);
[DllImport("SampleDLL.dll", CharSet = CharSet.Unicode)]
// C++ DLL側の文字コードがUnicodeの場合は"CharSet = CharSet.Unicode"と明示的に指定する
private static extern void SampleFunc03(int a, StringBuilder str);
/// <summary>
/// 構造体を引数に持つ関数のインポート例
/// </summary>
/// <param name="st">DLL 側に渡す構造体を指定します</param>
[DllImport("SampleDLL.dll")]
private static extern void SampleFunc04(SampleStruct st);
/// <summary>
/// DLL側からメンバにポインタを含む構造体を受け取る関数のインポート例
/// </summary>
/// <param name="pst">受け渡す構造体の先頭アドレスを示すポインタを指定する</param>
[DllImport("SampleDLL.dll")]
private static extern void SampleFunc05(IntPtr pst);
/// <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;
}
/// <summary>
/// DLLとの取り合いのために定義する構造体
/// LayoutKind.Sequentialを指定することで、C/C++同様、変数の宣言順通りにメモリに配置されるようにする
/// </summary>
[StructLayout(LayoutKind.Sequential)]
private struct SampleStruct2
{
public int length;
public IntPtr data;
}
static void Main(string[] args)
{
var dRet= SampleFunc01(1);
Console.WriteLine(dRet);
Console.WriteLine();
var str = "string型で文字列を渡すことができます。";
SampleFunc02(2, str);
var strb = new System.Text.StringBuilder(256);
strb.Append("文字列のバッファを渡す場合は StringBuilder クラスで受け渡します。");
SampleFunc03(3, strb);
Console.WriteLine(strb);
var structHoge = new SampleStruct()
{
index = 4,
name = "構造体サンプル",
data = new int[50],
};
structHoge.data[0] = 11;
structHoge.data[1] = 22;
structHoge.data[2] = 33;
SampleFunc04(structHoge);
// SampleStruct2構造体のサイズを取得する
// 指定サイズ分だけメモリ領域を確保して、その先頭アドレスをstructPiyoに格納する
var structPiyo = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(SampleStruct2)));
try
{
SampleFunc05(structPiyo);
// 受け取ったstructPiyoからSampleStruct2構造体の情報に構築し直す
var structFuga = (SampleStruct2)Marshal.PtrToStructure(structPiyo, typeof(SampleStruct2));
for (int i = 0; i < structFuga.length; i++)
{
// IntPtr型からdouble型の数値を取得するときは、一度Int64型に変換して、これをBitConverter.Int64BitsToDoubleメソッドでdouble型に変換する
var v = Marshal.ReadInt64(structFuga.data, i * sizeof(double));
Console.WriteLine("data[{0}] = {1}", i, BitConverter.Int64BitsToDouble(v));
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
finally
{
// 必ずメモリを解放する
Marshal.FreeHGlobal(sample06_a);
}
Console.ReadKey();
}
}
}
DllImport
属性には、DLLファイルのパスを指定する以外にも、下表のような引数を与えることができる。
他にも、細かい設定をするための引数が用意されているので、DllImportAttribute
クラスで検索すること。
名称 | 説明 |
---|---|
CallingConvention | エントリポイントの呼び出し規約を明示的に指定できる。 指定なしの場合は__stdcallとなる。 |
CharSet | 文字列パラメータをメソッドにマーシャリングし、名前マングルを制御する方法を指定する。 文字コードの相互変換する時に指定する。 |
EntryPoint | 呼び出すDLLエントリポイントの名前または序数を指定する。 DLLの関数名とC#上で使用する関数名を異なる名前にする時に指定する。 |