「ライブラリの基礎 - C++DLL」の版間の差分

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動
(文字列「source lang」を「syntaxhighlight lang」に置換)
10行目: 10行目:
== DllImport属性 ==
== DllImport属性 ==
<code>DLLImport</code>属性は、DLLエントリポイントを定義する関数を記述することで、DLLファイルに定義された関数を呼び出すことができる。<br>
<code>DLLImport</code>属性は、DLLエントリポイントを定義する関数を記述することで、DLLファイルに定義された関数を呼び出すことができる。<br>
  <source lang="csharp">
  <syntaxhighlight lang="csharp">
  [ DllImport( "DLL名" ) ]
  [ DllImport( "DLL名" ) ]
  </source>
  </source>
19行目: 19行目:
<br>
<br>
以下の例では、<code>EntryPoint</code>フィールドを使用して、<code>MessageBoxA</code>をMsgBoxAに置き換える方法を示している。<br>
以下の例では、<code>EntryPoint</code>フィールドを使用して、<code>MessageBoxA</code>をMsgBoxAに置き換える方法を示している。<br>
  <source lang="csharp">
  <syntaxhighlight lang="csharp">
  using System;
  using System;
  using System.Runtime.InteropServices;
  using System.Runtime.InteropServices;
31行目: 31行目:
<br>
<br>
また、以下の例では、DLLファイルに定義された関数を序数で指定している。<br>
また、以下の例では、DLLファイルに定義された関数を序数で指定している。<br>
  <source lang="csharp">
  <syntaxhighlight lang="csharp">
  [DllImport("DllName", EntryPoint = "Functionname")]
  [DllImport("DllName", EntryPoint = "Functionname")]
  [DllImport("DllName", EntryPoint = "#123")]  // 序数を使用するには、序数値の前にシャープ記号#を付ける必要がある
  [DllImport("DllName", EntryPoint = "#123")]  // 序数を使用するには、序数値の前にシャープ記号#を付ける必要がある
93行目: 93行目:
以下の例では、呼び出し規約を適用する方法をCdeclとしている。<br>
以下の例では、呼び出し規約を適用する方法をCdeclとしている。<br>
これは、呼び出し元がスタックをクリーンアップするために使用する必要がある。<br>
これは、呼び出し元がスタックをクリーンアップするために使用する必要がある。<br>
  <source lang="csharp">
  <syntaxhighlight lang="csharp">
  using System;
  using System;
  using System.Runtime.InteropServices;
  using System.Runtime.InteropServices;
126行目: 126行目:
== StructLayout属性 ==
== StructLayout属性 ==
StructLayout属性とは、クラスまたは構造体のデータメンバを、メモリ内でどのように配置するかを表す。<br>
StructLayout属性とは、クラスまたは構造体のデータメンバを、メモリ内でどのように配置するかを表す。<br>
  <source lang="csharp">
  <syntaxhighlight lang="csharp">
  [ StructLayout( LayoutKind列挙 ) ]
  [ StructLayout( LayoutKind列挙 ) ]
  </source>
  </source>
158行目: 158行目:
</center>
</center>
<br>
<br>
  <source lang="csharp">
  <syntaxhighlight lang="csharp">
  [StructLayout( LayoutKind.Sequential )]
  [StructLayout( LayoutKind.Sequential )]
  public struct Position
  public struct Position
179行目: 179行目:
<br>
<br>
以下の例では、UInt32型はCLSに準拠しないため、<code>CLSCompliant(false)</code>と指定する必要がある。<br>
以下の例では、UInt32型はCLSに準拠しないため、<code>CLSCompliant(false)</code>と指定する必要がある。<br>
  <source lang="csharp">
  <syntaxhighlight lang="csharp">
  [CLSCompliant( false )]
  [CLSCompliant( false )]
  public int SetValue( UInt32 value );
  public int SetValue( UInt32 value );
185行目: 185行目:
<br>
<br>
以下の例では、CLSCompliantAttribute属性をアセンブリ全体に適用する。<br>
以下の例では、CLSCompliantAttribute属性をアセンブリ全体に適用する。<br>
  <source lang="csharp">
  <syntaxhighlight lang="csharp">
  using System;
  using System;
  [assembly: CLSCompliant(true)]
  [assembly: CLSCompliant(true)]
