Let's start with an example, of Rust exporting a cb-calling function over the FFI:
#[no_mangle] pub unsafe extern "C"
fn call_cb (
state: *mut c_void,
cb: Option<unsafe extern "C" fn(state: *mut c_void, arg: i32) -> u8>,
) -> i8
{
if let Some(cb) = cb {
dbg!( cb(state, 42) );
0
} else {
-1
}
}
Now, let's consider a Rust-only case first, shall we? Not really FFI, but it should help us understand what other languages may be doing:
//! Rust, but potentially in another compilation environment (Rust-Rust FFI)
use ::std::{num::NonZeroI8, os::raw::c_void};
mod ffi {
use super::*;
extern "C" {
pub(super)
fn call_cb (
state: *mut c_void,
cb: Option<unsafe extern "C" fn(state: *mut c_void, arg: i32) -> u8>,
) -> i8;
}
}
fn call_cb<Closure : FnMut(i32) -> u8> (
mut closure: Closure, // dubbed `lambda` in C++
) -> Result<(), NonZeroI8>
{
let state: *mut c_void = <*mut _>::cast(&mut closure);
unsafe extern "C"
fn c_cb<Closure : FnMut(i32) -> u8> (
state: *mut c_void,
arg: i32,
) -> u8
{
(*state.cast::<Closure>())(arg)
}
let status = unsafe { ffi::call_cb(state, Some(c_cb::<Closure>)) };
if let Some(err) = NonZeroI8::new(status) {
Err(err)
} else {
Ok(())
}
}
e.g.
fn main ()
{
// e.g.
let closure = {
let upvar = 27;
move |arg: i32| {
dbg!(upvar, arg);
(upvar + arg) as u8
}
};
let _ = dbg!( call_cb(closure) );
}
Well, in C++, it can be kind of similar:
#include <cstdint>
#include <iostream>
extern "C" {
uint8_t call_cb (
void * state,
uint8_t (*cb)(void * state, int32_t arg)
);
}
for the exter
nal declaration, and then:
typedef struct cb_raw_parts {
void * state;
uint8_t (*cb)(void * state, int32_t arg);
} cb_raw_parts_t;
template<typename Lambda>
uint8_t c_cb (void * state, int32_t arg)
{
return (*static_cast<Lambda *>(state))(arg);
}
template<typename Lambda>
cb_raw_parts_t lambda_into_raw_parts (Lambda & lambda)
{
cb_raw_parts_t ret {};
ret.state = static_cast<void *>(&lambda);
ret.cb = c_cb<Lambda>;
return ret;
}
Usage:
int main(int argc, char const * argv[]) {
int upvar = 27;
auto lambda = [=](int32_t arg) -> uint8_t {
std::cout << upvar << " " << arg << std::endl;
return static_cast<uint8_t>(arg + upvar);
};
auto raw_parts = lambda_into_raw_parts(lambda);
auto status = call_cb(raw_parts.state, raw_parts.cb);
if (status != 0) {
std::cerr << "Call failed." << std::endl;
return -1;
}
return 0;
}
Now, the helper functions can be inlined: with +[]
syntax, it looks like we can force C++ to define a capture-less closure that shall be coerce into a raw pointer directly, allowing for c_cb<>
to be inlineable:
template<typename Lambda>
cb_raw_parts_t lambda_into_raw_parts (Lambda & lambda)
{
cb_raw_parts_t ret {};
ret.state = static_cast<void *>(&lambda);
ret.cb = +[](void * state, int32_t arg) -> uint8_t {
return (*static_cast<Lambda *>(state))(arg);
};
return ret;
}
and at that point even lambda_into_raw_parts
can be inlined, with the only hiccup that we'll lose access to the Lambda
generic / template type parameter name; and we'll need to use decltype(lambda)
in its stead:
Final C++ snippet
#include <cstdint>
#include <iostream>
extern "C" {
uint8_t call_cb (
void * state,
uint8_t (*cb)(void * state, int32_t arg)
);
}
int main(int argc, char const * argv[]) {
int upvar = 27;
auto lambda = [=](int32_t arg) -> uint8_t {
std::cout << upvar << " " << arg << std::endl;
return static_cast<uint8_t>(arg + upvar);
};
auto status = call_cb(
static_cast<void *>(&lambda),
+[](void * state, int32_t arg) -> uint8_t {
return (*static_cast<decltype(lambda) *>(state))(arg);
}
);
if (status != 0) {
std::cerr << "Call failed." << std::endl;
return -1;
}
return 0;
}
Caveats
-
If Rust is not to call the cb before returning, then make sure to correctly handle the "lifetime" of the C++ lambda captured state (e.g., use [=]
captures if possible). Heap-allocating it (and making sure not to free / drop-RAII it in the meantime) may be necessary.
-
Make sure Rust and C++ are using the same calling convetions when talking to each other.