SWIG を使って C++ のクラスを C# で読み込む方法

FFFTP2 のライブラリ化の前調査として、C++ のクラスに書き替えたときに、.NETから呼び出すことができるだろうか?というのを調べていました。基本 C# から C/C++ を呼び出すときは dllimport を使う訳ですが、これは「Cインターフェース」を使っているので、C++のクラスを直接呼び出すことはしていません。なので、C++の場合は、一度Cのインターフェースに直して呼び出さないといけないですよね。OpenCvSharp の場合も、OpenCV を呼び出すときに、dllimport を使っていますが、ひとつひとつ C インターフェスに直して組み替えています。ここの手間がなんとも言えないのと、将来的に C++ の構造が変わったときにはこの部分を手作業で直さないといけませんよね(実際 OpenCV2 から OpenCV3 の変換のときに発生している問題で)。となれば、Xamarin.Android のバインド機能よろしく、*.jar ファイルから適当な C# のクラスを吐き出すような仕組みが欲しいところです。

本式にやるならば? CppSharp っぽいのですが https://github.com/mono/CppSharp どうやら「棘の道」だそうなので、ちょっと避けて、SWIG http://swig.org/ を試しています。
以前、Python から Cライブラリを呼び出すのに SWIG を使う話をちらっと聞いた後に、それ以降調べていなかったのですが、どうやら C++ にも対応しています。

SWIGのwindows版をダウンロード

Visual Studio 2017 で作業をするので、windows 版を落とします。https://sourceforge.net/projects/swig/files/ にある swigwin のほうをダウンロードして展開すると、swig.exe と変換インターフェースがある Lib/*.i 等がでてきます。

ここでは C/C++ を C# に変換するので、Lib/charp/*.i が使われますね。

サンプル

サンプルは http://github.com/moonmile/helloswig にあります。

– helloswig : Visual Studio で作った C++ の DLL プロジェクト
– hello : DLL を利用する C# プロジェクト

hello プロジェクトは可搬性を確認するために、.NET Core プロジェクトで作ってあります。Windows 上のみで動かす場合や Linux 上で mono を使う場合は .NET Framework のプロジェクトでも可能です。Windows 上のみで動作を考えるならば、DLL は MFC DLL でも動作します。

helloswig プロジェクト

helloswig.cpp に実体を定義します。これは DLL 内で動くコードですね。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "stdafx.h"
#include "helloswig.h"
 
Hello::Hello() : _name("") {}
 
void Hello::SetName(const char *name ) {
    _name = std::string(name);
}
 
const char * Hello::GetName() {
    return _name.c_str();
}
 
int Hello::Add(int x, int y) { return x + y; }
int Hello::Mul(int x, int y) { return x * y; }

helloswig.h は外部インターフェースです。

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma once
#include <string>
class Hello
{
private:
    std::string _name;
public:
    Hello();
    void SetName(const char *name);
    const char *GetName();
    int Add(int x, int y);
    int Mul(int x, int y);
};

SWIG の場合、デフォルトでヘッダファイルの全てのクラス&全てのメソッドが公開対象になるので、この部分はエクスポート用のものを作ったほうがよいかもしれません。後、異なる環境でビルドすることを考えると windows.h などの読み込みはできないので、結構「綺麗なヘッダファイル」が必要かもしれません。

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma once
#include <string>
class Hello
{
private:
    std::string _name;
public:
    Hello();
    void SetName(const char *name);
    const char *GetName();
    int Add(int x, int y);
    int Mul(int x, int y);
};

helloswig.i は、SWIG で取り込むための設定ファイルです。%module の部分が DLL の名前(C#側の dllimport で指定されます)。%include が公開するための C++ インターフェスです。

1
2
3
4
5
6
7
8
9
/* File : helloswig.i */
%module helloswig
 
%{
#include "helloswig.h"
%}
 
/* Let's just grab the original header file here */
%include "helloswig.h"

*.i ファイルはそのままではビルドできないので、swig.exe を呼び出すようにします。
ファイルのプロパティで、「カスタムビルドツール」を指定して、

コマンドラインと出力ファイルを指定します。

swig.exe のパスは環境によって合わせてください。swig.exe のあるフォルダの Lib フォルダーから設定済みの *.i ファイルを読み込むようです。

1
2
3
4
echo Invoking SWIG...
echo on
..\swig.exe -c++ -csharp "%(FullPath)"
%40echo off

一度、ビルドを通すと helloswig_wrap.cxx のようなラップをしたクラスが作成されます。中身を見ると解るのですが、一度 C インターフェースに直してしています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
#ifdef __cplusplus
extern "C" {
#endif
 
SWIGEXPORT void * SWIGSTDCALL CSharp_new_Hello() {
  void * jresult ;
  Hello *result = 0 ;
   
  result = (Hello *)new Hello();
  jresult = (void *)result;
  return jresult;
}
 
 
SWIGEXPORT void SWIGSTDCALL CSharp_Hello_SetName(void * jarg1, char * jarg2) {
  Hello *arg1 = (Hello *) 0 ;
  char *arg2 = (char *) 0 ;
   
  arg1 = (Hello *)jarg1;
  arg2 = (char *)jarg2;
  (arg1)->SetName((char const *)arg2);
}
...

これと同時に、helloswigPINVOKE.cs のような C# 側でインポートするクラスが作られます。

1
2
3
4
5
6
7
8
9
10
...
  [global::System.Runtime.InteropServices.DllImport("helloswig", EntryPoint="CSharp_new_Hello")]
  public static extern global::System.IntPtr new_Hello();
 
  [global::System.Runtime.InteropServices.DllImport("helloswig", EntryPoint="CSharp_Hello_SetName")]
  public static extern void Hello_SetName(global::System.Runtime.InteropServices.HandleRef jarg1, string jarg2);
 
  [global::System.Runtime.InteropServices.DllImport("helloswig", EntryPoint="CSharp_Hello_GetName")]
  public static extern string Hello_GetName(global::System.Runtime.InteropServices.HandleRef jarg1);
...

hello プロジェクト

C# 側は、もともと何もないクラスですが、C++ プロジェクト(helloswigプロジェクト)
で作成した *.cs ファイル(Hello.cs, helloswig.cs, helloswigPINVOKE.cs)を追加します。

Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;
 
namespace hello
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello swig world.");
 
            var hello = new Hello();
            hello.SetName("Tomoaki Masuda");
            var name = hello.GetName();
            Console.WriteLine($"name: {name}");
 
            hello.SetName("Jhon doe");
            name = hello.GetName();
            Console.WriteLine($"name: {name}");
 
            int x = 10;
            int y = 20;
            Console.WriteLine($"{x} + {y} = {hello.Add(x, y)}");
            Console.WriteLine($"{x} * {y} = {hello.Mul(x, y)}");
 
            Console.WriteLine("end");
        }
    }
}

