Day 055 — C の qsort_r と pthread_create に Rust のクロージャを渡す:userdata・panic・所有権の往復

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

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

Day 053 では Rust から C を呼ぶだけ、Day 054 では C と Rust の間でメモリの所有権を受け渡しました。今日は向きが逆になります。C が Rust の関数を呼び返す(コールバック)ときに、Rust のクロージャをどう渡すかです。

C の関数ポインタは「関数の番地」だけで、クロージャのように変数を捕まえておく場所がありません。そこで C の API は、関数ポインタと一緒に void *(userdata、context、thunk、arg など名前はいろいろ)を受け取り、コールバックを呼ぶときにその void * をそのまま返してくる、という形を取ります。Rust 側はこの void * にクロージャのアドレスを入れ、C から呼ばれる小さな関数(トランポリン)の中で元の型に戻して呼びます。

Rust の呼び出し側 ──(fnptr = trampoline::<F>, userdata = &mut closure)──▶ C の関数
                                                                         │ 何度でも
Rust のクロージャ ◀──(userdata を &mut F に戻して呼ぶ)── trampoline::<F> ◀─┘

このとき決めなければならないことは3つあります。

問い同期コールバック(今日の qsort_r)非同期コールバック(今日の pthread_create)
C はいつまで userdata を持つか関数が戻るまで。戻った後は二度と呼ばない関数が戻った後も。別スレッドで後から呼ぶ
クロージャをどこに置くか呼び出し側のスタックでよい(&mut F を渡す)ヒープに置き、所有権ごと C に預ける(Box::into_raw)。受け取った側が Box::from_raw で回収
クロージャの型に要る条件FnMut だけ。&mut で外の変数を借りてもよいFnOnce + Send + 'static(別スレッドで、いつまで生きるか分からない)

もう1つが panic です。Rust 1.81 から、extern "C" 関数の外へ panic が抜けようとするとプロセスが abort します(それ以前は未定義動作)。C のフレームを Rust の流儀で巻き戻す保証が無いからです。コールバックの中で panic しうるなら、トランポリンで catch_unwind して預かり、C の関数が戻った後に resume_unwind で投げ直す必要があります。

最後に、今日使う qsort_r にはOS によって引数の順番が違うという落とし穴があります(出題者の macOS SDK のヘッダと POSIX で確認)。

実装qsort_r の宣言比較関数の userdata の位置
macOS(BSD 由来。SDK の _stdlib.h 371 行目で確認)qsort_r(base, nel, width, void *thunk, int (*compar)(void *, const void *, const void *))第1引数
glibc / POSIX.1-2024(Issue 8 で追加)qsort_r(base, nmemb, size, int (*compar)(const void *, const void *, void *), void *arg)第3引数
FreeBSD 14 以降POSIX の順番に変更。旧順番も C11 の _Generic で受け付ける第3引数(推奨)

どちらの宣言も「ポインタと整数の並び」なので、取り違えてもコンパイルもリンクも通ります。壊れるのは実行したときです(下の実測で SIGSEGV)。

📝 問題

cargo new ffi_day055(edition 2024)の src/main.rs に、次の2つを実装してください。外部クレートは使いません(qsort_r・pthread_create・pthread_join は OS の C 標準ライブラリにあり、Rust の std が既にリンクしています)。対象は macOS と Linux(glibc)で、それ以外の OS ではコンパイルエラーで構いません。

(1) 同期コールバック: c_sort_by

/// C の qsort_r に Rust のクロージャを比較関数として渡して並べ替える
///
/// # Safety
/// ???((4) の Q1 で書く)
pub unsafe fn c_sort_by<T, F>(v: &mut [T], cmp: F)
where
    F: FnMut(&T, &T) -> Ordering;

要件:

  • a. qsort_r の宣言とトランポリンを #[cfg(target_os = "macos")] と #[cfg(all(target_os = "linux", target_env = "gnu"))] で分ける。macOS で cargo test が通り、cargo check --target aarch64-unknown-linux-gnu(事前に rustup target add aarch64-unknown-linux-gnu)も通ること
  • b. クロージャを C に渡すのにヒープ確保をしない(Box を使わない)。その代わり、クロージャは外の変数を &mut で借りたり、Rc のような Send でない値を捕まえたりしてよい
  • c. cmp が panic してもプロセスを abort させない。qsort_r が戻った後で、同じ panic を c_sort_by の呼び出し元に投げ直す。panic の後は cmp を二度と呼ばない。panic で止まった後も v の要素は失われも重複もしない(並べ替えの途中の順番でよい)
  • d. 要素数が 2 未満、または要素の型のサイズが 0 のときは qsort_r を呼ばない

(2) 非同期コールバック: c_spawn

pub struct CThread<R> { /* ??? */ }

/// Rust のクロージャを、C の pthread_create で作ったスレッドで実行する
pub fn c_spawn<F, R>(f: F) -> std::io::Result<CThread<R>>
where
    F: FnOnce() -> R + Send + 'static,
    R: Send + 'static;

impl<R> CThread<R> {
    /// 終了を待って戻り値を返す。スレッド内で panic したら Err(payload)
    pub fn join(self) -> std::thread::Result<R>;
}
impl<R> Drop for CThread<R> { /* join されずに捨てられたら、ここで join する */ }

要件:

  • クロージャは Box::into_raw で C に預け、スレッド開始関数の中で Box::from_raw で回収する。pthread_create が失敗したら、預けたはずの Box を取り戻して解放し、io::Error を返す
  • 戻り値 R もスレッド開始関数の戻り値(void *)で運ぶ
  • スレッド内の panic は abort させず、join の Err にする
  • 同じスレッドを2回 join しない(POSIX では未定義動作)

(3) テスト

cargo test で次を数えて確かめてください。

  • ランダムな入力で c_sort_by の結果が slice::sort と一致する(長さ 0, 1, 2, 3, 7, 64, 1000)
  • 50 回目の比較で panic させたとき、panic が呼び出し元に届き、比較関数の呼び出し回数がちょうど 50 で止まり、v が元の要素の並べ替えになっている
  • c_sort_by と c_spawn のクロージャ、c_spawn の戻り値が、それぞれちょうど1回 drop される(ヒント: drop 回数を数える型をクロージャに持たせる)
  • スレッド内の panic が join の Err として返る

(4) 記述

  • Q1: なぜ c_sort_by は unsafe fn にしなければならないのか。# Safety に何を書くか。同じ形のクロージャを受け取る std の slice::sort_by はなぜ safe fn でいられるのか、と対比して答えてください
  • Q2: 同僚が「CThread の Drop で必ず join するのだから、スレッドが CThread より長生きすることはない。F: 'static は外して、ローカル変数を借りるクロージャも渡せるようにしよう」と提案しました。この提案が安全でない理由を説明してください

💡 ヒント

ヒント1 — 方向性

同期と非同期で違うのは「C が userdata を持っている期間」だけです。その期間より長く、クロージャが同じ番地に生きていればよい。同期ならスタック、非同期ならヒープです。panic については「C のフレームの上で巻き戻さない」ことだけを守り、情報は userdata に入れて持ち帰ります。

