Rust program calling Rust method from C++ code

When interfacing with a C++ class on Rust, I came up with this problem: calling back a Rust's method (function from a specific instance) from C++.

This post is a continuation of Pass pointer to itself so C++ can call - #11 by guerlando

The idea here is: instantiate a C++ object in Rust, and then pass a Rust method to this C++ object, so this C++ object can call the method later.

Suppose we want to interface with a C++ class SomeClass from Rust. On the Rust side, we can have a struct SomeClass. So, I want C++'s SomeClass to call a callback in Rust's SomeClass. I followed the advices posted on the answers above, and I'm almost there. A minimal almost reproducible example (dockerized and devcontainer dockerized) can be found here: https://github.com/lucaszanella/cpp_calls_rust_back/tree/0d3d0ae50451be023729e193b492b5fb57a63ae0

As you see in https://github.com/lucaszanella/cpp_calls_rust_back/blob/0d3d0ae50451be023729e193b492b5fb57a63ae0/cpp_calls_rust_back/src/main.rs#L31, I don't know what to pass in these parts, so I marked ?

My example is simple. On creation of Rust's SomeClass, it creates the C++'s SomeClass. Then, we need to pass a pointer to the Rust's SomeClass instance and also the callback. This is what set_rust_object and set_callback does. Then the C++ object should be able to call Rust's function call_on_some_class which should forward the call to Rust's SomeClass.

My example almost builds, so it'd be nice if someone could help me finish it.

I'm not sure if your confusion stems from OO parlance, i.e. that "methods" need to have a "self" receiver.

Methods in Rust are just normal functions with a bit of special syntax to please those coming from Java. (:stuck_out_tongue:) You can call a method just like you would call a free function. You fully-qualify the function name through the type or trait it is defined on, then you pass self as the first argument, then any other arguments. Like this:

struct Foo {
    x: u32,
}

impl Foo {
    fn do_bar(&self, y: u32) -> u32 {
        self.x + y
    }
}

let foo = Foo { x: 42 };

let sum_1 = foo.do_bar(9);
assert_eq!(sum_1, 51);

// this is exactly the same!
let sum_2 = Foo::do_bar(&foo, 9);
assert_eq!(sum_2, 52);

Therefore, calling a Rust method from C++ is nothing more than calling a plain Rust function from C++. For that, you'll have to have at hand:

  • a pointer to the Rust function
  • the values of its prospective arguments, including self.

So you only need to write setters for your C++ class that set its own, appropriate fields. Here is a complete example.

1 Like

sorry, I forgot to mention a very important thing. I cannot rely on the Rust's struct being #[repr(C)], this is where the main problems come from. Because of this, I have to cast the Rust struct when passing to C++ and when receiving from C++, this is the main problem

Someone more familiar with FFI may come along and correct me shortly, bit I think #[repr(C)] is unnecessary as long as:

  • The Rust structure is Sized (i.e. uses a thin pointer),
  • The FFI only refers to a reference/pointer rather than the struct itself, and
  • The C++ code never attempts to dereference the Rust pointer itself.

Yeah, you don't have to use repr(C) if all you do is pass around a pointer. I just slapped it on the type to silence the warning.

Lol, don't do that.

It's only really useful if every field inside the type is also #[repr(C)] and you want to pass the object by value. If the type contains non-#[repr(C)] types (Vec, HashMap, String, etc.) then it still won't be FFI-safe but you may be lulled into a false sense of security.

Instead, like you said, just put the thing behind a pointer and the C side can treat it like an opaque struct.

extern "C" {
    pub fn cpp_new_some_class() -> *mut c_void;
    pub fn cpp_some_class_set_callback(instance: *mut c_void, callback_in_rust: *mut c_void);
    pub fn cpp_some_class_set_rust_object(instance: *mut c_void, rust_object: *mut c_void);
}

You should use an appropriate function type for the callback instead of just writing *mut c_void.

