C++の応用 - C Sharp DLLの使用

2024年10月14日 (月) 10:45時点におけるWiki (トーク | 投稿記録)による版 (文字列「__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…)
(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)

概要

C++からC#ライブラリの関数を呼び出す方法は、複数の方法が存在しており、各々にメリットおよびデメリットがある。
下表に代表的な5種類の方法を示す。

C++からC#ライブラリのメソッドを呼ぶ方法
方法 メリット デメリット
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;
 }


明示的リンクを行う場合
  1. まず、LoadLibrary関数を使用して、C++/CLIライブラリを読み込む。
  2. 次に、GetProcAddress関数を使用して、C++/CLIライブラリ内の関数オブジェクトのアドレスを取得する。
  3. 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をインストールする。
  • SUSE
    インストール - 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プロジェクトを作成して、アセンブリ情報を設定する。

  1. C# DLLプロジェクトのプロパティを開く。
  2. プロパティ画面左にある[アプリケーション]タブを選択して、[アセンブリ情報]ボタンを押下する。
  3. [アセンブリをCOM参照可能にする]チェックボックスにチェックを入力して、[OK]ボタンを押下する。


次に、ビルドの設定を行う。

  1. C# DLLプロジェクトのプロパティを開く。
  2. プロパティ画面左にある[ビルド]タブを選択して、[COM相互運用機能の登録]チェックボックスにチェックを入力する。
  3. 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, &params, &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#プロジェクトに対するデバッグを有効にする手順を記載する。

  1. [ソリューションエクスプローラー]に表示されているC++プロジェクトを右クリックして、[プロパティ]を選択する。
  2. [<プロジェクト名> プロパティページ]画面にて、[構成プロパティ] - [デバッグ]を選択する。
  3. [デバッガーの種類]項目を、[混合]または[自動]に設定して、[OK]ボタンを押下する。