C++ユーザーの為のリンクの話1、2で C++ を使ってリンクについて簡単にまとめました。ここでは Rust からこれらを使うための方法に付いてまとめていきます。
API と ABI
Rust の話に入る前に、リンクの話を C で少し復習しましょう。
# include <stdio.h>
void func_a() {
printf("This is func_a!\n");
}
void func_a();
int main() {
func_a();
}
$ gcc -c a.c
$ gcc -c main.c
$ gcc main.o a.o
$ ./a.out
This is func_a!
C の場合は gcc コマンドを使ってこの様にリンクして実行します。これにより main 関数から処理が開始され、func_a 関数に処理が移り、printf を実行して main に戻り、プログラムは終了するわけです。
この時 C言語の中で main.c から a.c の中の関数 func_a をどうやって呼び出せばいいかを記述している部分は
void func_a();
この宣言部分です。抽象的なC言語としてはこの宣言だけあれば どこかにある func_a() 関数に処理を移せば良い事は定まっているため、このプログラムはどのように実行すればいいか定まりコンパイル出来るわけです。このような言語内のインターフェースを一般にAPIと呼びます。このレベルでC言語はどのような処理系でコンパイルするか、LinuxなのかWindowsなのかに依らないのでAPIは移植性がある一方、C言語内の概念になるので他の言語からは呼び出せません。
一方リンクの話で書いたように、Linux/GCCではこの宣言はオブジェクト main.o には func_a は Undefined なシンボルとして含まれ、リンク時にどうやって呼び出されるかが決まります。共有ライブラリの場合はさらに実行時に ld-linux.so が探してくるんでしたね。このように main.o の中にある機械語(例えば x86_64 や aarch64)から a.o の関数にどうやってアクセスすればいいかを規程しているのがABIです。CPUが理解出来る機械語には「関数」や「構造体」の概念は無く、これはC言語等の高級言語で与えられるもので、抽象的に定義されたC言語のプログラムを実際のCPUで動かす為にはこれを機械語に翻訳してあげる必要があるわけです。特に関数をどうやって呼び出すかを機械語でどう行うか、CPU上のどのレジスタに引数を置いて戻り値をどのレジスタに格納するか、等に関する規程の事を呼出規約(Calling Convention)と呼びます。これらは CPU の命令セット(ISA、x86_64等)だけでなく OS にも依存します。LLVM では x86_64-pc-windows-msvc, aarch64-unknown-linux-gnu の様な文字列で呼出規約や機械語の格納形式(ELFやPEなど)を識別し、この文字列を target-triple と呼びます(トリプル?4節あるが??とは思ってはいけない)。上記の例で言えば func_a() への ABI は target-triple 毎に定まり、これは機械語レベルで処理が正しければ呼出側・実装側がどの言語で実装されたかに依りません。
Rust の FFI
前置きが長くなりましたが Rust に置ける Foreign Function Interface (FFI) について見ていきましょう。FFIでは共有ライブラリやアーカイブに対してABIでアクセスします。幸い Rust には C と同様の ABI アクセスを行う関数を定義する extern "C" 句を定義できます
extern "C" {
fn func_a();
}
fn main() {
unsafe {
func_a();
}
}
これが上の main.c と対応するわけですね。nightly Rust では extern 句は他の呼出規約、例えば GPU 上での Device 関数の呼出の為の extern "ptx-kernel" の様な機能があります。FFIの呼出は Rust の意味での安全性が保証出来無いので unsafe であり、これを正しく呼び出すことはFFIを書くプログラマの責任になります。
Rust をコンパイルする rustc は gcc の様に直接オブジェクトファイルを引数に取れないので一旦 a.o をアーカイブにしておきます
$ ar crs liba.a a.o
これを rustc を使ってリンクしましょう
$ rustc main.rs -L. -la
$ ./main
This is func_a!
-L はライブラリを探すパスを追加するオプション、-l リンクするライブラリを指定するオプションです。-lxxx で libxxx.so か libxxx.a を探します。これで Rust から ABI 経由で C で実装された func_a を呼び出す事が出来ました。
構造体の受け渡し
機械語の世界では単一のメモリ空間からデータを取ってくる事は可能なのでポインタは存在しますが、C や Rust の struct は機械語の世界には存在しません。典型的には構造体は順番に要素を並べたものなのですが、Rustでは最適化の為要素の順番を入れ替えたりや間に空白をはさむか(パディング)事が許されているため、CやC++での構造体の定義と一見同じように見えても実際のメモリ配置が同じで無い可能性があります。なので ABI で呼び出される側と呼び出す側が同じようにアクセスにはどちらかに合わせる必要があります。
Rust にはこの目的の為に #[repr(C)] 注釈を使って構造体を定義する事が出来ます
# [repr(C)]
struct A {
a: u8,
b: u32,
c: u64,
}
これは C で
struct A {
uint8_t a;
uint32_t b;
uint64_t c;
}
の様に定義した場合と同一のメモリ配置になります。残念ながら C++ では (struct, class のどちらで定義しようとも)構造体が standard-layout で無い場合にはメモリ配置について互換性を維持出来無いので対応できません。
*-sys crate を作る
以上は素の rustc を使いましたが、ほとんどの Rust ユーザーは cargo 経由で使っているはずです。crateの書き方については次の記事で解説します