Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 MyStruct will not receive bindings because the impl has a type parameter (U).
    • This also means that blanket impls (e.g. impl<T> Trait for T) are not supported.
  • Traits with const parameters (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.