First, some logical errors with the code:
-
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
orisize
). -
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)
See also