Rust and C++ interoperability - c++ lambdas

Hello!
I wrote a library in Rust. I want to use the library in a C++ project. I am doing binding to C/C++ now.
I have a problem with passing C++ lambda to C API (Rust) on the C++ side.
My program loses lambda's context - SIGSEGV - Segmentation violation signal

I based on:

  1. FFI - The Rustonomicon
  2. Rust Closures in FFI · Michael-F-Bryan

Have any of you done such a thing?
Where can I find an example project?

I particularly care about some C ++ lambda wrapper tools.

I found it:

  1. stackoverflow
  2. C-Cpp-Notes
  3. nextptr
1 Like

I don't think lambdas are FFI safe. Try manually passing a pointer to the invoked function and another one to its environment instead.

1 Like

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 external 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.

1 Like

Can you show us a simplified example of how you are passing the lambda to Rust?

A lambda is a C++ object with an operator() method, but Rust doesn't understand C++ operators and interpreting a pointer to an object as a function pointer isn't going to work. That means you would need to pass Rust a void * pointer to the lambda then some sort of trampoline function which can cast the void * back to the lambda and invoke it.

I did solve it(I think so).
I have to store callback objects in a class on the C ++ side (FunctionCallback and CClosure).

I use cxx.rs for binding:

#[cxx::bridge]
mod ffi {
    #[derive(Debug)]
    #[repr(i32)]
    pub enum CbType
    {
        UNREGISTERED,
        ERROR,
        BOOL,
        U8,
        U16,
        U32,
        U64,
        F32,
    }

    pub struct CClosure
    {
        cb: *const c_char,
        cb_args: *mut c_char
    }

    #[namespace = "rust_part"]
    extern "Rust" 
    {
        type Driver;
        unsafe fn create_driver(proto_spec: &CxxString) -> Result<Box<Driver>>;
        unsafe fn set_callback(self: &mut Driver, callback: CClosure, cb_type: CbType);
    }
}

I use CClosure for passing lambda, it's a little hack because CXX does not support callbacks.

Rust code:

use crate::bindings::ffi::CClosure;

type GenericCallback = *const c_char;
type ClosureArgs = *mut c_char;

pub type UnregisteredVarCallbackBox = Box<dyn FnMut(Pin<&mut CxxString>, u32, ClosureArgs)>;  

pub struct UnregisteredVarClosure
{
    function: UnregisteredVarCallbackBox,
    args: *mut c_char
}

pub struct Driver
{
    var_map: HashMap<Var, u32>,
    callback_unregistered: Option<UnregisteredVarClosure>,
}

impl Driver 
{
    pub unsafe fn set_callback(&mut self, callback: CClosure, cb_type: ffi::CbType)
    {
        match cb_type
        {
            ffi::CbType::UNREGISTERED => { 
                self.set_callback_unregistered(callback);
             }, 
            _ => { println!("set_callback_generic unknown cb_type: {:?}", cb_type); }
        }
    }

    pub unsafe fn set_callback_unregistered(&mut self, callback: CClosure)
    {
        println!("set_callback_unregistered");
        let function = std::mem::transmute::<*const c_char, fn(Pin<&mut CxxString>, u32, ClosureArgs)>(callback.cb);
        self.callback_unregistered = Some(UnregisteredVarClosure{function: Box::new(function), args: callback.cb_args});
    }
}

Calling the callback from Rust:

let var_cb = self.callback_unregistered.as_mut().expect("callback_unregistered is None");

let cb_func = var_cb.function.as_mut();

if var_cb.args.is_null() {
   println!("var_cb.args is null");
                        //exit
}
                    
let len = self.var_map.len() as u32; //field struct Driver
cb_func(index, len, var_cb.args);

C++ Class header:

using callback_closure_t = void (*) (rust::string, uint32_t, void* context);
using FunctionCallback = std::function<void(std::string t, uint32_t size)>;

class MyClass 
{     
public:
    MyClass();
    void set_cb(FunctionCallback callback_lambda);

private:
    ::rust_part::Driver* _driver{nullptr};
    CClosure _unresignedCb;
    FunctionCallback _callback_object;
};

C++ Class:

void MyClass::set_cb(FunctionCallback callback_lambda)
{
    callback_closure_t adpter_for_lambda = [](std::string t, uint32_t n, void* context) 
    {
        FunctionCallback* pFunc = reinterpret_cast<FunctionCallback*>(context);
        (*pFunc)(t, n);
    };

    _callback_object = callback_lambda; 

    _unresignedCb.cb_args = (char*)&_callback_object;
    _unresignedCb.cb = (const char*)adpter_for_lambda;
        
    _driver->set_callback(_unresignedCb, CbType::UNREGISTERED);
}

C++ main:

(...)
    
 MyClass driver = MyClass::MyClass();

 uint32_t counter = 100;

 auto callback_lambda = [&](std::string id, uint32_t var_count){ 
        std::cout << "[callback_lambda] var_count = " << var_count << " | id: " << id << " | counter = " << counter++ << std::endl;
 };

 driver.set_cb(callback_lambda);

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.