C++でlog4netを使う

 こんにちは。皆さん、ログ出力ってどうされてますでしょうか。私の開発してたシステムで、C++とC#のプロセスでそれぞれログを出力していました。C++ではspdlogというロギングライブラリを使用していたのですが、とある問題が発生したため急遽他のロガーを使用する必要がありました。C#ではlog4netというロギングライブラリを使用しており、非常に使い勝手が良かったためC++でも使えればよかったのですが、log4netのC++版と言えるlog4cxxは使用するまでが異常に面倒くさいため、この際log4netをラップしたDLLを作成し、それをC++で呼び出そうということになりました。

C++からC#を呼び出すために

 一言で、DLLを呼び出すと言ってもC++でC#のDLLを直接呼び出すことはできません。通常、C#で作られたクラスやメソッドはマネージコード(CLRに管理され、中間言語にコンパイルされるコード)になり、ガベージコレクションなどの機能を利用することができます。しかし、C++で作られるプログラムはアンマネージコード(機械語に直接コンパイルされるコード)となり、マネージコードで作られたDLLを直接呼び出すことはできません。
 そこで、C++/CLIの出番となります。C++/CLIはマネージコードとアンマネージコードを両立させたコードを書くことができ、実装部をマネージコード、呼び出し部分をアンマネージコードで書けばC++でもC#のクラスやメソッドを間接的に利用することができます。

実際に呼び出す手順

まずはC#でlog4netをラップしたクラスを作ります。

Info出力だけの最低限の機能しかありませんが、FatalやWarnもメソッド追加すればいいだけなので今回はこれで行きます。

using log4net;
using System.IO;
using System.Xml;

namespace ClassLibrary
{
    public class LoggerWrapper
    {
        static public void Init(string configPath, string repoName)
        {
            XmlDocument log4netConfig = new XmlDocument();
            log4netConfig.Load(File.OpenRead(configPath));
            var repo = log4net.LogManager.CreateRepository(repoName);
            log4net.Config.XmlConfigurator.Configure(repo, log4netConfig["log4net"]);
        }

        public LoggerWrapper(string repoName, string name)
        {
            logger = LogManager.GetLogger(repoName, name);
        }

        public void info(string str)
        {
            logger.Info(str);
        }

        private ILog logger = null;
    }
}

上記のラッパークラスをさらにラップするクラスをC++/CLIで書きます。
下記画像のプロジェクトテンプレートがない場合は、Visual Studio Installerよりダウンロードしてください。(個別のコンポーネントからCLIで検索したら出ると思います)

ラッパークラスのヘッダーです。名前を 「ManagedLoggerWrapper.h」としています。
ラッパークラスのコンストラクタとコピーコンストラクタを隠蔽しています。これは、ファクトリーの関数(CreateLogger)からでしかインスタンスを生成できないようにするためです。
また、この部分のコードはC++でインクルードする際に読み込まれるため、#pragma unmanagedを指定しています。

#pragma once
#include <string>
#include <memory>

#pragma unmanaged

#ifdef EXPORTWRAPPER
#define EXPORT __declspec(dllexport)
#else
#define EXPORT __declspec(dllimport)
#endif

class ManagedLoggerWrapper 
{
public:
    virtual void Info(const std::string& str) = 0;
    virtual ~ManagedLoggerWrapper() = default;
protected:
    ManagedLoggerWrapper() = default;
private:
    ManagedLoggerWrapper (const ManagedLoggerWrapper&) = delete;
    ManagedLoggerWrapper (ManagedLoggerWrapper&&) = delete;
    ManagedLoggerWrapper & operator=(const ManagedLoggerWrapper&) = delete;
    ManagedLoggerWrapper & operator=(ManagedLoggerWrapper&&) = delete;
};

EXPORT extern std::shared_ptr<ManagedLoggerWrapper> CreateLogger(const std::string& repoName, const std::string& name);
EXPORT extern void Init(const std::string& configpath, const std::string& repoName);

ラッパークラスの実装部分です。
marshal_as は、stdの型(std::string や std::vector 等) をマネージコードに合った型に変換してくれる関数です。

#define EXPORTWRAPPER
#include "ManagedLoggerWrapper.h"
#pragma managed
#include <gcroot.h>
#include <msclr/marshal_cppstd.h>
using namespace msclr::interop;

#using "C#でビルドしたDLL"

public class ManagedLoggerWrapperImpl : public ManagedLoggerWrapper
{
public:

    ManagedLoggerWrapperImpl(const std::string& repoName, const std::string& name)
    {
        managedObject = gcnew ClassLibrary::LoggerWrapper(
            marshal_as<System::String^>(repoName),
            marshal_as<System::String^>(name)
        );
    }

    void Info(const std::string& str) override
    {
        managedObject->info(marshal_as<System::String^>(str));
    }

    static void Init(const std::string& configpath, const std::string& repoName)
    {
        ClassLibrary::LoggerWrapper::Init(
            marshal_as<System::String^>(configpath),
            marshal_as<System::String^>(repoName)
        );
    }

private:

    gcroot<ClassLibrary::LoggerWrapper^> managedObject;

};

std::shared_ptr<ManagedLoggerWrapper> __cdecl CreateLogger(const std::string& repoName, const std::string& name)
{
    return static_cast<std::shared_ptr<ManagedLoggerWrapper>>(new ManagedLoggerWrapperImpl(repoName, name));
}

void __cdecl Init(const std::string& configpath, const std::string& repoName)
{
    ManagedLoggerWrapperImpl::Init(configpath, repoName);
}

あとは上記コードをビルドして、C++で使うだけです。
test.config は ただコンソールに出力するだけの設定にしています。

#include <ManagedLoggerWrapper.h>

#pragma comment(lib, "C++/CLIでビルドしたDLLと同時に生成されたlib")

int main()
{
	Init("test.config", "Hoge");
	auto logger = CreateLogger("Hoge", "Fuga");
	logger->Info("Hello!!");
	return 0;
}

ちゃんと出力されました。

独り言

 今回は.NET Framework のCLRを使いましたが、.NET Core のCLRを使うプロジェクトテンプレートもあったので、もしかしたら .NET Core の機能をC++で使えるのかもしれません……後日検証してみようと思います。
 あと、log4netがマルチプロセスに対応していないことを後から知って痛い目を見ました。少し調べたらわかるし、少し考えたらわかるはずですが、有名なライブラリだから大丈夫と思って使ってしまっていました……。今後のためにも、ロギング専用のサービスは作っておくべきですね。