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

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動
17行目: 17行目:
== サンプルコード ==
== サンプルコード ==
下記に、C++ DLLを呼ぶC# EXEのソースコードを記載する。<br>
下記に、C++ DLLを呼ぶC# EXEのソースコードを記載する。<br>
<br>
  <source lang="c#">
  <source lang="c#">
  // DllImportを使用するために必要
  // DllImportを使用するために必要

2019年11月2日 (土) 08:34時点における版

概要

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)
 SampleDll.cpp
 
 #include <stdio.h>
 #include <string.h>
 #include "SampleDll.h"
 
 typedef struct tagSampleStruct
 {
    int index;
    char name[128];
    int data[50];
 } SampleStruct, *pSampleStruct;
 
 double __stdcall SampleFunc01(int a)
 {
    printf(--<SampleDll:SampleFunc01>--\r\n");
    printf("a = %d\r\n", a);
    printf("---------------------------\r\n");
 
    return 3.14;
 }
 
 void __stdcall Sample02(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側から返す場合は、string型ではなくStringBuilderクラスを使用する必要がある。 StringBuilderクラスは受け渡しの両方が可能なので、文字列はStringBuilderクラスを使用すべきである。

C++では構造体のサイズはコンパイル時に決定されるが、C#では実行時に決定される。 したがって、C#側で構造体のサイズを予め指定する必要がある。 この場合、構造体は固定長サイズとなるため、配列などを定義する場合は異なるサイズの配列を後からインスタンス化することができなくなる。

 using System;
 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("Tips_Win32DLL.dll")]
        private static extern void Sample04(SampleStruct st);
 
        /// <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;
        }
 
       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();
       }
    }
 }


DllImport属性には、DLLファイルのパスを指定する以外に次のような引数を与えることもできる。
これ他にも細かい設定をするための引数が用意されているので、DllImportAttributeクラスで検索すること。

名称 説明
CallingConvention エントリポイントの呼び出し規約を明示的に指定できる。
指定なしの場合は__stdcallとなる。
CharSet 文字列パラメータをメソッドにマーシャリングし、名前マングルを制御する方法を指定する。
文字コードの相互変換する時に指定する。
EntryPoint 呼び出すDLLエントリポイントの名前または序数を指定する。
DLLの関数名とC#上で使用する関数名を異なる名前にする時に指定する。