プログラミング言語Rustのα版が出たので遊んでみようかなという。(と言ってやってることはRustである必要性が全然ないけど)
ここで使っている環境はOS X 10.9.5 (アーキテクチャはx86_64)で、C++コンパイラはClangである。ここでやっているのは激しく環境依存なのだが、まあでもコンパイラがGCCかClangなら多くの環境で上手くいくんじゃないかと思う(知らん)。
背景
Rustはネイティブコードにコンパイルする言語である。ネイティブコードにコンパイルする多くの言語と同じように、RustからはC言語の関数を呼び出せる(こういう風に別のプログラミング言語の関数を呼び出すインターフェースをFFIという)。RustのFFIの詳細はThe Rust Foreign Function Interface Guideに書かれている。
まあ、ネイティブコードにコンパイルする言語がC言語の関数を呼び出せるというのは普通だ。C言語で書かれた有名なライブラリはたくさんあるし、OSが提供するAPIもC言語の関数として定義されていることが多いので、C言語の関数を呼び出せるというのは重要だ。(一方、PythonとかRubyとかのスクリプト言語は、その処理系用にCで拡張機能を用意してやることによって外部のライブラリ等を呼び出せるようにしている。C言語のライブラリがあってもバインディングがないと使えない。(まあPythonのctypesのように、libffiのバインディングを作ってやればできなくはないけど))
C言語で書かれたライブラリが使えるのはいいのだが、では、C++で書かれたライブラリを使うにはどうしたらいいのか。ネイティブコードにコンパイルする多くの言語はC言語の関数を呼び出せるのに対し、C++のクラスなどを直接使える言語というのはないと言っていい。(一応D言語がC++の機能にアクセスできるが、現在のところはクラスの非virtualメンバ関数にはアクセスできないなど、いろいろ限定されている。)(あと、WindowsのCOMの話はここでは扱わない)
なんでC言語とC++でこんなに違うのかというと、C言語のABIはプラットフォーム(OS、CPU)ごとにだいたい共通なのに対し、C++のABIはコンパイラによって違うということがある。同じC++でも、MSVCでコンパイルしたC++のライブラリをMinGWでコンパイルしたライブラリで使う、ということはできない。
だから、C++で書かれたライブラリを他の(ネイティブコードにコンパイルする)言語で使いたい場合は、一旦C言語のwrapperを作って、それを介して間接的に呼び出すというのが一般的だ。(一方、スクリプト言語の場合は、拡張機能をC言語で書けることが多いので、C++でそのスクリプト言語の拡張機能を書いてやれば良い)
が、ここではあえて茨の道を行ってみる。つまり、C++のクラスのメンバ関数などを他の言語(ここではRust)から直接呼び出すことを目指す。
方針
C++の関数やメンバ関数は、この環境ではCの関数とみなせるので、それらをCの関数としてインポートする。
ではやってみよう。
C++のコード
普通に、コンストラクタがあって、デストラクタがあって、適当なメンバ関数があるだけのコードである。C++の標準ライブラリで面倒なことになると嫌なので、iostreamではなくstdioを使った。あと、Fooクラスのインスタンスを確保と解放する関数もC++で書いておく。
このコードを、次のようにコンパイルしてやる。
$ clang -c foo.cpp $ ls foo.cpp foo.o
マングル名
C++の関数の名前は、コンパイルする時に型とか名前空間の情報を埋め込んだ「マングル名」に置き換えられる。C++以外の言語からC++の関数を叩くには、このマングル名が必要である。というわけで、nmコマンドでオブジェクトファイルを覗いてみる。
$ nm foo.o 00000000000001d0 s EH_frame0 00000000000002b0 s EH_frame1 0000000000000170 s GCC_except_table5 000000000000019c s GCC_except_table6 00000000000001bc s L_.str U __Unwind_Resume 00000000000000b0 T __Z6newFooi 00000000000002d0 S __Z6newFooi.eh 0000000000000110 T __Z9deleteFooP3Foo 0000000000000300 S __Z9deleteFooP3Foo.eh 0000000000000080 T __ZN3Foo5greetEv 0000000000000288 S __ZN3Foo5greetEv.eh 0000000000000020 T __ZN3FooC1Ei 0000000000000210 S __ZN3FooC1Ei.eh 0000000000000000 T __ZN3FooC2Ei 00000000000001e8 S __ZN3FooC2Ei.eh 0000000000000060 T __ZN3FooD1Ev 0000000000000260 S __ZN3FooD1Ev.eh 0000000000000050 T __ZN3FooD2Ev 0000000000000238 S __ZN3FooD2Ev.eh U __ZdlPv U __Znwm U ___gxx_personality_v0 U _printf
__Z6newFooi
というのがnewFoo
関数、__Z9deleteFooP3Foo
というのがdeleteFoo
関数、__ZN3Foo5greetEv
がFoo::greet
に対応しているようである。ここで出てくる名前はCの普通の関数名の先頭にアンダースコア(_
)をつけたものなので、これらの関数をCの関数としてインポートする際は_Z6newFooi
という名前でインポートする。 GCCの使うマングル名はItanium ABIというやつに沿っているらしいので、マングル名の規則について知りたかったら読むとよさそう。
Rustのコード
RustからはC++のFoo
クラスの中身(メンバ変数など)にアクセスする必要はないので、対応する型はenum Foo {}
としておく(opaque typeというやつ)。Foo
へのポインタ型Foo *
に対応するRustの型は*mut Foo
となる。
C++で書いたnewFoo
関数の引数はint
型だが、これに対応するRustの型はlibc crateで定義されているc_int
というのを使えば良さそうだ。ただ、今回はどうせ環境依存のコードを書いているということで、Rustのi32
型を使ってやっても良かったかもしれない(OS XはLP64環境なのでint
型は32ビットのはず)。
あと、newFoo
等の関数を使うにはfoo.o
をリンクする必要があるので、#[link_args]
でfoo.o
をリンクするように指示してやる(あまりいいやり方ではない気がするが)。
あとはこのソースをコンパイルしてやれば良いが、C++のコード中でnew
とかdelete
演算子を使っているので、リンクする時にC++の標準ライブラリをリンクする必要があることに注意。
$ rustc test.rs -lstdc++ test.rs:3:1: 3:19 warning: use of unstable item test.rs:3 extern crate libc; ^~~~~~~~~~~~~~~~~~ note: this feature may not be used in the beta release channel test.rs:4:34: 4:39 warning: unused import, #[warn(unused_imports)] on by default test.rs:4 use libc::types::os::arch::c95::{c_int}; ^~~~~ test.rs:1:1: 1:23 warning: unstable feature test.rs:1 #![feature(link_args)] ^~~~~~~~~~~~~~~~~~~~~~ note: this feature may not be used in the beta release channel
なんかいろいろwarningが出てきた。どうやら、このコードはβ版になると動かないっぽい?まあ面倒くさいことは後で考えることにして、とりあえず実行してみる。
$ ./test Hello world! x=42
何のひねりもない。
移植性について
他のコンパイラでやろうとするとどうなるか。(ここに書いたのはただの妄想であって、実際に試してみた訳ではない)
マングル名
GCCなどの使うItanium ABI(さっきもリンク貼った)は、マングル後の名前がCの正当な識別子になっているみたいだが、MSVCのマングル名は普通に?
とか@
とかの記号が出てくるっぽいので辛そう。
Itanium ABIの場合は、Rustのマクロ機能でマングル名を作るマクロとか作れるかもしれない。
[2015年1月16日 22時47分 追記] #[link_name]
というattributeを使えばシンボルの名前を文字列で指定できるから良さそう?参照
呼び出し規約
x86上のC言語とかでプログラミングをしたことがある人は知っていると思うが、x86には関数の呼び出し規約がcdeclとかstdcallとかいろいろある。特に、MSVCでは、C++のクラスのメンバ関数はthiscallという呼び出し規約で、this
ポインタをレジスタで渡すらしい。ここにRustのFFIが対応している呼び出し規約が書いてあるが、見た感じではthiscallには対応していないようである。もしもx86のMSVCで同じことをやろうとするとRustのコンパイラに手を加えないといけないかもしれない。
ただ、x86_64(amd64)であれば、呼び出し規約は1つしかないような感じなので、難しいことは考えなくていい。ARMは知らん。
C++を他の言語から叩くときに考えられる問題点
- 仮想関数
- vtblのレイアウトが分かれば多分何とかなる。
- 派生クラスを作って仮想関数をオーバーライドすることは頑張ればできるだろうか?
- クラスのサイズ(
sizeof(Foo)
)- Rustの側でオブジェクトを作ってやる時に必要になりそう。
- 多重継承とかの時にも必要?
- C++だと
sizeof
で取得できるが、他の言語で同じことをやるのは闇っぽい。#[repr(C)]
をつけたRustのstructを作ってやってcore::memの関数を使えばなんとかなるだろうか。
- C++の標準ライブラリ/STL
- コンパイラのABIが同じでも、ライブラリの実装によってABIが変わってきそうなのでやばい。
- RustとC++で
std::string
とか受け渡ししたい。
- インライン関数
- 多分オブジェクトファイルには出てこないので、C++以外の言語からは原理的に使えない。
参考リンク
- Rust言語について: The Rust Programming Language
- Itanium C++ ABIについて: Itanium C++ ABI
- D言語が持っているC++ FFI: Interfacing to C++ – D Programming Language
- RustのFFIについて: The Rust Foreign Function Interface Guide
- C++を直接Haskellから叩く記事(大いに参考にさせてもらった): CPlusPlus from Haskell – HaskellWiki
- RustにC++へのFFIをつけようぜ的な議論: