How do I pass a closure through raw pointers as an argument to a C function?

First, some logical errors with the code:

  1. It is not correct to cast pointers to i32 on many platforms (like 64-bit). Pointers may use all of those bits. Truncating a pointer and then calling a function at the truncated address will lead to Really Bad Things. Generally you want to use a machine-sized integer (usize or isize).

  2. The sum value needs to be mutable.

The meat of the problem is that closures are concrete types that take up a size unknown to the programmer, but known to the compiler. The C function is limited to taking a machine-sized integer.

Because closures implement one of the Fn* traits, we can take a reference to the closure’s implementation of that trait to generate a trait object. Taking a reference a trait leads to a fat pointer that contains two pointer-sized values. In this case, it contains a pointer to the data that is closed-over and a pointer to a vtable, the concrete methods that implement the trait.

In general, any reference to or Box of a dynamically-sized type type is going to generate a fat pointer.

On a 64-bit machine, a fat pointer would be 128 bits in total, and casting that to a machine-sized pointer would again truncate the data, causing Really Bad Things to happen.

The solution, like everything else in computer science, is to add more layers of abstraction:

use std::os::raw::c_void;

fn enum_wnd_proc(some_value: i32, lparam: usize) {
    let trait_obj_ref: &mut &mut FnMut(i32) -> bool = unsafe {
        let closure_pointer_pointer = lparam as *mut c_void;
        &mut *(closure_pointer_pointer as *mut _)
    };
    println!(
        "predicate() executed and returned: {}",
        trait_obj_ref(some_value)
    );
}

fn main() {
    let mut sum = 0;
    let mut closure = |some_value: i32| -> bool {
        println!("I'm summing {} + {}", sum, some_value);
        sum += some_value;
        sum >= 100
    };

    let mut trait_obj: &mut FnMut(i32) -> bool = &mut closure;
    let trait_obj_ref = &mut trait_obj;

    let closure_pointer_pointer = trait_obj_ref as *mut _ as *mut c_void;
    let lparam = closure_pointer_pointer as usize;

    enum_wnd_proc(20, lparam);
}

We take a second reference to the fat pointer, which creates a thin pointer. This pointer is only one machine-integer in size.

Maybe a diagram will help (or hurt)?

Reference -> Trait object -> Concrete closure
 8 bytes       16 bytes         ?? bytes

Because we are using raw pointers, it is now the programmers responsibility to make sure that the closure outlives where it is used! If enum_wnd_proc stores the pointer somewhere, you must be very careful to not use it after the closure is dropped.


As a side note, using mem::transmute when casting the trait object:

use std::mem;
let closure_pointer_pointer: *mut c_void = unsafe { mem::transmute(trait_obj) };

Produces a better error message:

error[E0512]: transmute called with types of different sizes
  --> src/main.rs:26:57
   |
26 |     let closure_pointer_pointer: *mut c_void = unsafe { mem::transmute(trait_obj) };
   |                                                         ^^^^^^^^^^^^^^
   |
   = note: source type: &mut dyn std::ops::FnMut(i32) -> bool (128 bits)
   = note: target type: *mut std::ffi::c_void (64 bits)

Error E0512.


See also

Leave a Comment