Rust bindings for C++ classes and structs
A C++ class or struct is mapped to a Rust struct with the same fields. If
any subobject of the class cannot be represented in Rust, the class itself will
still have bindings, but the relevant subobject will be
private.
Example
Given the following C++ header:
struct Position {
int x;
int y;
};
Crubit will generate a struct with the same layout:
#[derive(Clone, Copy, ::ctor::MoveAndAssignViaCopy)]
#[cfi_encoding = "8Position"]
#[repr(C)]
///CRUBIT_ANNOTATE: cpp_type=Position
///CRUBIT_ANNOTATE: cpp_move_constructible=
pub struct Position {
pub x: ::ffi_11::c_int,
pub y: ::ffi_11::c_int,
}
Which can be used like this:
fn main() {
// Can pass this to functions accepting a `Position`, and so on.
let _ = example_lib::Position { x: 1, y: 2 };
}
For an example of a Rust-movable class with a destructor, see examples/cpp/trivial_abi_struct/.
Fields
The fields on the Rust struct type are the corresponding Rust types:
- If the C++ field has primitive type, then the Rust field uses the corresponding Rust type.
- Similarly, if the C++ field has pointer type, then the Rust field has the corresponding Rust pointer type.
- If the field has a user-defined type, such as a class type or enum, then the Rust field uses the bindings for that type.
Unsupported fields
Subobjects that do not receive bindings are made private, and replaced with an
opaque blob of [MaybeUninit<u8>; N], as well as a comment in the generated
source code explaining why the subobject could not receive bindings. For
example, base class subobjects are not exposed as fields, so the space of the
object occupied by a base class will instead be this opaque blob of bytes, with
no comment. (See Base classes for what you can do with base
classes instead.)
Specifically, the following subobjects are hidden and replaced with opaque blobs:
- Base class subobjects
- Non-
publicfields (privateorprotectedfields) - Fields that have nontrivial destructors
- Fields whose type does not have bindings
- Fields that have any unrecognized attribute, including
no_unique_address - Fields of a packed struct whose Rust type has
#[repr(align(N))]
A Rust struct with opaque blobs is ABI-incompatible with the C++ struct or class that it corresponds to. As a consequence, if the struct is used for FFI outside of Crubit, it should not be passed by value. Within Crubit, it can’t be passed by value in function pointers, but can otherwise be used as normal.
Static data members
Public static data members get bindings: compile-time integer, bool, integer
array, and string_view constants become associated consts of the Rust struct,
and other static data members become Rust statics. See Static data
members for details.
Rust-movable classes
The easiest C++ classes to work with are “Rust-movable”, meaning they support
being relocated in memory using memcpy without running the move constructor.
When passed and returned by value, Rust-movable classes will use the direct
type. For example, T Identity(T) becomes pub fn Identity(_: T) -> T.
If it is non-Rust-movable, then Crubit will instead use in-place initialization
types, Ctor, and requires a bit of syntactic overhead when moving objects
around. The same function might instead become pub fn Foo(_: Ctor![T]) -> Ctor![T].
(For an introduction to Ctor![T], see
crubit.rs/types/non_rust_movable/intro_short.)
Types are considered Rust-movable by default, meaning they can be relocated
using memcpy. If the type defines a destructor or copy/move constructor, then
it requires a special annotation to be considered Rust-movable:
ABSL_ATTRIBUTE_TRIVIAL_ABI.
If it has a non-Rust-movable field or base class, then it is not Rust-movable.
It can be worth putting some effort into designing the type to be Rust-movable. crubit.rs/cpp/cookbook#rust_movable describes, in more detail, how to go about this.
Some examples of Rust-movable types:
- any primitive type (integers, character types, floats, etc.)
- raw pointers
string_viewstruct tm, or any other type in the C standard libraryunique_ptrandshared_ptr, in the Clang unstable ABI.absl::Status
Some examples of types that are not Rust-movable:
- (For now)
std::string,std::vector, and other nontrivial standard library types. - (For now)
absl::flat_hash_map,absl::AnyInvocable, and other nontrivial types used throughout the C++ ecosystem, even outside the standard library. absl::Mutex,absl::Notification, and other non-movable types.
Base classes
Rust has no inheritance, so a derived class and its base classes become
unrelated Rust structs. The base class subobject is an opaque
blob, so base class fields aren’t accessible through the
derived struct. With oo_casting, Rust code can upcast to
the base class and read the fields there.
Inherited methods
By default, methods that a class inherits from its base classes are not part of the derived class’s bindings, and no comment is emitted about them. They still exist on the base class’s bindings.
struct Base {
bool IsValid() const;
};
struct Derived : Base {};
Here, Base::IsValid() has bindings, but Derived has no IsValid() method in
Rust.
To get inherited methods, add //features:oo_casting to the
aspect_hints of the cc_library that defines the derived class. This is an
experimental feature. With it:
-
The derived struct gets the public methods, including static methods, of its public base classes, so
derived.IsValid()works in Rust. This only covers base classes defined in the samecc_libraryas the derived class, and leaves out methods whose name comes from more than one base class. -
The derived struct implements
oops::Inherits<Base>for each public base class, including base classes from othercc_librarytargets. Rust code can then use theoops::Upcasttrait to convert a&Derived,*const Derived, or*mut Derivedto the base class type. For example, to call a method of a base class in anothercc_library, upcast first:#![allow(unused)] fn main() { use oops::Upcast as _; // The type annotation is required. let base: &Base = unsafe { derived.upcast() }; }For a
virtualbase class, useoops::VirtualUpcaston a raw pointer instead. Rust code that uses these traits depends on//support:oops.
The tests in
rs_bindings_from_cc/test/struct/inheritance/
show both.
Without oo_casting, add a method to the derived class in C++ that forwards to
the base class method, or a function that returns the object as a pointer to its
base class.
using Base::Method; declarations in the derived class don’t get bindings, and
produce a “Function aliases are not yet supported” error comment. With
oo_casting, the method is usually inherited anyway. It’s missing only if the
using declaration makes a method of a non-public base class accessible, or
brings back a base class method that a derived class method of the same name
hides.
Virtual methods
Calling a C++ virtual method from Rust works like calling any other method, and
dispatches virtually. This includes pure virtual methods. Because a class with
virtual methods is not Rust-movable, non-const methods take
self: Pin<&mut Self>.
An abstract class (one with pure virtual methods that aren’t overridden) gets bindings for its methods but no constructor, so Rust code works with it through references and pointers to objects created elsewhere. A subclass that overrides the methods gets bindings for its overrides and can be constructed. The methods it doesn’t override are inherited methods.
Overriding a C++ virtual method in Rust, or otherwise implementing a C++
interface in Rust, is not supported. The workaround is a C++ subclass that
forwards to a Rust type; see
examples/cpp/virtual/.
Packed structs
A packed C++ struct becomes a packed Rust struct with the same layout:
__attribute__((packed))(orABSL_ATTRIBUTE_PACKED) on the struct becomes#[repr(C, packed)].- A struct defined under
#pragma pack(N)becomes#[repr(C, packed(N))]. If the pragma doesn’t lower the alignment of any field or base class, it doesn’t change the layout, and the struct is treated like any other struct.
For example:
struct __attribute__((packed)) PackedStruct {
uint8_t a;
uint32_t b; // At offset 1.
uint16_t c; // At offset 5.
};
becomes a 7-byte Rust struct with alignment 1:
#![allow(unused)]
fn main() {
#[repr(C, packed)]
pub struct PackedStruct {
pub a: u8,
pub b: u32,
pub c: u16,
}
}
Some packed structs don’t get bindings:
- Structs with
__attribute__((packed))on individual fields. - Packed structs with an explicit alignment (such as
alignasor__attribute__((aligned(N)))) greater than the packing alignment.
A field of a packed struct whose Rust type has #[repr(align(N))], directly or
in one of its own fields, becomes a private opaque blob,
because Rust doesn’t allow such types in packed structs. A struct type gets
#[repr(align(N))] if, for example, it has an explicit alignment, or it has an
alignment greater than 1 and has bitfields or fields whose type doesn’t have
bindings.
Note: Clang doesn’t pack fields whose type isn’t POD for the purpose of layout
(for example, a type with a user-defined copy constructor) and warns about this
with -Wpacked-non-pod. If such a field has an alignment greater than 1, the
Rust bindings won’t compile, because the Rust struct packs every field.
Using fields of packed structs
Fields of a packed struct may be unaligned, so Rust doesn’t allow references to
them (E0793). Reading a
field by value (let b = s.b;) works, but anything that implicitly takes a
reference, such as println!("{}", s.b) or s.b.to_string(), doesn’t compile.
Copy the field out first, for example with braces:
#![allow(unused)]
fn main() {
println!("{}", { s.b });
let text = { s.b }.to_string();
}
Assigning to a field (s.b = 5;) works as usual. For fields that aren’t Copy,
use raw pointers (&raw const s.field or &raw mut s.field) with
ptr::read_unaligned and ptr::write_unaligned.
Debug for packed structs
Crubit ignores CRUBIT_TRAIT_DERIVE(Debug) on packed structs, because
#[derive(Debug)] takes references to the fields.
Instead, with the experimental //features:record_impl_debug
feature, packed structs implement Debug like other structs, with these
differences:
- Each field is copied out before it’s printed. If a field that would be
printed isn’t
Copy(or ismutable, and thus wrapped in aCell), no fields are printed, and the output isPackedStruct { .. }, like for a union. - Base classes aren’t printed, since they may be unaligned too.
For example, with the struct above, format!("{s:?}") is
PackedStruct { a: 10, b: 200, c: 3000 }. The tests in
rs_bindings_from_cc/test/struct/packed/
show more examples.
Attributes
Crubit does not support most attributes on structs and their fields. If a struct
is marked using any attribute other than alignment, packing, or
ABSL_ATTRIBUTE_TRIVIAL_ABI, it will not receive bindings. If a field is marked
using any other attribute, it will be replaced with a private opaque blob.