If you pull it out into a type definition like type Callback = unsafe extern "C" fn(instance: *mut c_void, some_arg: *const c_char) the compiler (and any readers of the code) can help make sure the callback is correct. Having a function where both arguments are *mut c_void will probably bite you down the track when you mix up the instance and callback_in_rust pointers.

For the callback itself you'll need a "trampoline" function with the correct signature and calling convention, then internally it'll translate the arguments to the equivalent Rust types and call the appropriate method on your instance.

unsafe extern "C" fn trampoline(this: *mut c_void, string_argument: *const c_char) {
    let some_class = &*(this as *const SomeClass);

    let string_argument = CStr::from_ptr(string_argument).to_string_lossy();
    some_class.some_method(&string_argument);
}

Okay, so this gets a bit tricky because your Rust instance needs to have a stable address (i.e. by putting it behind a Box), but for the purpose of safety we also need to make sure consumers of Box<SomeClass> don't move the SomeClass instance out of the box. Rust (deliberately) doesn't have assignment operators, so if you move the SomeClass off the heap and onto the stack there would be no way of telling C++ that the Rust object's address has changed.

To do this I'd declare something like this:

pub struct SomeClass(Box<Inner>);

struct Inner {
    cpp_some_class_pointer: *mut c_void
}

Then in the SomeClass::new() method you create a Box<Inner> and wire up the callback and instance pointers. This should be done inside the constructor and not in any "helper" methods because it's a Rust anti-pattern for users to be given a half-constructed object.

Another thing to keep in mind is that C++ now holds a "reference" to your SomeClass for the entirety of its lifetime, so all methods will need to use shared references (&self) and use internal mutability. Otherwise you'll be violating Rust's borrowing rules and neither rustc or miri will be able to help you.

This sort of cyclic type (the Rust type has a pointer to C++ and C++ has a pointer to Rust) may also give you troubles around the object's destruction. Presumably your C++ class will want to have a destructor that automatically destroys the Rust type, and the Rust type's Drop impl will want to destroy the C++ type.


Without having more information on what you are trying to achieve I can't propose a better solution, but I feel like the current implementation may have over-complicated things.

Can you get away with just giving the C++ class a closure and then using closing over a Rc<MyRustState> if the callback needs to update things on the Rust side? If so, you may be able to use some of the ideas from an article I wrote:

I know, it was just for the sake of example.

1 Like

I understood what you said. I implemented your ideas, and added the trampoline (which was simply another function name on my example).

The only problem I'm having now is in lines 36 and 40 https://github.com/lucaszanella/cpp_calls_rust_back/blob/9a40a42803423bff69837bd8094cc490898a14b8/cpp_calls_rust_back/src/main.rs#L40 in the part self.0 as *mut c_void. I don't know how to pass an instance of SomeClass to C++.

I took a read on your closures example, it's a very good trick but I think I need to stick with the simpler C style calls.

For context on what I'm doing, I made a C++ OpenVPN client, and I need it to deliver packets to Rust's side of this OpenVPN client. And of course, the Rust users of this OpenVPN class must have a way to pass Rust a callback to deliver the packets to them.

I'm not quite sure what you mean by this, the closures example is just an example of how you get an appropriate pointer and extern "C" function pointer for invoking the callback.

I think you should be passing &*self.0 as *const Inner as *mut c_void. You need the &* to dereference the box and get a reference to its contents.

If that's the case, I'd probably do something like this for the Rust side and something like this for C++.

If the Rust callbacks need access to the OpenVPN client (e.g. so it can send packets of its own or kill the connection) then I'd also pass in a *mut OpenVpnClient pointer to the callbacks and make the C++ expose shim functions for calling various methods on it.

