Callbacks across FFI¶
Level: 301 · deep dive
One line: C takes a callback as a bare function pointer plus a void * for your data, so a Rust closure crosses in two halves — an extern "C" fn trampoline and a pointer to the closure — and a panic inside it aborts the process unless the function is declared extern "C-unwind".
Stub — an outline, not a lesson. There is no runnable example behind this page yet, so nothing on it has been through the check that backs every other claim in this library. The bullets below are the questions the finished page has to answer.
What it has to cover¶
- The C shape:
void each(int (*cb)(void *user, int value), void *user).qsorthas no user pointer andqsort_rdoes — and the macOS man page putsthunkbefore the comparator and as the comparator's first argument, where glibc puts it last in both. What does a Rust declaration for each look like? - Why a closure cannot be the callback: only a closure that captures nothing coerces to a
fnpointer (Function pointers shows theE0308), and a capturing closure is a struct with nowhere to go in a bare pointer. - The trampoline: a generic
extern "C" fn trampoline<F: FnMut(c_int)>(user: *mut c_void, value: c_int)that castsuserback to&mut Fand calls it, monomorphised once per closure type. What must its// SAFETY:comment argue — that the pointer is live, of typeF, and not aliased for the duration of the call? - How long the user data lives: C may keep the pointer after
registerreturns.Box::into_rawon the closure, and anunregisterthat takes it back withBox::from_raw— the ownership rules from Data across the boundary. - Panics: a panic that reaches the end of an
extern "C"function aborts — "panic in a function that cannot unwind", then "thread caused non-unwinding panic. aborting." — the behaviour Rust 1.81.0 made standard.extern "C-unwind", stable since 1.71.0, lets the panic unwind through the boundary instead; when is unwinding through C frames acceptable, and when iscatch_unwindinside the trampoline the only sound answer? - rustc's allow-by-default
ffi_unwind_callslint flags calls to foreign functions or function pointers with an unwinding ABI — which crates would want it on? - Threads: C may invoke the callback from a thread it created. Which bounds —
Send,Sync,'static— does that put onF, and what doesthread::spawn's signature already say about the same question?
The trap it exists for¶
&mut closure as *mut _ as *mut c_void handed to a C library that stores it, with the closure living on the stack of a function that returns. The callback fires later, through a pointer to a stack frame that no longer exists — and no borrow checker saw it, because the reference became a raw pointer at the boundary.
Where this sits¶
- Function pointers covers
fnagainst closures; Calling C covers calling out; Catching a signal covers the one callback restricted to async-signal-safe work; What a panic costs coverscatch_unwind. This page covers only the callback-plus-user-data pattern and unwinding across it.
See also¶
- Function pointers — "nothing can ride along" in a bare
fn - What a closure is — the struct the trampoline points at
- Calling C — the other direction, and
#[unsafe(no_mangle)]for exporting - Catching a signal — an
extern "C" fnhandler, with stricter rules still - What a panic costs —
catch_unwind, which "exists so a panic does not cross an FFI boundary into C" - Data across the boundary — the boxed closure C holds between calls
- FFI-safe types —
Option<extern "C" fn(…)>as a nullable callback - Panics in unsafe code — what a half-finished callback leaves behind
If you are coming from another language¶
- C. The
void *userargument is C's closure:pthread_createtakes one for the thread's start function,qsort_rfor the comparator. Rust's trampoline is the function C programmers write by hand, with the cast back to the right type generated per closure rather than written once per call site. - C++. A lambda with no captures converts to a function pointer and a capturing one does not — the same rule as Rust's — and
std::functioncannot cross a C boundary for the same reason aBox<dyn FnMut>cannot. An exception escaping into C frames is the same hazard as a panic, which is why a C++ callback registered with a C library catches everything before it returns. - Python.
ctypes.CFUNCTYPEwraps a Python function as a C function pointer, and thectypesdocs ↗ warn to keep a reference to the callback object for as long as C uses it — the garbage collector will otherwise free it while C still holds the pointer, which is this page's trap in Python. - Go. cgo cannot pass a Go func to C; the pattern is an
//exported Go function as the trampoline and aruntime/cgo.Handle(Go 1.17) as thevoid *— an integer that stands for the Go value, deleted withh.Delete()when C is done, which isBox::from_rawby another name.
Po polsku¶
C przyjmuje wywołanie zwrotne (callback) jako goły wskaźnik do funkcji plus void * na dane użytkownika, więc domknięcie Rusta przechodzi przez granicę w dwóch częściach: trampolina extern "C" fn, która rzutuje wskaźnik z powrotem na domknięcie, i sam wskaźnik do domknięcia. Trzeba pilnować, jak długo te dane żyją — C może trzymać wskaźnik dłużej niż trwa wywołanie. Panika wewnątrz extern "C" fn od Rusta 1.81 przerywa proces (abort); extern "C-unwind" pozwala odwinąć stos przez granicę, a catch_unwind w trampolinie w ogóle do tego nie dopuszcza.
Szukaj po polsku: wywołania zwrotne przez FFI · trampolina · odwijanie stosu przez granicę języków · rust ffi callback closure void pointer trampoline · rust extern C-unwind panic abort