「C++の応用 - C Sharp DLLの使用」の版間の差分
467行目: | 467行目: | ||
|} | |} | ||
</center> | </center> | ||
<br> | |||
<u>※注意3</u><br> | |||
* <u>C# DLLがx64の場合、C++ EXEもx64でビルドする必要がある。</u><br><u>同様に、C++ EXE -> C# COM DLL(ラッパーDLL) -> C# DLLとする場合も、ラッパーDLLはx64である必要がある。</u> | |||
* <u>C# DLLがx86の場合、C++ EXEもx86でビルドする必要がある。</u><br><u>同様に、C++ EXE -> C# COM DLL(ラッパーDLL) -> C# DLLとする場合も、ラッパーDLLはx86である必要がある。</u> | |||
<br><br> | <br><br> | ||
2021年12月15日 (水) 17:36時点における版
概要
C++ EXEからC# DLLの関数を呼び出す方法は、幾つか方法が存在しており、各々にメリットとデメリットがある。
下記の表1に代表的な4種類の方法を示す。
表1. C++ EXEからC# DLLの関数を呼び出す方法
方法 | メリット | デメリット |
---|---|---|
C++/CLIを使う | 最も簡単 VisualStudioのIntelliSenseも使用可能 |
プロジェクトの設定で[CLIを使う]に変更する必要がある |
.NET DLLExportを使用して、 C# DLLのメソッドをエクスポートする |
[CLIを使う]に変更しなくてよいGetProcAddress 関数が使用できるため、よく知られた方法で関数を呼び出す事が出来る |
C# DLL側のソースコードが無い場合は利用できない |
C# DLLに対するC++/CLIのラッパーDLLを作成して、 C++ EXEから使う |
[CLIを使う]に変更しなくてよい COMを使用しない場合において、 元のプロジェクトの設定を変更したくない場合に使用可能 |
やり方がスマートではない |
C# DLLをCOM参照可能にする | [CLIを使う]に変更しなくてよい | C++ EXEのコード量が増えて面倒である |
上表1において、以下の手順を記載する。
- C++/CLIを使う
- C# DLL側で関数をエクスポートする
- C++/CLIのラッパープロジェクトを作成する
- C# DLLをCOM参照可能にする
C++/CLIを使う方法
// SampleDLL.cs
namespace SampleDLL
{
public class Class1
{
public static int Sum(int a, int b)
{
return a + b;
}
}
}
Visual C++のプロジェクト設定を開いて、[共通言語ランタイム サポート (/clr)]に変更する。
// SampleEXE.cpp
#include <Windows.h>
#include <iostream>
#using "SampleDLL.dll"
using namespace SampleDLL;
int main()
{
std::cout << Class1::Sum(1, 2) << std::endl;
return 0;
}
C# DLL側で関数をエクスポートする方法
まず、プロジェクトを作成してソースコードを記述する。
// SampleDLL.cs
namespace SampleDLL
{
public class Class1
{
[DllExport]
public static int Sum(int a, int b)
{
return a + b;
}
}
}
// SampleEXE.cpp
#include <Windows.h>
#include <iostream>
typedef int (*Sum)(int a, int b);
int main()
{
auto hModule = LoadLibrary(L"DllExportTest.dll");
auto sum = reinterpret_cast<Sum>(GetProcAddress(hModule, "Sum"));
std::cout << sum(1, 2) << std::endl;
return 0;
}
次に、DllExport.batをダウンロードして、DllExport.batをC# DLLのslnファイルと同じ階層に配置する。
続いて、コマンドプロンプトを開いて以下のコマンドを実行して、.NET DLLExportを起動する。
DllExport.bat -action Configure
.NET DLLExportダイアログにて、[Installed]チェックボックスにチェックを入力して、[Apply]ボタンを押下する。
最後に、C# DLLのプロジェクトをリビルドすると、作成した関数がエクスポートされる。
C++/CLIのラッパープロジェクトを使用する方法
まず、以下に示すソースコードのC# DLLを作成する。
// CSharpDLL.cs
namespace CSharpDLL
{
public static class CSharpDLLClass
{
public static void ShowValue(ref int value)
{
DialogResult result = MessageBox.Show("C# Message Box", "C# Message Box", MessageBoxButtons.OKCancel);
if (result == DialogResult.OK)
{
value = 1;
}
else
{
value = 2;
}
return;
}
}
}
次に、以下に示すようなソースコードのC++/CLI DLLを作成する。
その時、ソリューションエクスプローラからC++/CLIプロジェクトを右クリックして[参照の追加]を選択、上記で作成したC# DLLファイルを追加する。
また、Visual C++のプロジェクト設定を開いて、[構成プロパティ] - [全般] - [共通言語ランタイム サポート (/clr)]に変更する。
同様に、[構成プロパティ] - [C/C++] - [プリプロセッサ] - [プリプロセッサの定義]項目に、DLL
プリプロセッサを追加する。
// CppCLIDLL.cpp
#include "stdafx.h"
#include "CppCLIDLL.h"
using namespace System;
using namespace System::Reflection;
using namespace CSharpDLL;
namespace CppCLIDll
{
public ref class CppCLIClass
{
public:void ShowCSharpMessageBox(int *value)
{
CSharpDLLClass::ShowValue(*value);
return;
}
};
}
void ShowMessageBox(int *value)
{
CppCLIDll::CppCLIClass clsCLI;
clsCLI.ShowCSharpMessageBox(value);
}
// CppCLIDLL.h
#pragma once
#ifdef DLL
__declspec(dllexport) void ShowMessageBox(int *value);
#else
__declspec(dllimport) void ShowMessageBox(int *value);
#endif
最後に、以下に示すようなC++のソースコードのプロジェクトを作成する。
その時、メニューバーから[Visual C++のプロジェクト設定]を選択して、[構成プロパティ] - [C/C++] - [全般] - [追加のインクルードディレクトリ]項目に、
CppCLIDLL.hが存在するディレクトリを追加する。
同様に、[構成プロパティ] - [リンカー] - [全般] - [追加のライブラリディレクトリ]項目に、CppCLIDLL.libが存在するディレクトリを追加する。
さらに、[構成プロパティ] - [リンカー] - [入力] - [追加の依存ファイル]項目に、CppCLIDLL.libを追加する。
// CppEXE.cpp
#include "stdafx.h"
#include <windows.h>
#include "TestApp.h"
#include "CppCLI.h"
int _tmain()
{
int result = 0;
ShowMessageBox(&result);
if (result == 1)
{
printf("Ok Was Pressed \n");
printf("%d\n", result);
}
else if (result == 2)
{
printf("Cancel Was Pressed \n");
printf("%d\n", result);
}
else
{
printf("Unknown result \n");
}
system("pause");
return 0;
}
C# DLLをCOM参照可能にしてC++ EXEから使用する方法
まず、C# DLLプロジェクトを作成して、アセンブリ情報を設定する。
- C# DLLプロジェクトのプロパティを開く。
- プロパティ画面左にある[アプリケーション]タブを選択して、[アセンブリ情報]ボタンを押下する。
- [アセンブリをCOM参照可能にする]チェックボックスにチェックを入力して、[OK]ボタンを押下する。
次に、ビルドの設定を行う。
- C# DLLプロジェクトのプロパティを開く。
- プロパティ画面左にある[ビルド]タブを選択して、[COM相互運用機能の登録]チェックボックスにチェックを入力する。
- C# DLLプロジェクトのプロパティを保存する。
※注意1
[COM相互運用機能の登録]は、regasm
コマンドによるCOMのレジストリ登録を、ビルド時に自動で行う機能と思われる。
そのため、開発時(デバッグ時)は有効にした方が便利であるが、インストール時はCOMのレジストリ登録が自動で行われないため注意すること。
※注意2
Windowsにログインしているアカウントの権限によっては、ビルド時にレジストリへの登録に失敗するエラーが発生することがある。
その時は、Visual Studioを管理者権限で実行すれば登録できる。
C# DLLでは、以下のような内容のソースコードを記述する。
この時、C++ EXE側から呼ぶクラスには、以下の属性を付加する。
- ComVisible
- ClassInterface
- Guid (Visual Studioの[ツール]メニューバー - [GUIDの作成]を選択する)
// CSharpCOMDLL.csファイル
using System;
using System.Runtime.InteropServices;
namespace CSharpCOMDLL
{
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDual)]
[Guid("85555B74-E2E0-4493-9869-3CE95F13CB99")] // Visual Studioの[ツール]メニューバー - [GUIDの作成]を選択する
public class CSharpCOMDLLClass
{
public Int32 Add(Int32 iParam1, Int32 iParam2)
{
int iRet = iParam1 + iParam2;
return (Int32)iRet;
}
public Int32 AddStr([MarshalAs(UnmanagedType.BStr)]string str) // 文字列を指定する場合はマーシャリングする
{
Console.WriteLine(str);
return (Int32)0;
}
}
}
C++ EXEプロジェクトを作成して、以下のような内容のソースコードを記述する。
// main.cppファイル
#include <iostream>
#include <Windows.h>
IDispatch *pIDisp = NULL;
IUnknown *pIUnk = NULL;
long Init(void);
long Finalize(void);
long AddInt(long p_Number1, long p_Number2);
long AddStr();
int main()
{
// COMの初期化処理
Init();
// C# DLLのメソッドを呼ぶ
int l_Result = Add(300, 500);
//後処理
Finalize();
printf("Calc Result : %d", l_Result);
return 0;
}
// 初期化関数
long Init(void)
{
// COMの初期化
::CoInitialize(NULL);
// ProcIDからCLSIDを取得(ネームスペース名.クラス名)
CLSID clsid;
HRESULT h_result = CLSIDFromProgID(L"CSharpCOMDLL.CSharpCOMDLLClass", &clsid); // 第1引数は、呼び出すC# DLLの<名前空間名>.<クラス名>にすること
if (FAILED(h_result))
{
return -1;
}
// インスタンスの生成
h_result = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&pIUnk);
if (FAILED(h_result))
{
return -2;
}
// インターフェースの取得(pIDispは共通変数)
h_result = pIUnk->QueryInterface(IID_IDispatch, (void**)&pIDisp);
if (FAILED(h_result))
{
return -3;
}
return 0;
}
// COMの終了処理
long Finalize()
{
// インスタンスの開放
pIDisp->Release();
// インターフェイスの開放
pIUnk->Release();
// COMの開放
::CoUninitialize();
return 0;
}
// C# DLLのメソッドを呼ぶ
long AddInt(long p_Number1, long p_Number2)
{
// メソッド名からID(DISPID)を取得(関数名の設定)
DISPID dispid = 0;
OLECHAR *Func_Name[] = { SysAllocString (L"Add") }; // C# DLLの呼び出すメソッド名を指定する
HRESULT h_result = pIDisp->GetIDsOfNames(IID_NULL, Func_Name, 1, LOCALE_SYSTEM_DEFAULT, &dispid);
if (FAILED(h_result))
{
return -1;
}
// メソッドに渡すパラメータを作成(DISPPARAMS、 VariantInit等)
DISPPARAMS params = {0};
params.cNamedArgs = 0;
params.rgdispidNamedArgs = NULL;
params.cArgs = 2; // 呼び出す関数の引数の数
// 引数の指定 (順番が逆になることに注意すること)
VARIANTARG* pVarg = new VARIANTARG[params.cArgs];
pVarg[0].vt = VT_I4;
pVarg[0].lVal = p_Number2;
pVarg[1].vt = VT_I4;
pVarg[1].lVal = p_Number1;
params.rgvarg = pVarg;
VARIANT vRet;
VariantInit(&vRet);
// C# DLLのメソッドを呼ぶ(pIDisp->Invoke)
pIDisp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, ¶ms, &vRet, NULL, NULL);
delete[] pVarg;
return vRet.lVal;
}
long AddStr()
{
// メソッド名からID(DISPID)を取得(関数名の設定)
DISPID dispid = 0;
OLECHAR *Func_Name[] = { SysAllocString (L"AddStr") };
HRESULT h_result = pIDisp->GetIDsOfNames(IID_NULL, Func_Name, 1, LOCALE_SYSTEM_DEFAULT, &dispid);
if (FAILED(h_result))
{
return -1;
}
// メソッドに渡すパラメータを作成(DISPPARAMS、 VariantInit等)
DISPPARAMS params = {0};
params.cNamedArgs = 0;
params.rgdispidNamedArgs = NULL;
params.cArgs = 1; // 呼び出すメソッドの引数の数
// 引数の指定 (順番が逆になることに注意すること)
VARIANT var;
DISPPARAMS dispParams;
var.vt = VT_BSTR; // 引数に渡すデータ型をBSTRにする
var.bstrVal = SysAllocString(L"あいうえお"); // 引数に渡す文字列
dispParams.cArgs = 1;
dispParams.rgvarg = &var;
dispParams.cNamedArgs = 0;
dispParams.rgdispidNamedArgs = NULL;
VARIANT vRet;
VariantInit(&vRet);
// C# DLLのメソッドを呼ぶ(pIDisp->Invoke)
printf("[OK] Invoke start\r\n");
h_result = pIDisp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispParams, &vRet, NULL, NULL);
if (FAILED(h_result))
{
printf("[NG] Invoke failed\r\n");
return -2;
}
return vRet.lVal;
}
管理者権限でPowerShellまたはコマンドプロンプトを実行する。
次に、regasm
コマンドを使用して、C# DLL(COM)を登録する。
C# DLLのプラットフォーム(x86/x64)により、x86/x64向けのregasm
と合致させる必要があることに注意する。
また、regasm.exeをC++ EXE側のプロジェクトディレクトリにコピーして実行しても構わない。
# プロジェクトがx86の場合 C:\Windows\Microsoft.NET\Framework\<.NETのバージョン>\regasm /codebase <C# DLLのファイル名>.dll
# プロジェクトがx64の場合 C:\Windows\Microsoft.NET\Framework64\<.NETのバージョン>\regasm /codebase <C# DLLのファイル名>.dll
C++ EXEを実行する場合、以下の内容のバッチファイルを作成して、管理者権限で実行する。
常にregasm
コマンドを実行する場合、C# DLLプロジェクトの[COM相互運用機能の登録]の設定は不要である。
ただし、デバッグ時においては有効にした方が便利である。
rem exerun.batファイル
@echo off
cd %~dp0
regasm /codebase <C# DLLのファイル名>.dll
start /wait <C++ EXEのファイル名>.exe
echo exeからの戻り値は %ERRORLEVEL% です
pause
下表に、C#のデータ型、C++のデータ型、VARTYPEの関係を示す。
C++から引数を指定する場合、および、C# DLLからの戻り値を取得するために、VARIANT型を使用する必要がある。
そのため、C#、C++、VARIANT型の関係を理解する必要がある。
C++ | C# | VARTYPE | 使用するメンバ |
---|---|---|---|
SHORT (short) | short (System.Int16) | VT_I2 | iVal |
INT (int) LONG (long) |
int (System.Int32) | VT_I4 | lVal |
BOOL (long) | bool (System.Boolean) | VT_BOOL | boolVal |
LPCSTR (const char *) LPCWSTR (const wchar_t *) |
string (System.String) | VT_BSTR | bstrVal |
FLOAT (float) | float (System.Single) | VT_R4 | fltVal |
DOUBLE (double) | double (System.Double) | VT_R8 | dblVal |
※注意3
- C# DLLがx64の場合、C++ EXEもx64でビルドする必要がある。
同様に、C++ EXE -> C# COM DLL(ラッパーDLL) -> C# DLLとする場合も、ラッパーDLLはx64である必要がある。 - C# DLLがx86の場合、C++ EXEもx86でビルドする必要がある。
同様に、C++ EXE -> C# COM DLL(ラッパーDLL) -> C# DLLとする場合も、ラッパーDLLはx86である必要がある。
デバッグ
C++プロジェクトからC#プロジェクトに対するデバッグを有効にする手順を記載する。
- [ソリューションエクスプローラー]に表示されているC++プロジェクトを右クリックして、[プロパティ]を選択する。
- [<プロジェクト名> プロパティページ]画面にて、[構成プロパティ] - [デバッグ]を選択する。
- [デバッガーの種類]項目を、[混合]または[自動]に設定して、[OK]ボタンを押下する。