ヒント2 — アプローチ
  • トランポリンは unsafe extern "C" fn trampoline<T, F>(...) のようにジェネリックに書けます。trampoline::<T, F> と型を指定すると、クロージャの型ごとに別の関数ができ、その関数ポインタを C に渡せます(#[no_mangle] は付けない)
  • userdata には F 単体ではなく、struct SortCtx<F> { cmp: F, panic: Option<Box<dyn Any + Send>> } を入れると、panic の中身を持ち帰れます
  • std::panic::catch_unwind はクロージャが UnwindSafe であることを求めます。&mut を捕まえるクロージャは UnwindSafe ではないので、「panic 後にその値を使わない(あるいは使っても壊れない)」と自分で判断したうえで AssertUnwindSafe で包みます
  • Ordering はフィールドを持たない enum で、Less = -1, Equal = 0, Greater = 1 なので ord as c_int で C の比較結果になります
  • pthread_create / pthread_join は失敗を errno ではなく戻り値で返します。io::Error::from_raw_os_error(rc) で包めます
  • pthread_t の型も OS で違います(macOS はポインタ、glibc は unsigned long)
  • Q2 は「mem::forget は safe fn である」がすべての出発点です
ヒント3 — コード骨格
#[cfg(target_os = "macos")]
unsafe extern "C" fn trampoline<T, F>(ctx: *mut c_void, a: *const c_void, b: *const c_void) -> c_int
where F: FnMut(&T, &T) -> Ordering {
    unsafe { compare::<T, F>(ctx, a, b) }        // 本体は OS 共通の関数へ
}
#[cfg(all(target_os = "linux", target_env = "gnu"))]
unsafe extern "C" fn trampoline<T, F>(a: *const c_void, b: *const c_void, ctx: *mut c_void) -> c_int
where F: FnMut(&T, &T) -> Ordering {
    unsafe { compare::<T, F>(ctx, a, b) }
}

unsafe fn compare<T, F: FnMut(&T, &T) -> Ordering>(ctx: *mut c_void, a: *const c_void, b: *const c_void) -> c_int {
    let ctx = unsafe { &mut *ctx.cast::<SortCtx<F>>() };
    if ctx.panic.is_some() { return 0; }
    let (a, b) = unsafe { (&*a.cast::<T>(), &*b.cast::<T>()) };
    match panic::catch_unwind(AssertUnwindSafe(|| (ctx.cmp)(a, b))) {
        Ok(ord) => ord as c_int,
        Err(p) => { ctx.panic = Some(p); 0 }
    }
}

extern "C" fn thread_start<F, R>(arg: *mut c_void) -> *mut c_void
where F: FnOnce() -> R + Send + 'static, R: Send + 'static {
    let f = unsafe { Box::from_raw(arg.cast::<F>()) };
    let result = panic::catch_unwind(AssertUnwindSafe(f));
    Box::into_raw(Box::new(result)).cast()
}

✅ 模範解答

(1)(2)(3) コード

// src/main.rs — Day 055: FFI ③ C から Rust を呼ぶ(userdata 付きコールバック)
// cargo run / cargo test でそのまま実行できます(edition = "2024"、追加クレート不要)。
// qsort_r / pthread_create / pthread_join は OS の C 標準ライブラリ(macOS: libSystem、
// Linux: glibc)にあり、Rust の std が既にリンクしているので build.rs も -l 指定も要りません。

use std::any::Any;
use std::cmp::Ordering;
use std::ffi::{c_int, c_void};
use std::io;
use std::marker::PhantomData;
use std::mem::{ManuallyDrop, MaybeUninit};
use std::panic::{self, AssertUnwindSafe};
use std::ptr;

// ---------------------------------------------------------------------------
// 1. qsort_r: 「呼び出し中だけ」C がクロージャを持つ(同期コールバック)
// ---------------------------------------------------------------------------

// qsort_r は OS ごとに引数の順番が違う。宣言を取り違えても、どちらも
// 「ポインタ3つ」なのでコンパイルもリンクも通ってしまう(→ 実行時に壊れる)。
#[cfg(target_os = "macos")]
unsafe extern "C" {
    // macOS(BSD 由来): void qsort_r(void *base, size_t nel, size_t width, void *thunk,
    //                                 int (*compar)(void *, const void *, const void *));
    fn qsort_r(
        base: *mut c_void,
        nel: usize,
        width: usize,
        thunk: *mut c_void,
        compar: unsafe extern "C" fn(*mut c_void, *const c_void, *const c_void) -> c_int,
    );
}

#[cfg(all(target_os = "linux", target_env = "gnu"))]
unsafe extern "C" {
    // glibc / POSIX.1-2024: void qsort_r(void *base, size_t nmemb, size_t size,
    //                         int (*compar)(const void *, const void *, void *), void *arg);
    fn qsort_r(
        base: *mut c_void,
        nmemb: usize,
        size: usize,
        compar: unsafe extern "C" fn(*const c_void, *const c_void, *mut c_void) -> c_int,
        arg: *mut c_void,
    );
}

/// userdata として C に渡す中身。クロージャと「途中で起きた panic」を一緒に運ぶ
struct SortCtx<F> {
    cmp: F,
    panic: Option<Box<dyn Any + Send + 'static>>,
}

/// 比較の本体(OS 共通)。引数の順番だけを OS 別のトランポリンが並べ替える
///
/// # Safety
/// `ctx` は `c_sort_by` が作った `SortCtx<F>` を指し、`a` と `b` は並べ替え中の
/// 配列内の有効な `T` を指すこと。
unsafe fn compare<T, F>(ctx: *mut c_void, a: *const c_void, b: *const c_void) -> c_int
where
    F: FnMut(&T, &T) -> Ordering,
{
    // SAFETY: 関数の契約により ctx は生きている SortCtx<F>。qsort_r は比較関数を
    // 同時に複数呼ばない(単一スレッドで順に呼ぶ)ので、&mut は他と重ならない。
    let ctx = unsafe { &mut *ctx.cast::<SortCtx<F>>() };
    if ctx.panic.is_some() {
        return 0; // 一度 panic したら以後は比較しない。C 側には「等しい」と答え続ける
    }
    // SAFETY: 関数の契約により a, b は有効な T。
    let (a, b) = unsafe { (&*a.cast::<T>(), &*b.cast::<T>()) };
    match panic::catch_unwind(AssertUnwindSafe(|| (ctx.cmp)(a, b))) {
        Ok(ord) => ord as c_int, // Less = -1, Equal = 0, Greater = 1
        Err(payload) => {
            ctx.panic = Some(payload); // C のフレームを越えて巻き戻さず、預かる
            0
        }
    }
}

#[cfg(target_os = "macos")]
unsafe extern "C" fn trampoline<T, F>(ctx: *mut c_void, a: *const c_void, b: *const c_void) -> c_int
where
    F: FnMut(&T, &T) -> Ordering,
{
    // SAFETY: qsort_r(macOS) は thunk を第1引数で返してくる。契約は compare と同じ。
    unsafe { compare::<T, F>(ctx, a, b) }
}

#[cfg(all(target_os = "linux", target_env = "gnu"))]
unsafe extern "C" fn trampoline<T, F>(a: *const c_void, b: *const c_void, ctx: *mut c_void) -> c_int
where
    F: FnMut(&T, &T) -> Ordering,
{
    // SAFETY: qsort_r(glibc) は arg を第3引数で返してくる。契約は compare と同じ。
    unsafe { compare::<T, F>(ctx, a, b) }
}

/// C の `qsort_r` に Rust のクロージャを比較関数として渡して並べ替える。
/// `cmp` が panic した場合は、`qsort_r` が戻った後で同じ panic を呼び出し元に再送する。
///
/// # Safety
/// `cmp` は `v` の要素に対して全順序(反射律・反対称律・推移律を満たし、同じ組に
/// 対して毎回同じ結果)を返すこと。POSIX / ISO C は全順序でない比較関数を渡したときの
/// `qsort` の動作を定めておらず、glibc 2.38 以前では条件次第で配列の外を読み書きする
/// (Qualys, 2024-01-30)。この条件は型でも実行時検査でも保証できないので呼び出し側の責任とする。
///
/// 注意(このラッパー自身の前提): `cmp` が panic すると、以後の比較には 0 を返すので
/// それまでの答えと矛盾しうる。この矛盾でメモリを壊さない `qsort_r`(macOS: 実測で反証なし、
/// glibc 2.39 以降: 非推移的な比較関数に対して強化済み)でだけ使うこと。
pub unsafe fn c_sort_by<T, F>(v: &mut [T], cmp: F)
where
    F: FnMut(&T, &T) -> Ordering,
{
    if v.len() < 2 || size_of::<T>() == 0 {
        return; // 要素幅 0 の型は並べ替えても区別がつかない。qsort_r に width 0 を渡さない
    }
    let mut ctx = SortCtx { cmp, panic: None };
    let ctx_ptr: *mut c_void = (&raw mut ctx).cast(); // Box 不要: qsort_r が戻るまで ctx は生きている
    let base: *mut c_void = v.as_mut_ptr().cast();

    // SAFETY: base/len/size は同じ &mut [T] から取ったので食い違わない。&mut 借用により
    // qsort_r の実行中に他から v に触れない。ctx はこの関数のスタックにあり、qsort_r が
    // 戻るまで有効。trampoline::<T, F> は ctx を SortCtx<F>、要素を T として読む。
    // 比較関数が全順序であることは、この関数の # Safety として呼び出し側が保証する。
    #[cfg(target_os = "macos")]
    unsafe {
        qsort_r(base, v.len(), size_of::<T>(), ctx_ptr, trampoline::<T, F>);
    }
    #[cfg(all(target_os = "linux", target_env = "gnu"))]
    unsafe {
        qsort_r(base, v.len(), size_of::<T>(), trampoline::<T, F>, ctx_ptr);
    }

    if let Some(payload) = ctx.panic {
        panic::resume_unwind(payload); // C のフレームを抜けた後なので、ここからは普通に巻き戻せる
    }
}

// ---------------------------------------------------------------------------
// 2. pthread_create: 「呼び出しが戻った後も」C がクロージャを持つ(非同期コールバック)
// ---------------------------------------------------------------------------

#[cfg(target_os = "macos")]
#[allow(non_camel_case_types)]
type pthread_t = usize; // C では struct _opaque_pthread_t *(ポインタ幅の値として扱う)
#[cfg(all(target_os = "linux", target_env = "gnu"))]
#[allow(non_camel_case_types)]
type pthread_t = std::ffi::c_ulong; // glibc: unsigned long

unsafe extern "C" {
    fn pthread_create(
        thread: *mut pthread_t,
        attr: *const c_void, // const pthread_attr_t *。NULL で既定値
        start: extern "C" fn(*mut c_void) -> *mut c_void,
        arg: *mut c_void,
    ) -> c_int;
    fn pthread_join(thread: pthread_t, retval: *mut *mut c_void) -> c_int;
}

/// `c_spawn` が返すハンドル。`join` で結果を受け取る。
/// `join` せずに捨てた場合は Drop で join する(std の JoinHandle と違い detach しない)。
#[must_use = "Drop で join するので、捨てるとその場で待ちます"]
pub struct CThread<R> {
    tid: pthread_t,
    _result: PhantomData<fn() -> R>,
}

/// C のスレッドが最初に呼ぶ関数。`arg` の所有権をここで Rust に戻す
extern "C" fn thread_start<F, R>(arg: *mut c_void) -> *mut c_void
where
    F: FnOnce() -> R + Send + 'static,
    R: Send + 'static,
{
    // SAFETY: arg は c_spawn が Box::into_raw(Box<F>) で作り、pthread_create に渡したもの。
    // pthread_create はこの関数を1回だけ呼ぶので、from_raw もちょうど1回。
    let f = unsafe { Box::from_raw(arg.cast::<F>()) };
    let result: std::thread::Result<R> = panic::catch_unwind(AssertUnwindSafe(f));
    // 結果もヒープに置いて所有権ごと C に返す。受け取るのは CThread::join / Drop
    Box::into_raw(Box::new(result)).cast()
}

/// Rust のクロージャを、C の `pthread_create` で作ったスレッドで実行する。
pub fn c_spawn<F, R>(f: F) -> io::Result<CThread<R>>
where
    F: FnOnce() -> R + Send + 'static,
    R: Send + 'static,
{
    let arg: *mut c_void = Box::into_raw(Box::new(f)).cast(); // 所有権を C に預ける
    let mut tid = MaybeUninit::<pthread_t>::uninit();
    // SAFETY: thread_start::<F, R> は arg を Box<F> として1回だけ回収する。attr は NULL(既定値)。
    let rc = unsafe { pthread_create(tid.as_mut_ptr(), ptr::null(), thread_start::<F, R>, arg) };
    if rc != 0 {
        // スレッドは作られず、arg は誰にも渡っていない。取り戻して drop する(std と同じ処理)
        // SAFETY: arg は直前の Box::into_raw の戻り値で、まだ誰も from_raw していない。
        drop(unsafe { Box::from_raw(arg.cast::<F>()) });
        return Err(io::Error::from_raw_os_error(rc)); // pthread 系は errno ではなく戻り値でエラーを返す
    }
    Ok(CThread {
        // SAFETY: pthread_create が 0 を返したので tid は書き込み済み。
        tid: unsafe { tid.assume_init() },
        _result: PhantomData,
    })
}

impl<R> CThread<R> {
    /// スレッドの終了を待ち、戻り値を返す。スレッド内で panic した場合は Err(payload)
    pub fn join(self) -> std::thread::Result<R> {
        let me = ManuallyDrop::new(self); // Drop の join を走らせない(二重 join は UB)
        // SAFETY: me.tid はまだ join も detach もされていない(join(self) は1回しか呼べない)。
        unsafe { join_raw(me.tid) }
    }
}

impl<R> Drop for CThread<R> {
    fn drop(&mut self) {
        // SAFETY: join されずに捨てられたハンドル。ここで1回だけ join する。
        let _ = unsafe { join_raw::<R>(self.tid) }; // 結果(panic の payload 含む)は捨てる
    }
}

/// # Safety
/// `tid` は `thread_start::<_, R>` で起動し、まだ join も detach もしていないスレッド。
unsafe fn join_raw<R>(tid: pthread_t) -> std::thread::Result<R> {
    let mut ret: *mut c_void = ptr::null_mut();
    // SAFETY: 関数の契約どおり。
    let rc = unsafe { pthread_join(tid, &mut ret) };
    assert_eq!(rc, 0, "pthread_join failed: {}", io::Error::from_raw_os_error(rc));
    // SAFETY: thread_start が Box::into_raw(Box<thread::Result<R>>) で返したもの。回収は1回だけ。
    *unsafe { Box::from_raw(ret.cast::<std::thread::Result<R>>()) }
}

// ---------------------------------------------------------------------------

fn main() {
    // 同期コールバック: クロージャが外の変数を &mut で借りても良い('static 不要)
    let mut words = vec!["pear", "fig", "banana", "kiwi", "apple"];
    let mut calls = 0u32;
    // SAFETY: (len, 辞書順) の組は全順序。
    unsafe {
        c_sort_by(&mut words, |a, b| {
            calls += 1;
            a.len().cmp(&b.len()).then(a.cmp(b))
        })
    };
    println!("qsort_r by (len, lex) = {words:?}  (comparisons: {calls})");

    // 比較関数の panic は abort せず、呼び出し元の panic として戻ってくる
    let mut v = vec![3, 1, 2];
    let caught = panic::catch_unwind(AssertUnwindSafe(|| {
        // SAFETY: panic するまでの比較は i32 の全順序。
        unsafe { c_sort_by(&mut v, |_: &i32, _: &i32| -> Ordering { panic!("boom in comparator") }) }
    }));
    let msg = caught.unwrap_err().downcast::<&str>().map(|s| *s).unwrap_or("?");
    println!("panic came back as a Rust panic: {msg:?}, v still has {} elements", v.len());

    // 非同期コールバック: クロージャの所有権を C のスレッドに渡す(Send + 'static が必要)
    let data: Vec<u64> = (1..=4).collect(); // ヒープを持つ値をスレッドへムーブする
    let h = c_spawn(move || data.iter().sum::<u64>()).expect("pthread_create");
    println!("c_spawn sum = {:?}", h.join());

    let h = c_spawn(|| -> u8 { panic!("boom in thread") }).expect("pthread_create");
    println!("c_spawn panicked -> is_err = {}", h.join().is_err());
}

// ---------------------------------------------------------------------------
// テスト
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::Cell;
    use std::rc::Rc;
    use std::sync::Arc;
    use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};

    /// drop された回数を数える値(クロージャに持たせて「ちょうど1回解放」を確かめる)
    struct DropCount(Arc<AtomicUsize>);
    impl Drop for DropCount {
        fn drop(&mut self) {
            self.0.fetch_add(1, SeqCst);
        }
    }

    /// 乱数の代わり(外部クレートなし): xorshift64
    fn xorshift(seed: &mut u64) -> u64 {
        *seed ^= *seed << 13;
        *seed ^= *seed >> 7;
        *seed ^= *seed << 17;
        *seed
    }

    #[test]
    fn sorts_like_std_for_many_random_inputs() {
        let mut seed = 0x9E37_79B9_7F4A_7C15;
        for n in [0usize, 1, 2, 3, 7, 64, 1000] {
            let orig: Vec<i64> = (0..n).map(|_| (xorshift(&mut seed) % 100) as i64 - 50).collect();
            let mut ours = orig.clone();
            unsafe { c_sort_by(&mut ours, |a, b| a.cmp(b)) };
            let mut expected = orig.clone();
            expected.sort();
            assert_eq!(ours, expected, "n = {n}");
        }
    }

    #[test]
    fn closure_can_borrow_mutably_and_capture_non_send() {
        // 同期コールバックなので Rc(!Send)も &mut も使える
        let seen = Rc::new(Cell::new(0));
        let mut log = Vec::new();
        let mut v = vec![5, 4, 3, 2, 1];
        unsafe {
            c_sort_by(&mut v, |a, b| {
                seen.set(seen.get() + 1);
                log.push((*a, *b));
                a.cmp(b)
            })
        };
        assert_eq!(v, [1, 2, 3, 4, 5]);
        assert_eq!(seen.get(), log.len());
        assert!(!log.is_empty());
    }

    #[test]
    fn works_for_non_copy_elements_and_drops_each_once() {
        // String を並べ替えても、qsort_r はビット単位で移動するだけなので二重解放は起きない
        let mut v: Vec<String> = ["delta", "alpha", "charlie", "bravo"].map(String::from).to_vec();
        unsafe { c_sort_by(&mut v, |a, b| a.cmp(b)) };
        assert_eq!(v, ["alpha", "bravo", "charlie", "delta"]);
    }

    #[test]
    fn comparator_panic_is_resumed_and_elements_are_a_permutation() {
        let mut v: Vec<u32> = (0..200).rev().collect();
        let mut n = 0;
        let r = panic::catch_unwind(AssertUnwindSafe(|| unsafe {
            c_sort_by(&mut v, |a, b| {
                n += 1;
                if n == 50 {
                    panic!("50th comparison");
                }
                a.cmp(b)
            })
        }));
        let payload = r.unwrap_err();
        assert_eq!(*payload.downcast_ref::<&str>().unwrap(), "50th comparison");
        assert_eq!(n, 50, "panic 後は比較関数を1回も呼ばない");
        let mut sorted = v.clone();
        sorted.sort();
        assert_eq!(sorted, (0..200).collect::<Vec<_>>(), "要素は失われず重複もしない");
    }

    #[test]
    fn sort_closure_is_dropped_exactly_once() {
        let drops = Arc::new(AtomicUsize::new(0));
        let guard = DropCount(drops.clone());
        let mut v = vec![2, 1];
        unsafe {
            c_sort_by(&mut v, move |a, b| {
                let _keep = &guard; // クロージャに所有させる
                a.cmp(b)
            })
        };
        assert_eq!(drops.load(SeqCst), 1);
    }

    #[test]
    fn zero_sized_elements_are_noop() {
        let mut v = vec![(); 10];
        unsafe { c_sort_by(&mut v, |_, _| unreachable!()) };
        assert_eq!(v.len(), 10);
    }

    #[test]
    fn spawn_returns_value_and_frees_closure_once() {
        let drops = Arc::new(AtomicUsize::new(0));
        let guard = DropCount(drops.clone());
        let h = c_spawn(move || {
            let _keep = guard;
            std::thread::current().id()
        })
        .unwrap();
        let child = h.join().unwrap();
        assert_ne!(child, std::thread::current().id(), "別スレッドで実行された");
        assert_eq!(drops.load(SeqCst), 1);
    }

    #[test]
    fn spawn_panic_becomes_err_not_abort() {
        let h = c_spawn(|| -> i32 { panic!("inside C thread") }).unwrap();
        let err = h.join().unwrap_err();
        assert_eq!(*err.downcast_ref::<&str>().unwrap(), "inside C thread");
    }

    #[test]
    fn dropping_handle_joins_and_frees_result() {
        let drops = Arc::new(AtomicUsize::new(0));
        let d = drops.clone();
        {
            let _h = c_spawn(move || DropCount(d)).unwrap(); // 戻り値そのものが DropCount
        } // ここで join → 戻り値の Box を回収して drop
        assert_eq!(drops.load(SeqCst), 1);
    }

    #[test]
    fn many_threads_in_parallel() {
        let hs: Vec<_> = (0..16u64).map(|i| c_spawn(move || i * i).unwrap()).collect();
        let total: u64 = hs.into_iter().map(|h| h.join().unwrap()).sum();
        assert_eq!(total, (0..16u64).map(|i| i * i).sum());
    }
}

