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