ライブラリの基礎 - C++DLL

提供:MochiuWiki : SUSE, EC, PCB
2021年6月8日 (火) 16:29時点におけるWiki (トーク | 投稿記録)による版 (→‎構造体のマーシャリング)
ナビゲーションに移動 検索に移動

概要

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を読み込めない場合、モジュールが見つからないとして、FileNotFoundExceptionDllNotFoundException等の例外が投げられる。
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から次のような関数がエクスポートされているとする。

 void WINAPI ConvertToShort(char *pstr, short *pret);


上記の関数において、C# EXEから使用する時は、第1引数のchar*型は文字列なので、string型またはStringBuilder型を渡す。
第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)
 {
    // 2バイトのメモリ確保
    IntPtr buffer = new IntPtr();
    buffer = Marshal.AllocHGlobal(2);
 
    // C++ DLLの関数を呼ぶ
    _ConvertToShort(str, buffer);
 
    // 2バイトのメモリをshort型に変換
    short sval = Marshal.ReadInt16(buffer);
 
    // メモリの開放
    Marshal.FreeHGlobal(buffer);
 
    return sval;
 }


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を呼び出す方法を記述する。

手順としては、以下の処理の流れとなる。

  1. C# EXEでマネージド配列(C# EXEの配列)を定義する。
  2. C# EXEにおいて、アンマネージド配列(C++ DLLの配列)のメモリを確保する。
  3. マネージド配列の内容を、上記で確保したアンマネージド配列のメモリにコピーする。
  4. C++ DLLの関数を実行する時、ポインタを渡す。
  5. 使用したアンマネージド配列のメモリを解放する。


 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がデバッグできない場合、以下の手順で、混合モードデバッグを有効にする。

  1. [ソリューションエクスプローラー]からC#プロジェクトを右クリックして、[プロパティ]を選択する。
  2. [プロパティページ]画面が表示されるので、[デバッグ]タブ - [アンマネージ コード デバッグを有効にする]([ネイティブコードのデバッグを有効にする])を選択する。
  3. [プロパティページ]画面を閉じる。


[デバッガーを有効にする] - [デバッグ] ページ (プロジェクト デザイナー) | MSDN ネイティブ コードのデバッグ | MSDN

この設定をしていない場合、C++DLLからのエラーにより、アプリケーションが終了することがある。

※注意
Visual Studio 2017以降、プロジェクトのプロパティの代わりにlaunchSettings.jsonファイルを使用して、
.NET Coreアプリでネイティブコードの混合モードデバッグを有効にする必要がある。

詳細については、マネージドコードとネイティブコードのデバッグに関するページを参照すること。