実測(出題者の環境: rustc 1.96.0 / edition 2024 / Apple M1 / macOS 26.5.1、2026-09-25)

$ cargo run 2>/dev/null qsort_r by (len, lex) = ["fig", "kiwi", "pear", "apple", "banana"] (comparisons: 7) panic came back as a Rust panic: "boom in comparator", v still has 3 elements c_spawn sum = Ok(10) c_spawn panicked -> is_err = true $ cargo test running 10 tests test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out # --release でも同じ $ cargo clippy --all-targets # 警告 0 件 $ cargo check --target aarch64-unknown-linux-gnu --all-targets # エラー 0 件(glibc 側の cfg も型検査は通る)

2>/dev/null を外すと、panic のたびに既定の panic hook が thread 'main' panicked at ...: boom in comparator を標準エラーに出します。これは catch_unwind で捕まえても出る表示で、abort ではありません(終了コード 0)。

🐧
Linux では実行していません。cargo check は型とシグネチャの整合までで、glibc の qsort_r が本当にこの順番で userdata を返すかは、POSIX と glibc のマニュアルの記述に依っています。

(4) Q1 の回答: なぜ unsafe fn か

# Safety には「cmp は v の要素に対して全順序を返すこと(同じ組に毎回同じ答え、推移律を満たす)」と書きます。

