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

提供:MochiuWiki : SUSE, EC, PCB
2019年12月5日 (木) 14:31時点における192.168.1.17による版 (→‎サンプルコード)
ナビゲーションに移動 検索に移動

概要

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

例えば、C++ DLLから次のような関数がエクスポートされているとする。

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


上記の関数において、C# EXEから使用するときは、char*型は文字列なのでstring型を渡す。
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で構造体を定義しなければいけない。
WindowsのDLL(Win32 API)と.NET Frameworkでは型の管理方法が違うため、実際には型の相互変換(マーシャリング)が行われる。
尚、BOOL型の実体はLONG型と同じなので、boolの代わりにintを指定することも可能である。

表1. Win32 APIでの型名と対応するC#の型
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の作成方法はコチラを参照する。
下記にも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++)
             {
                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#上で使用する関数名を異なる名前にする時に指定する。