Day 053 — C標準ライブラリ(strlen・div・qsort)を、呼ぶ側が絶対にUBを起こせない安全なRust APIで包む

2026-09-23 🟣 上級者 / Phase 3 実装 FFI(C言語との相互運用)

📚 背景知識(読んでから問題へ)

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 の原因になります。
📌
今日の C 関数は OS の C 標準ライブラリ(macOS: libSystem、Linux: glibc / musl)にある strlen・div・qsort だけを使います。Rust の標準ライブラリはもともとこれらをリンクしているため、build.rs も外部クレートも C コンパイラも不要 で、cargo test だけで本物の FFI 呼び出しを検証できます。

📝 問題

cargo new ffi_day053(edition 2024)のプロジェクトで、src/main.rs に以下を実装してください。外部クレートは使いません。

  1. 宣言: 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・正を返す比較関数
  2. 型: div_t に対応する #[repr(C)] struct DivT を定義し、size_of と std::mem::offset_of! でレイアウトが int 2つ分であることをテストしてください。
  3. 安全なラッパー: 次の3つを、外から呼ぶ側が絶対に UB を起こせないシグネチャで実装してください。
    • fn c_strlen(s: &CStr) -> usize
    • fn c_div(a: i32, b: i32) -> Option<(i32, i32)> — C で未定義動作になる入力には None
    • fn c_sort(v: &mut [i32]) — qsort と、自分で書いた extern "C" の比較関数を使う
  4. テスト: 最低限、次を cargo test で検証してください。
    • c_strlen が「文字数」ではなく「バイト数」を返すこと("日本" → 6)
    • c_div の結果が Rust の / と % に一致すること(負数を含む)、(1, 0) と (i32::MIN, -1) が None
    • c_sort が i32::MIN / i32::MAX を含む配列・空配列・要素1つの配列を正しく扱うこと
  5. 記述: 次の2つに1〜2文ずつ答えてください。
    • (a) strlen("hello".as_ptr() as *const c_char) はコンパイルが通る。なぜ危険か
    • (b) C でよく見る比較関数 return *a - *b; を Rust の extern "C" fn にそのまま移すと何が起きるか(debug ビルドと release ビルドで答えが違う)

🔍 ヒント(段階的開示)

ヒント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 解説

1
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本に絞り、そこで弾いています(是非は設計思想で扱います)。
2
#[repr(C)] で構造体の配置を固定する
Rust の既定レイアウトはフィールドを並べ替えてよいことになっています。C から値として返ってくる div_t を正しく受け取るには #[repr(C)] で「宣言順に並べ、C と同じ整列規則を使う」ことを指示します。offset_of! のテストがあれば、将来だれかがフィールドを並べ替えたときにテストが止めてくれます。ただし C 標準自体は div_t の並び順を規定していません。今日のコードは macOS / glibc / musl で確認済みの { quot, rem } 順が前提で、移植性が要るなら libc クレートの定義を使うのが実務の答えです。
3
前提条件を「型」で満たす(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行ずつ書いたものになっています。
4
前提条件を「if」で満たす(c_div)
c_div の前提条件は i32 の型では表せないので、if で弾いて Option で返します。i32::MIN / -1 は数学的には 2147483648 ですが i32 に収まらず、C では UB、Rust の / では panic です。テストで「C の div も Rust の整数除算も 0 方向への切り捨て」(C99 以降の共通仕様)を確かめています。
5
コールバックは panic させない
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.292.2 ms
Rust sort_unstable11.9 / 11.6 / 11.5 / 13.3 / 11.911.9 ms
Rust sort(安定ソート)15.3 / 15.4 / 14.8 / 15.4 / 14.815.3 ms
📏
実測: rustc 1.96.0 --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" ブロックを書いた

error: extern blocks must be unsafe --> src/main.rs:2:1 | 2 | / extern "C" { 3 | | fn strlen(s: *const c_char) -> usize; 4 | | } | |_^

Rust 2021 までのコードや古い記事をそのまま写すと出ます。unsafe extern "C" { に直します。edition 2021 のプロジェクトでは出ません(cargo new の既定は 2024)。

エラー2: &str のポインタをそのまま渡した

error[E0308]: mismatched types --> src/main.rs:6:29 | 6 | let n = unsafe { strlen("hello".as_ptr()) }; | ------ ^^^^^^^^^^^^^^^^ expected `*const i8`, found `*const u8`
🚫
ここで 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):

thread 'main' panicked at src/main.rs:8:5: attempt to subtract with overflow thread 'main' panicked at .../library/core/src/panicking.rs:225:5: panic in a function that cannot unwind (バックトレースの後、終了コード 134 = SIGABRT)

release ビルド(cargo run --release):

[-3, 0, 5, 42, 2147483647, -2147483648] ← エラーなし・i32::MIN が末尾
⚠️
読み方: 1つ目の panic が本当の原因(引き算のオーバーフロー)、2つ目の 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 ヘッダをそのまま includeimport "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 境界を越えるインタプリタからの変換が入り重い通常の関数呼び出しと同じ。ただし今日の実測どおりインライン化は効かない
コールバックで例外/panicC++ 例外が C を越えるのは未定義Go の panic が C を越えるのは不可例外は境界で扱いが規定される例外は ctypes が握りつぶして警告extern "C" から漏れると abort(1.81〜)。越えさせたいなら extern "C-unwind"
安全なラッパーを書く文化ライブラリ次第ライブラリ次第ライブラリ次第ライブラリ次第xxx-sys(生の宣言)と xxx(安全なラッパー)にクレートを分けるのが慣例
🐹
Go 経験者向けの要点: cgo は「C を呼ぶたびにランタイムの都合でスタックを切り替える」ため呼び出しそのものが重く、「細かい C 関数を何度も呼ぶ」設計が不利です。Rust にはランタイムが無いので呼び出し自体は C 同士と同じコストで、代わりに安全性の責任をすべて書き手が負うという形でコストを払います。

🏆 実務での使いどころ

  • -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。記述は各資料の要旨で、原文の引用ではありません。

🧭 今日扱わなかった範囲(次回以降)

同じテーマを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 するラッパー型を作り、「誰が解放するか」を型で表現する

🎯 自己評価

自分の回答

気づき・メモ