理由は、qsort の仕様(ISO C・POSIX)が比較関数に全順序を要求していて、守られなかったときの動作を定めていないからです。これは机上の話ではなく、Qualys が 2024-01-30 に公開した調査で、glibc 1.04(1992年)から 2.38 までの qsort は、非推移的な比較関数(例: return a - b; のようにオーバーフローするもの)と、malloc の失敗が重なると配列の外を読み書きすることが示されました。glibc 側は「使い方の誤り(未定義動作)なので glibc の脆弱性ではない」としつつ、実装を強化しています。

Rust の safe fn は「safe なコードからどう呼んでも未定義動作にならない」という約束です。c_sort_by を safe にすると、safe なコードが |_, _| Ordering::Less のようなでたらめな比較を渡しただけで、古い glibc 上ではメモリ破壊に届きます。型でも実行時検査でもこれを防げないので、unsafe fn にして呼び出し側に約束させるしかありません。

一方 std の slice::sort_by は、比較関数が全順序でなくてもメモリ安全であるように実装されています(Ord のドキュメントは、実装が誤っていても未定義動作にしてはならないと明記しています)。実際に同じでたらめな比較関数を渡すと、Rust 1.81 以降のソートは user-provided comparison function does not correctly implement a total order で panic しうる(下の実測で 200 回中 200 回)ものの、要素は常に元の並べ替えのままでした。「間違った比較関数を渡されたときに、何が保証されるか」が C の qsort と Rust の sort_by の本質的な違いで、それが safe / unsafe の差になります。

