Passing Go/Rust callbacks to a C library
FFI callback mechanics.
C needs a plain function pointer; in Go use //export with cgo, in Rust an extern "C" fn; carry state via a void* user-data param.
WHAT THIS TESTS Whether you understand that C callbacks are plain function pointers, how each language produces a compatible function, how to carry state, and the undefined behavior risks at the boundary.
A GOOD ANSWER COVERS The C signature expects a pointer to a function with C calling convention and no captured environment. In Rust you write an extern "C" fn matching the parameter types and pass it where the function pointer is expected. Because such a function cannot capture, you smuggle state through the conventional void* user-data argument: the C API passes it back to the callback, where you cast it from *mut c_void to your concrete type. In Go with cgo you mark a function //export name so cgo emits a C-callable symbol, then pass C.name as the pointer; since Go closures and Go pointers cannot cross into C arbitrarily, you typically register your state in a Go-side map keyed by an integer handle and pass that handle as the user data, looking it up inside the exported function. Safety considerations dominate: a panic in Rust or a Go panic must not unwind across the FFI boundary, since that is undefined behavior, so you catch_unwind in Rust and recover in Go; the state must outlive every invocation; and you must respect cgo's pointer-passing rules.
COMMON WRONG ANSWERS Passing a capturing Rust closure directly as a fn pointer; only non-capturing functions coerce to extern "C" fn. Passing a Go pointer to Go memory into C and storing it, violating cgo rules. Letting exceptions or panics propagate across the boundary.
LIKELY FOLLOW-UPS How do you thread context with the void* parameter and reconstruct it safely? Why does cgo restrict passing Go pointers to C? How do thread and goroutine contexts complicate callbacks invoked from C threads? How do you ensure the registered state is not garbage collected?
ONE CONCRETE EXAMPLE Rust: unsafe extern "C" fn cb(result: c_int) { ... } passed as Some(cb); for state, an API variant taking void* receives Box::into_raw(state) and the callback does &mut *(ud as *mut State). Go: //export goCallback with a global map of handle to closure; pass C.int handle as user data and the exported function recovers the closure from the map, wrapping the body in recover to prevent panics crossing into C.
Read the original → doc.rust-lang.org
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.