📚 背景知識(読んでから問題へ)
Day 052 の結びで「# Safety で契約を明文化し、安全なラッパーで隠蔽する」パターンが FFI にそのまま使われると予告しました。今日はそれを実際にやります。
FFI(Foreign Function Interface)は「別の言語でコンパイルされた関数を呼ぶ仕組み」です。Rust から C を呼ぶとき、コンパイラが確認できるのは Rust 側に書いた宣言の型だけ です。C 側の実体(.so / .dylib / libc の中身)が本当にその型なのかは、リンク時にも実行時にも誰も確かめません。だから FFI の宣言は丸ごと unsafe 扱いになります。
| 部品 | 役割 | 何を保証する責任が誰にあるか |
|---|---|---|
unsafe extern "C" { ... } | C の関数シグネチャを Rust に教える | 宣言を書いた人が「C 側の実体とシグネチャが一致している」ことを保証(Rust 2024 から unsafe が必須) |
#[repr(C)] | 構造体のフィールド配置を C と同じ規則にする | Rust の既定(repr(Rust))はフィールドを並べ替えてよいので、C とやり取りする型には必須 |
CStr / CString / c"..." | NUL 終端の C 文字列を型で表す | &str は NUL 終端ではない。&CStr なら「NUL 終端・途中に NUL なし」を型が保証 |
extern "C" fn(コールバック) | C から呼ばれる Rust 関数 | panic を C 側へ巻き戻してはいけない(Rust 1.81 以降、extern "C" 関数から panic が漏れるとプロセスが abort) |
Rust 2024 で加わった重要な変更がもう1つあります。unsafe extern ブロックの中の各関数に safe fn と書けるようになりました。「引数がどんな値でもメモリ安全性を壊さない C 関数」は safe fn と宣言すれば、呼び出し側で unsafe {} が不要になります。
safe と書くこと自体が「この関数はどんな引数でも UB にならない」という宣言者の約束です。コンパイラは C 側の中身を確認しません。嘘を書けばそれが UB の原因になります。strlen・div・qsort だけを使います。Rust の標準ライブラリはもともとこれらをリンクしているため、build.rs も外部クレートも C コンパイラも不要 で、cargo test だけで本物の FFI 呼び出しを検証できます。📝 問題
cargo new ffi_day053(edition 2024)のプロジェクトで、src/main.rs に以下を実装してください。外部クレートは使いません。
- 宣言:
unsafe extern "C"ブロックで次の3関数を宣言してください。divはsafe fnとして宣言し、strlenとqsortは通常の(unsafe な)宣言にします。C の宣言( <string.h>/<stdlib.h>)補足 size_t strlen(const char *s);sは NUL 終端必須div_t div(int numer, int denom);div_tは{ int quot; int rem; }。denom == 0とINT_MIN / -1は C 標準で未定義動作void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));comparは負・0・正を返す比較関数 - 型:
div_tに対応する#[repr(C)] struct DivTを定義し、size_ofとstd::mem::offset_of!でレイアウトがint2つ分であることをテストしてください。 - 安全なラッパー: 次の3つを、外から呼ぶ側が絶対に UB を起こせないシグネチャで実装してください。
fn c_strlen(s: &CStr) -> usizefn c_div(a: i32, b: i32) -> Option<(i32, i32)>— C で未定義動作になる入力にはNonefn c_sort(v: &mut [i32])—qsortと、自分で書いたextern "C"の比較関数を使う
- テスト: 最低限、次を
cargo testで検証してください。c_strlenが「文字数」ではなく「バイト数」を返すこと("日本"→ 6)c_divの結果が Rust の/と%に一致すること(負数を含む)、(1, 0)と(i32::MIN, -1)がNonec_sortがi32::MIN/i32::MAXを含む配列・空配列・要素1つの配列を正しく扱うこと
- 記述: 次の2つに1〜2文ずつ答えてください。
- (a)
strlen("hello".as_ptr() as *const c_char)はコンパイルが通る。なぜ危険か - (b) C でよく見る比較関数
return *a - *b;を Rust のextern "C" fnにそのまま移すと何が起きるか(debug ビルドと release ビルドで答えが違う)
- (a)
🔍 ヒント(段階的開示)
ヒント1 — 方向性
ラッパーの設計は「C の関数が要求する前提条件を、Rust の型か if 文のどちらかで先に満たす」ことに尽きます。strlen の前提(NUL 終端)は型で、div の前提(0 除算・オーバーフローなし)は if で、qsort の前提(有効な領域・正しい要素サイズ・排他アクセス)は &mut [i32] という型と size_of::<i32>() で満たせます。
ヒント2 — アプローチ
- C の型は
std::ffi::{c_char, c_int, c_void}にあります。size_tは Rust ではusizeを使うのが慣例です - 関数ポインタ
int (*)(const void*, const void*)はOption<unsafe extern "C" fn(*const c_void, *const c_void) -> c_int>と書きます。Optionで包むのは C の関数ポインタが NULL になりうることを型で表すため(Noneが NULL と同じビット表現になることが保証されています) - 文字列リテラルから
&CStrを作るには Rust 1.77 以降のc"hello"リテラルが使えます - 空スライスの
as_mut_ptr()は「どこも指さない(dangling だが非 null の)ポインタ」です。C に渡す前に長さ 0・1 は早期リターンすると安全側に倒せます
ヒント3 — コード骨格
use std::cmp::Ordering;
use std::ffi::{CStr, c_char, c_int, c_void};
#[repr(C)]
struct DivT { quot: c_int, rem: c_int }
unsafe extern "C" {
fn strlen(s: *const c_char) -> usize;
safe fn div(numer: c_int, denom: c_int) -> DivT;
fn qsort(base: *mut c_void, nmemb: usize, size: usize,
compar: Option<unsafe extern "C" fn(*const c_void, *const c_void) -> c_int>);
}
fn c_strlen(s: &CStr) -> usize {
// SAFETY: ???
unsafe { strlen(s.as_ptr()) }
}
fn c_div(a: i32, b: i32) -> Option<(i32, i32)> {
if /* C で UB になる2条件 */ { return None; }
let r = div(a, b);
Some((r.quot, r.rem))
}
unsafe extern "C" fn cmp_i32(a: *const c_void, b: *const c_void) -> c_int {
let (a, b) = unsafe { (*(a as *const i32), *(b as *const i32)) };
match a.cmp(&b) { /* -1 / 0 / 1 */ }
}
fn c_sort(v: &mut [i32]) {
if v.len() < 2 { return; }
// SAFETY: ???
unsafe { qsort(/* ??? */) }
}
✅ 模範解答
// src/main.rs — Day 053: FFI(C言語との相互運用)
// cargo run / cargo test でそのまま実行できます(edition = "2024"、追加クレート不要)。
// C 側の実体は OS の C 標準ライブラリ(macOS: libSystem / Linux: glibc・musl)で、
// Rust の std が既にリンクしているため build.rs も -l 指定も要りません。
use std::cmp::Ordering;
use std::ffi::{CStr, c_char, c_int, c_void};
/// C の `div_t`(<stdlib.h>)と同じメモリ配置を持つ構造体。
/// C 標準は「quot と rem を持つ」とだけ定め、並び順は規定していない。
/// macOS / glibc / musl の実装はいずれも { int quot; int rem; } の順。
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct DivT {
quot: c_int,
rem: c_int,
}
// Rust 2024 では extern ブロック自体に `unsafe` が必須。
// 「この宣言(シグネチャ)が C 側の実体と一致している」ことを書き手が保証する、という意味。
unsafe extern "C" {
/// # Safety
/// `s` は NUL 終端された有効な C 文字列を指していなければならない。
fn strlen(s: *const c_char) -> usize;
/// C99 7.20.6.2: 結果が表現できない場合(b == 0、INT_MIN / -1)の動作は未定義。
/// その前提条件さえ満たせばメモリには触れないので、`safe fn` として宣言できる。
safe fn div(numer: c_int, denom: c_int) -> DivT;
/// # Safety
/// `base` は `nmemb * size` バイトの有効な書き込み可能領域、
/// `compar` はその要素2つへのポインタを受け取り、全順序に従う値を返す関数。
fn qsort(
base: *mut c_void,
nmemb: usize,
size: usize,
compar: Option<unsafe extern "C" fn(*const c_void, *const c_void) -> c_int>,
);
}
/// `strlen` の安全なラッパー。`&CStr` という型が「NUL 終端済み・有効」を保証する。
fn c_strlen(s: &CStr) -> usize {
// SAFETY: `&CStr` は NUL 終端された有効なバイト列への参照であることを型が保証し、
// この呼び出しの間 `s` は借用されていて解放されない。
unsafe { strlen(s.as_ptr()) }
}
/// `div` の安全なラッパー。C で未定義動作になる入力を Rust 側で先に弾く。
fn c_div(a: i32, b: i32) -> Option<(i32, i32)> {
if b == 0 || (a == i32::MIN && b == -1) {
return None;
}
// `safe fn` として宣言したので unsafe ブロックは不要。
// ただし「上の if で C の前提条件を満たしている」ことが safe 宣言の正しさの根拠。
let r = div(a, b);
Some((r.quot, r.rem))
}
/// qsort に渡す比較関数。C から呼ばれるので `extern "C"` の ABI で定義する。
/// `a - b` ではなく `cmp` を使う(差分はオーバーフローして順序が壊れる)。
unsafe extern "C" fn cmp_i32(a: *const c_void, b: *const c_void) -> c_int {
// SAFETY: qsort は、c_sort が渡した base 配列内の要素(i32 として整列済み)
// へのポインタだけをこの関数に渡す。
let (a, b) = unsafe { (*(a as *const i32), *(b as *const i32)) };
match a.cmp(&b) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
}
}
/// `qsort` の安全なラッパー。`&mut [i32]` が「有効・排他・長さ既知」を保証する。
fn c_sort(v: &mut [i32]) {
if v.len() < 2 {
return; // 空スライスの as_mut_ptr() は dangling。C 側に渡さない
}
// SAFETY: base は v の先頭、nmemb*size は v のバイト長と一致し、
// &mut 借用により qsort 実行中に他から触られない。cmp_i32 は全順序を返し panic しない。
unsafe {
qsort(
v.as_mut_ptr().cast::<c_void>(),
v.len(),
size_of::<i32>(),
Some(cmp_i32),
);
}
}
fn main() {
println!("c_strlen(c\"hello\") = {}", c_strlen(c"hello"));
println!("c_div(7, -2) = {:?}", c_div(7, -2));
println!("c_div(1, 0) = {:?}", c_div(1, 0));
let mut v = vec![5, -3, i32::MAX, 0, i32::MIN, 42];
c_sort(&mut v);
println!("c_sort = {v:?}");
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
#[test]
fn strlen_counts_bytes_not_chars() {
assert_eq!(c_strlen(c""), 0);
assert_eq!(c_strlen(c"hello"), 5);
// 「日本」は UTF-8 で 6 バイト。strlen はバイト数を返す
let s = CString::new("日本").unwrap();
assert_eq!(c_strlen(&s), 6);
}
#[test]
fn cstring_rejects_interior_nul() {
// 途中に NUL があると C 側で文字列が切れるので、構築時点で Err
assert!(CString::new("ab\0cd").is_err());
}
#[test]
fn div_truncates_toward_zero_like_rust() {
assert_eq!(c_div(7, 2), Some((3, 1)));
assert_eq!(c_div(-7, 2), Some((-3, -1)));
assert_eq!(c_div(7, -2), Some((-3, 1)));
// Rust の / と % と一致することを確認
for (a, b) in [(7, 2), (-7, 2), (7, -2), (-7, -2), (i32::MIN, 3)] {
assert_eq!(c_div(a, b), Some((a / b, a % b)));
}
}
#[test]
fn div_rejects_inputs_that_are_ub_in_c() {
assert_eq!(c_div(1, 0), None);
assert_eq!(c_div(i32::MIN, -1), None);
}
#[test]
fn repr_c_layout_matches_two_ints() {
assert_eq!(size_of::<DivT>(), 2 * size_of::<c_int>());
assert_eq!(std::mem::offset_of!(DivT, quot), 0);
assert_eq!(std::mem::offset_of!(DivT, rem), size_of::<c_int>());
}
#[test]
fn sort_handles_extremes_empty_and_one() {
let mut v = vec![5, -3, i32::MAX, 0, i32::MIN, 42];
c_sort(&mut v);
assert_eq!(v, vec![i32::MIN, -3, 0, 5, 42, i32::MAX]);
let mut empty: Vec<i32> = vec![];
c_sort(&mut empty);
assert!(empty.is_empty());
let mut one = vec![1];
c_sort(&mut one);
assert_eq!(one, vec![1]);
}
#[test]
fn sort_matches_std_sort_on_pseudo_random_input() {
// 外部クレートなしの簡易 LCG(Numerical Recipes の定数)
let mut x: u32 = 12345;
let mut v: Vec<i32> = (0..10_000)
.map(|_| {
x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
x as i32
})
.collect();
let mut expected = v.clone();
expected.sort_unstable();
c_sort(&mut v);
assert_eq!(v, expected);
}
}
実測(出題者の環境: rustc 1.96.0 / edition 2024 / Apple M1 macOS、2026-09-23)
$ cargo run
c_strlen(c"hello") = 5
c_div(7, -2) = Some((-3, 1))
c_div(1, 0) = None
c_sort = [-2147483648, -3, 0, 5, 42, 2147483647]
$ cargo test
running 7 tests
.......
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
$ cargo clippy # 警告 0 件
(5)(a) の回答
&str は NUL 終端ではない(長さを別に持つ「太いポインタ」)ので、strlen は "hello" の 5 バイトの後ろのメモリを NUL が見つかるまで読み続けます。範囲外読み取りという UB で、型エラーにならないのは as キャストが「*const u8 → *const i8」の変換だけを見ていて、NUL 終端という意味は検査しないからです。&CStr を引数に取るラッパーにすれば、この誤用はコンパイル時に不可能になります。
(5)(b) の回答
i32::MIN - 1 のような差がオーバーフローします。debug ビルドではオーバーフロー検査で panic し、それが extern "C" 関数から外へ出ようとした瞬間に プロセスが abort(終了コード 134)します。release ビルドではオーバーフロー検査が無いので値が回り込み、何のエラーも出ずに誤った順序(i32::MIN が末尾)で「ソート完了」します。どちらも実測しました(コンパイルエラーが出た場合参照)。
🪜 Step-by-Step 解説
unsafe extern "C" ブロックで「C 側との約束」を書くextern "C" は「C の呼び出し規約(ABI)で呼ぶ」という指定です。ブロック先頭の unsafe は Rust 2024 で必須になりました。以前は書かなくてよかったため「宣言そのものが嘘だったら UB」という責任の所在がコード上に見えませんでした。中の各関数は、何も書かなければ unsafe fn 扱いです。div だけ safe fn にしたのはメモリに触れない純粋な整数演算だからですが、C 標準は b == 0 と INT_MIN / -1 を UB と定めているので、厳密には「どんな引数でも安全」ではありません。今日は呼び出し経路を c_div の1本に絞り、そこで弾いています(是非は設計思想で扱います)。
#[repr(C)] で構造体の配置を固定するRust の既定レイアウトはフィールドを並べ替えてよいことになっています。C から値として返ってくる
div_t を正しく受け取るには #[repr(C)] で「宣言順に並べ、C と同じ整列規則を使う」ことを指示します。offset_of! のテストがあれば、将来だれかがフィールドを並べ替えたときにテストが止めてくれます。ただし C 標準自体は div_t の並び順を規定していません。今日のコードは macOS / glibc / musl で確認済みの { quot, rem } 順が前提で、移植性が要るなら libc クレートの定義を使うのが実務の答えです。
c_strlen・c_sort)
| C 側の前提条件 | Rust 側でどう満たしたか |
|---|---|
strlen: s が NUL 終端 | 引数を &CStr にする。構築時に検査済み |
qsort: base が nmemb * size バイトの有効領域 | &mut [i32] から as_mut_ptr()・len()・size_of::<i32>()。同じスライス由来なので食い違わない |
qsort: 実行中に他から書き換えられない | &mut 借用が排他性を保証 |
qsort: 比較関数が全順序を返す | a.cmp(&b) を使い、差分計算をしない |
// SAFETY: コメントは Day 051/052 と同じく、この表の右列を1行ずつ書いたものになっています。
c_div)c_div の前提条件は i32 の型では表せないので、if で弾いて Option で返します。i32::MIN / -1 は数学的には 2147483648 ですが i32 に収まらず、C では UB、Rust の / では panic です。テストで「C の div も Rust の整数除算も 0 方向への切り捨て」(C99 以降の共通仕様)を確かめています。
cmp_i32 は C の qsort から呼ばれます。Rust の panic は「スタックを巻き戻す(unwind)」仕組みですが、C のスタックフレームを Rust の流儀で巻き戻すことはできません。Rust 1.81 以降、extern "C" 関数から panic が外に出ようとするとその場でプロセスを abort する挙動に統一されました(それ以前は UB)。今日の match a.cmp(&b) はオーバーフローも panic も起こしません。
💡 設計思想・なぜこう書くのか
FFI の境界は「型で守れる約束」と「人間が守る約束」の境界線です。 Rust の関数同士なら、&[i32] を渡せばコンパイラが寿命・長さ・排他性をすべて追跡します。C に渡した瞬間、それは *mut c_void と usize にばらされ、コンパイラは何も追跡できなくなります。Rust の流儀は、ばらす処理を安全なラッパーの中の1箇所に閉じ込め、外側には &CStr や &mut [i32] のような「約束を型に焼き込んだ」引数だけを見せることです。
判断が分かれる点: div を safe fn にするか
| 選択肢 | 利点 | 欠点 | 今日の判断 |
|---|---|---|---|
A. safe fn div + c_div で入力検査 | 呼び出し側に unsafe が1つ減り、safe の意味(メモリに触れない)を学べる | 厳密には「任意の引数で UB にならない」を満たさない。c_div を経由せず div(1, 0) と直接書けてしまう | 採用(学習用・非公開で呼び出し経路が1本) |
B. unsafe fn div + c_div で入力検査 | 契約違反の可能性が unsafe {} として呼び出し側に見える。Reference の safe の定義に厳密に従う | 呼び出し箇所に unsafe が増える | 公開 API・複数人が触るコードならこちら |
C. C を呼ばず Rust の / と % | FFI 自体が不要。checked_div で表現できる | 今日の学習目的に合わない | 実務で div だけ必要ならこれが正解 |
前提が変わると判断も変わります。モジュール外に公開する、または複数人が触るなら A → B に切り替えます。C でしか実装が無い機能(SQLite、zlib、OpenSSL、OS の API)を呼ぶ場合にだけ FFI を選び、Rust に同等品があるものは C を呼ばない、というのが実務の基本線です。
「C を呼ぶコスト」の実測(乱数列 100 万個の i32 を並べ替え)
| 方法 | 5 回の実測(ms) | 中央値 |
|---|---|---|
C の qsort(今日の c_sort) | 122.6 / 90.8 / 90.5 / 134.9 / 92.2 | 92.2 ms |
Rust sort_unstable | 11.9 / 11.6 / 11.5 / 13.3 / 11.9 | 11.9 ms |
Rust sort(安定ソート) | 15.3 / 15.4 / 14.8 / 15.4 / 14.8 | 15.3 ms |
--release、Apple M1、macOS。examples/bench.rs で Instant 計測、各回で結果の一致を assert_eq! で確認。導出(未検証): 約 7.7 倍の差の主因は、
qsort が比較のたびに関数ポインタ経由の間接呼び出しをし、void* 越しで型も分からないためインライン化もベクトル化もできない点。プロファイラでの内訳確認はしていません。Rust のソートはジェネリクスの単相化で比較が本体に埋め込まれます。🛑 コンパイルエラーが出た場合
以下はすべて、出題者の環境(rustc 1.96.0、edition 2024、aarch64 macOS)で実際に出た出力です。
エラー1: unsafe を付けずに extern "C" ブロックを書いた
Rust 2021 までのコードや古い記事をそのまま写すと出ます。unsafe extern "C" { に直します。edition 2021 のプロジェクトでは出ません(cargo new の既定は 2024)。
エラー2: &str のポインタをそのまま渡した
as *const c_char を足してエラーを消すのが最悪の修正です。型は合っても NUL 終端ではないので、問題 (5)(a) の範囲外読み取りになります。正しい修正は c"hello" か CString::new(...) です。なお c_char は aarch64 macOS・x86_64 では i8、aarch64 Linux では u8 なので、aarch64 Linux ではこのエラー自体が出ない(誤りに気付く機会すらない)点に注意。実行時: 比較関数 a - b がオーバーフローした(コンパイルは通る)
debug ビルド(cargo run):
release ビルド(cargo run --release):
panic in a function that cannot unwind は「extern "C" 関数から panic が漏れようとしたので abort した」という Rust 1.81 以降の安全装置です。release では検査が無いのでエラーすら出ず誤った結果が返ります。テストを debug でしか回していないと、本番(release)でだけ起きる不具合になります。🌐 他言語との比較
| 観点 | C++ | Go(cgo) | Java(FFM API) | Python(ctypes) | Rust |
|---|---|---|---|---|---|
| C 関数の宣言方法 | extern "C" で C ヘッダをそのまま include | import "C" の上のコメントに C コードを書く | Linker と FunctionDescriptor で実行時に組み立て(Java 22 で正式化、JEP 454) | CDLL で読み込み、argtypes / restype を実行時に設定 | unsafe extern "C" で型付き宣言。大きなヘッダは bindgen で自動生成 |
| 宣言が間違っていたとき | コンパイラが C ヘッダと突き合わせるので多くは型エラー | cgo が C ヘッダを読むので多くは検出 | 実行時まで分からない | 実行時まで分からない | 検出されない(だから unsafe)。ヘッダ由来なら bindgen 生成で防ぐ |
| 呼び出しのコスト | 通常の関数呼び出しと同じ | goroutine のスタックから OS スレッドのスタックへの切り替えが入り、Go 同士より明確に重い | JNI より軽いが JIT 境界を越える | インタプリタからの変換が入り重い | 通常の関数呼び出しと同じ。ただし今日の実測どおりインライン化は効かない |
| コールバックで例外/panic | C++ 例外が C を越えるのは未定義 | Go の panic が C を越えるのは不可 | 例外は境界で扱いが規定される | 例外は ctypes が握りつぶして警告 | extern "C" から漏れると abort(1.81〜)。越えさせたいなら extern "C-unwind" |
| 安全なラッパーを書く文化 | ライブラリ次第 | ライブラリ次第 | ライブラリ次第 | ライブラリ次第 | xxx-sys(生の宣言)と xxx(安全なラッパー)にクレートを分けるのが慣例 |
🏆 実務での使いどころ
-sysクレートと安全なラッパーの2層構造:libsqlite3-sys/rusqlite、openssl-sys/openssl、libgit2-sys/git2のように、生のextern宣言だけを持つ-sysクレートと、今日のc_strlenのような安全なラッパーを持つクレートを分けるのが Rust エコシステムの標準形。今日の解答はこの2層を1ファイルに縮めたもの- OS API の呼び出し:
std自体が内部で libc の関数(read・write・mmap等)を FFI で呼んでいる。stdに無い OS 機能を使うときはlibcクレートの宣言を使って今日と同じ形のラッパーを書く - 既存の C/C++ 資産を段階的に Rust へ移す: Firefox(Stylo 等)や Android では、C/C++ の既存コードを残したまま新しい部品だけを Rust で書き、FFI でつないでいる。境界の安全なラッパーの品質が移行全体の安全性を決める
- ホットパスでは FFI を避ける判断: 今日の実測のように、C の汎用関数(
qsort・bsearch)はコールバック経由で遅くなりがち。Rust に同等品がある処理は Rust で書き、FFI は「C にしか無い機能」に限定する
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
&str を .as_ptr() as *const c_char で C に渡す | 型エラーを as で消せてしまう | &str は NUL 終端ではない。CStr / CString / c"..." を使う。型エラーは「消す」ものではなく「意味を考える」合図 |
#[repr(C)] を付け忘れる | 手元で動いてしまうことがある(たまたま並び順が同じ) | Rust は既定レイアウトを保証しない。C とやり取りする構造体には必ず付け、offset_of! でテストする |
比較関数を *a - *b で書く | C の教科書やネットの例に多い | 極端な値でオーバーフローする。debug では abort、release では黙って誤った順序。cmp で -1/0/1 を返す |
safe fn は「Rust が安全性を確かめてくれた」印だと思う | safe という語感 | safe は宣言者の約束。コンパイラは C 側の中身を確認していない。嘘の safe は UB の原因になる |
CString::new(s).unwrap().as_ptr() を1行で書いて C に渡す | メソッドチェーンの見た目がきれい | CString がその文の終わりで破棄され、C には解放済みメモリへのポインタが渡る。let cs = CString::new(s)?; と束縛してから cs.as_ptr() |
コールバック内で unwrap() や添字アクセスを使う | 普段の Rust の書き方をそのまま持ち込む | どれも panic しうる。extern "C" 関数内の panic は abort。必要なら std::panic::catch_unwind で受け止めてエラーコードに変換する |
📖 一次資料
参照日: 2026-09-23。記述は各資料の要旨で、原文の引用ではありません。
- The Rust Reference — External blocks(
unsafe extern・safe/unsafe修飾子の定義): doc.rust-lang.org/reference/items/external-blocks.html - The Rust Edition Guide — Unsafe
externblocks(Rust 2024 での必須化): doc.rust-lang.org/edition-guide/rust-2024/unsafe-extern.html - Rust 1.81.0 リリースノート(
extern "C"関数から漏れた panic を abort に統一): blog.rust-lang.org/2024/09/05/Rust-1.81.0/ - The Rustonomicon — FFI(
repr(C)・NULL 関数ポインタとOption・コールバック): doc.rust-lang.org/nomicon/ffi.html - ISO/IEC 9899:1999(C99)7.20.6.2
div、7.20.5.2qsort(WG14 N1256 草案): open-std.org/.../n1256.pdf - JEP 454: Foreign Function & Memory API(Java 22 で正式化): openjdk.org/jeps/454
- cgo のドキュメント: pkg.go.dev/cmd/cgo
🧭 今日扱わなかった範囲(次回以降)
同じテーマを3問続けるルールに従い、FFI は残り2回で次を扱う予定です。今日は「Rust → C を呼ぶ」「データは借用で渡すだけ」に絞りました。
- 所有権が境界を越えるケース:
CString::into_raw/from_raw、C が確保したメモリを誰が解放するか(Dropで C のfreeを呼ぶラッパー) - C から Rust を呼ぶ方向:
#[unsafe(no_mangle)]・cdylib・cbindgenによるヘッダ生成 - ユーザーデータ付きコールバック:
void *userdata経由でクロージャを渡すパターン(qsort_rの引数順が macOS と glibc で違う問題を含む) - ツール:
bindgen(ヘッダからの自動生成)とccクレート(自作 C のビルド)。今日は外部クレートなし・オフラインで再現できることを優先して割愛 extern "C-unwind": panic を意図的に C 境界の向こうへ伝える ABI
🚀 次のステップ
- 発展:
cargo run --releaseとcargo runの両方で、問題 (5)(b) のa - b比較関数を実際に動かし、終了コード(echo $?)の違いを確かめる。そのうえでstd::panic::catch_unwindでコールバック内の panic を受け止め、0 を返して abort を防ぐ版を書いてみる(ただし「順序が壊れたまま完了する」ことに注意) - 次回予告: FFI ② 所有権が境界を越えるとき — C の
strdupが返したポインタをDropでfreeするラッパー型を作り、「誰が解放するか」を型で表現する