(4) Q2 の回答: 'static を外せない理由

std::mem::forget は safe fn です。CThread を mem::forget(handle) すると Drop が走らず join されないまま、スレッドは借りたローカル変数を読み続けます。関数が戻ってスタックが片付いた後もスレッドが読めば use-after-free で、これを safe なコードだけで起こせてしまいます。「Drop が必ず走る」ことを前提にした安全性は Rust では成り立ちません。

これは Rust 1.0 の直前(2015年)に実際に起きた問題で、当時の std::thread::scoped(ガードの Drop で join する API)が同じ理由で不健全と分かり、削除されました(通称 Leakpocalypse。RFC 1066 で mem::forget は safe と正式に決まった)。借用するスレッドを安全に作る正解は、Rust 1.63 で入った std::thread::scope で、scope 関数自身が戻る前に全スレッドを join する(ハンドルを forget しても scope の戻り際に join される)形になっています。

追加の実測: 失敗の仕方を見る

実測1: macOS で qsort_r を glibc の順番で宣言してしまった

// examples/wrong_order.rs — macOS で qsort_r を glibc の順番で宣言してしまったら?
// cargo run --example wrong_order
use std::ffi::{c_int, c_void};

unsafe extern "C" {
    // 誤り: glibc / POSIX.1-2024 の順番(compar が第4、arg が第5)で宣言している
    fn qsort_r(
        base: *mut c_void,
        nmemb: usize,
        size: usize,
        compar: unsafe extern "C" fn(*const c_void, *const c_void, *mut c_void) -> c_int,
        arg: *mut c_void,
    );
}

unsafe extern "C" fn cmp_i32(a: *const c_void, b: *const c_void, ctx: *mut c_void) -> c_int {
    unsafe {
        *ctx.cast::<u32>() += 1; // 比較回数を数える
        (*a.cast::<i32>()).cmp(&*b.cast::<i32>()) as c_int
    }
}

fn main() {
    let mut v = [3i32, 1, 2];
    let mut count = 0u32;
    println!("before: {v:?}");
    unsafe {
        qsort_r(v.as_mut_ptr().cast(), v.len(), 4, cmp_i32, (&raw mut count).cast());
    }
    println!("after: {v:?}, comparisons = {count}");
}
$ cargo run --example wrong_order ; echo $? before: [3, 1, 2] 139 # 128 + 11 = SIGSEGV。"after:" は表示されない
💥
macOS の qsort_r は第4引数(こちらは比較関数のつもり)を userdata として、第5引数(こちらは &mut count のつもり)を関数ポインタとして呼び出し、スタック上のデータの番地へジャンプして落ちました。コンパイラ・リンカとも警告 0 件です。

実測2: catch_unwind を使わずにコールバックの中で panic した

$ cargo run --example no_catch ; echo $? thread 'main' panicked at examples/no_catch.rs:16:5: boom without catch_unwind thread 'main' panicked at .../library/core/src/panicking.rs:225:5: panic in a function that cannot unwind (スタックトレース。19 番目が no_catch::cmp_panics、20 番目が <unknown> = libSystem の qsort_r) thread caused non-unwinding panic. aborting. 134 # 128 + 6 = SIGABRT

(examples/no_catch.rs は unsafe extern "C" fn cmp_panics(...) -> c_int { panic!(...) } を qsort_r に渡すだけの 23 行です)

実測3: でたらめな比較関数を macOS の qsort_r と Rust の sort_by に渡した

// examples/inconsistent.rs — 全順序でない比較関数を macOS の qsort_r に渡し続けたら?
// cargo run --release --example inconsistent
use std::ffi::{c_int, c_void};

unsafe extern "C" {
    fn qsort_r(
        base: *mut c_void,
        nel: usize,
        width: usize,
        thunk: *mut c_void,
        compar: unsafe extern "C" fn(*mut c_void, *const c_void, *const c_void) -> c_int,
    );
}

unsafe extern "C" fn random_cmp(seed: *mut c_void, _: *const c_void, _: *const c_void) -> c_int {
    let s = unsafe { &mut *seed.cast::<u64>() };
    *s ^= *s << 13;
    *s ^= *s >> 7;
    *s ^= *s << 17;
    (*s % 3) as c_int - 1 // -1 / 0 / 1 をでたらめに返す
}