Some things to point out:

  • There are no cycles here, main() owns a *mut OpenVpnClient (a pointer to the C++ class), OpenVpnClient owns Callbacks, and Callbacks owns the boxed Rust object
    • this means no shared ownership, so the Rust type can happily use &mut self methods
  • RAII - Creating an OpenVpnClient and initializing it with our callbacks and pointer to a Rust object is done in one operation (the openvpn_client_new() function in C++)
  • I've used an explicit struct to encapsulate the rust object pointer, callback trampolines, and destructor. This lets me separate the Rust object receiving packets from the shims used to expose it across the FFI boundary
  • I made Callbacks::new() generic over any T: Read + Write, if you only need it to be used with one class you can delete everything within the angle brackets
  • I haven't handled the possibility of panics ("exception safety"), but you should probably wrap calls to Rust methods in std::panic::catch_unwind() for good measure - like C++, unwinding across the FFI boundary is UB in Rust

(making the callbacks emulate Read + Write was kinda arbitrary here, I just used it because it'd give more understandable names than writing SomeClass or callback)

1 Like

On

by doing

pub fn set_callback(&mut self, parent: *mut c_void) {
    unsafe {cpp_some_class_set_callback(&*self.0 as *const Inner as *mut c_void, parent)}
}

with &*self.0 as *const Inner as *mut c_void, I'm passing a pointer to Inner, instead of to SomeClass, so my trampoline:

unsafe extern "C" fn trampoline(this: *mut c_void, i: u32) {
    let some_class = &mut *(this as *mut SomeClass);

    //let string_argument = CStr::from_ptr(string_argument).to_string_lossy();
    some_class.do_something(i);
}

will not work (undefined behaviour as I try to cast Inner as SomeClass).

If I simply passed &*self as *const SomeClass as *mut c_void, parent instead of &*self.0 as *const Inner as *mut c_void, parent, then I could use it in my trampoline, but as you said, it could change addresses.

What you think about std::pin::Pin?

Anyways, I understood your very very great example, and I did a minor modification. There's just one thing that I'd like to be different from your example, and it's what is causing me trouble from the beggining.

My modification: https://github.com/lucaszanella/cpp_calls_rust_back/blob/72407d4ecf5c94d5f04a30c16149837df9f89f3c/cpp_calls_rust_back/src/main.rs

As you can see, there's a problem in line 28: https://github.com/lucaszanella/cpp_calls_rust_back/blob/72407d4ecf5c94d5f04a30c16149837df9f89f3c/cpp_calls_rust_back/src/main.rs#L28

I want the class itself to handle the read and writes from the trampolines. The problem is that I cannot pass a pointer to the class itself before creating it.

One way I could solve this, is to make the trampolines write/read to/from a struct X inside the OVPNClient struct itself, but I don't see how struct X could hold a pointer to its parent OVPNClient and I also think this is not a very good thing to do.

Your suggestions made a very good impact on my design, and I'd like to know what you have to say about this.

1 Like

If using the Inner-SomeClass architecture, you should be casting to Inner and making all SomeClass methods defer to Inner.

i.e.

pub struct SomeClass(Box<Inner>);

impl SomeClass {
  fn foo(&self) { self.0.foo() }
}

struct Inner { ... }

impl Inner { 
  fn foo(&self) { todo!() }
}

You can do that if you want, but what is your story around ownership?

So questions like who owns what, when do things get dropped, how do you make sure the C++ object's destructors run, how do I transfer control between C++ and Rust, how do I prevent double-frees (Rust object drops the C++ object which drops the Rust object which segfaults), how do I want end users to use this code, how do I manage something which is temporarily in a half-initialized state, etc.

These sorts of cyclic/self-referential designs tend to be non-trivial in any language with manual memory management (hence why I tried to avoid it), it's just you'll get a lot more push-back in Rust because it likes to make muddy ownership stories awkward to implement.

That said, you may want to check out Box::new_uninit() and Box::assume_init() if you want uninitialized heap memory (it'll give you a pointer that can be passed to C++). They are trivial to implement in stable Rust:

fn new_boxed_uninit<T>() -> Box<MaybeUninit<T>> {
  Box::new(MaybeUninit::uninit())
}

unsafe fn assume_init<T>(boxed: Box<MaybeUninit<T>>) -> Box<T> {
  let raw = Box::into_raw(boxed);
  Box::from_raw(raw.cast())
}

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.