C++ bindings for Rust traits
Crubit allows C++ callers to invoke Rust trait implementations. For each trait SomeTrait, Crubit generates a matching C++ struct SomeTrait. To call a trait
method SomeTrait::some_fn implemented by SomeType, C++ callers can use
SomeTrait::impl<SomeType>::some_fn(args...).
Example
Given the following Rust crate:
pub trait MyTrait {
fn add_with(&self, y: i32) -> i32;
fn describe(&self) -> &'static str;
}
#[derive(Clone, Copy, Default)]
pub struct MyStruct {
x: i32,
}
impl MyStruct {
pub fn new(x: i32) -> Self {
Self { x }
}
}
impl MyTrait for MyStruct {
fn add_with(&self, y: i32) -> i32 {
self.x + y
}
fn describe(&self) -> &'static str {
"MyStruct"
}
}
You can call the trait from C++ using the following code:
int main(int argc, char* argv[]) {
// The generated bindings are in a namespace with the same name as the
// target crate:
example_crate::MyStruct s = example_crate::MyStruct::new_(2);
uint32_t result =
example_crate::MyTrait::impl<example_crate::MyStruct>::add_with(s, 3);
rs::StrRef description =
example_crate::MyTrait::impl<example_crate::MyStruct>::describe(s);
std::cout << "Result: " << result
<< "\ndescription: " << description.to_string_view() << std::endl;
return 0;
}
Each trait has an associated impl member which is generic upon the Self type
of the trait implementation, and which provides all of the associated types,
consts, and functions.
Checking if a trait is implemented
C++ users can check if a trait implementation is available using rs::where_v<T, SomeTrait>. This is useful when writing templated C++ functions, as it can be
used with requires (or enable_if) to specify that a particular trait is
implemented by a type parameter. For example:
template <typename T>
requires(rs::where_v<T, example_crate::MyTrait>)
uint32_t add_with_2(T const& self) {
return example_crate::MyTrait::impl<T>::add_with(self, 2);
}
Limitations
Some trait implementations will not receive bindings:
- Trait implementations with generic parameters (e.g.
impl<T> ...) will not receive bindings.- For example, an implementation
impl<U> TwoArgs<i32, U> for MyStructwill not receive bindings because theimplhas a type parameter (U). - This also means that blanket impls (e.g.
impl<T> Trait for T) are not supported.
- For example, an implementation
- Traits with
constparameters (e.g.trait T<const V: usize>) do not yet receive bindings. - Trait methods will not receive bindings if their parameter or return types are not yet supported by Crubit.
How does it work?
Under the hood, Crubit will generate three bindings from the example Rust crate.
A template specialization for impl MyTrait for MyStruct:
template <>
struct ::rs::impl<::example_crate::MyStruct, ::example_crate::MyTrait> {
static constexpr bool kIsImplemented = true;
static ::std::int32_t add_with(::example_crate::MyStruct const& self,
::std::int32_t y);
static ::rs::StrRef describe(::example_crate::MyStruct const& self);
};
Our generated rs::impl specialization is an implementation detail of binding
generation. It holds the actual thunks that call into rust to implement our
trait, but they should be accessed through our template struct MyTrait:
struct CRUBIT_INTERNAL_RUST_TYPE(":: example_crate_golden :: MyTrait") MyTrait {
template <typename T>
using impl = ::rs::impl<T, MyTrait>;
};
Each struct generated for a Rust trait has the impl member that provides
access to the generated rs::impl. This is the preferred way to reference the
generated rs::impl.
Finally, a struct for MyStruct:
struct CRUBIT_INTERNAL_RUST_TYPE(":: example_crate_golden :: MyStruct") alignas(
4) [[clang::trivial_abi]] MyStruct final {
public:
// Default::default
MyStruct();
// No custom `Drop` impl and no custom "drop glue" required
~MyStruct() = default;
MyStruct(MyStruct&&) = default;
MyStruct& operator=(MyStruct&&) = default;
// Rust types that are `Copy` get trivial, `default` C++ copy constructor and
// assignment operator.
MyStruct(const MyStruct&) = default;
MyStruct& operator=(const MyStruct&) = default;
MyStruct(::crubit::UnsafeRelocateTag, MyStruct&& value);
static ::example_crate::MyStruct new_(::std::int32_t x);
private:
// Field type has been replaced with a blob of bytes: Field is private
::std::array<unsigned char, 4> x;
private:
static void __crubit_field_offset_assertions();
};
Our struct is generated normally and exists purely so we have something to implement our trait for.