fn main() {
    let mut seed = 0x2545_F491_4F6C_DD1Du64;
    let mut runs = 0u64;
    let mut elems = 0u64;
    for n in [2usize, 3, 4, 5, 6, 7, 8, 16, 100, 1000, 10_000] {
        for _ in 0..if n <= 100 { 2000 } else { 200 } {
            let mut v: Vec<u32> = (0..n as u32).collect();
            unsafe {
                qsort_r(v.as_mut_ptr().cast(), n, 4, (&raw mut seed).cast(), random_cmp);
            }
            let mut s = v.clone();
            s.sort_unstable();
            assert!(s.iter().copied().eq(0..n as u32), "not a permutation at n={n}");
            runs += 1;
            elems += n as u64;
        }
    }
    println!("{runs} runs / {elems} elements: every result was a permutation");

    // 比較: Rust の sort_by に同じでたらめ比較を渡す(UB にはならない。panic はありうる)
    let mut panics = 0;
    for _ in 0..200 {
        let mut v: Vec<u32> = (0..10_000).collect();
        let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            v.sort_by(|_, _| unsafe {
                match random_cmp((&raw mut seed).cast(), std::ptr::null(), std::ptr::null()) {
                    -1 => std::cmp::Ordering::Less,
                    0 => std::cmp::Ordering::Equal,
                    _ => std::cmp::Ordering::Greater,
                }
            })
        }));
        if r.is_err() {
            panics += 1;
        }
        let mut s = v.clone();
        s.sort_unstable();
        assert!(s.iter().copied().eq(0..10_000));
    }
    println!("Rust sort_by x200 (n=10000): panicked {panics} times, always a permutation");
}
$ cargo run --release --example inconsistent 2>/dev/null 18400 runs / 2502000 elements: every result was a permutation Rust sort_by x200 (n=10000): panicked 200 times, always a permutation
🔬
macOS の qsort_r は 18,400 回・のべ 250 万要素で一度も要素を壊しませんでした。ただしこれは反証できなかっただけで、Apple のドキュメントが保証しているわけではありません。c_sort_by を unsafe fn のままにする理由は変わりません。

🪜 Step-by-Step 解説

1
OS ごとの違いを cfg で「宣言とトランポリン」だけに閉じ込める

qsort_r の宣言、トランポリン、呼び出しの3か所だけを cfg で分け、比較の本体 compare は共通にしました。違いが引数の並びだけなので、トランポリンは「並べ替えて共通の関数を呼ぶ」だけの3行で済みます。

cfg に当てはまらない OS(Windows、musl など)では qsort_r も trampoline も存在しないので、コンパイルエラーになります。「知らない OS では黙って動く」より「知らない OS ではビルドが止まる」ほうが安全です。実測1のとおり、宣言を間違えると SIGSEGV で、しかも何も警告されないからです。

pthread_t も同じく OS で型が違います(macOS: struct _opaque_pthread_t *、glibc: unsigned long)。64 ビット環境ではどちらも 8 バイトの値として渡されるので、macOS 側は usize で受けています。

2
トランポリンはジェネリックな extern "C" fn
qsort_r(base, v.len(), size_of::<T>(), ctx_ptr, trampoline::<T, F>);

trampoline::<T, F> は、呼び出しごとの T と F(クロージャの型は1つ1つ別の型です)で単相化され、それぞれ別の関数として生成されます。C から見ればただの関数ポインタです。#[no_mangle] を付けないので、同じ名前の関数が何個できてもシンボルは衝突しません。

3
同期コールバックは借用で足りる — Box 不要
let mut ctx = SortCtx { cmp, panic: None };
let ctx_ptr: *mut c_void = (&raw mut ctx).cast();

qsort_r は戻った後に比較関数を呼ばないので、ctx は c_sort_by のスタックにあれば十分です。コードを読むと、このクロージャを渡す経路にヒープ確保はありません(導出。アロケータで数えてはいません)。その代わり、クロージャは 'static でも Send でもなくてよく、テスト closure_can_borrow_mutably_and_capture_non_send のように &mut Vec や Rc<Cell<_>> を捕まえられます。

&raw mut ctx(Rust 1.82 以降の構文)は、参照を作らずに直接生ポインタを作ります。&mut ctx as *mut _ でも動きますが、「ここから先は生ポインタとして扱う」ことが読み手に伝わります。

4
panic は「預かって、C の外で投げ直す」
  1. トランポリンの中で catch_unwind し、payload を ctx.panic に入れて 0 を返す
  2. 以後の呼び出しでは ctx.panic.is_some() を見て、クロージャを呼ばずに 0 を返す
  3. qsort_r が戻ったら resume_unwind(payload) で投げ直す

qsort には「途中で止めて」と伝える手段がないので、C 側には最後まで何かを答え続けるしかありません。テスト comparator_panic_is_resumed_and_elements_are_a_permutation は、比較がちょうど 50 回で止まり、200 個の要素が1つも欠けていないことを確かめています。qsort_r は要素を入れ替えるだけなので、途中で比較の答えが変わっても要素の集合は変わりません(ただし、次の Step の注意があります)。

⚖️
このラッパー自身が作る矛盾: panic の後に 0(等しい)を返すと、それ以前に「a < b」と答えた組に「a == b」と答えることになり、全順序ではなくなります。つまり panic した時点で、Q1 の # Safety の前提はラッパー自身が破っています。これが許されるのは、qsort_r が非推移的な比較関数でもメモリを壊さない実装であるときだけで、今日のコードはその前提(macOS: 実測3で反証できず/glibc 2.39 以降: 強化済み)をドキュメントに書いています。もっと厳密にするなら panic 時は std::process::abort() を選ぶことになります(下の判断2)。
5
非同期コールバックは所有権ごと預ける — Box::into_raw と from_raw を1回ずつ
let arg: *mut c_void = Box::into_raw(Box::new(f)).cast();       // c_spawn: C に預ける
let f = unsafe { Box::from_raw(arg.cast::<F>()) };               // thread_start: 回収する

into_raw と from_raw がちょうど1回ずつ対応していることが安全性のすべてです。

  • pthread_create が成功した場合: thread_start がちょうど1回呼ばれ、そこで回収
  • 失敗した場合: thread_start は呼ばれないので、c_spawn 自身が from_raw して drop

2つ目を忘れるとリーク、成功時にも from_raw すると二重解放です。この形は std の std::thread::spawn の実装とそっくり同じで、Rust 1.96 の library/std/src/sys/thread/unix.rs には「スレッドが起動しなかったので data は消費されていない。Box を作り直して解放してよい」というコメント付きで同じ処理があります。

戻り値も同じ方法で運びます。thread_start が Box<thread::Result<R>> を into_raw して void * で返し、pthread_join で受け取った側が from_raw します。テスト dropping_handle_joins_and_frees_result は、戻り値そのものを drop 回数を数える型にして、ハンドルを捨てたときに戻り値がちょうど1回 drop されることを確かめています。

6
型の条件は「C が何をするか」から決まる
条件必要な理由外すと
F: Sendクロージャは作ったスレッドとは別のスレッドで実行・drop されるRc を捕まえたクロージャが通り、2つのスレッドから参照カウントを同時に書き換えうる
F: 'staticいつ終わるか分からないスレッドが持つ(Q2)ローカル変数を借りたクロージャが通り、mem::forget で use-after-free
R: Send戻り値は子スレッドで作られ、join した親スレッドに渡るRc を返せてしまう
c_sort_by の F には何も要らない同じスレッドで、関数が戻る前に使い終わる—

この表の上2行が実際にコンパイルエラーとして現れる様子は「コンパイルエラーが出た場合」に載せています。

7
二重 join を型で防ぐ — join(self) と ManuallyDrop