C# 側の main コードは、通常に C# のコードになります。
SWIG で出力した C# コードをいちいち追加しないといけないのですが、まあそのあたりはバッチ処理をするとかリンクで解決するとかできるでしょう。

ビルドして実行する

.NET Core でインポートできる DLL は x64 のみなので、そのあたりを注意しながら実行ファイルを作ります。

  1. C++プロジェクト(helloswig)を x64/Debug あるいは x64/Release でビルド.
  2. C#プロジェクト(hello)をビルド。.NET Core なのでコマンドラインから dotnet build でも ok です。
  3. 作成した dll を .NET Core の実行ファイルの場所にコピー(bin/Debug/netcoreapp2.0/ にコピー)
  4. dotnet run する。

Ubuntu環境でビルド実行する

さて、Windows 環境で動作が確認できたので、Ubuntuに持って行って動かしてみましょう。コードをごっそりと Ubuntu に持って行って、共有ファイルを作成するための Makefile を作ります。

1
2
3
4
5
6
7
8
all: libhelloswig.so
 
libhelloswig.so: helloswig.o
    g++ -sharad helloswig.o helloswig_wrap.o -o libhelloswig.so
helloswig.o: helloswig.cpp
    g++ -c helloswig.cpp
helloswig_wrap.o: helloswig_wrap.cpp
    g++ -c helloswig_wrap.cpp

あまりに久々だったので、ベタな makefile ですが、まあこれで libhelloswig.so がビルドできます。
本来ならば、ubuntu 上で swig を動かしてラップする C# コードを吐き出すところですが、既に Windows 上で作成しているので再利用します。ちなみに、ubuntu 上では「sudo apt-get install swig」で SWIG がインストール可能です。

libhelloswig.so ファイルを指定するために LD_LIBRARY_PATH を設定して dotnet run します。

export LD_LIBRARY_PATH=~/swig/helloswig

ここで、ちょっと面白いのは .NET Core で DllImport を使うと、Windows の場合は「helloswig.dll」を参照し、Ubuntu 上では「libhelloswig.so」を参照するところですね。こんため、Windows の SWIG で出力した C# ラッパーがそのまま Ubuntu でも使えます。

1
2
[global::System.Runtime.InteropServices.DllImport("helloswig", EntryPoint="CSharp_new_Hello")]
public static extern global::System.IntPtr new_Hello();

共有ファイルに lib を付けておくと便利です。

Raspberry Pi上で実行する

では、ラズパイ上で動かしてみましょう。Raspberry Piでは、.NET Core がビルドできないのであらかじめ Windows 側で C# コードをビルドしておきます。

dotnet publish -r linux-arm

C++ のプロジェクトはRaspberry Pi上で make します。

これで、

– Windows
– Ubuntu
– Raspberry Pi

の3つの環境で、.NET Core + SWIG + C++ライブラリが同じソースで動作ができました。

SWIGの難点

SWIG自体は、C# へのコンバートだけでなく、JavaやPythonなどへのコンバートがサポートされているので、色々な言語で活用ができます。まあ、小さな自前の C/C++ ライブラリを Python などで使う分にはこれでいけそうな気がするのですが、Windows上の複雑なライブラリをコンバートしようとすると難しそうな点がいくつかあります。

– Windows.h などの Unix 系にないインクルードファイルは読み込めない
– LPCTSTR などの定義が読み込めない(別途定義が必要)
– 文字列系のインターフェースは const char * か wchar_t に限られる?

最後のひとつは、std_string.i や std_wstring.i のインターフェースはあるのですが、SWIGTYPE_p_std__string.cs や SWIGTYPE_p_wchar_t.cs のような独自なクラスが作られるのでちょっと違う感じなんですよね。ただし、const char * は string に変換されるので、dllimport の仕組みのままのようです。ただし、このあたりは独自に *.i ファイルを作ればよいので、ピンポイントで std::string から string に変換するとか、std::wstring からコード変換するとかはできそうな感じなのですが。このあたりは別途調査。

カテゴリー: 開発, C#, C++ パーマリンク