I've been trying to write some Rust code that passes a callback into a C library which then invokes it. I think that I've got the basic structure, and it works when I compile in debug mode (1.22.1, x86_64-unknown-linux-gnu) ... but everything breaks when I compile in release mode. Would anyone be able to help me figure out if I'm doing something wrong or if I'm triggering some obscure compiler bug here?
Here's my C code (native.c
):
typedef struct RustCallback {
void (*function)(void *closure_data);
void *closure_data;
} RustCallback;
int
testcase(RustCallback *callback)
{
callback->function(callback->closure_data);
return 0;
}
And here's my Rust code (testcase.rs
):
use std::os::raw::{c_void, c_int};
#[repr(C)]
struct RustCallback {
pub function: Option<unsafe extern "C" fn(closure_data: *mut c_void)>,
pub closure_data: *mut c_void,
}
extern "C" {
fn testcase(callback: *mut RustCallback) -> c_int;
}
extern "C" fn c_callback<F>(ctxt: *mut c_void) where F: FnMut() {
let f: &mut F = unsafe { &mut *(ctxt as *mut F) };
f()
}
fn new_cb<F>(mut f: F) -> RustCallback where F: FnMut() {
RustCallback {
function: Some(c_callback::<F>),
closure_data: &mut f as *mut _ as *mut c_void,
}
}
fn main() {
let x = 7usize;
println!("OUTSIDE: {:12x} {:12x}", x, &x as *const _ as u64);
let mut cb = new_cb(|| {
println!(" INSIDE: {:12x} {:12x}", x, &x as *const _ as u64);
});
let rv = unsafe { testcase(&mut cb) };
if rv != 0 {
println!("prevent rv from disappearing");
}
}
If it comes in handy, here's a ninja script (build.ninja
) that compiles debug and release versions of the code. Hopefully it is easy to parse even if you are not familiar with ninja:
rule cc
command = cc -O0 -ffunction-sections -fdata-sections -fPIC -g -m64 -Wall -Wextra -MD -MF $out.d -o $out -c $in
description = CC $out
depfile = $out.d
deps = gcc
rule ar_rel
command = ar crs $out $in
description = AR $out
rule rustc
command = rustc --crate-name $out $in --crate-type bin --emit=link -C debuginfo=2 $extra_args -L native=. -l static=$static_name
description = RUST $out
build native.o: cc native.c
build libnative.a: ar_rel native.o
build debug: rustc testcase.rs | libnative.a
static_name = native
build release: rustc testcase.rs | libnative.a
static_name = native
extra_args = -C opt-level=3
Here's the type of output I get:
$ ./debug
OUTSIDE: 7 7ffd47cf28e0
INSIDE: 7 7ffd47cf28e0
$ ./release
OUTSIDE: 7 7ffd6299d290
INSIDE: 55669ae31b40 55669b0474d0
And here are some of the fun things I've found:
- If I restructure the code to not use the RustCallback struct, I can get something that works reliably in both modes
- If I ignore the return value of the
testcase
function, it works in both modes (!) - The code shown above fails in both modes if I compile with nightly
These features tend to make me think that I'm doing something that's not quite right, but if I am, I can't figure out what's going on. Anyone have any insight?
Thanks!