C++ネイティブからのC#コード呼び出しの注意点
この記事は Misoca+弥生 Advent Calendar 2019 の16日目エントリーです。
初めまして、弥生のエンジニア、肥後です。
給与チームのエンジニアをしております。
弊社のデスクトップアプリケーションはC++ネイティブとC#のコード双方を利用して開発を行っています。
このうち、C++ネイティブのコードからC#のコードを呼び出す際、ちょっとハマった点についてここで記載させていただきます。
そもそもやろうとしていたこと
弊社の某アプリケーションでコードクローンが見つかったため、『その解消できそうかとりあえずやってみてくれ』とのお達しがありました。
モジュール構成は以下の通りです。
項目 | 場所 | 備考 |
---|---|---|
サードパーティ製品用のプロキシモジュール | フォルダA | - |
上記プロキシ向けのCOMインターフェイス | フォルダB | 仕様上は弊社製品本体の各モジュールを参照することとなっているが、モジュール参照ではなくコードクローンが一部存在 |
弊社製品本体(exe、DLL等) | フォルダB | - |
とりあえずやってみた結果
実際開発に苦労してた時期だったからしゃあないなぁ、とか思いながら、クローン元を確認しつつ、該当箇所をDLL呼び出しに変えて動かしみました。
クローンの置き換えだけだから問題ないよね~、ポチッとな。
・・・
なんか落ちたがなorz
半泣きになりながらVisual Studioコンソールを確認したところ、EEFileLoadExceptionが出ているとのこと。
どこで落ちてるのかをステップで確認したところ、以下の『暗号化ユーティリティ』が見つからない模様。
モジュール | 場所 | 言語 |
---|---|---|
プロキシモジュール | フォルダA | ネイティブ(C++) |
COMモジュール | フォルダB | ネイティブ(C++) |
弊社製品本体:データ管理モジュール | フォルダB | ネイティブ(C++) |
弊社製品本体:暗号化ユーティリティ(COMモジュール) | フォルダB | マネージド(C#) |
さて、どうする?
MSDNなどで明確には記載されていないけど、どうやらマネージドのモジュールはそのままでは常に実行モジュールとサイドバイサイドでなくてはならない模様です。
これを回避するためにはリフレクションを使用して動的に処理を呼び出す必要がありますが、ここに記載された内容を見ると、それを使わなくても回避できそうな方法がある様なので試してみました。
- COMの機構を削除し、C++からの呼び出しにはC++/CLIのモジュール経由に変更
- 上記C++/CLIモジュール内部で、DLLのパスを指定してあらかじめロードだけしておく
(AppDomainを生成して)
機能提供元(?)の製品側にも影響が出るので双方で挙動を確認しましたが・・・
呼び出し元 | 場所 | 結果 |
---|---|---|
弊社製品 | フォルダB | OK |
プロキシモジュール | フォルダA | NG |
orz
さて、どうする?(その2)
マネージドコードでフォルダまたぎでどうにかアクセスさせるとなれば、後はリフレクションを使用する方法が考えられます。ただし、処理を実行する毎にDLLの読み込みが発生する訳でして、当然ながらパフォーマンスの劣化は避けられません。
とりわけ暗号化ユーティリティはテーブル内の文字列項目へのアクセス毎に呼ばれるので、その影響は、大げさな話ではなく計り知れないものとなります。
ではどうするか・・・、というところで頭を絞った結果、以下の情報をあらかじめキャッシュしておくという方法にたどり着きました
- モジュールのアセンブリ情報
- クラス情報
- メソッド情報やプロパティ情報
以下、簡単なプログラムで実例を示します。
この例ではプロパティの情報しかAssemblyLoaderUtilで取得していませんが、メソッドの情報を取得する様に使用を拡充することでもう少しやりたいことの幅は広がります。
※動作確認は行っていますが、コードの内容を試す場合は自己責任でお願いします。
また、コメントでも少し触れていますが、C++ネイティブに対して公開するヘッダファイルにはC++/CLI固有の定義やヘッダファイルのインクルードは記載しない様にしてください(コンパイルできなくなります)。
○呼び出し対象のC#のコード
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace FooBrr { /// <summary> /// FizzBuzz判定 /// </summary> public class FizzBuzz { /// <summary> /// Fizz(3の倍数(除く3と5の公倍数))かどうか /// </summary> public bool IsFizz { get; private set; } /// <summary> /// Buzz(5の倍数(除く3と5の公倍数))かどうか /// </summary> public bool IsBuzz { get; private set; } /// <summary> /// FizzBuzz(除く3と5の公倍数)かどうか /// </summary> public bool IsFizzBuzz { get;private set; } /// <summary> /// コンストラクタ /// </summary> /// <param name="num">対象の整数</param> public FizzBuzz(UInt32 num) { this.IsFizzBuzz = (num % 3 == 0 && num % 5 == 0); if(this.IsFizzBuzz) { this.IsFizz = false; this.IsBuzz = false; } else { this.IsFizz = num % 3 == 0; this.IsBuzz = num % 5 == 0; } } } }
○アセンブリ参照解決用のヘルパ(ヘッダ)
#pragma once namespace Adapter { /// <summary> /// C#モジュールのクラス呼び出し用ユーティリティ /// </summary> public ref class AssemblyLoaderUtil { private: /// <summary> /// モジュール名 /// </summary> System::String^ m_ModuleName; /// <summary> /// 型情報 /// </summary> System::Type^ m_TargetType; /// <summary> /// 読み込んでいるアセンブリ情報 /// </summary> System::Reflection::Assembly^ m_Assembly; /// <summary> /// コンストラクタ情報 /// </summary> System::Reflection::ConstructorInfo^ m_CI; /// <summary> /// プロパティ情報テーブル /// </summary> System::Collections::Generic::Dictionary<System::String^, System::Reflection::PropertyInfo^>^ m_PropertyTable; /// <summary> /// インスタンス情報 /// </summary> System::Object^ instance; /// <summary> /// モジュール初期化 /// </summary> /// <param name="moduleName">モジュール名</param> void InitModule(System::String^ moduleName); /// <summary> /// インスタンス生成 /// </summary> /// <param name="targetTypeName">対象の型名</param> /// <param name="params">コンストラクタ呼び出し用の引数</param> void CreateInstance(System::String^ targetTypeName, cli::array<System::Object^>^ params); /// <summary> /// プロパティ情報の生成 /// </summary> /// <param name="propertyNames">プロパティ名称の一覧</param> void CreatePropertyInfo(System::Collections::Generic::List<System::String^>^ propertyNames); public: /// <summary> /// コンストラクタ /// </summary> /// <param name="moduleName">モジュール名</param> /// <param name="targetTypeName">対象の型名称</param> /// <param name="params">コンストラクタのパラメータ</param> /// <param name="propertyNames">プロパティ名称の一覧</param> AssemblyLoaderUtil(System::String^ moduleName, System::String^ targetTypeName, cli::array<System::Object^>^ params, System::Collections::Generic::List<System::String^>^ propertyNames); /// <summary> /// プロパティのGetter実行 /// </summary> /// <param name="propertyName">プロパティ名</param> /// <returns>実行結果</returns> System::Object^ InvokePropertyGet(System::String^ propertyName); }; }
○アセンブリ参照解決用のヘルパ(実装)
#include "Stdafx.h" #include "AssemblyLoaderUtil.h" using namespace Adapter; /// <summary> /// コンストラクタ /// </summary> /// <param name="moduleName">モジュール名</param> /// <param name="targetTypeName">対象の型名称</param> /// <param name="params">コンストラクタのパラメータ</param> /// <param name="propertyNames">プロパティ名称の一覧</param> AssemblyLoaderUtil::AssemblyLoaderUtil(System::String^ moduleName, System::String^ targetTypeName, cli::array<System::Object^>^ params, System::Collections::Generic::List<System::String^>^ propertyNames) { InitModule(moduleName); CreateInstance(targetTypeName, params); CreatePropertyInfo(propertyNames); } /// <summary> /// モジュール初期化 /// </summary> /// <param name="moduleName">モジュール名</param> /// <remarks> /// <para>ロード対象のDLLとこの処理が所属するDLLが同階層に配置されている場合を想定している。</para> /// <para>ロード対象のDLLの階層が異なる場合、DLLのパスを何らかの方法で取得してAssembly::LoadFromの引数とすること。</para> /// </remarks> void AssemblyLoaderUtil::InitModule(System::String^ moduleName) { // ※実行中コードのパス取得(このDLLのパス) System::Reflection::Assembly^ thisAssembly = System::Reflection::Assembly::GetExecutingAssembly(); System::String^ thisPath = thisAssembly->Location; System::String^ directory = System::IO::Path::GetDirectoryName(thisPath); this->m_ModuleName = moduleName; System::String^ pathToManagedAssembly = System::IO::Path::Combine(directory, this->m_ModuleName); // ※モジュールを読み込んでキャッシュ this->m_Assembly = System::Reflection::Assembly::LoadFrom(pathToManagedAssembly); } /// <summary> /// インスタンス生成 /// </summary> /// <param name="targetTypeName">対象の型名</param> /// <param name="params">コンストラクタ呼び出し用の引数</param> void AssemblyLoaderUtil::CreateInstance(System::String^ targetTypeName, cli::array<System::Object^>^ params) { this->m_TargetType = this->m_Assembly->GetType(targetTypeName, false); if (params == nullptr || params->Length == 0) { // ※パラメータがない場合はパラメータなしでコンストラクタを取得して実行 this->m_CI = this->m_TargetType->GetConstructor(System::Type::EmptyTypes); cli::array<System::Object^>^ dummy = gcnew cli::array<System::Object^>(0); this->instance = this->m_CI->Invoke(dummy); } else { // ※パラメータがある場合は該当するパラメータのコンストラクタを取得して実行 System::Collections::Generic::List<System::Type^>^ typelist = gcnew System::Collections::Generic::List<System::Type^>(); System::Collections::Generic::List<System::Object^>^ arglist = gcnew System::Collections::Generic::List<System::Object^>(); for (int i = 0; i < params->Length; i++) { typelist->Add(params[i]->GetType()); arglist->Add(params[i]); } this->m_CI = this->m_TargetType->GetConstructor(typelist->ToArray()); this->instance = this->m_CI->Invoke(arglist->ToArray()); } } /// <summary> /// プロパティ情報の生成 /// </summary> /// <param name="propertyNames">プロパティ名称の一覧</param> /// <remarks> /// <para>今回のサンプルは呼び出すプロパティが決まっているので名前の一覧を外部から受け取っている。</para> /// <para>実際の処理に組み込む場合はpublicのプロパティをすべて取得しておいた方がいいかもしれない。</para> /// </remarks> void AssemblyLoaderUtil::CreatePropertyInfo(System::Collections::Generic::List<System::String^>^ propertyNames) { this->m_PropertyTable = gcnew System::Collections::Generic::Dictionary<System::String^, System::Reflection::PropertyInfo^>(); cli::array<System::Reflection::PropertyInfo^>^ ar = this->m_TargetType->GetProperties(); for (int i = 0; i < ar->Length; i++) { this->m_PropertyTable->Add(ar[i]->Name, ar[i]); } } /// <summary> /// プロパティのGetter実行 /// </summary> /// <param name="propertyName">プロパティ名</param> /// <returns>実行結果</returns> System::Object^ AssemblyLoaderUtil::InvokePropertyGet(System::String^ propertyName) { // ※指定された名称のプロパティ情報を取得 System::Reflection::PropertyInfo^ pi = this->m_PropertyTable[propertyName]; // ※プロパティを実行 System::Object^ dst = pi->GetValue(this->instance, nullptr); return dst; }
○DLLエクスポート用のヘッダ
#pragma once #ifdef _EXPORTING #define CLASS_DECLSPEC __declspec(dllexport) #else #define CLASS_DECLSPEC __declspec(dllimport) #endif
○ネイティブに公開されるアダプタ部分(ヘッダ)
// FizzBuzzAdapter.h #pragma once #include "DLLExport.h" namespace Adapter { /// <summary> /// C#のFizzBuzz判定呼び出しアダプタ /// </summary> /// <remarks> /// <para>本クラスが所属するDLLはC#のモジュールと同階層となることを想定している。</para> /// <para>C#のDLLば別階層となる場合は該当モジュールのパスをこのクラスからAssemblyLoaderUtilに渡す必要あり。</para> /// <para>また、このクラスヘッダはネイティブコードに公開されるため、C++/CLIのクラス定義等は記載できないことに注意。</para> /// </remarks> class CLASS_DECLSPEC FizzBuzzAdapter { public: /// <summary> /// Fizz(3の倍数(除く3と5の公倍数))かどうか /// </summary> /// <returns>条件に該当したらtrueを返す</returns> bool IsFizz(); /// <summary> /// Buzz(5の倍数(除く3と5の公倍数))かどうか /// </summary> /// <returns>条件に該当したらtrueを返す</returns> bool IsBuzz(); /// <summary> /// FizzBuzz(除く3と5の公倍数)かどうか /// </summary> /// <returns>条件に該当したらtrueを返す</returns> bool IsFizzBuzz(); /// <summary> /// コンストラクタ /// </summary> /// <param name="num">対象の整数</param> FizzBuzzAdapter(unsigned int num); private: class Impl; /// <summary> /// CSharpコード呼び出しをラップするクラスのポインタ /// </summary> Impl* m_pImpl; }; }
○ネイティブに公開されるアダプタ部分(実装)
// これは メイン DLL ファイルです。 #include "stdafx.h" #include <msclr/gcroot.h> #include <msclr/marshal_cppstd.h> #include <string> #include <string.h> #include "AssemblyLoaderUtil.h" #define _EXPORTING #include "FizzBuzzAdapter.h" using namespace Adapter; /// <summary> /// CLIインスタンス管理クラス /// </summary> class FizzBuzzAdapter::Impl { public: /// <summary> /// クラスローダー /// </summary> msclr::gcroot<AssemblyLoaderUtil^> m_pFizzBuzzClassLoader; /// <summary> /// コンストラクタ /// </summary> /// <param name="num">対象の整数</param> Impl(unsigned int num) { cli::array<System::Object^>^ param = { num }; System::Collections::Generic::List<System::String^>^ props = gcnew System::Collections::Generic::List<System::String^>(); props->Add("IsFizz"); props->Add("IsBuzz"); props->Add("IsFizzBuzz"); this->m_pFizzBuzzClassLoader = gcnew AssemblyLoaderUtil("FooBrr.dll", "FooBrr.FizzBuzz", param, props); } /// <summary> /// デストラクタ /// </summary> virtual ~Impl() { } }; /// <summary> /// Fizz(3の倍数(除く3と5の公倍数))かどうか /// </summary> /// <returns>条件に該当したらtrueを返す</returns> bool FizzBuzzAdapter::IsFizz() { return (bool)this->m_pImpl->m_pFizzBuzzClassLoader->InvokePropertyGet("IsFizz"); } /// <summary> /// Buzz(5の倍数(除く3と5の公倍数))かどうか /// </summary> /// <returns>条件に該当したらtrueを返す</returns> bool FizzBuzzAdapter::IsBuzz() { return (bool)this->m_pImpl->m_pFizzBuzzClassLoader->InvokePropertyGet("IsBuzz"); } /// <summary> /// FizzBuzz(除く3と5の公倍数)かどうか /// </summary> /// <returns>条件に該当したらtrueを返す</returns> bool FizzBuzzAdapter::IsFizzBuzz() { return (bool)this->m_pImpl->m_pFizzBuzzClassLoader->InvokePropertyGet("IsFizzBuzz"); } /// <summary> /// コンストラクタ /// </summary> /// <param name="num">対象の整数</param> FizzBuzzAdapter::FizzBuzzAdapter(unsigned int num) { this->m_pImpl = new FizzBuzzAdapter::Impl(num); }
まとめにかえて
.NETに限らず、MS系の技術に関する細かい情報は以外と日本語で出ていないことが多いです。
英語で調べて対応することはもちろんできますが、その時の知見をプロジェクト内に閉じ込めるのももったいない。
同じ箇所で躓く技術者さんは他にも必ずいますので。
そういう意味で、こういうのちゃんと公開することには意義があるのかもと思います。
(中身ちゃんとしてるかと言われればアレですが(^^;)
次回、Misoca+弥生 Advent Calendar 2019 の17日目は、kentaro_tsujinoさんです。
お楽しみに!