206行目: 206行目:
詳細は、以下のMSDNのWebサイトを参照すること。<br>
詳細は、以下のMSDNのWebサイトを参照すること。<br>
[https://msdn.microsoft.com/ja-jp/library/system.runtime.interopservices.marshalasattribute.aspx MarshalAsAttribute クラス (System.Runtime.InteropServices) | MSDN]<br>
[https://msdn.microsoft.com/ja-jp/library/system.runtime.interopservices.marshalasattribute.aspx MarshalAsAttribute クラス (System.Runtime.InteropServices) | MSDN]<br>
  <source lang="csharp">
  <syntaxhighlight lang="csharp">
  // パラメータへの適用
  // パラメータへの適用
  public void M1([MarshalAs(UnmanagedType.LPWStr)]string msg)
  public void M1([MarshalAs(UnmanagedType.LPWStr)]string msg)
242行目: 242行目:
</center>
</center>
<br>
<br>
  <source lang="csharp">
  <syntaxhighlight lang="csharp">
  void Method([in] int[] array);
  void Method([in] int[] array);
  </source>
  </source>
1,094行目: 1,094行目:
<br>
<br>
まず、以下にC++ DLLを記述する。<br>
まず、以下にC++ DLLを記述する。<br>
  <source lang="c++">
  <syntaxhighlight lang="c++">
  // SampleDLL.h
  // SampleDLL.h
   
   
1,102行目: 1,102行目:
  </source>
  </source>
<br>
<br>
  <source lang="c++">
  <syntaxhighlight lang="c++">
  // SampleDll.cpp
  // SampleDll.cpp
   
   
1,129行目: 1,129行目:
  </source>
  </source>
<br>
<br>
  <source lang="c++">
  <syntaxhighlight lang="c++">
  // モジュール定義ファイル
  // モジュール定義ファイル
  // SampleDll.def
  // SampleDll.def
1,145行目: 1,145行目:
以下の例では、ManagedCallBackというデリゲートを宣言している。<br>
以下の例では、ManagedCallBackというデリゲートを宣言している。<br>
C++ DLLのSampleCallBack関数にデリゲートを引数として渡すことで、既知のコールバック形式に自動的に変換される。<br>
C++ DLLのSampleCallBack関数にデリゲートを引数として渡すことで、既知のコールバック形式に自動的に変換される。<br>
  <source lang="c#">
  <syntaxhighlight lang="c#">
  using System;
  using System;
  using System.Runtime.InteropServices;
  using System.Runtime.InteropServices;

2021年11月24日 (水) 18:06時点における版

概要

C# EXEからC++ DLLへ様々なデータ型の変数を渡したい場合がある。

しかし、C#の変数とC++の変数はメモリへの配置が基本型以外異なるため、直接、C#からC++に渡すことができない。

例えば、文字列を扱う場合、C#のstring型とC++のstd::string型は同一ではないため、マーシャリングの処理が必要となる。
マーシャリングとは、異なる2つのシステム間において、データを交換できるようにデータを操作する処理を指す。


DllImport属性

DLLImport属性は、DLLエントリポイントを定義する関数を記述することで、DLLファイルに定義された関数を呼び出すことができる。

 [ DllImport( "DLL名" ) ]
 </source>
<br>
また、DLLファイルに定義された関数を、関数名または序数で指定するには、<code>DllImport</code>属性の<code>EntryPoint</code>フィルドを使用する。<br>
メソッド定義内の関数の名前がDLLエントリポイントと同じである場合は、その関数を<code>EntryPoint</code>フィルドで明示的に識別する必要はない。<br>
同じではない場合は、次のいずれかの記述で名前または序数を指定する。<br>
<br>
以下の例では、<code>EntryPoint</code>フィルドを使用して、<code>MessageBoxA</code>MsgBoxAに置き換える方法を示している。<br>
 <syntaxhighlight lang="csharp">
 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);
 }
 </source>
<br>
また、以下の例では、DLLファイルに定義された関数を序数で指定している。<br>
 <syntaxhighlight lang="csharp">
 [DllImport("DllName", EntryPoint = "Functionname")]
 [DllImport("DllName", EntryPoint = "#123")]  // 序数を使用するには、序数値の前にシャープ記号#を付ける必要がある
 </source>
