RustのFFI(Cの呼び出し)
FFI は Foreign Function Interface(他言語関数インターフェース)の略です。Rust がほかの言語で書かれたコードとやりとりするための仕組みです。その「ほかの言語」は、ほとんどの場合 C か、C のルールに従う何かです。C は、ほぼすべての言語やOSが理解できる「共通語」だからです。
母語が違う2人が、どちらも少しだけ英語を話せる場面を想像してみてください。C は、その英語のような共通語です。FFI を使えば、実績のある C のライブラリを Rust から使ったり、Python や Node.js、C のプログラムから呼び出せる高速な Rust のライブラリを書いたりできます。このページは unsafe のページの続きです。unsafe ブロックをまだ知らない人は、先にそちらを読んでください。
C の関数を呼び出す
Section titled “C の関数を呼び出す”Linux、macOS、Windows では、C の標準ライブラリがいつも Rust のプログラムにリンクされています。なので、最初に試すにはぴったりです。C の関数を呼ぶには、まず extern "C" ブロックの中でその関数を宣言します。名前、引数、戻り値の型を書くのです。そして、unsafe の中で呼び出します。
use std::ffi::c_int;
unsafe extern "C" { // C の `int abs(int x);` を Rust で書いたものです。 fn abs(x: c_int) -> c_int;}
fn main() { let n = unsafe { abs(-42) }; println!("abs(-42) = {n}");}実行結果:
abs(-42) = 42いくつかポイントがあります。
"C"は ABI の名前です。ABI とは、引数の渡し方や値の返し方のルールのことです。C のルールは、誰もが合意している共通のルールです。c_intは、C のintを Rust で表した名前です。std::ffiモジュールにはc_int、c_long、c_charなどの型があり、どのプラットフォームでも宣言が C とぴったり一致するようになっています。unsafe extern "C"と書いています。edition 2024 では、このunsafeは必須です。「この宣言は正しい」とあなたが約束することになるからです。古いコード(や edition 2021)では、ただのextern "C" { ... }と書かれていることがよくあります。unsafe付きの書き方はどちらの edition でも動くので、このページではすべてそれを使います。
すべての呼び出しが unsafe な理由と、それを隠す方法
Section titled “すべての呼び出しが unsafe な理由と、それを隠す方法”Rust のコンパイラは C のコードを読めません。abs が本当に int を受け取るのか、クラッシュする可能性があるのか、渡したポインタをずっと持ち続けるのか、まったくわからないのです。だから、外部の関数の呼び出しはすべて unsafe になります。正しく使う責任はあなたにある、ということです。
うれしいことに、真剣に考える必要があるのは一度だけです。よく使われるのは、小さな安全なラッパーを書くパターンです。ルールを確認してから unsafe な呼び出しをする、ふつうの Rust の関数です。ほかの人は、そのラッパーを呼ぶだけで済みます。
実際の例を見てみましょう。C では、abs(INT_MIN) は未定義動作です。答えが大きすぎて int に収まらないからです。このラッパーは、まずそのケースをチェックします。
use std::ffi::c_int;
unsafe extern "C" { fn abs(x: c_int) -> c_int;}
// 安全なラッパー:呼び出す側は `unsafe` を書かなくてよい。fn safe_abs(x: i32) -> Option<i32> { // C では abs(i32::MIN) に正しい答えがないので、受け付けない。 if x == i32::MIN { return None; } // SAFETY: x は i32::MIN ではないので、abs は正しい結果を返す。 Some(unsafe { abs(x) })}
fn main() { println!("{:?}", safe_abs(-42)); println!("{:?}", safe_abs(i32::MIN));}実行結果:
Some(42)None// SAFETY: コメントは、ぜひまねしたい Rust の習慣です。その unsafe ブロックがなぜ問題ないのかを説明しておけば、次に読む人が推測しなくて済みます。
どんな入力でも守るべきルールがまったくない C の関数なら、unsafe extern ブロックの中で safe fn と書いておけます。そうすると、unsafe なしで呼び出せます。abs はそういう関数ではありません(INT_MIN のことを思い出してください)。なので、unsafe のままにして、入力は自分でチェックします。
文字列:CString、CStr と NUL バイト
Section titled “文字列:CString、CStr と NUL バイト”FFI のバグのほとんどは文字列で起こります。Rust と C では、文字列の持ち方が違うからです。
- Rust の
String/&strは自分の長さを知っていて、0を含むどんなバイトでも持てます。 - C の文字列は、バイト列へのポインタにすぎません。文字列は最初の
0バイト(NUL と呼びます)で終わります。長さはどこにも保存されていません。
なので、&str をそのまま C に渡すことはできません。CString(英語)を使って、NUL で終わる、所有権を持ったコピーを作ります。そして .as_ptr() で、C が求める *const c_char を取り出します。
use std::ffi::{c_char, CString};
unsafe extern "C" { fn strlen(s: *const c_char) -> usize;}
fn c_strlen(text: &str) -> Option<usize> { // CString::new が最後に NUL を付けてくれる。 let c_text = CString::new(text).ok()?; // SAFETY: c_text は NUL で終わる正しい文字列で、この関数の終わりまで生きている。 Some(unsafe { strlen(c_text.as_ptr()) })}
fn main() { let hello = CString::new("hello").unwrap(); println!("{:?}", hello.as_bytes_with_nul());
println!("{:?}", c_strlen("hello")); println!("{:?}", c_strlen("héllo"));
match CString::new("hel\0lo") { Ok(_) => println!("ok"), Err(e) => println!("エラー: {e}"), }}実行結果:
[104, 101, 108, 108, 111, 0]Some(5)Some(6)エラー: nul byte found in provided data at position: 3この例では、バイト数がそのまま出力に出るので、文字列は英語のままにしています。nul byte found in ... は Rust の標準ライブラリが出すエラーメッセージで、「渡されたデータの3番目の位置に NUL バイトが見つかった」という意味です。
ここには驚くポイントが2つあり、どちらも大切です。
strlen("héllo")は5ではなく6です。C はバイト数を数えます。そしてéは UTF-8 では2バイトです(日本語の文字なら1文字3バイトです)。- テキストに
0バイトが含まれていると、CString::newは失敗します。C はそこで文字列が終わると思い込み、"lo"を黙って切り捨ててしまうからです。Rust はそれを許さずに、はっきり断ります。CString::newがResultを返すのはそのためです。
反対に、C から char * を受け取るときは、CStr(英語)で包みます。CStr と CString の関係は、&str と String の関係と同じで、借用したビューです。C の strchr は、最初にマッチした文字へのポインタを返すので、ちょうどいい例になります。
use std::ffi::{c_char, c_int, CStr, CString};
unsafe extern "C" { fn strchr(s: *const c_char, c: c_int) -> *mut c_char;}
fn find_from(text: &CStr, ch: u8) -> Option<String> { let ptr = unsafe { strchr(text.as_ptr(), ch as c_int) }; if ptr.is_null() { return None; // C は「見つからない」をヌルポインタで表す } // SAFETY: ptr は `text` の中を指していて、text は生きていて NUL で終わっている。 let found = unsafe { CStr::from_ptr(ptr) }; Some(found.to_string_lossy().into_owned())}
fn main() { let text = CString::new("hello, world").unwrap(); println!("{:?}", find_from(&text, b',')); println!("{:?}", find_from(&text, b'z'));
// c"..." リテラルは、NUL がすでに付いた &CStr になる。 println!("{:?}", find_from(c"rust-lang.org", b'.'));}実行結果:
Some(", world")NoneSome(".org")to_string_lossy() は、C のバイト列を Rust のテキストに変換します。C の文字列は UTF-8 であることを約束していないので、不正な UTF-8 は � に置き換えられます。エラーとして受け取りたいなら、代わりに .to_str() を使いましょう。
#[repr(C)] で構造体を共有する
Section titled “#[repr(C)] で構造体を共有する”Rust は、メモリを節約するために構造体のフィールドを並べ替えてもよいことになっています。C は決して並べ替えません。フィールドは書いた順にメモリに並び、間にパディング(詰め物)が入ります。たとえば、フィールドが u8、u32、u8 の構造体は、今の Rust のレイアウトでは8バイトですが(Rust が2つの u8 を隣どうしに移すため)、C のレイアウトでは12バイトになります。C と Rust でレイアウトの認識が食い違うと、C は間違ったバイトを読んでしまいます。しかも、何の警告も出ません。
そこで、構造体が言語の境界をまたぐときは、#[repr(C)] を付けて C のレイアウトを使うよう Rust に伝えます。C の div 関数は、quot(商)と rem(余り)という2つの int フィールドを持つ div_t 構造体を返します。
use std::ffi::c_int;
// C の typedef struct { int quot; int rem; } div_t; に対応する。#[repr(C)]#[derive(Debug)]struct DivT { quot: c_int, rem: c_int,}
unsafe extern "C" { fn div(numerator: c_int, denominator: c_int) -> DivT;}
fn main() { let result = unsafe { div(17, 5) }; println!("{result:?}"); println!("17 / 5 = {} 余り {}", result.quot, result.rem);}実行結果:
DivT { quot: 3, rem: 2 }17 / 5 = 3 余り 2C から Rust を呼び出す
Section titled “C から Rust を呼び出す”FFI は双方向に使えます。(ブロックなしで)extern "C" fn と書くと、C の呼び出しルールに従う Rust の関数になり、C のコードから呼び出せます。C の qsort は比較関数を引数に取るので、Rust の関数を渡してみましょう。
use std::ffi::{c_int, c_void};
unsafe extern "C" { fn qsort( base: *mut c_void, count: usize, size: usize, compare: extern "C" fn(*const c_void, *const c_void) -> c_int, );}
// C から呼ばれる Rust の関数。extern "C" fn compare_i32(a: *const c_void, b: *const c_void) -> c_int { // SAFETY: qsort が渡してくるのは、i32 の配列の中を指すポインタだけ。 let (a, b) = unsafe { (*(a as *const i32), *(b as *const i32)) }; a.cmp(&b) as c_int // Less = -1、Equal = 0、Greater = 1}
fn sort_with_c(numbers: &mut [i32]) { unsafe { qsort( numbers.as_mut_ptr() as *mut c_void, numbers.len(), std::mem::size_of::<i32>(), compare_i32, ); }}
fn main() { let mut numbers = [42, 7, 19, -3, 0]; sort_with_c(&mut numbers); println!("{:?}", numbers);}実行結果:
[-3, 0, 7, 19, 42](実際のコードなら、sort() を呼ぶだけで済みます。これは、C から Rust を呼び戻せることを見せるための例です。)
C のプログラムがあなたの関数を名前で見つける必要があるときは、#[no_mangle] を付けます。ふつう Rust は、別々のクレートの名前がぶつからないように、名前を _ZN4main11add_numbers17h...E のような形に「マングリング」(変形)します。#[no_mangle] を付けると、add_numbers というそのままの名前が残ります。
#[no_mangle]pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 { a + b}edition 2024 についての注意:このサイトの Playground の例は edition 2021 を使っていて、そこでは #[no_mangle] で問題ありません。edition 2024(新しく cargo new したプロジェクトのデフォルト)では、#[unsafe(no_mangle)] と書かなければなりません。マングリングされていない同じ名前の関数が2つあると、プログラムが壊れてしまうからです。新しい書き方は edition 2021 でも動きます。
C からリンクできるライブラリを作るには、Cargo.toml でクレートの種類を cdylib にします。
[lib]crate-type = ["cdylib"]// src/lib.rs(edition 2024)#[unsafe(no_mangle)]pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 { a + b}mathlib という名前のクレートなら、cargo build --release で target/release/libmathlib.so(macOS では .dylib、Windows では .dll)ができます。C のプログラムは、関数を宣言して、このライブラリとリンクします。
#include <stdio.h>#include <stdint.h>
int32_t add_numbers(int32_t a, int32_t b);
int main(void) { printf("2 + 3 = %d\n", add_numbers(2, 3)); return 0;}$ cc main.c -L target/release -lmathlib -o main$ LD_LIBRARY_PATH=target/release ./main2 + 3 = 5この C のヘッダーを手で書くのは、すぐに面倒になります。cbindgen(英語)を使えば、Rust のコードからヘッダーを生成できます。
build.rs と cc で自分の C コードをコンパイルする
Section titled “build.rs と cc で自分の C コードをコンパイルする”ここまでは、最初からリンクされている関数だけを使ってきました。自分の C ファイルを使うには、Cargo にコンパイルさせます。プロジェクトのルートにある build.rs というファイルはビルドスクリプトです。Cargo は、クレートをコンパイルする前にこれを実行します。cc(英語)クレートを使えば、数行で済みます。
cargo new hello-ccd hello-ccargo add --build cc#include <stdio.h>
int add_ints(int a, int b) { return a + b;}
void greet(const char *name) { printf("Cからこんにちは、%sさん!\n", name); fflush(stdout);}fn main() { // src/greet.c を静的ライブラリにコンパイルして、リンクする。 cc::Build::new().file("src/greet.c").compile("greet"); // C のファイルが変わったときだけ、このスクリプトを再実行する。 println!("cargo::rerun-if-changed=src/greet.c");}use std::ffi::{c_char, c_int, CString};
unsafe extern "C" { fn add_ints(a: c_int, b: c_int) -> c_int; fn greet(name: *const c_char);}
fn main() { let sum = unsafe { add_ints(2, 3) }; println!("2 + 3 = {sum}");
let name = CString::new("Ferris").unwrap(); unsafe { greet(name.as_ptr()) };}$ cargo run -q2 + 3 = 5Cからこんにちは、Ferrisさん!C コンパイラ(gcc、clang、Windows なら MSVC)がインストールされている必要があります。cc が自動で見つけてくれます。
何百もの関数を持つ大きな C ライブラリだと、extern の宣言をすべて手で書くのは時間がかかるうえ、間違えやすくなります。bindgen(英語)は C のヘッダーファイルを読んで、Rust の宣言を生成してくれます。これもふつうは build.rs から使います。crates.io にある -sys クレート(openssl-sys など)のほとんどは、この方法で作られています。
C の先へ:Python、Node.js、そして Web
Section titled “C の先へ:Python、Node.js、そして Web”C が共通語なので、同じ考え方で Rust はほかのたくさんの言語とも話せます。こうした言語のために生の extern "C" コードを書くことはめったにありません。クレートが unsafe な作業を引き受けて、安全で使いやすい API を用意してくれるからです。
- Python:PyO3(英語)を使うと、Python のモジュールを Rust で書けます(Rust から Python を呼ぶこともできます)。付属のツール
maturinで、ふつうのpipパッケージとしてビルド・公開できます。Polars や pydantic-core といったライブラリが使っています。 - Node.js:napi-rs(英語)で、Node.js のネイティブアドオンを Rust で作れます。TypeScript の型も生成してくれます。
- C や C++ のプロジェクト:上で見たように、
cdylibと cbindgen(英語)を使います。 - ブラウザ:C という意味での FFI ではありませんが、
wasm-bindgenが似た役割を果たします。RustとWebAssemblyを見てください。
よくある間違い
Section titled “よくある間違い”外部の関数を unsafe の外で呼び出す。
unsafe extern "C" { fn abs(x: i32) -> i32;}
fn main() { let n = abs(-7); println!("{n}");}error[E0133]: call to unsafe function `abs` is unsafe and requires unsafe function or block --> src/main.rs:6:13 |6 | let n = abs(-7); | ^^^^^^^ call to unsafe function「unsafe な関数 abs の呼び出しは unsafe なので、unsafe な関数かブロックの中でしか呼べない」というエラーです。直し方:呼び出しを unsafe { ... } で包みます。上で見たように、安全なラッパー関数の中に入れるとなおよいです。
もう消えた CString を指すポインタ。これはコンパイルできてしまいますが、本物のバグです。
use std::ffi::{c_char, CString};
unsafe extern "C" { fn strlen(s: *const c_char) -> usize;}
fn main() { let ptr = CString::new("hello").unwrap().as_ptr(); let len = unsafe { strlen(ptr) }; println!("{len}");}warning: this creates a dangling pointer because temporary `CString` is dropped at end of statement --> src/main.rs:8:46 |8 | let ptr = CString::new("hello").unwrap().as_ptr(); | ------------------------------ ^^^^^^ pointer created here | | | this `CString` is dropped at end of statement | = help: bind the `CString` to a variable such that it outlives the pointer returned by `as_ptr`この CString は一時的な値なので、; のところで解放されます。すると ptr は、解放済みのメモリを指すことになります。コンパイラは「一時的な CString が文の終わりで破棄されるので、ダングリングポインタができる」と警告しています。直し方:CString を自分の変数に入れておき(let s = CString::new("hello").unwrap();)、s が生きている間に s.as_ptr() を呼びます。この警告は決して無視しないでください。
edition 2024 のプロジェクトで、ただの extern "C" を使う。古いチュートリアルからコピーしたコードは、こんなふうに失敗します。
error: extern blocks must be unsafe --> src/main.rs:3:1 |3 | / extern "C" {4 | | fn add_ints(a: c_int, b: c_int) -> c_int;5 | | fn greet(name: *const c_char);6 | | } | |_^「extern ブロックは unsafe でなければならない」というエラーです。直し方:unsafe extern "C" { ... } と書きます。
#[repr(C)] を忘れる。エラーはまったく出ません。C がただ、でたらめな値を読むだけです。境界をまたぐ構造体と列挙型には、必ず #[repr(C)] を付けましょう。
- C の関数は
unsafe extern "C" { ... }ブロックの中で宣言します。型にはc_intやc_charなど、std::ffiにある C の型を使います。 - 外部の関数の呼び出しは、すべて
unsafeです。ルールを確認する安全な Rust の関数で一度だけ包み、// SAFETY:コメントを添えましょう。 - C の文字列は NUL バイトで終わります。C にテキストを渡すときは
CString、C からテキストを読むときはCStrを使います。C がポインタを使っている間は、CStringを生かしておきましょう。 - Rust はフィールドを並べ替えることがあるので、C と共有する構造体には
#[repr(C)]を付けます。 extern "C" fnと#[no_mangle](edition 2024 では#[unsafe(no_mangle)])で、C から Rust を呼び出せます。そのためのライブラリはcdylibでビルドします。build.rsとccで自分の C コードをコンパイルし、bindgenで宣言を生成します。PyO3 や napi-rs を使えば、Rust を Python や Node.js とつなげられます。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. extern "C" ブロックにある関数の呼び出しが、すべて unsafe なのはなぜですか?
2. CString::new("hel\0lo") はどうなりますか?
3. C と共有する構造体に #[repr(C)] が必要なのはなぜですか?
4. edition 2024 のクレートで、関数のそのままの名前を C のために残す属性はどう書きますか?
スコア: 0 / 4
さらに詳しく
Section titled “さらに詳しく”- FFI — The Rustonomicon(英語):公式の詳しい解説です。コールバック、リンク、ヌルになりうるポインタなどを扱っています。
std::ffi— 標準ライブラリのドキュメント(英語):CString、CStr、c_int、c_charなどを、例とともに説明しています。- External blocks — The Rust Reference(英語):
externブロック、ABI、safe/unsafeなアイテムの正確なルール。 - The bindgen User Guide(英語):C のヘッダーから Rust のバインディングを生成する方法。
- The PyO3 User Guide(英語):Rust で Python の拡張モジュールを書く方法。