Rust FFI Callbacks and User Data
A callback is a function that C calls while it works. C carries the state the callback needs through a plain void pointer, called user data.
The shape C expects
The C function takes your function and one extra pointer. It promises to hand that pointer back on every call.
void for_each(const int* values, int len,
void (*visit)(int value, void* user),
void* user);Pack the notebook
Totals is the notebook for this walk. It starts empty and travels with the call.
struct Totals {
sum: i32,
count: usize,
}Your callback, in Rust
The callback turns the plain pointer back into your struct. Then it updates the struct.
extern "C" fn visit(value: i32, user: *mut c_void) {
let totals = unsafe { &mut *(user as *mut Totals) };
totals.sum += value;
totals.count += 1;
}Hand both pointers over
One cast turns the struct pointer into the plain pointer C wants. The cast forgets the type on purpose.
let user = &mut totals as *mut Totals as *mut c_void;
unsafe { for_each(values.as_ptr(), 3, visit, user) };The walk fills the notebook
After the call, the struct holds what the callback collected. The C side never knew what it was carrying.
println!("sum = {}", totals.sum);
println!("visited = {}", totals.count);Example
use std::ffi::{c_int, c_void};
unsafe extern "C" {
fn for_each(values: *const c_int, len: c_int, visit: extern "C" fn(c_int, *mut c_void), user: *mut c_void);
}
struct Totals {
sum: i32,
count: usize,
}
extern "C" fn visit(value: c_int, user: *mut c_void) {
let totals = unsafe { &mut *(user as *mut Totals) };
totals.sum += value;
totals.count += 1;
}
fn main() {
let values = [10, 20, 30];
let mut totals = Totals { sum: 0, count: 0 };
let user = &mut totals as *mut Totals as *mut c_void;
unsafe { for_each(values.as_ptr(), values.len() as c_int, visit, user) };
println!("sum = {}", totals.sum);
println!("visited = {}", totals.count);
}Key points
- Callback: a function that C calls for you.
- You hand it over once.
- C decides when it runs
- The callback type must be extern "C".
- A plain Rust fn has the wrong calling rules
- Use the user data pointer to carry your state.
- Cast it back to your struct type inside the callback
- The state must outlive the whole walk.
- C may call back at any moment while the walk runs
- Keep the callback small and calm.
- It must never panic.
- Unwinding out of it into C is undefined behaviour
- C knows nothing about closures.
- A callback cannot capture variables the way a Rust closure does.
- A walker, a sorter, or an event loop can be the C code that calls the callback for each item.
In this example, a small C function does the walking. for_each takes the array, the callback visit, and one user pointer. Rust packs its state into Totals. It casts the address to the plain c_void pointer C expects. Each time C meets a value, it calls visit with that value and the same user pointer. visit casts the pointer back to Totals and updates it by hand. That cast does the job a closure capture does for free. After the walk, Totals holds sum = 60 and visited = 3.
Note
The user data pointer is the capture, done by hand
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
Why do C callbacks so often take a void* user argument?