<br>
<code>DllImport</code>属性には、DLLファイル名を指定する以外にも、下表のような引数を与えることができる。<br>
<center>
{| class="wikitable" style="background-color:#fefefe;"
|-
! style="background-color:#66CCFF;" | 名称 
! style="background-color:#66CCFF;" | 説明 
! style="background-color:#66CCFF;" | 既定値
|-
| EntryPoint || 呼び出すDLLエントリポイントの名前または序数を指定する。<br>DLLの関数名とC#上で使用する関数名を異なる名前にする時に指定する。 || 
|-
| CharSet || 文字列パラメタをメソッドにマシャリングして、名前マングルを制御する方法を指定する。<br>文字コドの相互変換する時に指定する。<br>指定なしの場合は、<code>CharSet.Auto</code>となる。 || CharSet.Auto 
|-
| SetLastError || Win32エラ情報を維持するかどうか指定する。<br>指定なしの場合は、<code>false</code>となる。 || FALSE 
|-
| ExactSpelling || エントリポイントの関数名を厳密に一致させるかどうか指定する。<br>指定なしの場合は、<code>false</code>となる。 || FALSE 
|-
| PreserveSig || 定義通りのメソッドのシグネチャを維持するかどうか指定する。
|-
| CallingConvention || アンマネジドコドを呼び出すための<u>エントリポイントの呼び出し規約</u>を、明示的に指定できる。<br>指定なしの場合は、<code>__stdcall</code>(StdCall)となる。<br>詳細は、下表を参照すること。 || StdCall 
|}
</center>
<br>
詳細は、以下のMSDNWebサイトを参照すること。<br>
[https://msdn.microsoft.com/ja-jp/library/system.runtime.interopservices.dllimportattribute.aspx DllImportAttribute クラス (System.Runtime.InteropServices) | MSDN]<br>
<br>
* 呼び出し規約(Calling Convention)
*: CallingConvention列挙型で、アンマネジドコドを呼び出すための呼び出し規約を指定する。<br>
*: 下表に、CallingConvention列挙型を示す。<br>
*: <br>
*: また、実行時に指定のDLLを読み込めない場合、<u>モジュルが見つからない</u>として、<code>FileNotFoundException</code><code>DllNotFoundException</code>等の例外が投げられる。<br>
 System.IO.FileNotFoundException はハンドルされませんでした。
 Message:  'System.IO.FileNotFoundException' のハンドルされていない例外が mscorlib.dll で発生しました
 追加情報:ファイルまたはアセンブリ '**.dll'、またはその依存関係の 1 つが読み込めませんでした。
 指定されたモジュルが見つかりません。
 これは参照しているDLLがない、またはそのDLLが依存しているDLLが適切に配置されていない場合に発生します。
 これは問題のDLLdumpbinで調べ、それを配置することで解決できます。
<br>
<center>
{| class="wikitable" style="background-color:#fefefe;"
|-
! style="background-color:#66CCFF;" | 列挙子 
! style="background-color:#66CCFF;" | 説明
|-
| Cdecl || 呼び出し元がスタックを消去する。<br>これを使用すると、<code>varargs</code>で関数を呼び出すことができる。<br>これは、<code>printf</code>関数等の可変長引数のメソッドの呼び出しで使用する。
|-
| StdCall || 呼び出し先がスタックを消去する。<br>アンマネジド関数を呼び出す既定値内のテキスト。
|-
| ThisCall || 最初の引数がthisポインタで、その他の引数はスタックにプッシュされる。<br>アンマネジドDLLからエクスポトしたクラスのメソッドを呼び出すために使用する。
|-
| Winapi || プラットフォムに応じた既定の呼び出し規約を使用する。<br>例えば、WindowsではStdCallWindows CE.NETではCdeclLinuxではCdeclである。<br>(正確には、この設定は呼び出し規約ではない)
|-
| FastCall || この呼び出し規約はサポトされていない。
|}
</center>
<br>
以下の例では、呼び出し規約を適用する方法をCdeclとしている。<br>
これは、呼び出し元がスタックをクリンアップするために使用する必要がある。<br>
 <syntaxhighlight lang="csharp">
 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");
    }
 }
 </source>
