コンテンツにスキップ

RustのFFI(Cの呼び出し)

FFI は Foreign Function Interface(他言語関数インターフェース)の略です。Rust がほかの言語で書かれたコードとやりとりするための仕組みです。その「ほかの言語」は、ほとんどの場合 C か、C のルールに従う何かです。C は、ほぼすべての言語やOSが理解できる「共通語」だからです。

母語が違う2人が、どちらも少しだけ英語を話せる場面を想像してみてください。C は、その英語のような共通語です。FFI を使えば、実績のある C のライブラリを Rust から使ったり、Python や Node.js、C のプログラムから呼び出せる高速な Rust のライブラリを書いたりできます。このページは unsafe のページの続きです。unsafe ブロックをまだ知らない人は、先にそちらを読んでください。

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つあり、どちらも大切です。

  1. strlen("héllo") は5ではなく6です。C はバイト数を数えます。そして é は UTF-8 では2バイトです(日本語の文字なら1文字3バイトです)。
  2. テキストに 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")
None
Some(".org")
▶ 実際に試してみよう

to_string_lossy() は、C のバイト列を Rust のテキストに変換します。C の文字列は UTF-8 であることを約束していないので、不正な UTF-8 は � に置き換えられます。エラーとして受け取りたいなら、代わりに .to_str() を使いましょう。

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 余り 2
▶ 実際に試してみよう

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;
}
Terminal window
$ cc main.c -L target/release -lmathlib -o main
$ LD_LIBRARY_PATH=target/release ./main
2 + 3 = 5

この C のヘッダーを手で書くのは、すぐに面倒になります。cbindgen(英語)を使えば、Rust のコードからヘッダーを生成できます。

build.rs と cc で自分の C コードをコンパイルする

Section titled “build.rs と cc で自分の C コードをコンパイルする”

ここまでは、最初からリンクされている関数だけを使ってきました。自分の C ファイルを使うには、Cargo にコンパイルさせます。プロジェクトのルートにある build.rs というファイルはビルドスクリプトです。Cargo は、クレートをコンパイルする前にこれを実行します。cc(英語)クレートを使えば、数行で済みます。

Terminal window
cargo new hello-c
cd hello-c
cargo add --build cc
src/greet.c
#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);
}
build.rs
fn main() {
// src/greet.c を静的ライブラリにコンパイルして、リンクする。
cc::Build::new().file("src/greet.c").compile("greet");
// C のファイルが変わったときだけ、このスクリプトを再実行する。
println!("cargo::rerun-if-changed=src/greet.c");
}
src/main.rs
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()) };
}
Terminal window
$ cargo run -q
2 + 3 = 5
Cからこんにちは、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を見てください。

外部の関数を 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 とつなげられます。

理解度チェック

1. extern "C" ブロックにある関数の呼び出しが、すべて unsafe なのはなぜですか?

2. CString::new("hel\0lo") はどうなりますか?

3. C と共有する構造体に #[repr(C)] が必要なのはなぜですか?

4. edition 2024 のクレートで、関数のそのままの名前を C のために残す属性はどう書きますか?

スコア: 0 / 4