I found myself with a problem that, from googling around, seems frequent: Rust complaining about a missing 'static
when storing a dyn FnMut(...) -> ...
into a Box
. More specifically this happened when interfacing a C library that works with callbacks. The two solutions I came up with however seem to silently break Rust's lifetime checks.
I use a solution presented in this stackoverflow answer, which I adapted hereafter:
use std::os::raw::c_void;
use std::mem;
use std::ptr;
/* ***********************************************************
* Code generated by bindgen
* ***********************************************************/
#[derive(Debug, Copy, Clone)]
pub struct context {
_unused: [u8; 0],
}
pub type context_t = *mut context;
pub type callback_fn_t = Option<unsafe extern "C" fn(ctx: context_t, data: *mut c_void) -> i32>;
pub type free_fn_t = Option<unsafe extern "C" fn(ctx: context_t, data: *mut c_void) -> ()>;
/*
extern "C" {
pub fn c_register_callback(ctx: context_t, cb: callback_fn_t, data: *mut c_void, free: free_fn_t);
pub fn c_call_callback(ctx: context_t);
pub fn c_free_context(ctx: context_t);
}
*/
/* ***********************************************************
* Mock implementation of the C API
* ***********************************************************/
static mut CONTEXT: context_t = ptr::null_mut() as *mut _;
static mut DATA: *mut c_void = ptr::null_mut() as *mut _;
static mut CB: callback_fn_t = None;
static mut FREE: free_fn_t = None;
extern "C" fn c_register_callback(ctx: context_t, cb: callback_fn_t, data: *mut c_void, free: free_fn_t) {
unsafe {
CONTEXT = ctx;
CB = cb;
FREE = free;
DATA = data;
}
}
extern "C" fn c_call_callback(ctx: context_t) {
unsafe {
if let Some(cb) = CB {
cb(ctx, DATA);
}
}
}
extern "C" fn c_free_context(ctx: context_t) {
unsafe {
if let Some(free) = FREE {
free(ctx, DATA);
}
}
}
/* ***********************************************************
* First version, using Box<Box<dyn FnMut(context_t) -> i32>>
* ***********************************************************/
extern "C" fn rust_generic_callback(ctx: context_t, data: *mut c_void) -> i32 {
let cb: &mut Box<dyn FnMut(context_t) -> i32> = unsafe { mem::transmute(data) };
cb(ctx)
}
extern "C" fn rust_generic_free(ctx: context_t, data: *mut c_void) {
let _: Box<Box<dyn FnMut(context_t) -> i32>> = unsafe { Box::from_raw(data as *mut _) };
}
fn rust_register_callback<F>(ctx: context_t, cb: F)
where F: FnMut(context_t) -> i32 {
let cb: Box<Box<dyn FnMut(context_t) -> i32>> = Box::new(Box::new(cb));
unsafe { c_register_callback(
ctx, Some(rust_generic_callback),
Box::into_raw(cb) as *mut _,
Some(rust_generic_free))
};
}
Note that contrary to the stack overflow question, I did not add + 'static
to the F
type in the definition of rust_register_callback
. Rust does not complain about it, even though obviously not all callbacks would work fine. Example:
fn install_callback_with_shorter_lifetime(ctx: context_t) {
let mut x = 42;
let y = &mut x;
let cb = move |ctx: context_t| -> i32 {
println!("Hello from callback, value is {}", *y);
*y = 45;
*y
};
rust_register_callback(ctx, cb);
unsafe { c_call_callback(ctx); }
}
fn main() {
let ctx: context_t = ptr::null_mut() as *mut _;
install_callback_with_shorter_lifetime(ctx);
unsafe { c_call_callback(ctx); }
unsafe { c_free_context(ctx); }
}
This prints 42 when c_call_callback
is called within install_callback_with_shorter_lifetime
, but garbage the second time it's called.
My question here is: why Rust didn't warn me about this potential lifetime issue?
Next, I tried the following:
/* ***********************************************************
* Second version, wrapping ctx into a Context object
* and wrapping callback in a MyCallback object
* ***********************************************************/
struct MyCallback<'a> {
pub cb: Box<dyn FnMut(context_t) -> i32 + 'a>,
}
struct Context {
pub ctx: context_t
}
extern "C" fn rust_generic_callback2(ctx: context_t, data: *mut c_void) -> i32 {
let my_callback: &mut MyCallback = unsafe { mem::transmute(data) };
(my_callback.cb)(ctx)
}
extern "C" fn rust_generic_free2(ctx: context_t, data: *mut c_void) {
let _: Box<MyCallback> = unsafe { Box::from_raw(data as *mut _) };
}
impl Context {
pub fn new() -> Self {
Context { ctx: ptr::null_mut() as *mut _ }
}
pub fn register_callback<'a: 'b, 'b, F>(&'b self, cb: F)
where F: FnMut(context_t) -> i32 + 'a {
let cb = MyCallback { cb: Box::new(cb) };
let cb = Box::new(cb);
unsafe { c_register_callback(
self.ctx, Some(rust_generic_callback2),
Box::into_raw(cb) as *mut _,
Some(rust_generic_free2))
};
}
pub fn call_callback(&self) {
c_call_callback(self.ctx);
}
}
impl Drop for Context {
fn drop(&mut self) {
c_free_context(self.ctx);
}
}
My initial version did not have lifetime specifiers (in MyCallback
and in register_callback
), and Rust was complaining about missing a lifetime specifier, and suggesting + 'static
. I had to read a lot of tutorials and posts on this forum to understand what this meant and realized "I don't want a 'static
lifetime, I know that the callback is not going to be called after the context is destroyed, I want a lifetime that reflects that". Hence I added a lifetime 'b
for the context 'a
for the closure, which must outlive 'b
. Adding the 'a
and 'b
lifetimes solved the compilation error, however it does not seem to solve the problem. I can still do something like this without any warning from Rust:
fn f(ctx: &Context) {
let mut x = 42;
let y = &mut x;
let cb3 = move |ctx: context_t| -> i32 {
println!("Hello from callback, value is {}", *y);
*y = 45;
*y
};
ctx.register_callback(cb3);
ctx.call_callback();
}
fn main() {
let ctx: Context = Context::new();
f(&ctx);
ctx.call_callback();
}
Once again, the first print statement works fine, while the second gives garbage data for the value of *y
.
Once again, my question is: what prevented Rust from detecting that I am doing something invalid? And how do I make it such that I can't do such a thing?
Note: There are a few calls to "unsafe" when calling the C functions, but the problem is not down to those unsafe codes. Most of them can actually be removed when using the Rust-based mock version of the C API.