詳細は、以下のMSDNWebサイトを参照すること。<br>
[https://msdn.microsoft.com/ja-jp/library/system.runtime.interopservices.callingconvention.aspx CallingConvention 列挙型 (System.Runtime.InteropServices) | MSDN]<br>
<br><br>

== StructLayout属性 ==
StructLayout属性とは、クラスまたは構造体のデタメンバを、メモリ内でどのように配置するかを表す。<br>
 <syntaxhighlight lang="csharp">
 [ StructLayout( LayoutKind列挙 ) ]
 </source>
<br>
下表に、LayoutKind列挙子を示す。<br>
<center>
{| class="wikitable"
|-
! LayoutKind列挙子 !! 説明
|-
| Sequential || 宣言される順番に従って並べる。
|-
| Explicit || <code>FieldOffsetAttribute</code>で独自のオフセットを指定して並べる。
|-
| Auto || 適切なレイアウトで並べる。<br>(これを指定すると、マネジドコド外からアクセスできない)
|}
</center>
<br>
下表に、StructLayout属性のパラメタを示す。<br>
<center>
{| class="wikitable"
|-
! StructLayout属性のパラメ !! 説明 !! 既定値
|-
| Pack || パックサイズを指定するint値である。<br>指定可能な値は、124816のいずれかである。 || 8
|-
| CharSet || 文字列のマシャリング方法を示すCharSet列挙である。 || CharSet.Auto
|-
| Size || 構造体またはクラスのサイズを指定する。 || 
|}
</center>
<br>
 <syntaxhighlight lang="csharp">
 [StructLayout( LayoutKind.Sequential )]
 public struct Position
 {
    public double x;
    public double y;
    public double z;
 }
 </source>
<br>
詳細は、以下のMSDNWebサイトを参照すること。<br>
[https://msdn.microsoft.com/ja-jp/library/system.runtime.interopservices.structlayoutattribute.aspx StructLayoutAttribute クラス (System.Runtime.InteropServices) | MSDN]<br>
<br><br>

== CLSCompliant属性 ==
CLSCompliant属性は、CLSへの準拠を検証するかどうかをコンパイラに指示する。<br>
<br>
外部から参照できない型やメンバに対しては、この属性を指定する必要は無い。<br>
指定する場合、<code>このアセンブリの外から認識できないため、CLS準拠の確認は '型' で実行されません</code>と警告が表示される。<br>
<br>
以下の例では、UInt32型はCLSに準拠しないため、<code>CLSCompliant(false)</code>と指定する必要がある。<br>
 <syntaxhighlight lang="csharp">
 [CLSCompliant( false )]
 public int SetValue( UInt32 value );
 </source>
<br>
以下の例では、CLSCompliantAttribute属性をアセンブリ全体に適用する。<br>
 <syntaxhighlight lang="csharp">
 using System;
 [assembly: CLSCompliant(true)]
 </source>
<br>
詳細は、以下のMSDNWebサイトを参照すること。<br>
[https://msdn.microsoft.com/ja-jp/library/system.clscompliantattribute.aspx CLSCompliantAttribute クラス (System) | MSDN]<br>
<br><br>

== SuppressUnmanagedCodeSecurity属性 ==
SuppressUnmanagedCodeSecurity属性は、アンマネジドコドの呼び出し時に実行されたスタックウォクを、実行時に省いて効率を大幅に向上させる。<br>
<br>
詳細は、以下のMSDNWebサイトを参照すること。<br>
[https://msdn.microsoft.com/ja-jp/library/system.security.suppressunmanagedcodesecurityattribute.aspx SuppressUnmanagedCodeSecurityAttribute クラス | MSDN]<br>
<br><br>

== MarshalAs属性 ==
MarshalAs属性は、マネジドコドとアンマネジドコドの間で、デタをマシャリングする方法を指定する。<br>
<br>
詳細は、以下のMSDNWebサイトを参照すること。<br>
[https://msdn.microsoft.com/ja-jp/library/system.runtime.interopservices.marshalasattribute.aspx MarshalAsAttribute クラス (System.Runtime.InteropServices) | MSDN]<br>
 <syntaxhighlight lang="csharp">
 // パラメータへの適用
 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";
 }
 </source>
<br><br>

== In属性 / Out属性 ==
In属性 / Out属性は、デタの渡し方を指示する。<br>
<center>
{| class="wikitable"
|-
!  !! 説明
|-
| In属性 || 呼び出し側にデタをマシャリングして渡すことを示す。
|-
| Out属性 || 呼び出し元にデタをマシャリングして戻すことを示す。
|}
</center>
<br>
 <syntaxhighlight lang="csharp">
 void Method([in] int[] array);
 </source>
<br><br>

== C#のデタ型とWindows APIのデタ型 ==
<center>
{| class="wikitable"
|-
! Windows APIのデタ型<br>(括弧内は対応するC言語の型) !! 対応するC#のデタ型<br>(括弧内は.NET Frameworkでの型名) !! 備考
|-
| HANDLE (void *) || System.IntPtr<br>System.UIntPtr || x864バイト<br>x648バイト
|-
| BYTE (unsigned char) || byte (System.Byte) || 
|-
| SHORT (short) || short (System.Int16) || 
|-
| WORD (unsigned short) || ushort (System.UInt16) || 
|-
| INT (int)<br>LONG (long) || int (System.Int32) || 
|-
| UINT (unsigned int)<br>DWORD, ULONG (unsigned long) || uint (System.UInt32) || 
|-
| BOOL (long) || bool (System.Boolean) || 
|-
| CHAR (char) || 文字を渡すとき<br>char (System.Char)<br>文字を受け取るとき<br>StringBuilder || 
|-
| WCHAR(wchar_t) || 文字を渡すとき<br>char (System.Char)<br>文字を受け取るとき<br>StringBuilder || 
|-
| LPSTR (char *, char[])<br>LPWSTR (wchar_t *, wchar_t[]) || 文字を渡すとき<br>string (System.String)<br>文字を受け取るとき<br>System.Text.StringBuilder || 
|-
| LPCSTR (const char *, const char[])<br>LPCWSTR (const wchar_t *, const wchar_t[]) || 文字を渡すとき<br>string (System.String)<br>文字を受け取るとき<br>System.Text.StringBuilder || 
|-
| FLOAT (float) || float (System.Single) || 
|-
| DOUBLE (double) || double (System.Double) || 
|}
</center>
<br><br>

== サンプルコ ==
==== 整数型および浮動小数点型のマシャリング ====
C++ DLLの作成方法は[[ライブラリの基礎 - DLLの作成(C/C++/MFC)|ライブラリの基礎 - DLLの作成(C/C++/MFC)]]を参照する。<br>
<br>
下記にもC++ DLLを記述する。<br>
 <syntaxhighlight lang="c++">
 // 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

 void __stdcall SampleArray01(int array[], int length);
 int* __stdcall SampleArray02(int length);
 void __stdcall SampleArray03(int array[], int length);


 // SampleDll.cpp
 
 #include <stdio.h>
 #include <string.h>
 #include "SampleDll.h"
 
 void __stdcall SampleArray01(int array[], int length)
 {
    for (int i = 0; i < length; i++)
    {
       std::cout << array[i] << std::endl;
    }
 }
 
 int* __stdcall SampleArray02(int length)
 {
    int* iary = new int[length];
 
    for (int i = 0; i < length; i++)
    {
        iary[i] = i;
    }
 
    return iary;
 }
 
 void __stdcall SampleArray03(int array[], int length)
 {
    for (int i = 0; i < length; i++)
    {
       arr[i] = i;
    }
 }


 // モジュール定義ファイル
 // SampleDll.def
 
 LIBRARY SampleDll
 
 EXPORTS
          ; 公開する関数名をリストアップ
          SampleArray01   @1
          SampleArray02   @2
          SampleArray03   @3


次に、C# EXEからC++ DLLを呼び出す方法を記述する。

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

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


 namespace SampleEXE
 {
    [DllImport("SampleDll.dll", EntryPoint = "SampleArray01", CallingConvention = CallingConvention.Cdecl)]
    static extern void _SampleArray01(IntPtr array, int length);
 
    [DllImport("SampleDll.dll", EntryPoint = "SampleArray02", CallingConvention = CallingConvention.Cdecl)]
    static extern IntPtr _SampleArray02(int length);
 
    [DllImport("SampleDll.dll", EntryPoint = "SampleArray03", CallingConvention = CallingConvention.Cdecl)]
    static extern IntPtr _SampleArray03(IntPtr array, int length);
 
    static void Main()
    {
       SampleArray01();
       SampleArray02();
       SampleArray03();
    }
 
    // SampleArray01
    // 配列を渡す場合
    private static void SampleArray01()
    {
       // 配列の定義
       var array = new int[] { 0, 1, 2, 3, 4 };
 
       // アンマネージド配列のメモリの確保
       IntPtr ptrRet = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(int)) * array.Length);
 
       // マネージド配列をアンマネージドにコピーする
       Marshal.Copy(array, 0, ptrRet, array.Length);
 
       // C++ DLLに配列を渡す(ポインタを渡す)
       _SampleArray01(ptrRet, array.Length);
 
       // アンマネージドメモリの解放
       Marshal.FreeCoTaskMem(ptrRet);  
    }
 
    // SampleArray02
    // 配列を受ける場合 (戻り値がint型ポインタ)
    private static void SampleArray02()
    {
       // 空の配列の定義
       int[] array = new int[5];
 
       // 戻り値のポインタをIntPtrで受け取る
       IntPtr ptrRet = _SampleArray02(array.Length);
 
       // アンマネージド配列からマネージド配列へコピー
       Marshal.Copy(ptrRet, array, 0, array.Length);
 
       foreach(int n in array)
       {
          Console.WriteLine(n + " ");
       }
    }
 
    // SampleArray03
    // 配列を受ける場合 (引数がint型の配列)
    private static void SampleArray03()
    {
       // 空の配列の定義
       int[] array = new int[5];
 
       // アンマネージド配列のメモリを確保
       IntPtr ptrRet = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(int)) * array.Length);
 
       // 引数でポインタを渡す
       _SampleArray03(ptrRet, array.Length);
 
       // アンマネージド配列からマネージド配列へコピー
       Marshal.Copy(ptrRet, array, 0, array.Length);
 
       // アンマネージド配列のメモリを解放
       Marshal.FreeCoTaskMem(ptrRet);
 
       foreach(int n in array)
       {
          Console.WriteLine(n + " ");
       }
    }
 }


