🎓 A1Learner
🔍

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?