If we want to introduce Rust into an existing C++ codebase, one of the most challenging apsects is the fact, that Rust’s trait semantics are not the same as those of a pure virtual class in C++. However, given that a C++ codebase will often require use to implement interfaces we somehow have to bridge this gap. In this post we’ll explore one way to make this happen.
Disclaimer
This post assumes C++ & Rust knowledge, further we assume some familiarity with the cxx crate.
The C++ Code
As usual, we’ll stick to a simple example as to not obscure what we’re doing. Consider the following C++ object model:

Simple, we have an interface (`IInterface`) specifying an interface, a single implementation of the interface (`CppImpl`) and a consumer (`Consumer`).
As basic C++ this would look as follows:
class IInterface { public: virtual ~IInterface() = default; virtual int SomeMethod(float param) = 0; }; class CppImpl: public IInterface { public: int SomeMethod(float param) override { return (int) (param + 1048.0f); } }; class Consumer { public: int consume(IInterface& instance, float param){ return instance.SomeMethod(param); } }; void main() { Consumer c; CppImpl i; auto result = c.consume(i, 47.0); assert(result == 1048 + 47); }
Introducing Rust
Now, let’s assume we want to introduce Rust into the above “project” and we want to have a Rust implementation of IInterface, how would we go about this? Straight inheritance across the language boundary is not possible, however we can do something that gets close.
With `cxx` it’s possible to expose a struct and its methods to C++ by creating an appropiate FFI:
pub struct IfImpl { } impl IfImpl { pub fn SomeMethod(&self, val: f32) -> i32 { val as i32 + 10 } } #[cxx::bridge] mod ffi { extern "Rust" { type IfImpl; fn SomeMethod(self: &IfImpl, val: f32) -> i32; } }
This is fairly straight forward, however, there are two elephants in the room here:
* How do we introduce our Rust implementation of the interface into the typehierarchy of the C++ project? Ideally in a typesafe way?
* How do we manage ownership of a Rust struct, that is passed over to C++ (e.g. if `consumer` were to store the instance in a vector or similar)?
It turns out, that the answer to both of these questions is some very basic C++ template programming. We introduce a wrapper class for the Rust structs, that is part of our target type hierarchy:
template<typename implTy> class RustWrapper: public IInterface { public: RustWrapper(rust::Box<implTy>&& t): t(std::move(t)){}; int SomeMethod(float value) { return t->SomeMethod(value); } private: rust::Box<implTy> t; };
Not all that much magic here: The RustWrapper relays all calls to interface methods to the embedded instance. Using a template here allows us to have all the typesafety we need, since the C++ compiler will check the types of the cxx interface against the implementation of our wrapper class (and also the presence of all required methods!). The neat bit here is, that the wrapper can be used for all structs that are supposed to implement `IInterface`.
What remains to be discussed is the ownership problem. `cxx` luckily brings support for Rusts “Box\<T>” type, which allows us to pass a pointer to a heap allocation across the boundary and keep it inside the RustWrapper class. The ownership of the underyling object now rests in the C++ part of the codebase and will behave as expected from C++. However we’ll have to construct our box on the Rust side:
pub struct IfImpl { } impl IfImpl { pub fn SomeMethod(&self, val: f32) -> i32 { val as i32 + 10 } } fn make_impl() -> Box<IfImpl> { return Box::new(IfImpl{}); } #[cxx::bridge] mod ffi { extern "Rust" { type IfImpl; fn SomeMethod(self: &IfImpl, val: f32) -> i32; fn make_impl() -> Box<IfImpl>; } }
And now, we can create use the whole lot in C++;
void main() { Consumer c; CppImpl i; auto result = c.consume(i, 47.0); assert(result == 1048 + 47); RustWrapper<IfImpl> impl(std::move(make_impl())); result = c.consume(impl, 255.0); assert(result == 255); }
Sidenotes
The example of this post shows one way to integrate Rust with an existing class hierarchy. Obviously there are details missing that will complicate things in a real-world scenario (e.g. the need to pass non-trivial types into methods). Further the requirement to use heap allocations to pass ownership through the language boundary is probably a no-go for some use cases (e.g. embedded). I’ll probably explore this topic further in future blog posts, so stay tuned.
1 thought on “Rust and C++ Interop: Interfaces across the language boundary”