Rustから直接C++を叩いてみる

プログラミング言語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関数、__ZN3Foo5greetEvFoo::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++以外の言語からは原理的に使えない。

参考リンク


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です