やりたいこと
色々な事情があり、すでにC#で作ってあるライブラリを、C++のアプリから呼びたい。
(なので、タイトルは正確に言うと、C#のdllを、COM参照可能にしたC#dllでラップして、C++アプリから呼ぶ
が正しい)
関連項目目次
やり方候補
こちらのページにあるように、いろいろなやり方があるっぽい。
やり方 | 特徴 |
---|---|
1.作ってあるC#ライブラリをCOM参照可能なC#でラップする | ラップC#を使う側のC++のコードが増える |
2.C++のアプリをC++/CLIでビルドしなおす | ・原則C++アプリの全テスト必要・ランタイム依存になる |
3.有志の方が作ったものを使用しC#(DLL)側で関数をエクスポートする | C#(DLL)側のソースコードが無い場合利用できない |
今回は、1.の方法で実験をした。つまり、下記のような構成とする。
プロジェクト名 | 内容 |
---|---|
DllCsプロジェクト | 仮想[すでにC#で作ってあるライブラリ] |
DllCsComWrapperプロジェクト | 仮想[すでにC#で作ってあるライブラリのC#のCOMラッパー] |
ConsoleApplication1プロジェクト | 仮想[C#ライブラリを呼びたいC++アプリ] |
コードは、ほぼほぼこちらのを参考にさせていただきました。ありがとうございます。
ToDo
仮想[すでにC#で作ってあるライブラリ] を実装
DllCsプロジェクトに、下記ファイルを追加。
引数を2つ取ってそれを足した値を返すメソッドAdd
を持っている。
namespace DllCs
{
public class DllCsClass
{
public static int Add(int a, int b) => a + b;
}
}
仮想[すでにC#で作ってあるライブラリのC#のCOMラッパー] を実装
DllCsComWrapperプロジェクトに、下記のファイルを追加。
上のファイルをラップしたメソッドを持っている。
using DllCs;
using System;
using System.Runtime.InteropServices;
namespace DllCsComWrapper
{
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDual)]
[Guid("85555B74-E2E0-4493-9869-3CE95F13CB99")]
public class DllCsComWrapperClass
{
public Int32 Add(Int32 param1, Int32 param2)
{
int ret = DllCsClass.Add(param1, param2);
return (Int32)ret;
}
}
}
仮想[すでにC#で作ってあるライブラリのC#のCOMラッパー] のプロジェクト設定をする
まず「アセンブリをCOM参照可能にする」にチェックを入れる。
※「COM相互運用機能の登録」は、後で出てくるregasmによるレジストリ登録をビルド時に自動でやってくれる機能と思われる。そのため、作成中のデバッグ時はチェックあった方が便利だが、これにチェックが入っていると、デバッグ中はCOMの登録が行われるため動いていたが、本番機ではCOM登録が行われないため動かない、となってしまうため注意が必要。
クラスには、下記の属性をつける(とりあえずおまじないとして、動くものをつくる。後で詳細調べること。)
- ComVisible
- ClassInterface
- Guid →VisualStudioのGUID付与ツールを使用する([ツール] > [GUIDの作成]から。)
※using DllCs;
を追加し、DllCsのdllを参照に追加しておくこと。
仮想[C#ライブラリを呼びたいC++アプリ] を実装する
ConsoleApplication1に下記のファイルを追加。
#include "pch.h"
#include <iostream>
#include <Windows.h>//追加
//グローバル変数
IDispatch *pIDisp = NULL;
IUnknown *pIUnk = NULL;
//プロトタイプ宣言
long _Init(void);
long _Finalize(void);
long _Add(long p_Number1, long p_Number2);
int main()
{
//変数宣言
int l_Result = 0;
//初期処理
_Init();
//合計処理
l_Result = _Add(300, 500);
//後処理
_Finalize();
printf("Calc Result : %d", l_Result);
return l_Result;
}
//***************************************************************************//
//初期化関数
//***************************************************************************//
long _Init(void)
{
CLSID clsid;
//COM初期化
::CoInitialize(NULL);
//ProcIDからCLSIDを取得(ネームスペース名.クラス名)
HRESULT h_result = CLSIDFromProgID(L"DllCsComWrapper.DllCsComWrapperClass", &clsid);
if (FAILED(h_result))
{
return -1;
}
//Instanceの生成
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;
}
//***************************************************************************//
//修了処理
//***************************************************************************//
long _Finalize(void)
{
pIDisp->Release();
pIUnk->Release();
::CoUninitialize();
return 0;
}
//***************************************************************************//
//合計処理呼出処理
//***************************************************************************//
long _Add(long p_Number1, long p_Number2)
{
//DISPIDの取得(関数名の設定)
DISPID dispid = 0;
OLECHAR *Func_Name[] = { SysAllocString (L"Add") };
HRESULT h_result = pIDisp->GetIDsOfNames(IID_NULL, Func_Name, 1, LOCALE_SYSTEM_DEFAULT, &dispid);
if (FAILED(h_result))
{
return -1;
}
//パラメータ作成
DISPPARAMS params;
::memset(¶ms, 0, sizeof(DISPPARAMS));
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);
//呼び出し
pIDisp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, ¶ms, &vRet, NULL, NULL);
delete[] pVarg;
return vRet.lVal;
}
下記の処理を行っている。
- 初期化処理
- CoInitialize(NULL)を呼ぶ
- ProcIDからCLSIDを取得
- Instanceの生成
- インターフェースの取得
- 終了処理
- Initで取得したものを解放
- Instanceを開放
- インターフェースを解放
- CoUninitialize()を呼ぶ
- Initで取得したものを解放
- Initと終了処理の間に、呼びたいC#のCOMラッパーを呼ぶ関数(ここでは
合計処理呼出処理
)を作る- メソッド名からIDを取得(GetIDsOfNames)
- メソッドに渡すパラメータを作成(DISPPARAMS、VariantInitなど)
- 呼び出し実施(pIDisp->Invoke)
仮想[C#ライブラリを呼びたいC++アプリ] の注意点
初期化処理について
CLSIDFromProgID(L"DllCsComWrapper.DllCsComWrapperClass", &clsid);
の一つ目の引数は、C#のラッパークラスの「<名前空間名>.<クラス名>」にすること。
合計処理呼出処理について
OLECHAR *Func_Name[] = { SysAllocString (L"Add") };
で定義する名前は、C#のラッパークラスの中の、呼びたいメソッド名にすること。
型について
戻り値や引数をやり取りするときに、下記のような対応で型を決める必要がある。
こちらの表より。
regasmでdllを登録
regasmを使用し、下記のコマンドで作成したラッパーDLLを登録する。
regasm /codebase DllCsComWrapper.dll
※regasmのありかは、下記の通り(環境による?)
32bit版:C:\Windows\Microsoft.NET\Framework\v4.0.30319
64bit版:C:\Windows\Microsoft.NET\Framework64\v4.0.30319
dllのプラットフォーム(32or64bit)により、32/64bitのあったものを使用しないと、うまく登録できない。
今回は、ここからregasm.exeをコピーして、実行するConsoleApplication1.exeと同じ階層に置いた。
作ったC++アプリを実行
下記のようなbatを管理者権限で実行し、作成したアプリを実行。
(面倒だったので、ここではregasmも実行時に毎回実行している)
@echo off
cd %~dp0
regasm /codebase DllCsComWrapper.dll
start /wait ConsoleApplication1.exe
echo exeからの戻り値は %ERRORLEVEL% です
pause
※ここで毎回regasmするなら、上の方でやっていた「COM相互運用機能の登録」のチェックはいらないっぽい。(デバッグするうえではあった方が便利)
実行結果
追記(190523)
C++からC#ラッパーに文字列を渡したいときは、下記のようにする。
using DllCs;
using System;
using System.Runtime.InteropServices;
namespace DllCsComWrapper
{
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDual)]
[Guid("85555B74-E2E0-4493-9869-3CE95F13CB99")]
public class DllCsComWrapperClass
{
public Int32 Add([MarshalAs(UnmanagedType.BStr)]string str) // ★マーシャリングする!
{
Console.WriteLine(str);
return (Int32)11;
}
}
}
受け取る文字列を、マーシャリングすることが必要。
※とりあえず文字列をラッパーに渡せることを見たいため、C#のライブラリを呼ぶ処理は割愛。
long _Add(long p_Number1, long p_Number2)
{
//DISPIDの取得(関数名の設定)
DISPID dispid = 0;
OLECHAR *Func_Name[] = { SysAllocString (L"Add") };
HRESULT h_result = pIDisp->GetIDsOfNames(IID_NULL, Func_Name, 1, LOCALE_SYSTEM_DEFAULT, &dispid);
if (FAILED(h_result))
{
return -1;
}
//パラメータ作成
DISPPARAMS params;
::memset(¶ms, 0, sizeof(DISPPARAMS));
params.cNamedArgs = 0;
params.rgdispidNamedArgs = NULL;
params.cArgs = 2; //呼び出す関数の引数の数
//引数設定
VARIANT var;
DISPPARAMS dispParams;
var.vt = VT_BSTR;// ★渡すデータの種別をBSTRにする
var.bstrVal = SysAllocString(L"mojiretsu");// ★渡す文字列
dispParams.cArgs = 1;
dispParams.rgvarg = &var;
dispParams.cNamedArgs = 0;
dispParams.rgdispidNamedArgs = NULL;
VARIANT vRet;
VariantInit(&vRet);
//呼び出し
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;
}
残項目
- ラッパーC#の属性に何があるかとその効果(ComVisibleとか)
コード
参考
C++からC#を呼ぶいろんな方法
https://kagasu.hatenablog.com/entry/2017/12/31/220239#%E2%85%A1-CCLI%E3%82%92%E4%BD%BF%E3%81%86%E6%96%B9%E6%B3%95
C++からC# DLL 超超超入門(3.の方法)
https://qiita.com/Midori_co583826/items/58d56e202f104ebf867a
GUIDの登録
http://tech.nitoyon.com/ja/blog/2008/07/31/c-sharp-com/
あまり参考にならなかったが、コードのサンプル
https://www.84kure.com/blog/2014/07/17/c-c%E3%81%8B%E3%82%89c%E3%81%AEdll%E3%82%92%E5%91%BC%E3%81%B6%E6%96%B9%E6%B3%95/
文字列を渡すときのマーシャリング(BSTR ⇒ string)
https://docs.microsoft.com/ja-jp/dotnet/framework/interop/default-marshaling-for-strings
COMラッパーに文字列を渡したいときに参考にしたサンプル
https://www.acot.net/WMI/sample4.html