join は self を値で受け取るので、同じハンドルで2回呼ぶことはコンパイラが止めます。ただし join の中で self がスコープを抜けると Drop がもう一度 join してしまうので、先に ManuallyDrop::new(self) で包んで Drop を止めています(Day 054 の into_raw と同じ形)。POSIX は、既に join したスレッドをもう一度 join したときの動作を定めていません。

🧭 設計思想・なぜこう書くのか

C のコールバック API は「関数ポインタ + void *」という最小の部品で、寿命も型もスレッドも何も表しません。 Rust のラッパーの仕事は、その void * について「誰が・いつまで・どのスレッドで」使うのかを C のドキュメントから読み取り、FnMut / FnOnce、Send、'static、&mut か Box かという Rust の型に翻訳することです。今日の2つの関数は、同じ「クロージャを C に渡す」でも、C の振る舞いが違うだけで型の条件がまったく違いました。

今日の判断で迷った点を3つ残します。

判断1: クロージャを userdata に入れる形

選択肢利点欠点今日の判断
A. ジェネリックなトランポリン trampoline::<T, F> + *mut SortCtx<F>間接呼び出しは C → トランポリンの1回だけ。クロージャの呼び出しはインライン化されうるクロージャの型ごとにトランポリンが生成され、コードが増える採用
B. &mut dyn FnMut を渡すトランポリンは1つで済む&mut dyn FnMut は2ワードの fat pointer で void * 1つに入らない。&mut &mut dyn FnMut と二重にする必要があり、呼び出しも vtable 経由になる呼び出し元の種類が非常に多く、バイナリサイズが問題になるとき
C. スレッドローカル変数にクロージャを置き、userdata を使わないuserdata の無い古い API(qsort、atexit、signal)でも使える再入(コールバックの中で同じ API を呼ぶ)で壊れる。スレッドをまたげないuserdata が無い API に限る

判断2: コールバック内の panic をどう扱うか

選択肢利点欠点今日の判断
A. catch_unwind で預かり、以後 0 を返し、C が戻ったら resume_unwind呼び出し元から見ると普通の panic と同じ。テストで #[should_panic] も使えるpanic 後の答えが矛盾する(Step 4)。qsort_r の実装が矛盾に強いことに依存採用(macOS と glibc 2.39 以降に限定してドキュメントに明記)
B. catch_unwind して std::process::abort()C 側に矛盾した答えを1つも返さない。最も厳密呼び出し元で panic を処理できない。サーバなら1リクエストの失敗でプロセスが落ちる対象に glibc 2.38 以前が含まれるとき
C. extern "C-unwind" で panic を C のフレームごと巻き戻すコードが一番短いC のフレームが巻き戻しに対応している保証が要る。qsort が内部で確保した一時領域などは解放されない採らない。C++ のように巻き戻しを前提に書かれた相手に限る

A と同じ「預かって、C の外で投げ直す」形は、実物のクレートでも使われています。curl クレート(libcurl のラッパー)と git2 クレート(libgit2 のラッパー)の src/panic.rs は、どちらも panic の payload をスレッドローカル変数の LAST_ERROR に預け、C の関数から戻った直後に resume_unwind しています。今日は userdata があるので SortCtx のフィールドに入れましたが、1つの C 関数の中で種類の違うコールバックが何度も呼ばれる libcurl のような API では、スレッドローカルに置くほうが扱いやすくなります。また libcurl のコールバックは「エラー値を返して転送を中止する」ことができるので、qsort と違って panic の後に C へ嘘の答えを返し続ける必要がありません。

判断3: CThread を捨てたときに join するか detach するか