文字列のマーシャリング (1)

C++ DLLの作成方法はライブラリの基礎 - DLLの作成(C/C++/MFC)を参照する。

下記にもC++ DLLを記述する。

 // SampleDLL.h
 
 int            __stdcall Mul(int x, int y)
 void           __stdcall MulUsePointer(int *x, int *y, int *result)
 void           __stdcall SetNameStr(const char *t)
 const char*    __stdcall GetNameStr()
 void           __stdcall SetNameWStr(const wchar_t *t)
 const wchar_t* __stdcall GetNameWStr()


 // SampleDll.cpp
 
 #include <stdio.h>
 #include <string.h>
 #include "SampleDll.h"
 
 static std::string _str   = "";
 static std::wstring _wstr = L"";
 
 int __stdcall Mul(int x, int y)
 {
    return x * y;
 }
 
 void __stdcall MulUsePointer(int *x, int *y, int *result)
 {
    *result = (*x) * (*y);
 }
 
 void __stdcall SetNameStr(const char *t)
 {
    _str = std::string(t);
 }
 
 const char* __stdcall GetNameStr()
 {
    return _str.c_str();
 }
 
 void __stdcall SetNameWStr(const wchar_t *t)
 {
    _wstr = std::wstring(t);
 }
 
 const wchar_t * __stdcall GetNameWStr()
 {
    return _wstr.c_str();
 }


 // SampleDll.def  // モジュール定義ファイル
 
 LIBRARY SampleDll
 
 EXPORTS
          ; 公開する関数名をリストアップ
          Mul             @1
          MulUsePointer   @2
          SetNameStr      @3
          GetNameStr      @4
          SetNameWStr     @5
          GetNameWStr     @6


