ライブラリの基礎 - C++DLL
概要
C# EXEからC++ DLLへ様々なデータ型の変数を渡したいときがある。
DllImport属性
DLLImport
属性は、DLLエントリポイントを定義する関数を記述することで、DLLファイルに定義された関数を呼び出すことができる。
[ DllImport( "DLL名" ) ]
また、DLLファイルに定義された関数を、関数名または序数で指定するには、DllImport
属性のEntryPoint
フィールドを使用する。
メソッド定義内の関数の名前がDLLエントリポイントと同じである場合は、その関数をEntryPoint
フィールドで明示的に識別する必要はない。
同じではない場合は、次のいずれかの記述で名前または序数を指定する。
以下の例では、EntryPoint
フィールドを使用して、MessageBoxA
をMsgBoxAに置き換える方法を示している。
using System;
using System.Runtime.InteropServices;
internal static class NativeMethods
{
[DllImport("user32.dll", EntryPoint = "MessageBoxA")]
internal static extern int MsgBoxA(IntPtr hWnd, string lpText, string lpCaption, uint uType);
}
また、以下の例では、DLLファイルに定義された関数を序数で指定している。
[DllImport("DllName", EntryPoint = "Functionname")]
[DllImport("DllName", EntryPoint = "#123")] // 序数を使用するには、序数値の前にシャープ記号#を付ける必要がある
DllImport
属性には、DLLファイル名を指定する以外にも、下表のような引数を与えることができる。
名称 | 説明 |
---|---|
EntryPoint | 呼び出すDLLエントリポイントの名前または序数を指定する。 DLLの関数名とC#上で使用する関数名を異なる名前にする時に指定する。 |
CharSet | 文字列パラメータをメソッドにマーシャリングして、名前マングルを制御する方法を指定する。 文字コードの相互変換する時に指定する。 指定なしの場合は、 CharSet.Auto となる。
|
SetLastError | Win32エラー情報を維持するかどうか指定する。 指定なしの場合は、 false となる。
|
ExactSpelling | エントリポイントの関数名を厳密に一致させるかどうか指定する。 指定なしの場合は、 false となる。
|
PreserveSig | 定義通りのメソッドのシグネチャを維持するかどうか指定する。 |
CallingConvention | アンマネージドコードを呼び出すためのエントリポイントの呼び出し規約を、明示的に指定できる。 指定なしの場合は、 __stdcall (StdCall)となる。詳細は、下表を参照すること。 |
詳細は、以下のMSDNのWebサイトを参照すること。
DllImportAttribute クラス (System.Runtime.InteropServices) | MSDN
- 呼び出し規約(Calling Convention)
- CallingConvention列挙型で、アンマネージドコードを呼び出すための呼び出し規約を指定する。
- 下表に、CallingConvention列挙型を示す。
- また、実行時に指定のDLLを読み込めない場合、モジュールが見つからないとして、
FileNotFoundException
やDllNotFoundException
等の例外が投げられる。
- CallingConvention列挙型で、アンマネージドコードを呼び出すための呼び出し規約を指定する。
System.IO.FileNotFoundException はハンドルされませんでした。 Message: 型 'System.IO.FileNotFoundException' のハンドルされていない例外が mscorlib.dll で発生しました 追加情報:ファイルまたはアセンブリ '**.dll'、またはその依存関係の 1 つが読み込めませんでした。 指定されたモジュールが見つかりません。 これは参照しているDLLがない、またはそのDLLが依存しているDLLが適切に配置されていない場合に発生します。 これは問題のDLLをdumpbinで調べ、それを配置することで解決できます。
列挙子 | 説明 |
---|---|
Cdecl | 呼び出し元がスタックを消去する。 これを使用すると、 varargs で関数を呼び出すことができる。これは、 printf 関数等の可変長引数のメソッドの呼び出しで使用する。
|
StdCall | 呼び出し先がスタックを消去する。 アンマネージド関数を呼び出す既定値内のテキスト。 |
ThisCall | 最初の引数がthisポインタで、その他の引数はスタックにプッシュされる。 アンマネージドDLLからエクスポートしたクラスのメソッドを呼び出すために使用する。 |
Winapi | プラットフォームに応じた既定の呼び出し規約を使用する。 例えば、WindowsではStdCall、Windows CE.NETではCdecl、LinuxではCdeclである。 (正確には、この設定は呼び出し規約ではない) |
FastCall | この呼び出し規約はサポートされていない。 |
以下の例では、呼び出し規約を適用する方法をCdeclとしている。
これは、呼び出し元がスタックをクリーンアップするために使用する必要がある。
using System;
using System.Runtime.InteropServices;
internal static class NativeMethods
{
// C#は可変長引数をサポートしていないため、全ての引数を明示的に定義する必要がある。
// スタックは呼び出し元によってクリーンアップされるため、CallingConvention.Cdeclを使用する必要がある。
// int printf( const char *format [, argument]... )
[DllImport("msvcrt.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
internal static extern int printf(String format, int i, double d);
[DllImport("msvcrt.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
internal static extern int printf(String format, int i, String s);
}
public class App
{
public static void Main()
{
NativeMethods.printf("\nPrint params: %i %f", 99, 99.99);
NativeMethods.printf("\nPrint params: %i %s", 99, "abcd");
}
}
詳細は、以下のMSDNのWebサイトを参照すること。
CallingConvention 列挙型 (System.Runtime.InteropServices) | MSDN
StructLayout属性
StructLayout属性とは、クラスまたは構造体のデータメンバを、メモリ内でどのように配置するかを表す。
[ StructLayout( LayoutKind列挙 ) ]
下表に、LayoutKind列挙子を示す。
LayoutKind列挙子 | 説明 |
---|---|
Sequential | 宣言される順番に従って並べる。 |
Explicit | FieldOffsetAttribute で独自のオフセットを指定して並べる。
|
Auto | 適切なレイアウトで並べる。 (これを指定すると、マネージドコード外からアクセスできない) |
下表に、StructLayout属性のパラメータを示す。
StructLayout属性のパラメータ | 説明 | 既定値 |
---|---|---|
Pack | パックサイズを指定するint値である。 指定可能な値は、1、2、4、8、16のいずれかである。 |
8 |
CharSet | 文字列のマーシャリング方法を示すCharSet列挙である。 | CharSet.Auto |
Size | 構造体またはクラスのサイズを指定する。 |
[StructLayout( LayoutKind.Sequential )]
public struct Position
{
public double x;
public double y;
public double z;
}
詳細は、以下のMSDNのWebサイトを参照すること。
StructLayoutAttribute クラス (System.Runtime.InteropServices) | MSDN
CLSCompliant属性
CLSCompliant属性は、CLSへの準拠を検証するかどうかをコンパイラに指示する。
外部から参照できない型やメンバに対しては、この属性を指定する必要は無い。
指定する場合、このアセンブリの外から認識できないため、CLS準拠の確認は '型' で実行されません
と警告が表示される。
以下の例では、UInt32型はCLSに準拠しないため、CLSCompliant(false)
と指定する必要がある。
[CLSCompliant( false )]
public int SetValue( UInt32 value );
以下の例では、CLSCompliantAttribute属性をアセンブリ全体に適用する。
using System;
[assembly: CLSCompliant(true)]
詳細は、以下のMSDNのWebサイトを参照すること。
CLSCompliantAttribute クラス (System) | MSDN
SuppressUnmanagedCodeSecurity属性
SuppressUnmanagedCodeSecurity属性は、アンマネージドコードの呼び出し時に実行されたスタックウォークを、実行時に省いて効率を大幅に向上させる。
詳細は、以下のMSDNのWebサイトを参照すること。
SuppressUnmanagedCodeSecurityAttribute クラス | MSDN
MarshalAs属性
MarshalAs属性は、マネージドコードとアンマネージドコードの間で、データをマーシャリングする方法を指定する。
詳細は、以下のMSDNのWebサイトを参照すること。
MarshalAsAttribute クラス (System.Runtime.InteropServices) | MSDN
// パラメータへの適用
public void M1([MarshalAs(UnmanagedType.LPWStr)]string msg)
{
}
// クラスのフィールドへの適用
class MsgText
{
[MarshalAs(UnmanagedType.LPWStr)]
public string msg = "Hello World";
}
// 戻り値への適用
[return: MarshalAs(UnmanagedType.LPWStr)]
public string GetMessage()
{
return "Hello World";
}
In属性 / Out属性
In属性 / Out属性は、データの渡し方を指示する。
説明 | |
---|---|
In属性 | 呼び出し側にデータをマーシャリングして渡すことを示す。 |
Out属性 | 呼び出し元にデータをマーシャリングして戻すことを示す。 |
void Method([in] int[] array);
C#のデータ型とWindows APIのデータ型
Windows 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
int __stdcall SampleFunc01(int a);
double __stdcall SampleFunc02(double a);
// SampleDll.cpp
#include <stdio.h>
#include <string.h>
#include "SampleDll.h"
int __stdcall SampleFunc01(int a)
{
printf(--<SampleDll:SampleFunc01>--\r\n");
printf("a = %d\r\n", a);
printf("---------------------------\r\n");
return 10;
}
double __stdcall SampleFunc02(double a)
{
printf("--<SampleDll:SampleFunc02>--\r\n");
printf("a = %f\r\n", a);
printf("---------------------------\r\n");
return 3.14;
}
// SampleDll.def // モジュール定義ファイル
LIBRARY SampleDll
EXPORTS
; 公開する関数名をリストアップ
SampleFunc01 @1
SampleFunc02 @2
次に、C# EXEからC++ DLLを呼び出す方法を記述する。
using System;
using System.Text;
using System.Runtime.InteropServices;
namespace SampleEXE
{
class Program
{
[DllImport("SampleDLL.dll")]
private static extern int SampleFunc01(int a);
[DllImport("SampleDLL.dll")]
private static extern double SampleFunc02(double a);
static void Main(string[] args)
{
var iRet= SampleFunc01(1);
Console.WriteLine(iRet);
Console.WriteLine();
var dRet= SampleFunc01(0.01);
Console.WriteLine(dRet);
Console.WriteLine();
Console.ReadKey();
}
}
}
ポインタのマーシャリング
例えば、C++ DLLから次のような関数がエクスポートされているとする。
void WINAPI ConvertToString(char *pstrRet);
void WINAPI ConvertToShort(short *psRet);
void WINAPI ConvertToDouble(double *pdRet);
上記の関数において、C# EXEから使用する時、ConvertToString関数の引数のchar*
型は文字列なので、string
型またはStringBuilder
型を渡す。
ConvertToShort関数の引数のshort*
型は、IntPtr
型を渡す。
(IntPtr
型は汎用ポインタを表す型であり、void*
型とほぼ同義)
ConvertToDouble関数の引数のdouble*
型は、IntPtr
型を渡す。
ただし、C#は厳しい型付け言語のため、曖昧さを解決するために、変換メソッドを経由する必要がある。
具体的には、IntPtr
型の変数にMarshal.AllocHGlobal
メソッドで必要なサイズのメモリを確保して、それをC++ DLLの関数へ受け渡しを行う。
Marshal.ReadInt16
メソッド(型により異なる)等で変換した後、確保したメモリをMarshal.FreeHGlobal
関数で解放するというプロセスを経る必要がある。
以下の例では、C++ DLLを呼ぶC# EXEのソースコードを記述している。
using System.Runtime.InteropServices; // DllImportを使用するために必要
// 呼び出し元の関数名を変更する
// C++ DLL側の文字コードがUnicodeの場合は"CharSet = CharSet.Unicode"と明示的に指定する
[DllImport("Sample.dll", EntryPoint = "ConvertToString", CharSet = CharSet.Unicode)]
extern static void _ConvertToString(StringBuilder pstrRet);
[DllImport("Sample.dll", EntryPoint = "ConvertToShort")]
extern static void _ConvertToShort(IntPtr psRet);
[DllImport("Sample.dll", EntryPoint = "ConvertToDouble")]
extern static void _ConvertToDouble(IntPtr pdRet);
public static string ConvertToString()
{
// 値を受け渡しを行う場合、StringBuilderクラスを使用する
var strRet = = new System.Text.StringBuilder(256);
// 値を渡すだけの場合、string型を使用する
//string strRet = "";
// C++ DLLの関数を呼ぶ
_ConvertToString(strRet);
return strRet;
}
public static short ConvertToShort()
{
// 2バイトのメモリ確保
IntPtr buffer = new IntPtr();
buffer = Marshal.AllocHGlobal(2);
// C++ DLLの関数を呼ぶ
_ConvertToShort(buffer);
// 2バイトのメモリをshort型に変換
short sRet = Marshal.ReadInt16(buffer);
// メモリの開放
Marshal.FreeHGlobal(buffer);
return sRet;
}
public static double ConvertToDouble()
{
// 8バイトのメモリ確保
IntPtr buffer = new IntPtr();
buffer = Marshal.AllocHGlobal(8);
// C++ DLLの関数を呼ぶ
_ConvertToDouble(buffer);
// IntPtr型からdouble型の数値を取得する
// Int64型へ変換後、BitConverter.Int64BitsToDoubleメソッドでdouble型に変換する
var i64Ret = Marshal.ReadInt64(buffer);
var dRet = BitConverter.Int64BitsToDouble(i64Ret);
// 必ずメモリを解放する
Marshal.FreeHGlobal(buffer);
return dRet;
}
IntPtr
型の変数は様々なものが入る。
構造体を取得することもできるが、C# EXEで構造体を定義しなければならない。
ネイティブコードと.NET Frameworkでは型の管理方法が違うため、実際には型の相互変換(マーシャリング)が行われる。
なお、Windows APIではBOOL型の実体はLONG型なので、.NET Frameworkではboolの代わりにintを指定することも可能である。
配列のマーシャリング
このセクションでは、C# EXEからC++ DLLへ配列を渡す方法を記載する。
まず、以下にC++ DLLを記述する。
// SampleDLL.h
#pragma once
int __stdcall SampleArray(int array[], int length);
// SampleDll.cpp
#include <stdio.h>
#include <string.h>
#include "SampleDll.h"
void __stdcall SampleArray(int array[], int length)
{
for (int i = 0; i < length; i++)
{
std::cout << array[i] << std::endl;
}
}
// モジュール定義ファイル
// SampleDll.def
LIBRARY SampleDll
EXPORTS
; 公開する関数名をリストアップ
SampleArray @1
次に、C# EXEからC++ DLLを呼び出す方法を記述する。
手順としては、以下の処理の流れとなる。
- C# EXEでマネージド配列(C# EXEの配列)を定義する。
- C# EXEにおいて、アンマネージド配列(C++ DLLの配列)のメモリを確保する。
- マネージド配列の内容を、上記で確保したアンマネージド配列のメモリにコピーする。
- C++ DLLの関数を実行する時、ポインタを渡す。
- 使用したアンマネージド配列のメモリを解放する。
namespace SampleEXE
{
[DllImport("SampleDll.dll", CallingConvention = CallingConvention.Cdecl)]
static extern void SampleArray(IntPtr array, int length);
static void Main()
{
// 配列の定義
var array = new int[Length] { 0, 1, 2, 3, 4 };
// アンマネージド配列のメモリの確保
IntPtr ptrRet = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(int)) * array.Length);
// マネージド配列をアンマネージドにコピーする
Marshal.Copy(array, 0, ptrRet, array.Length);
// C++ DLLに配列を渡す(ポインタを渡す)
SampleArray(ptrRet, array.Length);
// アンマネージドメモリの解放
Marshal.FreeCoTaskMem(ptrRet);
}
}
文字列のマーシャリング
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)
// SampleDll.cpp
#include <stdio.h>
#include <string.h>
#include "SampleDll.h"
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");
}
// SampleDll.def // モジュール定義ファイル
LIBRARY SampleDll
EXPORTS
; 公開する関数名をリストアップ
SampleFunc01 @1
SampleFunc02 @2
SampleFunc03 @3
次に、C# EXEからC++ DLLを呼び出す方法を記述する。
文字列をC++ DLL側に渡す場合は、string
型を使用する。
文字列をC++ DLL側から返す場合は、StringBuilder
クラスを使用する。
StringBuilder
クラスは受け渡しの両方が可能なので、文字列は全てStringBuilder
クラスを使用すべきである。
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);
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);
Console.ReadKey();
}
}
}
構造体のマーシャリング
C++ DLLの作成方法はライブラリの基礎 - DLLの作成(C/C++/MFC)を参照する。
下記にもC++ DLLを記述する。
// SampleDLL.h
void __stdcall SampleFunc01(SampleStruct st)
void __stdcall SampleFunc02(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;
void __stdcall SampleFunc01(SampleStruct st)
{
printf("--<SampleDll:Sample01>--\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 SampleFunc02(SampleStruct2 *pStructure)
{
dData[256] = {0};
printf("--<SampleDll:Sample02>--\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
次に、C# EXEからC++ DLLを呼び出す方法を記述する。
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="st">DLL 側に渡す構造体を指定します</param>
[DllImport("SampleDLL.dll")]
private static extern void SampleFunc01(SampleStruct st);
/// <summary>
/// DLL側からメンバにポインタを含む構造体を受け取る関数のインポート例
/// </summary>
/// <param name="pst">受け渡す構造体の先頭アドレスを示すポインタを指定する</param>
[DllImport("SampleDLL.dll")]
private static extern void SampleFunc02(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 structHoge = new SampleStruct()
{
index = 4,
name = "構造体サンプル",
data = new int[50],
};
structHoge.data[0] = 11;
structHoge.data[1] = 22;
structHoge.data[2] = 33;
SampleFunc01(structHoge);
// SampleStruct2構造体のサイズを取得する
// 指定サイズ分だけメモリ領域を確保して、その先頭アドレスをstructPiyoに格納する
var structPiyo = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(SampleStruct2)));
try
{
SampleFunc02(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();
}
}
}
関数ポインタのマーシャリング
このセクションでは、C# EXEからC++ DLLへコールバック関数を渡す方法を記載する。
まず、以下にC++ DLLを記述する。
// SampleDLL.h
#pragma once
int __stdcall SampleCallback(int handle, int lPalam);
// SampleDll.cpp
#include <stdio.h>
#include <string.h>
#include "SampleDll.h"
using UnManagedCallback = bool (*)(int);
int __stdcall SampleCallback(UnManagedCallback CallbackFunc, int lPalam)
{
int iRet = 0;
bool bRet = CallbackFunc(lPalam);
if(bRet)
{
iRet = 1;
}
else
{
iRet = -1;
}
return iRet;
}
// モジュール定義ファイル
// SampleDll.def
LIBRARY SampleDll
EXPORTS
; 公開する関数名をリストアップ
SampleCallback @1
次に、C# EXEからC++ DLLを呼び出す方法を記述する。
マネージドコールバック関数を定義する。
以下の例では、ManagedCallBackというデリゲートを宣言している。
C++ DLLのSampleCallBack関数にデリゲートを引数として渡すことで、既知のコールバック形式に自動的に変換される。
using System;
using System.Runtime.InteropServices;
namespace ConsoleSample01
{
delegate bool ManagedCallBack(int x);
class Program
{
[DllImport("SampleDLL.dll")]
private static extern int SampleCallback(ManagedCallBack CallBack, int lPalam);
static void Main(string[] args)
{
var CallBack = new ManagedCallBack(CallBackFunction);
var iRet = SampleCallback(CallBack, 0);
Console.WriteLine($"{iRet}");
Console.ReadKey();
}
public static bool CallBackFunction(int x)
{
Console.WriteLine($"{x}");
return true;
}
}
}
トラブル対処
可能な限り、デバッグビルドしたDLLとそのPDBファイルを用意する。
デバッグ
C++DLLがデバッグできない場合、以下の手順で、混合モードデバッグを有効にする。
- [ソリューションエクスプローラー]からC#プロジェクトを右クリックして、[プロパティ]を選択する。
- [プロパティページ]画面が表示されるので、[デバッグ]タブ - [アンマネージ コード デバッグを有効にする]([ネイティブコードのデバッグを有効にする])を選択する。
- [プロパティページ]画面を閉じる。
[デバッガーを有効にする] - [デバッグ] ページ (プロジェクト デザイナー) | MSDN ネイティブ コードのデバッグ | MSDN
この設定をしていない場合、C++DLLからのエラーにより、アプリケーションが終了することがある。
※注意
Visual Studio 2017以降、プロジェクトのプロパティの代わりにlaunchSettings.jsonファイルを使用して、
.NET Coreアプリでネイティブコードの混合モードデバッグを有効にする必要がある。
詳細については、マネージドコードとネイティブコードのデバッグに関するページを参照すること。