選択肢利点欠点今日の判断
A. Drop で join(待つ)戻り値の Box を必ず回収できる。リークしない捨てた場所でブロックする。無限ループのスレッドなら永遠に待つ採用(#[must_use] で捨てることに気付かせる)
B. Drop で pthread_detach(std の JoinHandle と同じ)捨てても待たない戻り値を void * で返す今日の設計では、誰も回収しない Box がリークする。std は戻り値を Arc の共有領域(Packet)に置くことでこれを避けている戻り値の置き場所を Arc に変えるなら B

A を選んでも Q2 のとおり 'static は外せません。Drop に安全性を頼らない、というのが Rust の API 設計の前提です。

🚨 コンパイルエラーが出た場合

以下はすべて出題者の環境(rustc 1.96.0、edition 2024、aarch64 macOS)で実際に出た出力です。修正版の main の先頭に数行を足してビルドしました。

エラー1: Rc を捕まえたクロージャを c_spawn に渡した(F: Send)

error[E0277]: `Rc<i32>` cannot be sent between threads safely --> src/main.rs:235:21 | 235 | let h = c_spawn(move || *shared + 1).unwrap(); | ------- -------^^^^^^^^^^^^ | | | | | `Rc<i32>` cannot be sent between threads safely | | within this `{closure@src/main.rs:235:21: 235:28}` | required by a bound introduced by this call | = help: within `{closure@src/main.rs:235:21: 235:28}`, the trait `Send` is not implemented for `Rc<i32>` note: required because it's used within this closure note: required by a bound in `c_spawn` --> src/main.rs:184:24 | 184 | F: FnOnce() -> R + Send + 'static, | ^^^^ required by this bound in `c_spawn`

読み方: 最後の note が「どの境界が要求したか」を指しています。c_spawn の Send です。同じ Rc を c_sort_by のクロージャに入れても(テスト closure_can_borrow_mutably_and_capture_non_send)エラーにならないのは、c_sort_by の F に Send を付けていないからです。境界を付けるかどうかは、C がそのクロージャを別スレッドへ持ち出すかどうかで決まります。

エラー2: ローカル変数を借りるクロージャを c_spawn に渡した(F: 'static)

error[E0373]: closure may outlive the current function, but it borrows `local`, which is owned by the current function --> src/main.rs:235:21 | 235 | let h = c_spawn(|| local.len()).unwrap(); | ^^ ----- `local` is borrowed here | | | may outlive borrowed value `local` | note: function requires argument type to outlive `'static` help: to force the closure to take ownership of `local` (and any other referenced variables), use the `move` keyword | 235 | let h = c_spawn(move || local.len()).unwrap(); | ++++

これが Q2 で同僚が外そうとした境界です。help のとおり move を付けて local の所有権ごとスレッドへ渡すのが正解で、'static を外すのは誤りです。

実行時1: 宣言の引数順を間違えた → SIGSEGV(終了コード 139)

「模範解答」の実測1を参照してください。コンパイラもリンカも何も言いません。 FFI の宣言は Rust 側に書いた型を誰も照合しないので(Day 053)、ヘッダを見て1行ずつ確かめるしかありません。bindgen でヘッダから宣言を生成すれば、この種の取り違えは起きにくくなります。

実行時2: extern "C" 関数から panic が漏れた → abort(終了コード 134)

「模範解答」の実測2を参照してください。panic in a function that cannot unwind と thread caused non-unwinding panic. aborting. の2行が目印です。スタックトレースで、あなたの extern "C" fn のすぐ下に <unknown>(C のライブラリ)が見えたら、コールバックから panic が漏れています。

🌐 他言語との比較

観点C++Go(cgo)Java(FFM API)Python(ctypes)Rust(今日)
クロージャを C の関数ポインタにする方法捕捉なしのラムダだけが関数ポインタに変換できる。捕捉ありは void * + トランポリンを自分で書くGo の関数を直接は渡せない。//export した関数 + cgo.Handle(Go 1.17〜)を userdata にするLinker.upcallStub が関数ポインタ(スタブ)を生成するCFUNCTYPE(...)(pyfunc) がスタブを生成するジェネリックなトランポリン + userdata
C が持っている間の寿命管理人間の責任Handle.Delete() を明示的に呼ぶ。呼び忘れるとリークスタブは Arena の寿命に従うPython 側で参照を持ち続けないと GC で解放され、C から呼ばれた瞬間に落ちる(公式ドキュメントの注意)同期は借用(コンパイラが保証)、非同期は Box を1回ずつ into_raw / from_raw
コールバック内の例外・panicC のフレームを越えて例外を投げるのは実装依存//export した関数の中で recover して止めるのが定石(C のフレームを越える panic は前提にしない)JVM が即座に終了する(Linker.upcallStub のドキュメント。try/catch で包むよう推奨)例外は標準エラーに表示されて握りつぶされるextern "C" なら abort(1.81〜)。catch_unwind で預かって投げ直す
別スレッドから呼ばれる安全性検査しないgoroutine とスレッドの対応は runtime が面倒を見る検査しないGIL を取り直してから呼ぶSend / 'static の境界としてコンパイル時に検査

Go 経験者向けの要点: cgo では「Go のポインタを C に保持させてはいけない」という規則があるため、cgo.Handle(ただの整数)を userdata にして、Go 側の表で値を引く形になります。Rust ではクロージャの番地をそのまま渡せる代わりに、その番地が有効な期間を &mut(同期)か Box(非同期)かで自分で選ぶ必要があります。

Java 経験者向けの要点: FFM API も「upcall の中の例外は外に出すな、自分で catch しろ」という規則で、今日の catch_unwind と同じ構造です。違いは、Rust では型の境界(Send + 'static)で「別スレッドから呼ばれてよいか」までコンパイル時に止められる点です。

🏭 実務での使いどころ

  • C ライブラリのコールバック API を包む: libcurl の書き込みコールバック(CURLOPT_WRITEFUNCTION + CURLOPT_WRITEDATA)、SQLite のユーザー定義関数(sqlite3_create_function_v2 の pApp と破棄関数 xDestroy)、GLib のシグナル(g_signal_connect_data の data と GDestroyNotify)は、どれも「関数ポインタ + void * + 破棄関数」の形です。破棄関数があるものは今日の非同期パターン(Box::into_raw し、破棄関数の中で from_raw)で包みます
  • panic の預かり: 上の判断2のとおり、curl クレートと git2 クレートはスレッドローカルの LAST_ERROR に payload を預けて C から戻った後に投げ直しています。Rust で C ライブラリのラッパーを書くときの定番の形です
  • Rust 製ライブラリにコールバックを登録させる(cdylib): 逆向き、つまり C の利用者が Rust のライブラリにコールバックを登録する API を作るときも、extern "C" fn(*mut c_void, ...) と void *userdata の組を受け取る形にします。その場合は「userdata を別スレッドから返すか」「いつまで保持するか」をヘッダのコメントに書くのが、C 側の利用者に対する今日の Send / 'static に相当します
  • コードレビューの観点: コールバックを渡すコードを見たら、「C はこの userdata をいつまで持つか」「どのスレッドから呼ぶか」「from_raw は成功時・失敗時それぞれ何回か」「コールバックの中で panic しうるか」の4点を確認します

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
qsort_r の宣言をどこかからコピーして使う同じ名前の関数だから同じ形だと思うmacOS と glibc で userdata の位置が違う。cfg で分け、知らない OS ではビルドを止める。取り違えると警告なしで SIGSEGV(実測)
同期コールバックでも Box::into_raw する「C に渡すものは Box」と覚えているC が関数の中でしか使わないならスタックの &mut で十分。Box にすると不要な確保が増え、from_raw の呼び忘れでリークする危険も増える
非同期コールバックにスタックの &mut closure を渡す同期の書き方をそのまま使う関数が戻った後に C が呼ぶと、片付いたスタックを指している。コンパイラは生ポインタの寿命を見ないので検出しない
コールバックの中で unwrap() してもよいRust の中ではいつもそうしているextern "C" 関数から panic が漏れると abort(実測で終了コード 134)。catch_unwind で預かる
catch_unwind すれば完全に安全panic が外に出なくなるのでC の関数に何を返すかという問題が残る。qsort には中止の手段が無く、panic 後の答えは全順序の約束を破る(Step 4)
Drop で join すれば 'static は要らないDrop が必ず走ると思っているmem::forget は safe。Drop に安全性を頼ってはいけない(2015年の thread::scoped の削除)
c_sort_by は F: Ord 相当の比較を受けるから safe にできるRust の sort_by が safe だからRust のソートは誤った比較でもメモリ安全に作られている。C の qsort にその保証は無い(glibc 2.38 以前は実際に壊れる)

📖 一次資料(参照日: 2026-09-25。記述は各資料の要旨で、原文の引用ではありません)

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

FFI テーマは今日で3問目です。今日は「libc にある2つの API でコールバックを渡す」ことに絞りました。

  • Linux での実行: glibc 側の分岐は cargo check で型検査しただけで、実行していません(開発機のメモリの都合でコンテナを起動していません)。Linux で cargo test を回すのが最初の宿題です
  • 破棄関数(destroy notify)付きの登録型 API: GLib や SQLite のように「登録 → 何度も呼ばれる → 解除時に破棄関数」という API。今日の pthread_create は「1回呼ばれて終わり」なので、from_raw の場所が1つで済みました。登録型では「最後の呼び出し」と「破棄」が別のタイミングになり、Send / Sync の判断も変わります
  • 比較関数が &T 経由で要素を書き換える場合: Cell / RefCell を含む要素を、qsort_r の実装が一時領域にコピーしてから比較に渡すと、書き換えが失われたり二重に残ったりする可能性があります。macOS・glibc の実装がコピーに対して比較を呼ぶかは確認していません。厳密にするなら T: Freeze 相当(内部可変性なし)を要求する設計になりますが、安定版の Rust にはそのトレイトがありません
  • Windows の qsort_s: Microsoft の qsort_s と C11 Annex K の qsort_s でも userdata の位置が違います。今日はビルド対象外にしました
  • 性能: Day 053 で qsort と Rust のソートの差(約 7.7 倍)を測ったので、今日は測っていません。userdata 経由のトランポリンで差が変わるかは未計測です

➡️ 次のステップ

  • 発展: c_spawn の戻り値を Arc<Mutex<Option<thread::Result<R>>>> の共有領域に置く形に変え、CThread の Drop を pthread_detach にしてください(std の JoinHandle と同じ設計)。そのうえで、ハンドルを捨ててもリークしないことを DropCount のテストで確かめてください。判断3の表の B を実装することになります
  • 次回予告: FFI ④ Rust のライブラリを C から使わせる(設計) — cdylib としてビルドし、C の利用者に渡す API(エラーの返し方、所有権、コールバックの登録と解除、ABI の安定性)を設計する。自己評価で FFI を3問「おおむね理解」以上にできていれば、次のテーマ マクロ①(macro_rules!) に進みます

🎯 自己評価(解いた後に記入)

自分の回答((4) の Q1・Q2 と、実装で迷った点)

気づき・メモ