次に、C# EXEからC++ DLLを呼び出す方法を記述する。

DllImpoort文は、DLLファイル名を指定する。
この時、C#の実行ファイルと同一階層にDLLファイルがある場合、DLLファイル名のみを指定するだけでよい。
異なる階層にDLLファイルがある場合、フルパスで指定する必要がある。

C++ DLLの関数を実行する時、"エントリーポイントが見つかりません。"とエラーが出力される場合、DllImport文に誤りがある可能性が高い。

 using System;
 using System.Runtime.InteropServices;
 
 namespace SampleCSharp
 {
    class Program
    {
       [DllImport("SampleDLL", EntryPoint = "Mul")]
       static extern int _Mul(int x, int y);
 
       [DllImport("SampleDLL", EntryPoint = "MulUsePointer")]
       static extern void _MulUsePointer(ref int x, ref int y, out int result);
 
       [DllImport("SampleDLL", EntryPoint = "SetNameStr", CharSet = CharSet.Ansi)]
       static extern void _SetNameStr(string t);
 
       [DllImport("SampleDLL", EntryPoint = "GetNameStr", CharSet = CharSet.Ansi)]
       static extern IntPtr _GetNameStr();
 
       [DllImport("SampleDLL", EntryPoint = "SetNameWStr", CharSet = CharSet.Unicode)]
       static extern void _SetNameWStr(string t);
 
       [DllImport("SampleDLL", EntryPoint = "GetNameWStr", CharSet = CharSet.Unicode)]
       static extern IntPtr _GetNameWStr();
 
       static void Main(string[] args)
       {
          int x = 10;
          int y = 20;
 
          int resultMul = _Mul(x, y);
          Console.WriteLine("Mul = " + resultMul);
 
          _MulUsePointer(ref x, ref y, out int resultMulUsePointer);
          Console.WriteLine("MulUsePointer = " + resultMulUsePointer);
 
          string testString = "東京都新宿 1-1";
          _SetNameStr(testString);
          Console.WriteLine("GetNameStr = " + Marshal.PtrToStringAnsi(_GetNameStr()));
 
          _SetNameWStr(testString);
          Console.WriteLine("GetNameWStr = " + Marshal.PtrToStringUni(_GetNameWStr()));
       }
    }
 }


