RustとCの橋渡し - CbindgenとCargo-cによるCバインディングとヘッダーの生成
Min-jun Kim
Dev Intern · Leapcell

はじめに
パフォーマンス、メモリ安全性、並行性に焦点を当てたRustは、システムプログラミングからWebAssemblyまで、さまざまな分野で急速に支持を得ています。しかし、世界は依然としてCで動いており、既存のCコードベースとの相互運用性は、Rustライブラリをより大きなプロジェクトに統合する上で、しばしば重要な要件となります。Rustのアプリケーションバイナリインターフェース(ABI)を直接消費することは、その絶え間ない進化により、不安定で複雑です。そこで、安定したCアプリケーションプログラミングインターフェース(API)の必要性が生じます。C互換のバインディングを通じてRustの機能性を公開することで、Rustの強みを利用しつつ、CおよびC互換言語の広大なエコシステムとの互換性を維持できます。この記事では、2つの強力なツール、cbindgen
とcargo-c
を使用して、Rustライブラリ向けのCヘッダーファイルとバインディングを生成するプロセスをガイドし、シームレスな統合を可能にし、Rustの創造性の新たな可能性を解き放ちます。
クロス言語相互運用のためのコアコンセプト
実践的な側面に飛び込む前に、RustとCの橋渡しに関わる主要な概念の基礎的な理解を確立しましょう。
- 外部関数インターフェース(FFI): FFIは、あるプログラミング言語で書かれたプログラムが、別のプログラミング言語で書かれた関数を呼び出したりサービスを使用したりすることを可能にするメカニズムです。この文脈では、RustのFFIはCコードとの対話、およびその逆を可能にします。
- C ABI(アプリケーションバイナリインターフェース): C ABIは、関数の呼び出し方法、メモリ内のデータのレイアウト、およびCで書かれた関数間でパラメータと戻り値がどのように渡されるかを定義します。Rustの関数をCに公開する際には、未定義の動作やクラッシュを防ぐために、C ABIの規則に従う必要があります。
no_mangle
属性: Rustでは、関数名はオーバーロードや一意のシンボル識別のサポートのために、コンパイラによって「マングル」されます。pub extern "C"
関数の前に配置される#[no_mangle]
属性は、この名前のマングルを防止し、コンパイルされた出力での関数の名前がソースコードの名前と一致することを保証します。これにより、Cコンパイラから発見可能になります。extern "C"
呼び出し規約: これは、関数がC呼び出し規約を使用することを示します。これは、引数がスタックにプッシュされる方法、戻り値がどのように処理されるか、および呼び出し後にスタックがどのようにクリーンアップされるかを決定します。FFIの正しい対話には、これに従うことが重要です。cbindgen
:pub extern "C"
関数およびC互換のデータ構造で注釈付けされたRustコードからC/C++ヘッダーファイルを自動生成するツールです。エラーが発生しやすく退屈なヘッダーファイルの作成を手動で行うプロセスを簡素化します。cargo-c
: RustプロジェクトからC互換ライブラリを作成するのに役立つCargoサブコマンドです。ビルドプロセスを自動化し、必要なCヘッダー、静的ライブラリ、動的ライブラリを生成し、Cビルドシステムとの統合を容易にするpkg-config
ファイルも生成できます。
Cヘッダーとバインディングの生成
cbindgen
とcargo-c
が実際にどのように機能するかを説明するために、例をステップバイステップで見ていきましょう。フィボナッチ数列を計算し、それをCに公開する簡単なRustライブラリを作成します。
ステップ 1: Rustライブラリの初期化
まず、新しいRustライブラリプロジェクトを作成します。
cargo new --lib fib_lib cd fib_lib
ステップ 2: Rust機能の実装
src/lib.rs
を編集して、フィボナッチ関数を含め、extern "C"
とno_mangle
を使用して公開します。簡単な構造体も定義します。
// src/lib.rs #[derive(Debug)] #[repr(C)] // C互換のメモリレイアウトを確保 pub struct MyData { pub value: i32, pub name: *const std::os::raw::c_char, // C互換の文字列ポインタ } /// N番目のフィボナッチ数を計算します。 /// # Safety /// この関数は非負のnに対して安全に呼び出すことができます。 #[no_mangle] pub extern "C" fn fibonacci(n: i32) -> i32 { if n <= 1 { n } else { fibonacci(n - 1) + fibonacci(n - 2) } } /// 提供されたデータを使用してメッセージを表示します。 /// # Safety /// `data`ポインタは有効であり、`MyData`構造体を指している必要があります。 /// `MyData`内の`name`フィールドは、有効なCスタイル文字列ポインタまたはnullである必要があります。 #[no_mangle] pub extern "C" fn greet_with_data(data: *const MyData) { if data.is_null() { println!("Received null data."); return; } unsafe { let actual_data = *data; let c_str = std::ffi::CStr::from_ptr(actual_data.name); let name_str = c_str.to_string_lossy(); println!("Hello, {}! Your value is {}", name_str, actual_data.value); } }
FFIの重要な考慮事項:
#[repr(C)]
: この属性は構造体にとって不可欠です。Rustコンパイラに、Cコンパイラがメモリ内の構造体のフィールドをレイアウトする方法を正確に指示し、ABIの非互換性を引き起こす可能性のある予期しないパディングや再配置を防ぎます。*const std::os::raw::c_char
: RustのString
およびstr
型はC互換ではありません。FFI境界を越えて文字列を渡すには、*const std::os::raw::c_char
(不変文字列の場合)または*mut std::os::raw::c_char
(可変文字列の場合)で表されるCスタイルのヌル終端文字列を使用します。Rustの所有権ルールは生のポインタには適用されないため、両側で慎重なメモリ管理が必要です。この例では、name
フィールドのメモリをC側が所有および管理すると仮定しています。unsafe
ブロック: 生のポインタやC FFIを扱う場合、unsafe
ブロックに遭遇することがよくあります。これらのブロックは、Rustコンパイラがメモリの安全性を保証できない操作(生のポインタの逆参照やC関数の呼び出しなど)を実行することを示します。これらの操作の安全性を確保することは、開発者の責任です。
ステップ 3: cbindgen
の統合
cbindgen
をCargo.toml
のビルド依存関係として追加します。
# Cargo.toml [package] name = "fib_lib" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib", "staticlib"] # 動的および静的Cライブラリを生成 [build-dependencies] cbindgen = "0.24" # 最新の安定バージョンを使用
ヘッダーを生成するためにcbindgen
を実行するビルドスクリプトbuild.rs
を作成します。
// build.rs extern crate cbindgen; use std::env; fn main() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); cbindgen::Builder::new() .with_crate(crate_dir) .with_language(cbindgen::Language::C) .generate() .expect("Unable to generate bindings") .write_to_file("target/fib_lib.h"); // targetディレクトリに出力 }
これで、cargo build
を実行すると、cbindgen
は自動的にtarget/fib_lib.h
を生成します。
ステップ 4: cargo-c
の統合
cargo-c
は、RustライブラリをC互換ライブラリとしてパッケージ化するのを簡素化します。まずインストールします。
cargo install cargo-c
これで、cargo cbuild
を実行するだけで、ライブラリをC互換でビルドできます。このコマンドは次のことを行います。
- Rustコードを静的ライブラリ(
.a
)および動的ライブラリ(Linuxでは.so
、macOSでは.dylib
、Windowsでは.dll
)にコンパイルします。 build.rs
スクリプトを実行し、fib_lib.h
を生成します。- これらの出力成果物を構造化されたディレクトリ(例:
target/release/capi/
)に配置します。
通常のビルド中にヘッダーをtarget
ディレクトリに直接出力するようにbuild.rs
を少し変更しましょう。その後、cargo-c
はそれを標準のC互換出力構造に移動します。
cargo cbuild --release
を実行すると、target/release/capi
に生成されたファイルが見つかります。
target/release/capi/
├── include/
│ └── fib_lib.h
├── lib/
│ ├── libfib_lib.a
│ └── libfib_lib.so (または .dylib/.dll)
└── pkgconfig/
└── fib_lib.pc
fib_lib.h
ファイルは次のようなものになります(簡略化されています)。
// fib_lib.h (cbindgenによって生成) #include <stdarg.h> #include <stdbool.h> #include <stdint.h> #include <stdlib.h> typedef struct MyData { int32_t value; const char *name; } MyData; int32_t fibonacci(int32_t n); void greet_with_data(const struct MyData *data);
ステップ 5: Cでのライブラリの消費
Rustライブラリを使用するためにCファイル(例: main.c
)を作成します。
// main.c #include <stdio.h> #include <stdlib.h> // malloc, free用 #include <string.h> // strdup用 #include "fib_lib.h" // 生成されたヘッダーを含める int main() { // fibonacci関数のテスト int n = 10; int result = fibonacci(n); printf("Fibonacci(%d) = %d\n", n, result); // greet_with_data関数のテスト MyData my_c_data; my_c_data.value = 42; my_c_data.name = strdup("World from C"); // Cスタイルの文字列を割り当てる greet_with_data(&my_c_data); free((void*)my_c_data.name); // 割り当てられたCスタイルの文字列を解放する return 0; }
生成されたRustライブラリでCプログラムをコンパイルしてリンクします。
# main.cとfib_lib.h、.a/.soファイルを含むディレクトリにいると仮定 # (パスはtarget/release/capi/を指すように調整してください) # 動的リンク用 (Linux/macOS) gcc main.c -o my_c_app -Itarget/release/capi/include -Ltarget/release/capi/lib -lfib_lib # 静的リンク用 (Linux/macOS) # gcc main.c -o my_c_app -Itarget/release/capi/include target/release/capi/lib/libfib_lib.a # Cアプリケーションを実行 ./my_c_app
次のような出力が表示されるはずです。
Fibonacci(10) = 55
Hello, World from C! Your value is 42
これは、シームレスな相互運用性が成功したことを示しています。CプログラムはRust関数を呼び出し、Rustで定義されたデータ構造をシームレスに使用しています。
アプリケーションシナリオ
- 既存のC/C++コードベースへのRustの統合: アプリケーション全体を書き直すことなく、メモリ安全性とパフォーマンスのためにRustをクリティカルなコンポーネントに使用します。
- 埋め込み可能なモジュールの作成: CまたはC FFIサポートを持つ他の言語で書かれたアプリケーションによってロードおよび使用できる、Rust搭載のプラグインや拡張機能を作成します。
- 低レベルOSコンポーネントの開発: Cインターフェースと対話する必要があるドライバ、カーネルモジュール、またはブートローダーにRustを使用します。
- クロス言語開発: RustとCの両方を完全に学習する必要なく、RustとCで作業するチーム間のコラボレーションを促進します。
結論
cbindgen
とcargo-c
を習得することで、RustプロジェクトからC互換のヘッダーとライブラリを簡単に生成できます。この強力な組み合わせは、可能性の世界を解き放ち、堅牢で安全なRustコードが広大なCエコシステムにシームレスに統合できるようにします。この橋渡し機能は、Rustがそのリーチと影響力を多様なソフトウェアランドスケープ全体に広げるための実用的で効率的なパスを提供します。