「C++の応用 - C Sharp DLLの使用」の版間の差分
細 (文字列「__FORCETOC__」を「{{#seo: |title={{PAGENAME}} : Exploring Electronics and SUSE Linux | MochiuWiki |keywords=MochiuWiki,Mochiu,Wiki,Mochiu Wiki,Electric Circuit,Electric,pcb,Mathematics,AVR,TI,STMicro,AVR,ATmega,MSP430,STM,Arduino,Xilinx,FPGA,Verilog,HDL,PinePhone,Pine Phone,Raspberry,Raspberry Pi,C,C++,C#,Qt,Qml,MFC,Shell,Bash,Zsh,Fish,SUSE,SLE,Suse Enterprise,Suse Linux,openSUSE,open SUSE,Leap,Linux,uCLnux,Podman,電気回路,電子回路,基板,プリント基板 |description={{PAGENAME}} - 電子回路とSUSE Linuxに関する情報 | This pag…) |
|||
(同じ利用者による、間の50版が非表示) | |||
1行目: | 1行目: | ||
== 概要 == | == 概要 == | ||
C++ | C++からC#ライブラリの関数を呼び出す方法は、複数の方法が存在しており、各々にメリットおよびデメリットがある。<br> | ||
下表に代表的な5種類の方法を示す。<br><br> | |||
<center> | <center> | ||
{| class="wikitable" | style="background-color:#fefefe;" | |||
{| class="wikitable" | |+ C++からC#ライブラリのメソッドを呼ぶ方法 | ||
|- | |- | ||
! 方法 ! | ! style="background-color:#66CCFF;" | 方法 | ||
! style="background-color:#66CCFF;" | メリット | |||
! style="background-color:#66CCFF;" | デメリット | |||
|- | |- | ||
| C++/ | | C++のプロジェクトをC++/CLIに設定する || C++のプロジェクトの設定において、[CLIを使う]に変更する。<br>VisualStudioのIntelliSenseも使用可能。 || C++/CLIに関するドキュメントが少ない。<br><br><u>Windowsのみ使用可能。</u> | ||
|- | |- | ||
| C# | | [https://github.com/3F/DllExport .NET DLLExport]を使用して、<br>C#ライブラリのメソッドをエクスポートする || C++/CLIを使用する必要がない。<br><br><code>GetProcAddress</code>関数が使用できるため、<br>よく知られた方法で関数を呼び出す事が出来る || C#ライブラリのソースコードが無い場合は利用できない。<br><br><u>Windowsのみ使用可能。</u> | ||
|- | |- | ||
| C# | | C++からC#ライブラリを呼ぶための<br>C++/CLIのラッパーライブラリを開発する || COMを使用しなくてよい。<br>C++およびC#のプロジェクトの設定を変更しなくてよい。 || C#ライブラリ、および、C++/CLIライブラリの2つのライブラリを作成する必要がある。<br>C++/CLIに関するドキュメントが少ない。<br><br><u>Windowsのみ使用可能。</u> | ||
|- | |- | ||
| C | | Embedded Monoを使用する || C++およびC#ライブラリの2つのプロジェクトを作成するだけでよい。<br><u>Linux、MacOSでも使用可能である。</u><br><br><u>ただし、Linux上で使用する場合、</u><br><u><code>async, await</code>や他の様々なシンタックスが使用できない等の</u><br><u>デメリットも多い。</u><br><br>C++のプロジェクトにおいて、<br>Monoに関連するヘッダファイルをインクルードして、C#ライブラリを呼ぶ。<br> || Linuxの場合は、.NET Standard (.NET Core / .NET 5以降ではない) またはMonoを使用して、<br>C#ライブラリを作成する必要がある。<br><br>Windowsの場合は、.NET FrameworkまたはMonoを使用して、C#ライブラリを作成する必要がある。<br><br>したがって、.NET Core / .NET 5以降は使用することができない。<br><br>また、実行環境にもMonoをインストールする必要がある。 | ||
|- | |||
| C#ライブラリをCOM参照可能にする || C++/CLIライブラリは不要である。 || C++のソースコード量が増えて設計が煩雑となる。<br><br>COM (Component Object Model) は、Windows独自の技術であり、<br><u>Linuxでは直接サポートされていないため、Windowsのみ使用可能である。</u><br><br>COMはWindowsのオブジェクト指向プログラミングモデルであり、<br>Microsoftの開発環境やWindows APIと密接に関連している。 | |||
|} | |} | ||
</center> | </center> | ||
<br> | <br> | ||
上表1において、以下の手順を記載する。<br> | |||
* C++のプロジェクトをC++/CLIに設定する | |||
* [https://github.com/3F/DllExport .NET DLLExport]を使用して、C#ライブラリのメソッドをエクスポートする | |||
* C++/CLIのラッパーライブラリを開発する | |||
* Embedded Monoを使用する | |||
* C#ライブラリをCOM参照可能にする | |||
<br><br> | |||
== C++/CLIを使う方法 == | == C++/CLIを使う方法 == | ||
< | <syntaxhighlight lang="c#"> | ||
// SampleDLL.cs | // SampleDLL.cs | ||
namespace SampleDLL | namespace SampleDLL | ||
34行目: | 43行目: | ||
} | } | ||
} | } | ||
</ | </syntaxhighlight> | ||
Visual C++のプロジェクト設定を開いて、[共通言語ランタイム サポート (/clr)]に変更する。<br> | Visual C++のプロジェクト設定を開いて、[共通言語ランタイム サポート (/clr)]に変更する。<br> | ||
< | <syntaxhighlight lang="c++"> | ||
// SampleEXE.cpp | // SampleEXE.cpp | ||
#include <Windows.h> | #include <Windows.h> | ||
49行目: | 58行目: | ||
return 0; | return 0; | ||
} | } | ||
</ | </syntaxhighlight> | ||
<br><br> | <br><br> | ||
== C# DLL側で関数をエクスポートする方法 == | == C# DLL側で関数をエクスポートする方法 == | ||
まず、プロジェクトを作成してソースコードを記述する。<br> | まず、プロジェクトを作成してソースコードを記述する。<br> | ||
< | <syntaxhighlight lang="c#"> | ||
// SampleDLL.cs | // SampleDLL.cs | ||
namespace SampleDLL | namespace SampleDLL | ||
67行目: | 76行目: | ||
} | } | ||
} | } | ||
</ | </syntaxhighlight> | ||
<br> | |||
< | <syntaxhighlight lang="c++"> | ||
// SampleEXE.cpp | // SampleEXE.cpp | ||
#include <Windows.h> | #include <Windows.h> | ||
83行目: | 92行目: | ||
return 0; | return 0; | ||
} | } | ||
</ | </syntaxhighlight> | ||
<br> | <br> | ||
次に、[https://github.com/3F/DllExport/releases DllExport.batをダウンロード]して、DllExport.batをC# DLLのslnファイルと同じ階層に配置する。<br><br> | 次に、[https://github.com/3F/DllExport/releases DllExport.batをダウンロード]して、DllExport.batをC# DLLのslnファイルと同じ階層に配置する。<br><br> | ||
89行目: | 98行目: | ||
DllExport.bat -action Configure | DllExport.bat -action Configure | ||
.NET DLLExportダイアログにて、[Installed]チェックボックスにチェックを入力して、[Apply]ボタンを押下する。<br> | .NET DLLExportダイアログにて、[Installed]チェックボックスにチェックを入力して、[Apply]ボタンを押下する。<br> | ||
[[ファイル: | [[ファイル:dotNET_DLLExport.png|フレームなし|中央]] | ||
<br> | <br> | ||
最後に、C# DLLのプロジェクトをリビルドすると、作成した関数がエクスポートされる。<br><br> | 最後に、C# DLLのプロジェクトをリビルドすると、作成した関数がエクスポートされる。<br><br> | ||
== C++/CLIのラッパープロジェクトを使用する方法 == | == C++/CLIのラッパープロジェクトを使用する方法 == | ||
< | ==== C#ライブラリの作成 ==== | ||
まず、C#ライブラリを作成する。<br> | |||
<syntaxhighlight lang="c#"> | |||
// CSharpDLL.cs | // CSharpDLL.cs | ||
namespace CSharpDLL | namespace CSharpDLL | ||
115行目: | 126行目: | ||
} | } | ||
} | } | ||
</ | </syntaxhighlight> | ||
<br> | |||
==== C++/CLIライブラリの作成 ==== | |||
次に、C++/CLIライブラリを作成する。<br> | |||
<br> | |||
ソリューションエクスプローラからC++/CLIプロジェクトを右クリックして[参照の追加]を選択、上記で作成したC# DLLファイルを追加する。<br> | |||
<br> | <br> | ||
また、Visual C++のプロジェクト設定を開いて、[構成プロパティ] - [全般] - [共通言語ランタイム サポート (/clr)]に変更する。<br> | |||
同様に、[構成プロパティ] - [C/C++] - [プリプロセッサ] - [プリプロセッサの定義]項目に、<code>DLL</code>プリプロセッサを追加する。<br> | |||
同様に、[構成プロパティ] - [C/C++] - [プリプロセッサ] - [プリプロセッサの定義] | <syntaxhighlight lang="c++"> | ||
< | |||
// CppCLIDLL.cpp | // CppCLIDLL.cpp | ||
#include "stdafx.h" | #include "stdafx.h" | ||
146行目: | 161行目: | ||
clsCLI.ShowCSharpMessageBox(value); | clsCLI.ShowCSharpMessageBox(value); | ||
} | } | ||
</ | </syntaxhighlight> | ||
< | <br> | ||
<syntaxhighlight lang="c++"> | |||
// CppCLIDLL.h | // CppCLIDLL.h | ||
#pragma once | #pragma once | ||
156行目: | 172行目: | ||
__declspec(dllimport) void ShowMessageBox(int *value); | __declspec(dllimport) void ShowMessageBox(int *value); | ||
#endif | #endif | ||
</ | </syntaxhighlight> | ||
<br> | <br> | ||
Visual C++ | ==== C++実行バイナリの作成 ==== | ||
C++実行バイナリのプロジェクトを作成する。<br> | |||
<br> | |||
===== 暗黙的リンクを行う場合 ===== | |||
メニューバーから[Visual C++のプロジェクト設定]を選択して、[構成プロパティ] - [C/C++] - [全般] - [追加のインクルードディレクトリ]項目に、<br> | |||
CppCLIDLL.hが存在するディレクトリを追加する。<br> | CppCLIDLL.hが存在するディレクトリを追加する。<br> | ||
<br> | |||
同様に、[構成プロパティ] - [リンカー] - [全般] - [追加のライブラリディレクトリ]項目に、CppCLIDLL.libが存在するディレクトリを追加する。<br> | 同様に、[構成プロパティ] - [リンカー] - [全般] - [追加のライブラリディレクトリ]項目に、CppCLIDLL.libが存在するディレクトリを追加する。<br> | ||
さらに、[構成プロパティ] - [リンカー] - [入力] - [追加の依存ファイル]項目に、CppCLIDLL.libを追加する。<br> | |||
< | <syntaxhighlight lang="c++"> | ||
// CppEXE.cpp | // CppEXE.cpp | ||
#include "stdafx.h" | #include "stdafx.h" | ||
#include <windows.h> | #include <windows.h> | ||
#include " | #include "CppEXE.h" | ||
#include " | #include "CppCLIDLL.h" | ||
int _tmain() | int _tmain() | ||
193行目: | 215行目: | ||
return 0; | return 0; | ||
} | } | ||
</ | </syntaxhighlight> | ||
<br> | |||
===== 明示的リンクを行う場合 ===== | |||
# まず、<code>LoadLibrary</code>関数を使用して、C++/CLIライブラリを読み込む。<br> | |||
# 次に、<code>GetProcAddress</code>関数を使用して、C++/CLIライブラリ内の関数オブジェクトのアドレスを取得する。<br> | |||
# C++/CLIライブラリの関数を呼び出す。 | |||
<br> | |||
<syntaxhighlight lang="c++"> | |||
// CppEXE.cpp | |||
#include "stdafx.h" | |||
#include <windows.h> | |||
#include "CppEXE.h" | |||
using fnShowMessageBox void(*)(int*); | |||
int _tmain() | |||
{ | |||
int result = 0; | |||
// C++/CLIライブラリを呼び出す | |||
auto hModule = ::LoadLibrary(L"CppCLIDLL.dll"); | |||
if (NULL == hModule) { | |||
return -1; | |||
} | |||
// C++/CLIライブラリの関数を読み込む | |||
auto ShowMessageBox = reinterpret_cast<fnShowMessageBox>(::GetProcAddress(hModule, "ShowMessageBox")); | |||
// C++/CLIライブラリの関数を実行する | |||
ShowMessageBox(&result); | |||
if (result == 1) { | |||
std::cout << "Ok Was Pressed" << std::endl; | |||
std::cout << result << std::endl; | |||
} | |||
else if (result == 2) { | |||
std::cout << "Cancel Was Pressed" << std::endl; | |||
std::cout << result << std::endl; | |||
} | |||
else { | |||
std::cout << "Unknown result" << std::endl; | |||
} | |||
system("pause"); | |||
return 0; | |||
} | |||
</syntaxhighlight> | |||
<br><br> | <br><br> | ||
== Monoを使用する場合 == | |||
==== Monoのインストール ==== | |||
* RHEL | |||
*: [[インストール - Mono|インストール - Mono(RHEL)]]を参照して、Monoをインストールする。<br> | |||
* SUSE | |||
*: [[インストール - Mono(SUSE)]]を参照して、Monoをインストールする。<br> | |||
* Windows | |||
*: [https://www.mono-project.com/download/stable/ Monoの公式Webサイト]にアクセスして、[Download Mono 64-bit (no GTK#)]を選択して、Monoをダウンロードする。 | |||
*: Monoをインストールする。 | |||
*: <br> | |||
*: 必要ならば、スタートメニューのMonoプログラムグループ下に[Monoコマンドプロンプトを開く]ショートカットを作成する。 | |||
*: このショートカットは、Mono関連のパス情報がすでに設定されたコマンドシェルを起動するものである。 | |||
<br> | |||
==== C#ライブラリの作成 ==== | |||
以下の例では、SampleLibraryという名前のライブラリを作成している。<br> | |||
<syntaxhighlight lang="c#"> | |||
using System; | |||
namespace SampleLibrary | |||
{ | |||
public class SampleClass | |||
{ | |||
public void SampleMethod(int intValue, string stringValue) | |||
{ | |||
Console.WriteLine($"Received int: {intValue}, string: {stringValue}"); | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
<br> | |||
==== C++実行ファイルの作成 ==== | |||
SUSEでは、コンパイル時において、<code>-L/usr/lib64 -lmono-2.0</code>オプション、および、<code>-I/usr/include/mono-2.0</code>オプションを付加する必要がある。<br> | |||
または、plg-configツールを使用することもできる。<br> | |||
g++ -o <アプリケーション名 例: SampleApp> main.cpp `pkg-config --cflags --libs mono-2` | |||
<br> | |||
C#ライブラリのメソッドを呼び出す<code>mono_runtime_invoke</code>関数に指定する引数を、以下に示す。<br> | |||
* 第1引数 : <code>void**</code>型 | |||
*: C#のメソッドがインスタンスメソッドである場合、メソッドを呼び出すインスタンスのポインタを指定する。 | |||
*: Staticメソッドの場合は、<code>nullptr</code>を指定する。 | |||
* 第2引数 : <code>void**</code>型 | |||
*: メソッドに渡す引数を指定する。 | |||
*: 引数がある場合、各引数の値へのポインタが配列として渡される。 | |||
*: 引数が無い場合は、<code>nullptr</code>を指定する。 | |||
* 第3引数 : <code>MonoObject**</code>型 | |||
*: エラーが発生した場合に、エラー情報を格納する<code>MonoObject</code>型のポインタを指定する。 | |||
*: エラーを取得しない場合は、<code>nullptr</code>を指定する。 | |||
<br> | |||
<syntaxhighlight lang="c++"> | |||
#include <iostream> | |||
#include <mono/jit/jit.h> | |||
#include <mono/metadata/assembly.h> | |||
#include <mono/metadata/object.h> | |||
#include <mono/metadata/appdomain.h> | |||
#include <mono/metadata/debug-helpers.h> | |||
#include <mono/metadata/exception.h> | |||
int main() | |||
{ | |||
// ドメインの初期化 | |||
MonoDomain *domain = mono_jit_init("SampleApp"); | |||
if (!domain) return -1; | |||
// C#ライブラリのアセンブリの読み込み (中間言語IL) | |||
MonoAssembly *assembly = mono_domain_assembly_open(domain, "SampleLibrary.dll"); | |||
if (!assembly) mono_jit_cleanup(domain); return -1; | |||
// アセンブリのイメージの読み込み (アセンブリ内のコード情報を保持しているもの) | |||
MonoImage *image = mono_assembly_get_image(assembly); | |||
if (!image) mono_jit_cleanup(domain); return -1; | |||
// 名前空間およびクラス名を指定 | |||
const char *nameSpace = "SampleLibrary"; // 名前空間を指定する | |||
const char *className = "SampleClass"; // クラス名を指定する | |||
MonoClass *klass = mono_class_from_name(image, nameSpace, className); | |||
if (!klass) mono_jit_cleanup(domain); return -1; | |||
// クラスのインスタンスを生成 | |||
MonoObject *instance = mono_object_new(domain, klass); | |||
if (!instance) mono_jit_cleanup(domain); return -1; | |||
// メソッド情報の取得 (以下のいずれかの形式でよい) | |||
//// 方法 1 --> | |||
////// C#ライブラリのメソッドの取得 | |||
MonoMethod *method = mono_class_get_method_from_name(klass, methodName, <メソッドの引数の数 例: 引数が無い場合は0を指定する>); | |||
//// <-- 方法 1 | |||
//// 方法 2 --> | |||
////// メソッド情報の取得 | |||
MonoMethodDesc *methodDesc = mono_method_desc_new("<名前空間名>.<クラス名>::<メソッド名>", true); | |||
if (!methodDesc) mono_jit_cleanup(domain); return -1; | |||
////// メソッドの検索 | |||
MonoMethod *method = mono_method_desc_search_in_class(methodDesc, klass); | |||
if (!method) mono_jit_cleanup(domain); return -1; | |||
//// <-- 方法 2 | |||
// C#ライブラリのメソッドに引数がある場合 (以下の例では、第1引数 : int型、第2引数 : string型) | |||
int intValue = 42; | |||
const char *stringValue = "Hello from C++"; | |||
// 引数を格納する配列 | |||
void *params[] = { | |||
&intValue, // int型へのポインタ | |||
mono_string_new(domain, stringValue) // string型へのポインタ | |||
}; | |||
// C#ライブラリのメソッドに引数がない場合 | |||
void *params[] = { nullptr }; | |||
// エラー情報が格納される変数 | |||
MonoObject *exc = nullptr; | |||
// C#ライブラリのメソッドの呼び出し | |||
// C#ライブラリのStaticメソッド(引数あり)を呼び出す場合 --> 例: mono_runtime_invoke(method, nullptr, params, nullptr); | |||
// C#ライブラリのStaticメソッド(引数なし)を呼び出す場合 --> 例: mono_runtime_invoke(method, nullptr, nullptr, nullptr); | |||
// C#ライブラリのStaticメソッド(引数あり)、かつ、エラー情報不要で呼び出す場合 --> 例: mono_runtime_invoke(method, nullptr, params, nullptr); | |||
mono_runtime_invoke(method, instance, params, &exc); | |||
// エラーの確認 | |||
if (exc != nullptr) { | |||
// エラー情報の取得 | |||
MonoClass *excClass = mono_object_get_class(exc); | |||
MonoMethod *toStringMethod = mono_class_get_method_from_name(excClass, "ToString", 0); | |||
MonoString *errorMessage = (MonoString*)mono_runtime_invoke(toStringMethod, exc, nullptr, nullptr); | |||
// エラーメッセージの出力 | |||
const char *pstrErrMessage = mono_string_to_utf8(errorMessage); | |||
std::cout << "Error: " << pstrErrMessage << std::endl; | |||
mono_free(pstrErrMessage); | |||
} | |||
// ドメインの解放 | |||
mono_jit_cleanup(domain); | |||
return 0; | |||
} | |||
</syntaxhighlight> | |||
<br> | |||
==== 引数 ==== | |||
===== ポインタを渡す場合 ===== | |||
C#ライブラリ側でデータ型のポインタを受け取り、その変数の値を変更しても、その変更がC++実行ファイル側に反映されることは期待できない。<br> | |||
これは、C#とC++が異なるメモリ管理とランタイム環境を持っており、それぞれ独自のメモリ管理を行うためである。<br> | |||
<br> | |||
C#では、ガベージコレクションが行われ、メモリの確保や解放はCLR (Common Language Runtime) によって管理されている。<br> | |||
一方、C++では開発者が手動でメモリの確保と解放を行う。<br> | |||
<br> | |||
したがって、C#ライブラリ内でデータ型のポインタを受け取り、その変数の値を変更したとしても、それはC#ランタイムの管理するメモリ内で行われることになる。<br> | |||
C++実行ファイル側では、C#ランタイムのメモリに直接アクセスできないため、その変更が反映されることはない可能性がある。<br> | |||
<br> | |||
異なるランタイム環境でのメモリ管理の違いからくる制約を考慮して、C++とC#間でデータの受け渡しを行う場合は、適切な手法やデータ構造を選択する必要がある。<br> | |||
例えば、C#ライブラリ側で変更可能な値を戻り値として返し、それをC++で受け取る等の方法がある。<br> | |||
<br> | |||
以下の例では、C++側でint型とMonoString*型を定義して、C#ライブラリ側で値を変更している。<br> | |||
<syntaxhighlight lang="c++"> | |||
// C++ | |||
// ...略 | |||
// メソッド情報の取得 | |||
const char *methodName = "func"; | |||
auto method = mono_class_get_method_from_name(mainClass, methodName, 2); | |||
int intValue = 0; | |||
MonoString *stringMonoValue = nullptr; | |||
// 引数を格納する配列 | |||
void *params[] = { | |||
&intValue, | |||
&stringMonoValue | |||
}; | |||
// C#ライブラリのメソッドの呼び出し | |||
MonoObject* excObject = nullptr; | |||
mono_runtime_invoke(method, classInstance, params, &excObject); | |||
if (excObject) | |||
{ | |||
MonoString *excString = mono_object_to_string(excObject, nullptr); | |||
const char *excCString = mono_string_to_utf8(excString); | |||
std::cout << "メソッドの実行時における例外 : " << excCString << std::endl; | |||
mono_jit_cleanup(domain); | |||
return -1; | |||
} | |||
else | |||
{ | |||
// 第1引数のint型を参照にしてC#ライブラリ側で変更した場合 | |||
std::cout << intValue << std::endl; // 100を出力する | |||
// 第2引数のMonoString*型を参照にしてC#ライブラリ側で変更した場合 | |||
std::string retStringMonoValue = mono_string_to_utf8(stringMonoValue); | |||
std::cout << retStringMonoValue << std::endl; // "abcあいう"を出力する | |||
} | |||
// ...略 | |||
</syntaxhighlight> | |||
<br> | |||
<syntaxhighlight lang="c#"> | |||
// C# | |||
public void func(ref int value1, ref string value2) | |||
{ | |||
value1 = 100; | |||
value2 = "abcあいう"; | |||
return; | |||
} | |||
</syntaxhighlight> | |||
<br> | |||
==== 戻り値がある場合 ==== | |||
===== 戻り値がint型の場合 ===== | |||
C#ライブラリが戻り値を返す場合は、<code>mono_runtime_invoke</code>関数でC#のメソッドを呼び出して、戻り値を<code>MonoObject*</code>型で受け取る。<br> | |||
その後、<code>mono_field_get_value</code>関数を使用して、<code>MonoObject*</code>型のオブジェクトからint型の戻り値を取得する。<br> | |||
<br> | |||
<u>※注意</u><br> | |||
<u><code>mono_runtime_invoke</code>関数の戻り値は<code>MonoObject*</code>型であるため、実際のint型のデータは内部的には<code>m_value</code>と呼ばれる領域に格納されていることである。</u><br> | |||
<u>これにより、int型の戻り値を取り出すことができる。</u><br> | |||
<br> | |||
<syntaxhighlight lang="c++"> | |||
// C#ライブラリがint型の戻り値を返す場合 | |||
MonoObject *pResult = mono_runtime_invoke(method, instance, params, &exc); | |||
// エラーの確認 | |||
if (exc != nullptr) { | |||
// エラー情報の取得 | |||
// ...略 | |||
} | |||
else | |||
{ | |||
// int型に変換 | |||
int returnValue; | |||
mono_field_get_value(pResult, mono_class_get_field_from_name(mono_object_get_class(result), "m_value", "System.Int32"), &returnValue); | |||
// 結果を出力 | |||
std::cout << "Method result: " << returnValue << std::endl; | |||
} | |||
</syntaxhighlight> | |||
<br> | |||
===== 戻り値がstring型の場合 ===== | |||
以下の例では、<code>mono_runtime_invoke</code>関数でC#のメソッドを呼び出して、戻り値を<code>MonoObject*</code>型で受け取る。<br> | |||
次に、<code>reinterpret_cast</code>を使用して<code>MonoObject*</code>型を<code>MonoString*</code>に変換する。<br> | |||
最後に、<code>mono_string_to_utf8</code>関数を使用して<code>string</code>型の戻り値を取得している。<br> | |||
<syntaxhighlight lang="c++"> | |||
// C#ライブラリがstring型の戻り値を返す場合 | |||
MonoObject *pResult = mono_runtime_invoke(method, instance, params, &exc); | |||
// エラーの確認 | |||
if (exc != nullptr) { | |||
// エラー情報の取得 | |||
// ...略 | |||
} | |||
else | |||
{ | |||
// string型に変換 | |||
MonoString *pResultString = reinterpret_cast<MonoString*>(pResult); | |||
const char *stringValue = mono_string_to_utf8(pResultString); | |||
// 結果を出力 | |||
std::cout << "Method result: " << stringValue << std::endl; | |||
// メモリの解放 | |||
mono_free(stringValue); | |||
} | |||
</syntaxhighlight> | |||
<br> | |||
===== 戻り値が配列の場合 (非コレクション) ===== | |||
以下の例では、C#ライブラリのメソッドの戻り値はint型の配列であり、C++でそれを受け取っている。<br> | |||
<br> | |||
まず、<code>mono_runtime_invoke</code>関数でC#のメソッドを呼び出して、戻り値を<code>MonoObject*</code>型で受け取る。<br> | |||
次に、<code>reinterpret_cast</code>を使用して<code>MonoObject*</code>型を<code>MonoArray*</code>に変換する。<br> | |||
最後に、<code>mono_array_get</code>関数を使用して<code>int</code>型の配列の各要素を取得している。<br> | |||
<br> | |||
<syntaxhighlight lang="c#"> | |||
// C#ライブラリ | |||
public int[] RetList() | |||
{ | |||
return [1, 2, 3]; | |||
} | |||
</syntaxhighlight> | |||
<br> | |||
<syntaxhighlight lang="c++"> | |||
// C++ | |||
MonoObject *pResult = mono_runtime_invoke(method, instance, nullptr, &exc); | |||
// エラーの確認 | |||
if (exc != nullptr) { | |||
// エラー情報の取得 | |||
// ...略 | |||
} | |||
else { | |||
// MonoObject*型をint型の配列に変換 | |||
MonoArray* resultArray = reinterpret_cast<MonoArray*>(pResult); | |||
// 配列の長さを取得 | |||
auto arrayLength = mono_array_length(resultArray); | |||
// int型の配列に変換 | |||
std::vector<int> ivec(0, 0); | |||
for (auto i = 0; i < arrayLength; i++) { | |||
// MonoArray*型をint型に変換 | |||
ivec.push_back(mono_array_get(resultArray, int, i)); | |||
} | |||
for (auto i : ivec) { | |||
std::cout << i << std::endl; | |||
} | |||
} | |||
</syntaxhighlight> | |||
<br> | |||
===== 戻り値がタプル型の場合 ===== | |||
タプル型において、C#からC++に直接渡すことは難しいため、複数の戻り値を配列や構造体で受け取る必要がある。<br> | |||
もし可能であれば、代わりに複数の引数を使用して値を取得することを検討することもできる。<br> | |||
<br> | |||
以下の例では、タプル型 (int, string) の戻り値を配列として受け取り、各要素を取り出している。<br> | |||
<u>ただし、戻り値の型や順序に依存するため、メソッドが変更されると対応が必要となることに注意する。</u><br> | |||
<br> | |||
<syntaxhighlight lang="c++"> | |||
// C#ライブラリがタプル型の戻り値を返す場合 | |||
struct TupleResult { | |||
int Field1; // int型 | |||
MonoString* Field2; // string型 | |||
}; | |||
// ...略 | |||
MonoObject *pResult = mono_runtime_invoke(method, instance, params, &exc); | |||
// エラーの確認 | |||
if (exc != nullptr) { | |||
// エラー情報の取得 | |||
// ...略 | |||
} | |||
else { | |||
// タプル型から個々の値を取得 | |||
TupleResult *tupleResult = static_cast<TupleResult*>(mono_object_unbox(pResult)); | |||
int intValue = tupleResult->Field1; | |||
std::string strValue = mono_string_to_utf8(tupleResult->Field2); | |||
// 結果を出力 | |||
std::cout << "Method result: " << intValue << ", " << strValue << std::endl; | |||
} | |||
</syntaxhighlight> | |||
<br><br> | |||
== C# DLLをCOM参照可能にしてC++ EXEから使用する方法 == | |||
まず、C# DLLプロジェクトを作成して、アセンブリ情報を設定する。<br> | |||
# C# DLLプロジェクトのプロパティを開く。 | |||
# プロパティ画面左にある[アプリケーション]タブを選択して、[アセンブリ情報]ボタンを押下する。 | |||
# [アセンブリをCOM参照可能にする]チェックボックスにチェックを入力して、[OK]ボタンを押下する。 | |||
<br> | |||
次に、ビルドの設定を行う。<br> | |||
# C# DLLプロジェクトのプロパティを開く。 | |||
# プロパティ画面左にある[ビルド]タブを選択して、[COM相互運用機能の登録]チェックボックスにチェックを入力する。 | |||
# C# DLLプロジェクトのプロパティを保存する。 | |||
<br> | |||
<u>※注意1</u><br> | |||
<u>[COM相互運用機能の登録]は、<code>regasm</code>コマンドによるCOMのレジストリ登録を、ビルド時に自動で行う機能と思われる。</u><br> | |||
<u>そのため、開発時(デバッグ時)は有効にした方が便利であるが、インストール時はCOMのレジストリ登録が自動で行われないため注意すること。</u><br> | |||
<br> | |||
<u>※注意2</u><br> | |||
<u>Windowsにログインしているアカウントの権限によっては、ビルド時にレジストリへの登録に失敗するエラーが発生することがある。</u><br> | |||
<u>その時は、Visual Studioを管理者権限で実行すれば登録できる。</u><br> | |||
<br> | |||
C# DLLでは、以下のような内容のソースコードを記述する。<br> | |||
この時、C++ EXE側から呼ぶクラスには、以下の属性を付加する。<br> | |||
* ComVisible | |||
* ClassInterface | |||
* Guid (Visual Studioの[ツール]メニューバー - [GUIDの作成]を選択する) | |||
<syntaxhighlight lang="c#"> | |||
// 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; | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
<br> | |||
C++ EXEプロジェクトを作成して、以下のような内容のソースコードを記述する。<br> | |||
<syntaxhighlight lang="c++"> | |||
// 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; | |||
} | |||
</syntaxhighlight> | |||
<br> | |||
管理者権限でPowerShellまたはコマンドプロンプトを実行する。<br> | |||
次に、<code>regasm</code>コマンドを使用して、C# DLL(COM)を登録する。<br> | |||
C# DLLのプラットフォーム(x86/x64)により、x86/x64向けの<code>regasm</code>と合致させる必要があることに注意する。<br> | |||
<br> | |||
また、regasm.exeをC++ EXE側のプロジェクトディレクトリにコピーして実行しても構わない。<br> | |||
# プロジェクトがx86の場合 | |||
C:\Windows\Microsoft.NET\Framework\<.NETのバージョン>\regasm /codebase <C# DLLのファイル名>.dll | |||
# プロジェクトがx64の場合 | |||
C:\Windows\Microsoft.NET\Framework64\<.NETのバージョン>\regasm /codebase <C# DLLのファイル名>.dll | |||
<br> | |||
C++ EXEを実行する場合、以下の内容のバッチファイルを作成して、管理者権限で実行する。<br> | |||
常に<code>regasm</code>コマンドを実行する場合、C# DLLプロジェクトの[COM相互運用機能の登録]の設定は不要である。<br> | |||
<u>ただし、デバッグ時においては有効にした方が便利である。</u><br> | |||
<syntaxhighlight lang="bat"> | |||
rem exerun.batファイル | |||
@echo off | |||
cd %~dp0 | |||
regasm /codebase <C# DLLのファイル名>.dll | |||
start /wait <C++ EXEのファイル名>.exe | |||
echo exeからの戻り値は %ERRORLEVEL% です | |||
pause | |||
</syntaxhighlight> | |||
<br> | |||
下表に、C#のデータ型、C++のデータ型、VARTYPEの関係を示す。<br> | |||
<br> | |||
C++から引数を指定する場合、および、C# DLLからの戻り値を取得するために、VARIANT型を使用する必要がある。<br> | |||
そのため、C#、C++、VARIANT型の関係を理解する必要がある。<br> | |||
<center> | |||
{| class="wikitable" | style="background-color:#fefefe;" | |||
|- | |||
! style="background-color:#66CCFF; width: 200px;" | C++ | |||
! style="background-color:#66CCFF; width: 200px;" | C# | |||
! style="background-color:#66CCFF; width: 200px;" | VARTYPE | |||
! style="background-color:#66CCFF; width: 200px;" | 使用するメンバ | |||
|- style="text-align: center;" | |||
| SHORT (short) || short (System.Int16) || VT_I2 || iVal | |||
|- style="text-align: center;" | |||
| INT (int)<br>LONG (long) || int (System.Int32) || VT_I4 || lVal | |||
|- style="text-align: center;" | |||
| BOOL (long) || bool (System.Boolean) || VT_BOOL || boolVal | |||
|- style="text-align: center;" | |||
| LPCSTR (const char *)<br>LPCWSTR (const wchar_t *) || string (System.String) || VT_BSTR || bstrVal | |||
|- style="text-align: center;" | |||
| FLOAT (float) || float (System.Single) || VT_R4 || fltVal | |||
|- style="text-align: center;" | |||
| DOUBLE (double) || double (System.Double) || VT_R8 || dblVal | |||
|} | |||
</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> | |||
== デバッグ == | |||
C++プロジェクトからC#プロジェクトに対するデバッグを有効にする手順を記載する。<br> | |||
<br> | |||
# [ソリューションエクスプローラー]に表示されているC++プロジェクトを右クリックして、[プロパティ]を選択する。<br> | |||
# [<プロジェクト名> プロパティページ]画面にて、[構成プロパティ] - [デバッグ]を選択する。 | |||
# [デバッガーの種類]項目を、[混合]または[自動]に設定して、[OK]ボタンを押下する。 | |||
<br><br> | |||
{{#seo: | |||
|title={{PAGENAME}} : Exploring Electronics and SUSE Linux | MochiuWiki | |||
|keywords=MochiuWiki,Mochiu,Wiki,Mochiu Wiki,Electric Circuit,Electric,pcb,Mathematics,AVR,TI,STMicro,AVR,ATmega,MSP430,STM,Arduino,Xilinx,FPGA,Verilog,HDL,PinePhone,Pine Phone,Raspberry,Raspberry Pi,C,C++,C#,Qt,Qml,MFC,Shell,Bash,Zsh,Fish,SUSE,SLE,Suse Enterprise,Suse Linux,openSUSE,open SUSE,Leap,Linux,uCLnux,Podman,電気回路,電子回路,基板,プリント基板 | |||
|description={{PAGENAME}} - 電子回路とSUSE Linuxに関する情報 | This page is {{PAGENAME}} in our wiki about electronic circuits and SUSE Linux | |||
|image=/resources/assets/MochiuLogo_Single_Blue.png | |||
}} | |||
__FORCETOC__ | __FORCETOC__ | ||
[[カテゴリ:C++]] | [[カテゴリ:C++]] |
2024年10月14日 (月) 10:45時点における最新版
概要
C++からC#ライブラリの関数を呼び出す方法は、複数の方法が存在しており、各々にメリットおよびデメリットがある。
下表に代表的な5種類の方法を示す。
方法 | メリット | デメリット |
---|---|---|
C++のプロジェクトをC++/CLIに設定する | C++のプロジェクトの設定において、[CLIを使う]に変更する。 VisualStudioのIntelliSenseも使用可能。 |
C++/CLIに関するドキュメントが少ない。 Windowsのみ使用可能。 |
.NET DLLExportを使用して、 C#ライブラリのメソッドをエクスポートする |
C++/CLIを使用する必要がない。GetProcAddress 関数が使用できるため、よく知られた方法で関数を呼び出す事が出来る |
C#ライブラリのソースコードが無い場合は利用できない。 Windowsのみ使用可能。 |
C++からC#ライブラリを呼ぶための C++/CLIのラッパーライブラリを開発する |
COMを使用しなくてよい。 C++およびC#のプロジェクトの設定を変更しなくてよい。 |
C#ライブラリ、および、C++/CLIライブラリの2つのライブラリを作成する必要がある。 C++/CLIに関するドキュメントが少ない。 Windowsのみ使用可能。 |
Embedded Monoを使用する | C++およびC#ライブラリの2つのプロジェクトを作成するだけでよい。 Linux、MacOSでも使用可能である。 ただし、Linux上で使用する場合、 async, await や他の様々なシンタックスが使用できない等のデメリットも多い。 C++のプロジェクトにおいて、 Monoに関連するヘッダファイルをインクルードして、C#ライブラリを呼ぶ。 |
Linuxの場合は、.NET Standard (.NET Core / .NET 5以降ではない) またはMonoを使用して、 C#ライブラリを作成する必要がある。 Windowsの場合は、.NET FrameworkまたはMonoを使用して、C#ライブラリを作成する必要がある。 したがって、.NET Core / .NET 5以降は使用することができない。 また、実行環境にもMonoをインストールする必要がある。 |
C#ライブラリをCOM参照可能にする | C++/CLIライブラリは不要である。 | C++のソースコード量が増えて設計が煩雑となる。 COM (Component Object Model) は、Windows独自の技術であり、 Linuxでは直接サポートされていないため、Windowsのみ使用可能である。 COMはWindowsのオブジェクト指向プログラミングモデルであり、 Microsoftの開発環境やWindows APIと密接に関連している。 |
上表1において、以下の手順を記載する。
- C++のプロジェクトをC++/CLIに設定する
- .NET DLLExportを使用して、C#ライブラリのメソッドをエクスポートする
- C++/CLIのラッパーライブラリを開発する
- Embedded Monoを使用する
- C#ライブラリを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#ライブラリの作成
まず、C#ライブラリを作成する。
// 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ライブラリの作成
次に、C++/CLIライブラリを作成する。
ソリューションエクスプローラから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++実行バイナリの作成
C++実行バイナリのプロジェクトを作成する。
暗黙的リンクを行う場合
メニューバーから[Visual C++のプロジェクト設定]を選択して、[構成プロパティ] - [C/C++] - [全般] - [追加のインクルードディレクトリ]項目に、
CppCLIDLL.hが存在するディレクトリを追加する。
同様に、[構成プロパティ] - [リンカー] - [全般] - [追加のライブラリディレクトリ]項目に、CppCLIDLL.libが存在するディレクトリを追加する。
さらに、[構成プロパティ] - [リンカー] - [入力] - [追加の依存ファイル]項目に、CppCLIDLL.libを追加する。
// CppEXE.cpp
#include "stdafx.h"
#include <windows.h>
#include "CppEXE.h"
#include "CppCLIDLL.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;
}
明示的リンクを行う場合
- まず、
LoadLibrary
関数を使用して、C++/CLIライブラリを読み込む。 - 次に、
GetProcAddress
関数を使用して、C++/CLIライブラリ内の関数オブジェクトのアドレスを取得する。 - C++/CLIライブラリの関数を呼び出す。
// CppEXE.cpp
#include "stdafx.h"
#include <windows.h>
#include "CppEXE.h"
using fnShowMessageBox void(*)(int*);
int _tmain()
{
int result = 0;
// C++/CLIライブラリを呼び出す
auto hModule = ::LoadLibrary(L"CppCLIDLL.dll");
if (NULL == hModule) {
return -1;
}
// C++/CLIライブラリの関数を読み込む
auto ShowMessageBox = reinterpret_cast<fnShowMessageBox>(::GetProcAddress(hModule, "ShowMessageBox"));
// C++/CLIライブラリの関数を実行する
ShowMessageBox(&result);
if (result == 1) {
std::cout << "Ok Was Pressed" << std::endl;
std::cout << result << std::endl;
}
else if (result == 2) {
std::cout << "Cancel Was Pressed" << std::endl;
std::cout << result << std::endl;
}
else {
std::cout << "Unknown result" << std::endl;
}
system("pause");
return 0;
}
Monoを使用する場合
Monoのインストール
- RHEL
- インストール - Mono(RHEL)を参照して、Monoをインストールする。
- インストール - Mono(RHEL)を参照して、Monoをインストールする。
- SUSE
- インストール - Mono(SUSE)を参照して、Monoをインストールする。
- インストール - Mono(SUSE)を参照して、Monoをインストールする。
- Windows
- Monoの公式Webサイトにアクセスして、[Download Mono 64-bit (no GTK#)]を選択して、Monoをダウンロードする。
- Monoをインストールする。
- 必要ならば、スタートメニューのMonoプログラムグループ下に[Monoコマンドプロンプトを開く]ショートカットを作成する。
- このショートカットは、Mono関連のパス情報がすでに設定されたコマンドシェルを起動するものである。
C#ライブラリの作成
以下の例では、SampleLibraryという名前のライブラリを作成している。
using System;
namespace SampleLibrary
{
public class SampleClass
{
public void SampleMethod(int intValue, string stringValue)
{
Console.WriteLine($"Received int: {intValue}, string: {stringValue}");
}
}
}
C++実行ファイルの作成
SUSEでは、コンパイル時において、-L/usr/lib64 -lmono-2.0
オプション、および、-I/usr/include/mono-2.0
オプションを付加する必要がある。
または、plg-configツールを使用することもできる。
g++ -o <アプリケーション名 例: SampleApp> main.cpp `pkg-config --cflags --libs mono-2`
C#ライブラリのメソッドを呼び出すmono_runtime_invoke
関数に指定する引数を、以下に示す。
- 第1引数 :
void**
型- C#のメソッドがインスタンスメソッドである場合、メソッドを呼び出すインスタンスのポインタを指定する。
- Staticメソッドの場合は、
nullptr
を指定する。
- 第2引数 :
void**
型- メソッドに渡す引数を指定する。
- 引数がある場合、各引数の値へのポインタが配列として渡される。
- 引数が無い場合は、
nullptr
を指定する。
- 第3引数 :
MonoObject**
型- エラーが発生した場合に、エラー情報を格納する
MonoObject
型のポインタを指定する。 - エラーを取得しない場合は、
nullptr
を指定する。
- エラーが発生した場合に、エラー情報を格納する
#include <iostream>
#include <mono/jit/jit.h>
#include <mono/metadata/assembly.h>
#include <mono/metadata/object.h>
#include <mono/metadata/appdomain.h>
#include <mono/metadata/debug-helpers.h>
#include <mono/metadata/exception.h>
int main()
{
// ドメインの初期化
MonoDomain *domain = mono_jit_init("SampleApp");
if (!domain) return -1;
// C#ライブラリのアセンブリの読み込み (中間言語IL)
MonoAssembly *assembly = mono_domain_assembly_open(domain, "SampleLibrary.dll");
if (!assembly) mono_jit_cleanup(domain); return -1;
// アセンブリのイメージの読み込み (アセンブリ内のコード情報を保持しているもの)
MonoImage *image = mono_assembly_get_image(assembly);
if (!image) mono_jit_cleanup(domain); return -1;
// 名前空間およびクラス名を指定
const char *nameSpace = "SampleLibrary"; // 名前空間を指定する
const char *className = "SampleClass"; // クラス名を指定する
MonoClass *klass = mono_class_from_name(image, nameSpace, className);
if (!klass) mono_jit_cleanup(domain); return -1;
// クラスのインスタンスを生成
MonoObject *instance = mono_object_new(domain, klass);
if (!instance) mono_jit_cleanup(domain); return -1;
// メソッド情報の取得 (以下のいずれかの形式でよい)
//// 方法 1 -->
////// C#ライブラリのメソッドの取得
MonoMethod *method = mono_class_get_method_from_name(klass, methodName, <メソッドの引数の数 例: 引数が無い場合は0を指定する>);
//// <-- 方法 1
//// 方法 2 -->
////// メソッド情報の取得
MonoMethodDesc *methodDesc = mono_method_desc_new("<名前空間名>.<クラス名>::<メソッド名>", true);
if (!methodDesc) mono_jit_cleanup(domain); return -1;
////// メソッドの検索
MonoMethod *method = mono_method_desc_search_in_class(methodDesc, klass);
if (!method) mono_jit_cleanup(domain); return -1;
//// <-- 方法 2
// C#ライブラリのメソッドに引数がある場合 (以下の例では、第1引数 : int型、第2引数 : string型)
int intValue = 42;
const char *stringValue = "Hello from C++";
// 引数を格納する配列
void *params[] = {
&intValue, // int型へのポインタ
mono_string_new(domain, stringValue) // string型へのポインタ
};
// C#ライブラリのメソッドに引数がない場合
void *params[] = { nullptr };
// エラー情報が格納される変数
MonoObject *exc = nullptr;
// C#ライブラリのメソッドの呼び出し
// C#ライブラリのStaticメソッド(引数あり)を呼び出す場合 --> 例: mono_runtime_invoke(method, nullptr, params, nullptr);
// C#ライブラリのStaticメソッド(引数なし)を呼び出す場合 --> 例: mono_runtime_invoke(method, nullptr, nullptr, nullptr);
// C#ライブラリのStaticメソッド(引数あり)、かつ、エラー情報不要で呼び出す場合 --> 例: mono_runtime_invoke(method, nullptr, params, nullptr);
mono_runtime_invoke(method, instance, params, &exc);
// エラーの確認
if (exc != nullptr) {
// エラー情報の取得
MonoClass *excClass = mono_object_get_class(exc);
MonoMethod *toStringMethod = mono_class_get_method_from_name(excClass, "ToString", 0);
MonoString *errorMessage = (MonoString*)mono_runtime_invoke(toStringMethod, exc, nullptr, nullptr);
// エラーメッセージの出力
const char *pstrErrMessage = mono_string_to_utf8(errorMessage);
std::cout << "Error: " << pstrErrMessage << std::endl;
mono_free(pstrErrMessage);
}
// ドメインの解放
mono_jit_cleanup(domain);
return 0;
}
引数
ポインタを渡す場合
C#ライブラリ側でデータ型のポインタを受け取り、その変数の値を変更しても、その変更がC++実行ファイル側に反映されることは期待できない。
これは、C#とC++が異なるメモリ管理とランタイム環境を持っており、それぞれ独自のメモリ管理を行うためである。
C#では、ガベージコレクションが行われ、メモリの確保や解放はCLR (Common Language Runtime) によって管理されている。
一方、C++では開発者が手動でメモリの確保と解放を行う。
したがって、C#ライブラリ内でデータ型のポインタを受け取り、その変数の値を変更したとしても、それはC#ランタイムの管理するメモリ内で行われることになる。
C++実行ファイル側では、C#ランタイムのメモリに直接アクセスできないため、その変更が反映されることはない可能性がある。
異なるランタイム環境でのメモリ管理の違いからくる制約を考慮して、C++とC#間でデータの受け渡しを行う場合は、適切な手法やデータ構造を選択する必要がある。
例えば、C#ライブラリ側で変更可能な値を戻り値として返し、それをC++で受け取る等の方法がある。
以下の例では、C++側でint型とMonoString*型を定義して、C#ライブラリ側で値を変更している。
// C++
// ...略
// メソッド情報の取得
const char *methodName = "func";
auto method = mono_class_get_method_from_name(mainClass, methodName, 2);
int intValue = 0;
MonoString *stringMonoValue = nullptr;
// 引数を格納する配列
void *params[] = {
&intValue,
&stringMonoValue
};
// C#ライブラリのメソッドの呼び出し
MonoObject* excObject = nullptr;
mono_runtime_invoke(method, classInstance, params, &excObject);
if (excObject)
{
MonoString *excString = mono_object_to_string(excObject, nullptr);
const char *excCString = mono_string_to_utf8(excString);
std::cout << "メソッドの実行時における例外 : " << excCString << std::endl;
mono_jit_cleanup(domain);
return -1;
}
else
{
// 第1引数のint型を参照にしてC#ライブラリ側で変更した場合
std::cout << intValue << std::endl; // 100を出力する
// 第2引数のMonoString*型を参照にしてC#ライブラリ側で変更した場合
std::string retStringMonoValue = mono_string_to_utf8(stringMonoValue);
std::cout << retStringMonoValue << std::endl; // "abcあいう"を出力する
}
// ...略
// C#
public void func(ref int value1, ref string value2)
{
value1 = 100;
value2 = "abcあいう";
return;
}
戻り値がある場合
戻り値がint型の場合
C#ライブラリが戻り値を返す場合は、mono_runtime_invoke
関数でC#のメソッドを呼び出して、戻り値をMonoObject*
型で受け取る。
その後、mono_field_get_value
関数を使用して、MonoObject*
型のオブジェクトからint型の戻り値を取得する。
※注意
mono_runtime_invoke
関数の戻り値はMonoObject*
型であるため、実際のint型のデータは内部的にはm_value
と呼ばれる領域に格納されていることである。
これにより、int型の戻り値を取り出すことができる。
// C#ライブラリがint型の戻り値を返す場合
MonoObject *pResult = mono_runtime_invoke(method, instance, params, &exc);
// エラーの確認
if (exc != nullptr) {
// エラー情報の取得
// ...略
}
else
{
// int型に変換
int returnValue;
mono_field_get_value(pResult, mono_class_get_field_from_name(mono_object_get_class(result), "m_value", "System.Int32"), &returnValue);
// 結果を出力
std::cout << "Method result: " << returnValue << std::endl;
}
戻り値がstring型の場合
以下の例では、mono_runtime_invoke
関数でC#のメソッドを呼び出して、戻り値をMonoObject*
型で受け取る。
次に、reinterpret_cast
を使用してMonoObject*
型をMonoString*
に変換する。
最後に、mono_string_to_utf8
関数を使用してstring
型の戻り値を取得している。
// C#ライブラリがstring型の戻り値を返す場合
MonoObject *pResult = mono_runtime_invoke(method, instance, params, &exc);
// エラーの確認
if (exc != nullptr) {
// エラー情報の取得
// ...略
}
else
{
// string型に変換
MonoString *pResultString = reinterpret_cast<MonoString*>(pResult);
const char *stringValue = mono_string_to_utf8(pResultString);
// 結果を出力
std::cout << "Method result: " << stringValue << std::endl;
// メモリの解放
mono_free(stringValue);
}
戻り値が配列の場合 (非コレクション)
以下の例では、C#ライブラリのメソッドの戻り値はint型の配列であり、C++でそれを受け取っている。
まず、mono_runtime_invoke
関数でC#のメソッドを呼び出して、戻り値をMonoObject*
型で受け取る。
次に、reinterpret_cast
を使用してMonoObject*
型をMonoArray*
に変換する。
最後に、mono_array_get
関数を使用してint
型の配列の各要素を取得している。
// C#ライブラリ
public int[] RetList()
{
return [1, 2, 3];
}
// C++
MonoObject *pResult = mono_runtime_invoke(method, instance, nullptr, &exc);
// エラーの確認
if (exc != nullptr) {
// エラー情報の取得
// ...略
}
else {
// MonoObject*型をint型の配列に変換
MonoArray* resultArray = reinterpret_cast<MonoArray*>(pResult);
// 配列の長さを取得
auto arrayLength = mono_array_length(resultArray);
// int型の配列に変換
std::vector<int> ivec(0, 0);
for (auto i = 0; i < arrayLength; i++) {
// MonoArray*型をint型に変換
ivec.push_back(mono_array_get(resultArray, int, i));
}
for (auto i : ivec) {
std::cout << i << std::endl;
}
}
戻り値がタプル型の場合
タプル型において、C#からC++に直接渡すことは難しいため、複数の戻り値を配列や構造体で受け取る必要がある。
もし可能であれば、代わりに複数の引数を使用して値を取得することを検討することもできる。
以下の例では、タプル型 (int, string) の戻り値を配列として受け取り、各要素を取り出している。
ただし、戻り値の型や順序に依存するため、メソッドが変更されると対応が必要となることに注意する。
// C#ライブラリがタプル型の戻り値を返す場合
struct TupleResult {
int Field1; // int型
MonoString* Field2; // string型
};
// ...略
MonoObject *pResult = mono_runtime_invoke(method, instance, params, &exc);
// エラーの確認
if (exc != nullptr) {
// エラー情報の取得
// ...略
}
else {
// タプル型から個々の値を取得
TupleResult *tupleResult = static_cast<TupleResult*>(mono_object_unbox(pResult));
int intValue = tupleResult->Field1;
std::string strValue = mono_string_to_utf8(tupleResult->Field2);
// 結果を出力
std::cout << "Method result: " << intValue << ", " << strValue << std::endl;
}
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]ボタンを押下する。