文字列のマーシャリング (2)

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 DisplayFunc(SampleStruct1 Structure)
 void   __stdcall GetFunc(SampleStruct1 Structure)
 void   __stdcall SetFunc(SampleStruct2 *pStructure)


 // SampleDll.cpp
 
 #include <stdio.h>
 #include <string.h>
 #include "SampleDll.h"
 
 typedef struct tagSampleStruct
 {
    int  index;
    char name[128];
    int  data[50];
 } SampleStruct1, *pSampleStruct;
 
 typedef struct tagSampleStruct2
 {
    int    length;
    double *data;
 } SampleStruct2, *pSampleStruct2;
 
 void __stdcall DisplayFunc(SampleStruct1 st)
 {
    printf("--<SampleDll:DisplayFunc>--\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 GetFunc(SampleStruct1 *pst)
 {
    printf("--<SampleDll:GetFunc>--\r\n");
    pst->index = 10;
    memcpy_s(pst->name, sizeof(pst->name), "Tarou Yamada", sizeof("Tarou Yamada"));
    memset(pst->data, 20, sizeof(pst->data));
    printf("------------------------\r\n");
 }
 
 void __stdcall SetFunc(SampleStruct2 *pStructure)
 {
    printf("--<SampleDll:SetFunc>--\r\n");
 
    memset(pStructure, 0, sizeof(SampleStruct2));
 
    // double dAryData[256] = {0.0f};
    pStructure->length                  = 10;
    double dAryData[pStructure->length] = {0.0f};
    pStructure->data                    = dAryData;
 
    for(int i = 0; i < pStructure->length; i++)
    {
       dAryData[i] = (i + 1) / 10.0f;
    }
 
    printf("------------------------\r\n");
 }


 // SampleDll.def  // モジュール定義ファイル
 
 LIBRARY SampleDll
 
 EXPORTS
          ; 公開する関数名をリストアップ
          DisplayFunc   @1
          GetFunc       @2
          SetFunc       @3


次に、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", EntryPoint = "DisplayFunc", CallingConvention = CallingConvention.Cdecl)]
       private static extern void _DisplayFunc(SampleStruct1 st);
 
       /// <summary>
       /// DLL側からメンバにポインタを含む構造体を受け取る関数のインポート例
       /// </summary>
       /// <param name="pst">受け渡す構造体の先頭アドレスを示すポインタを指定する</param>
       [DllImport("SampleDLL.dll", EntryPoint = "GetFunc", CallingConvention = CallingConvention.Cdecl)]
       private static extern void _GetFunc(IntPtr pst);
 
       /// <summary>
       /// DLL側からメンバにポインタを含む構造体を受け取る関数のインポート例
       /// </summary>
       /// <param name="pst">受け渡す構造体の先頭アドレスを示すポインタを指定する</param>
       [DllImport("SampleDLL.dll", EntryPoint = "SetFunc", CallingConvention = CallingConvention.Cdecl)]
       private static extern void _SetFunc(IntPtr pst);
 
       /// <summary>
       /// DLLとの取り合いのために定義する構造体
       /// LayoutKind.Sequentialを指定することで、C/C++同様、変数の宣言順通りにメモリに配置されるようになる
       /// </summary>
       [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
       private struct SampleStruct1
       {
          /// <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)
       {
          SampleFunc01();
          SampleFunc02();
          SampleFunc03();
 
          Console.ReadKey();
       }
 
       private static void SampleFunc01()
       {
          var structure = new SampleStruct1()
          {
             index = 4,
             name = "構造体サンプル",
             data = new int[50],
          };
 
          structure.data[0] = 11;
          structure.data[1] = 22;
          structure.data[2] = 33;
 
          _SampleFunc01(structure);
       }
 
       private static void SampleFunc02()
       {
          var structure = new SampleStruct1()
          {
             index = 0,
             name = "",
             data = new int[50],
          };
 
          // COMタスクメモリアロケータから、C#の構造体のサイズ分のメモリブロックを割り当てる
          IntPtr structurePtr = Marshal.AllocCoTaskMem(Marshal.SizeOf(structure));
 
          _GetFunc(structurePtr);
 
          // Marshal.PtrToStructureメソッドを使用して、IntPtr変数が示すメモリに格納された情報をC#の構造体にコピーする
          structure = (SampleStruct1)Marshal.PtrToStructure(structurePtr, structure.GetType());
 
          Marshal.FreeCoTaskMem(structurePtr);
 
          Console.WriteLine($"index : {structure.index}");
          Console.WriteLine($"name : {structure.name}");
          Console.WriteLine($"data : {structure.data}");
       }
 
       private static void SampleFunc03()
       {
          // SampleStruct2構造体のサイズを取得する
          // 指定サイズ分だけメモリ領域を確保して、その先頭アドレスをstructure1に格納する
          var structure1 = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(SampleStruct2)));
          try
          {
             // C++ DLLのSampleFunc02関数の実行
             SetFunc(structure1);
 
             // 受け取ったstructure1からSampleStruct2構造体の情報に構築し直す
             var structure2 = (SampleStruct2)Marshal.PtrToStructure(structure1, typeof(SampleStruct2));
             for (int i = 0; i < structure2.length; i++)
             {
                // IntPtr型からdouble型の数値を取得するときは、一度Int64型に変換して、これをBitConverter.Int64BitsToDoubleメソッドでdouble型に変換する
                var v = Marshal.ReadInt64(structure2.data, i * sizeof(double));
                Console.WriteLine("data[{0}] = {1}", i, BitConverter.Int64BitsToDouble(v));
             }
          }
          catch (Exception ex)
          {
             System.Diagnostics.Debug.WriteLine(ex);
          }
          finally
          {
             // 必ずメモリを解放する
             Marshal.FreeHGlobal(structure1);
          }
       }
    }
 }


関数ポインタのマーシャリング

このセクションでは、C# EXEからC++ DLLへコールバック関数を渡す方法を記載する。

まず、以下にC++ DLLを記述する。

<syntaxhighlight lang="c++">
// SampleDLL.h

#pragma once
int __stdcall SampleCallback(int handle, int lPalam);
</source>


<syntaxhighlight lang="c++">
// 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;
}
</source>


<syntaxhighlight lang="c++">
// モジュール定義ファイル
// SampleDll.def

LIBRARY SampleDll

EXPORTS
         ; 公開する関数名をリストアップ
         SampleCallback   @1
</source>


次に、C# EXEからC++ DLLを呼び出す方法を記述する。

マネージドコールバック関数を定義する。
以下の例では、ManagedCallBackというデリゲートを宣言している。
C++ DLLのSampleCallBack関数にデリゲートを引数として渡すことで、既知のコールバック形式に自動的に変換される。

<syntaxhighlight lang="c#">
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;
       }
   }
}
</source>

トラブル対処

可能な限り、デバッグビルドしたDLLとそのPDBファイルを用意する。

デバッグ

C++DLLがデバッグできない場合、以下の手順で、混合モードデバッグを有効にする。

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


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

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

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

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