Safest way to call Rust from C++ (for Rust program)

One way to construct and destruct C++ objects from Rust is to call the constructor and return an int64_t pointer to Rust. Then, Rust can call methods on the object by passing the int64_t which will be cast to the pointer again.

void do_something(int64_t pointer, char* methodName, ...) {
    //cast pointer and call method here
}

however this is extremely unsafe. Instead I tend to store the pointer into a map and pass the map key to Rust, so it can call C++ back:

void do_something(int id, char* methodName, ...) {
    //retrieve pointer from id and call method on it
}

Now, imagine I create, from Rust, a C++ object that calls Rust back. I could do the same: give C++ an int64_t and then C++ calls Rust:

#[no_mangle]
pub fn do_something(pointer: i64, method_name: &CString, ...) {

}

but that's also insecure. Instead I'd do something similar as C++, using an id:

#[no_mangle]
pub fn do_something(id: u32, method_name: &CString, ...) {
    //search id in map and get object
    //execute method on the object
}

however, this isn't possible, as Rust does not have support for static variables like a HashMap. And Rust's lazy_static is immutable.

The only way to do safe calls from C++ back to Rust is to pass the address of something static (the function do_something) so calling it will always point to something concrete. Passing pointers is insecure as it could stop existing. However there should be a way for this function to maintain a map of created objects and ids.

So, how to safely call Rust object functions from C++? (for a Rust program, not a C++ program). Can I maintain such a list as I said?

  • You can pass pointers between Rust and C++ directly. You don't need to cast them to integers or hack around with global maps and UIDs.
  • And none of this will ever be safe, pretty much by definition, since the languages don't have compatible memory models and semantics.
  • A limited set of interactions can be made somewhat safer using tools like cxx.
2 Likes

This is one of the main uses of interior mutability: you can store an object in the lazy_static that allows the operations you need to do through an &-reference. For example, you could store something like this:

(My apologies for the closure-heavy API here; there are other ways to do this, but this is apparently the mood Iā€™m in this morning)

pub struct Registry<T> {
   table: Mutex<HashMap<u32, Arc<Mutex<T>>>>,
   next_id: AtomicU32
}

impl<T> Default for Registry<T> {
    fn default()->Self {
        Registry {
            table: Default::default(),
            next_id: Default::default()
        }
    }
}

impl<T> Registry<T> {
    pub fn new()->Self { Default::default() }

    fn new_id(&self)->u32 {
        use std::sync::atomic::Ordering::*;
        self.next_id.fetch_add(1, Relaxed)
    }

    fn with_table<O>(&self, thunk: impl FnOnce(&mut HashMap<u32, Arc<Mutex<T>>>)->O)->O {
        thunk(&mut self.table.lock().unwrap())
    }

    pub fn insert(&self, val:T)->u32 {
        let id = self.new_id();
        let val = Arc::new(Mutex::new(val));
        self.with_table(|t| t.insert(id, val));
        id
    }

    pub fn update<O>(&self, id:u32, thunk:impl FnOnce(&mut T)->O)->Result<O,&'static str> {
        match self.with_table(|t| t.get(&id).cloned()) {
            Some(val) => Ok(thunk(&mut *(val.lock().unwrap()))),
            None => Err("item not present")
        }
    }
    
    /// If item is present and not in use, returns `Some(Ok(T))`
    /// If item is not present, returns `None`
    /// If item is present but currently in use, returns `Some(Err(Arc<Mutex<T>>))`
    ///    NB: item is still removed in this case!

    pub fn remove(&self, id:u32)->Option<Result<T, Arc<Mutex<T>>>> {
        self.with_table(|t| t.remove(&id))
            .map(|arc| Arc::try_unwrap(arc)
                           .map(|mutex| mutex.into_inner().unwrap()))
    }
}

(Playground)

1 Like

I am somewhat confused by the jump from "call Rust safely from C++" to suggesting handles and hashmaps to keep objects alive. Is it because C++ couldn't otherwise manage the lifecycle of the Rust object, destroying it when it's gone out of scope in C++? Or is it the reverse, a way to defer destruction to avoid use after free?

In either case, my impression from the proposed approach and the following observation:

is that you are pursuing a way to call Rust from C++ that is safer than calling C++ from C++. That strikes me as beyond what would be necessary in practice. If we aren't going to use handles and hashmaps every time C++ is called from C++, I don't see a reason to reach for that for Rust called from C++ either.

In response to "So, how to safely call Rust object functions from C++?", this is directly supported by the cxx crate. The tutorial on the website has one example of this but in brief it would be something like:

// src/main.rs

#[cxx::bridge]
mod ffi {
    extern "Rust" {
        type YourRustType;

        fn your_method(self: &YourRustType);
    }

    unsafe extern "C++" {
        fn do_cpp_stuff(thing: Box<YourRustType>);
    }
}

pub struct YourRustType {...}
impl YourRustType {
    pub fn your_method(&self) {...}
}

fn main() {
    let thing = Box::new(YourRustType {...});
    ffi::do_cpp_stuff(thing);
}
// impl.cc

#include "rust/cxx.h"
#include "src/main.rs.h"

void do_cpp_stuff(rust::Box<YourRustType> thing) {
  thing->your_method();
}

This passes ownership of thing to C++, but you could alternatively pass just a reference instead.

    let thing = YourRustType {...};
    ffi::do_cpp_stuff(&thing);
void do_cpp_stuff(const YourRustType &thing) {
2 Likes