読者です 読者をやめる 読者になる 読者になる

亀岡的プログラマ日記

京都のベッドタウン、亀岡よりだらだらとお送りいたします。

C++のライブラリを自動でC#にラップするライブラリ、Mono CppSharp

MonoプロジェクトのGithubをうろうろしてるとなんか面白そうなのを見つけました、ってので。

ってのを見つけました。こいつは、C++のDLLを自動でラップしてくれるコードを吐いてくれる、というライブラリです。C++ネイティブのライブラリをC#から呼びたい、って場面も結構あるんですよね。特に数値計算系の処理とか。なので、昔から色々プロジェクトは盛んで、monoもCXXIってのもやってます。また、C#に限らない汎用的なツールとしてSWIGってのもあります。

これらのツールと大きくアプローチが異なるところがありまして、それは、この人、中にLLVM/Clangを飼っています。C#コードを生成する際のC++のパースにClangを用いているのですよ。なので、SWIGのように事前の型定義をしておく必要がありません。

一方で、CXXIがマルチプラットフォームをはなっから志向していたのと異なり、今のところちゃんと動くのはWindowsのみです(Mac版は現在鋭意開発中っぽい)。

実際にコードを生成してみる

最新のビルドは、以下の手順に従うしかないです。とちゅうのLLVMコンパイルがかなり時間かかりますので、覚悟の上でどうぞ。

一応プレビルドも用意しては有るのですが、どれくらいちゃんと動くかは未検証です。最近のアップデートには少なくとも追随できていなさそう。(最近のアプデはMac用の設定追加が主な気もするけれど)

ビルドができたら、エクスポートするC++コードを作っておきましょう。今回はシンプルにこんなのを(サンプルそのまんまだけどね!)

#pragma once

#define DllExport   __declspec( dllexport )

class DllExport Foo
{
public:
    Foo();
    ~Foo();
    int a;
    float b;
};

int DllExport FooAdd(Foo* foo);

ライブラリができたら、以下のDLLを参照します。

  • CppSharp.Parser.dll
  • CppSharp.AST.dll
  • CppSharp.Generators.dll

そして、CppSharp.ILibraryを実装したクラスをつくってやります。このインターフェースを使って、C++ファイルをパースしてC#コードを生成するプログラムを作成します。 とりあえず作るだけならSetupを実装するだけでOK。

class SampleLibrary : ILibrary
{
    public void Setup(Driver driver)
    {
        var options = driver.Options;
        options.GeneratorKind = GeneratorKind.CSharp;
        options.LibraryName = "SampleLib";
        options.Headers.Add(@".\Foo.h");
        options.Libraries.Add(@".\SampleLib.lib");
        options.OutputDir = "Sample";
    }
    public void SetupPasses(Driver driver)
    {
    }

    public void Preprocess(Driver driver, ASTContext ctx)
    {

    }

    public void Postprocess(Driver driver, ASTContext lib)
    {
    }
}

あとはProgram.csにて以下のように呼び出せば変換をしてくれます。

class Program
{
    static void Main(string[] args)
    {
        ConsoleDriver.Run(new SampleLibrary());
        Console.ReadKey();
    }
}

生成結果は以下の様な感じに。メモリアドレスを直にマッピングしている感じですね。当然ながらunsafeです。

//----------------------------------------------------------------------------
// This is autogenerated code by CppSharp.
// Do not edit this file or all your changes will be lost after re-generation.
//----------------------------------------------------------------------------
using System;
using System.Runtime.InteropServices;
using System.Security;

namespace SampleLib
{
    public unsafe partial class Foo : IDisposable, CppSharp.Runtime.ICppMarshal
    {
        [StructLayout(LayoutKind.Explicit, Size = 8)]
        public struct Internal
        {
            [FieldOffset(0)]
            public int a;

            [FieldOffset(4)]
            public float b;

            [SuppressUnmanagedCodeSecurity]
            [DllImport("SampleLib", CallingConvention = global::System.Runtime.InteropServices.CallingConvention.ThisCall,
                EntryPoint="??0Foo@@QAE@XZ")]
            internal static extern global::System.IntPtr Foo_0(global::System.IntPtr instance);

            [SuppressUnmanagedCodeSecurity]
            [DllImport("SampleLib", CallingConvention = global::System.Runtime.InteropServices.CallingConvention.ThisCall,
                EntryPoint="??0Foo@@QAE@ABV0@@Z")]
            internal static extern global::System.IntPtr Foo_1(global::System.IntPtr instance, global::System.IntPtr _0);

            [SuppressUnmanagedCodeSecurity]
            [DllImport("SampleLib", CallingConvention = global::System.Runtime.InteropServices.CallingConvention.Cdecl,
                EntryPoint="?FooAdd@@YAHPAVFoo@@@Z")]
            internal static extern int FooAdd_0(global::System.IntPtr foo);
        }

        public global::System.IntPtr __Instance { get; protected set; }

        int CppSharp.Runtime.ICppMarshal.NativeDataSize
        {
            get { return 8; }
        }

        void CppSharp.Runtime.ICppMarshal.MarshalManagedToNative(global::System.IntPtr instance)
        {
        }

        void CppSharp.Runtime.ICppMarshal.MarshalNativeToManaged(global::System.IntPtr instance)
        {
        }

        internal Foo(Foo.Internal* native)
            : this(new global::System.IntPtr(native))
        {
        }

        internal Foo(Foo.Internal native)
            : this(&native)
        {
        }

        internal Foo(global::System.IntPtr native)
        {
            __Instance = native;
        }

        public Foo()
        {
            __Instance = Marshal.AllocHGlobal(8);
            Internal.Foo_0(__Instance);
        }

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            Marshal.FreeHGlobal(__Instance);
        }

        public static int FooAdd(Foo foo)
        {
            var arg0 = foo == (Foo) null ? global::System.IntPtr.Zero : foo.__Instance;
            var __ret = Internal.FooAdd_0(arg0);
            return __ret;
        }

        public int a
        {
            get
            {
                var __ptr = (Internal*)__Instance.ToPointer();
                return __ptr->a;
            }

            set
            {
                var __ptr = (Internal*)__Instance.ToPointer();
                __ptr->a = value;
            }
        }

        public float b
        {
            get
            {
                var __ptr = (Internal*)__Instance.ToPointer();
                return __ptr->b;
            }

            set
            {
                var __ptr = (Internal*)__Instance.ToPointer();
                __ptr->b = value;
            }
        }
    }
}

これは、素直にC#から呼べます。

[TestMethod]
public void TestMethod()
{
    var sut = new Foo();

    sut.a = 3;
    sut.b = 4.2f;

    Assert.AreEqual(3,sut.a);
    Assert.AreEqual(4.2f, sut.b);
    Assert.AreEqual( (int)( 3 + 4.2f), Foo.FooAdd(sut));
}

とまぁ、こんな感じでシンプルなライブラリだとちゃんと動きました!

でもそんなに甘くない

・・・なのですが、Githubのプロジェクトに入っている大きめのライブラリ(Clang自体をこいつでラップしようとしてたりする)をやろうと思ったらエラーが結構出てしまったり、まだまだ安定しているとは言いがたい(し、これからもどうなるかもわからない。)です。

でもCXXIがもう二年更新がされていないあたりで、Monoプロジェクト的にはこっちに力点をおいてるのかなぁ、とは思える現状にはなってます。ライブラリの発想自体もmonoらしくぶっ飛んでて面白いので、個人的には今年ちょっと注目。