Rust に少し C を加える
Rust プロジェクトの中で C または C++ を使うことは、大きく 2 つの部分から成ります。
- Rust から利用できるように公開された C API をラップすること
- Rust コードと統合できるように C または C++ コードをビルドすること
C++ には Rust コンパイラが対象にできる安定した ABI がないため、Rust を C または C++ と組み合わせる際には C ABI を使用することが推奨されます。
インターフェースを定義する
Rust から C または C++ のコードを利用する前に、リンクされるコード内にどのようなデータ型と関数シグネチャが存在するのかを(Rust 側で)定義する必要があります。C または C++ では、この情報を定義したヘッダーファイル(.h または .hpp)を include します。Rust では、これらの定義を手作業で Rust に移し替えるか、ツールを使ってこれらの定義を生成する必要があります。
まずは、これらの定義を C/C++ から Rust に手作業で変換する方法を見ていきます。
C 関数とデータ型をラップする
通常、C または C++ で書かれたライブラリは、公開インターフェースで使われるすべての型と関数を定義したヘッダーファイルを提供します。たとえば、次のようなファイルです。
/* ファイル: cool.h */
typedef struct CoolStruct {
int x;
int y;
} CoolStruct;
void cool_function(int i, char c, CoolStruct* cs);
これを Rust に変換すると、インターフェースは次のようになります。
/* ファイル: cool_bindings.rs */
#[repr(C)]
pub struct CoolStruct {
pub x: cty::c_int,
pub y: cty::c_int,
}
extern "C" {
pub fn cool_function(
i: cty::c_int,
c: cty::c_char,
cs: *mut CoolStruct
);
}
各要素を説明するために、この定義を少しずつ見ていきましょう。
#[repr(C)]
pub struct CoolStruct { ... }
デフォルトでは、Rust は struct に含まれるデータの順序、パディング、サイズを保証しません。C コードとの互換性を保証するために、#[repr(C)] 属性を付けます。これにより Rust コンパイラに対して、構造体内のデータ配置に常に C と同じ規則を使うよう指示します。
pub x: cty::c_int,
pub y: cty::c_int,
C または C++ における int や char の定義には柔軟性があるため、cty で定義されているプリミティブデータ型を使うことが推奨されます。これにより、C の型が Rust の型に対応付けられます。
extern "C" { pub fn cool_function( ... ); }
この文は、C ABI を使う cool_function という関数のシグネチャを定義しています。関数本体を定義せずにシグネチャだけを定義しているため、この関数の定義は別の場所で提供されるか、静的ライブラリから最終的なライブラリまたはバイナリへリンクされる必要があります。
i: cty::c_int,
c: cty::c_char,
cs: *mut CoolStruct
上で見たデータ型と同様に、関数引数のデータ型も C 互換の定義を使って定義しています。わかりやすさのために、引数名も同じものを保持しています。
ここでは 1 つ新しい型、*mut CoolStruct が出てきます。C には Rust の参照、すなわち &mut CoolStruct のような概念がないため、代わりに生ポインターを使います。このポインターのデリファレンスは unsafe であり、実際には null ポインターである可能性もあるため、C または C++ コードとやり取りする際には、Rust で通常期待される保証を満たすよう注意する必要があります。
インターフェースを自動生成する
これらのインターフェースは手作業で生成することもできますが、面倒でミスも起こりやすいため、bindgen というツールを使って自動的に変換できます。bindgen の使い方については bindgen user’s manual を参照してください。一般的な流れは次のとおりです。
- Rust と一緒に使いたいインターフェースまたはデータ型を定義している、すべての C または C++ ヘッダーを集めます。
bindings.hファイルを作成し、ステップ 1 で集めた各ファイルを#include "..."します。- この
bindings.hファイルと、コードのコンパイルに使用するコンパイルフラグをbindgenに渡します。ヒント: 生成されるコードを#![no_std]互換にするには、Builder.ctypes_prefix("cty")/--ctypes-prefix=ctyとBuilder.use_core()/--use-coreを使用してください。 bindgenは生成した Rust コードをターミナルの出力に表示します。この出力は、bindings.rsのようなプロジェクト内のファイルにパイプできます。このファイルを Rust プロジェクトで使えば、外部ライブラリとしてコンパイル・リンクされた C/C++ コードとやり取りできます。ヒント: 生成されたバインディング内の型にcty接頭辞が付いている場合は、ctycrate を使うことを忘れないでください。
C/C++ コードをビルドする
Rust コンパイラは C または C++ のコード(あるいは C インターフェースを提供する他言語のコード)を直接コンパイルする方法を知らないため、Rust 以外のコードは事前にコンパイルしておく必要があります。
組み込みプロジェクトでは、これは多くの場合、C/C++ コードを静的アーカイブ(cool-library.a など)にコンパイルすることを意味します。そうすることで、最後のリンクステップで Rust コードと結合できます。
使いたいライブラリがすでに静的アーカイブとして配布されている場合は、コードを再ビルドする必要はありません。上で説明したように提供されたインターフェースヘッダーファイルを変換し、コンパイル/リンク時にその静的アーカイブを含めるだけで済みます。
コードがソースプロジェクトとして存在する場合は、C/C++ コードを静的ライブラリにコンパイルする必要があります。これは、既存のビルドシステム(make、CMake など)を起動するか、必要なコンパイル手順を cc crate を使う形に移植することで行えます。どちらの場合にも、build.rs スクリプトを使う必要があります。
Rust の build.rs ビルドスクリプト
build.rs スクリプトは Rust 構文で書かれたファイルであり、プロジェクトの依存関係がビルドされた後で、かつプロジェクト自体がビルドされる前に、コンパイルを実行しているマシン上で実行されます。
完全なリファレンスは こちら にあります。build.rs スクリプトは、コード生成(たとえば bindgen の利用)、Make のような外部ビルドシステムの呼び出し、あるいは cc crate を使った C/C++ の直接コンパイルに役立ちます。
外部ビルドシステムを起動する
複雑な外部プロジェクトやビルドシステムを持つプロジェクトでは、相対パスをたどり、固定コマンド(make library など)を呼び出し、その後に生成された静的ライブラリを target ビルドディレクトリ内の適切な場所へコピーすることで、std::process::Command を使って他のビルドシステムを「shell out」して呼び出すのが最も簡単な場合があります。
あなたの crate が no_std の組み込みプラットフォームを対象としている場合でも、build.rs が実行されるのはその crate をコンパイルしているマシン上だけです。つまり、コンパイルホスト上で動作する Rust crate はどれでも使えます。
cc crate で C/C++ コードをビルドする
依存関係や複雑さが限られているプロジェクト、あるいは最終的なバイナリや実行ファイルではなく静的ライブラリを生成するようにビルドシステムを変更するのが難しいプロジェクトでは、代わりに cc crate を使うほうが簡単な場合があります。これは、ホストが提供するコンパイラに対する Rust らしいインターフェースを提供します。
単一の C ファイルを静的ライブラリの依存物としてコンパイルする最も単純なケースでは、cc crate を使った build.rs スクリプトの例は次のようになります。
fn main() {
cc::Build::new()
.file("src/foo.c")
.compile("foo");
}
build.rs はパッケージのルートに配置します。すると cargo build は、パッケージのビルド前にそれをコンパイルして実行します。libfoo.a という名前の静的アーカイブが生成され、target ディレクトリに配置されます。