概要
C++でバックエンド、C#やHTML5+JavaScriptでフロントエンド、という案件は、よくある世の中。
ところで、C#でログファイルを定義しているのに、バックエンド側であるC++に渡せなくてちょっと悔しい思いをしている人はいないでしょうか。(基本的に古くてレガシーな技術なので、今後も使えるという保証はありませんが)C++/CLIを介して、C#で定義されたNLogをC++で使うことが出来ますので、今回はそれを紹介。
というか、C++/CLIを最も有効に使えている方法かもしれないと個人的に思う方法。普通に考えて、こんな複雑なことが出来るとは・・・
私もやってみて、MicrosoftがSQL ServerなどでC++/CLIやILなどの技術をなかなか捨てられん理由がよう分かりました。
参考記事
こちらの記事を参考に
ここの内容にある「妥協点」の直接的な解決法だと思います。
制約条件
- C++がダイナミックライブラリの場合は、クラスオブジェクトを渡せないので、使用できません。スタティックライブラリを使うとき専用の方法です。
全体観
ちょっとややこしすぎるだろ~という内容ですが、原理的にはis-a関係、has-a関係を組み合わせまくることで何とかなります。ただシングルトンを多用するので、ログ吐き用途などの相当汎用的な機能ではない限り、普通の機能として使うのはちょっと向いていないです。あと、C++/CLIを使う前提のラッパーを書ける人なんてそんなに多くいるのだろうか
-
C++/CLIでは、C++側にNativeLoggingを継承したClrLoggingのオブジェクトを渡します。そうすると、C++側では、NativeLoggingのメソッドを実行することで、C++/CLIで定義されたメソッドが実行されます。(ポリモーフィズム(多態性)に基づいた考え方です。ピンとこなかった人はオブジェクト指向の勉強を少ししておきましょう。)
-
Loggingクラスはシングルトンです。したがって、**ClrLoggingからLoggingを直接実行することが出来てしまいます。**ということは、C++側からのNativeLoggingの実行に対して、Loggingの実行メソッドを用意しておけば、マネージド側の関数を実行することが出来ます。
-
C++/CLIのクラスLoggingから、C#でUserNLogを継承します。UserNLogは、NLogの実体であるLogManagerを呼び出すことが出来ます。
ポイント
-
この方法のミソは、ClrLoggingからシングルトンを経由してLoggingを呼び出すことが出来てしまう点です。本来このような使い方は、どちらかと言えば禁忌とされることが多いと思いますが、出来てしまうのが驚きです.
-
この方法は、ある意味では、現時点でのC++からC#のコードを実行する特殊な手段の一つです。この方法を解説する記事はあんまりないと思います(
需要もないけどね).一応、C++/CLIのデフォルトにはないはずのnugetライブラリとか、githubからクローンしてきたライブラリなんかも使えるのがおいしいかも
そういえば・・・
.NET Framework時代では、C++/CLIからC#のライブラリを実行することは比較的容易で、それはC#のライブラリもC++/CLIのライブラリも割とパッケージの依存性が近かったのですが、近年のVS2017, VS2019ではC++/CLIからC#のパッケージをデフォルトの画面を使って設定する機能が削減されている気がします。
Referencesからパッケージを設定できるのですが、NLogのようにnuget経由で取ってきたパッケージを検知しないような動きになっていますね。その関係で、C++/CLIは、昨今発展を続けているOSSの恩恵を受けにくくなっています。将来的には、個人的にはラッパーとしての機能は便利なので、そこだけは残してほしいかなあ・・・。 正直、早くCppSharpやSWIGなどのクロスプラットフォーム技術に乗り換えたいという思いはある
実行結果
こんな感じで、下のコードでは定義していないDEBUGが表示されているのがいい。日付のフォーマットやログフォーマット、ログレベルが変え放題なのも素晴らしい。
バックエンドの処理ログって、大体低レベル水準に落としたいという思いが強い気がするので、C++用のログレベルを落として、C#用のログレベルを上げる、みたいな器用なことも出来そうですね。
ソースコード
ややこしいのでざっくり紹介します
class NativeLogging {
private:
static NativeLogging* _logging_global;
public:
NativeLogging() {
}
NativeLogging(const NativeLogging& obj) {
}
virtual ~NativeLogging() {
}
virtual void PrintInfo(std::wstring text) = 0;
virtual void PrintError(std::wstring text) = 0;
public:
static void SetGlobalLoggingObject(NativeLogging* logging);
static NativeLogging* LogObject();
/* おまけ:実際の使い方 */
template <typename T>
static bool ExecuteAndReturn(T func, std::wstring error_message) {
int Ret = func();
/* ラムダ式で出てきた結果を受けて、その結果の返り値が0以外の場合はエラーを出力 */
if (Ret != 0) {
std::wstringstream logging;
logging << error_message << L":" << std::to_wstring(Ret);
/* こんな感じで使う */
SysLog::NativeLogging::LogObject()->PrintError(logging.str());
return false;
}
return true;
}
};
基本的には説明通りですが、最後に少しおまけ(使用例)をつけています。ラムダ式funcを受けて、その結果でエラーが出る場合にエラーメッセージを出す、みたいな書き方をしています。
PrintInfo, PrintErrorのように、抽象化しておいて、子クラスであるClrLoggingに明確に渡す書き方をするのがポイントです。
#include "NativeLogging.h"
#include <msclr/marshal_cppstd.h>
using namespace msclr::interop;
public ref class Logging {
private:
static Logging^ _log_object;
private:
SysLog::NativeLogging* _log_ptr = nullptr;
public:
Logging() {
_log_object = nullptr;
}
Logging(const Logging% obj) {
}
!Logging() {
this->~Logging();
}
virtual ~Logging() {
}
static void InitInstance(Logging^ logging) {
_log_object = logging;
}
static Logging^ GetInstance() {
return _log_object;
}
public:
virtual System::Void PrintInfo(System::String^ text) {
/* ここの内容をC#では、NLogの内容として設定する */
}
virtual System::Void PrintError(System::String^ text) {
/* ここの内容をC#では、NLogの内容として設定する */
}
public:
void SetLoggingPtr(SysLog::NativeLogging* log_ptr) {
_log_ptr = log_ptr;
}
void PrintInfoNative(std::wstring text) {
this->PrintInfo(marshal_as<System::String^>(text));
}
void PrintErrorNative(std::wstring text) {
this->PrintError(marshal_as<System::String^>(text));
}
};
class ClrLogging : public SysLog::NativeLogging {
public:
ClrLogging() { }
ClrLogging(const ClrLogging& obj) { }
virtual ~ClrLogging() {}
void PrintInfo(std::wstring text) override {
Logging^ obj = Logging::GetInstance();
obj->PrintInfoNative(text);
}
void PrintError(std::wstring text) override {
Logging^ obj = Logging::GetInstance();
obj->PrintErrorNative(text);
}
};
先ほどのコメントの通り・・・です。PrintInfo, PrintErrorの実装に、PrintInfoNativeやPrintErrorNativeを実行して、これをシングルトンから呼び出しているのがポイントかな。
public class UserNLog : Logging
{
private static Logger logger = LogManager.GetCurrentClassLogger();
public static void Init()
{
InitInstance(new UserNLog());
}
public override void PrintInfo(string str)
{
/* ここの内容をC#では、NLogの内容として設定する */
logger.Info(str);
}
public static void PrintDebug(string str)
{
/* ここの内容をC#では、NLogの内容として設定する */
logger.Debug(str);
}
public static void PrintWarning(string str)
{
logger.Warn(str);
}
public override void PrintError(string str)
{
logger.Error(str);
}
}