C++ bindings for Rust structs
A Rust struct is mapped to a C++ class/struct with the same fields. If any
field cannot be represented in C++, the struct itself will still have bindings,
but the relevant field will be private.
If a struct is not C++ movable, then it will be passed
by-value via the rs::Movable wrapper type.
See Movable Types.
Example
Given the following Rust module:
#[derive(Default, Clone)]
pub struct Struct {
pub a: i32,
}
Crubit will generate the following bindings:
struct CRUBIT_INTERNAL_RUST_TYPE(":: example_crate_golden :: Struct") alignas(4)
[[clang::trivial_abi]] Struct final {
public:
::std::int32_t a{};
private:
static void __crubit_field_offset_assertions();
};
Fields
The fields on the C++ class are the corresponding Rust types:
- If the Rust field has primitive type, then the C++ field uses the corresponding C++ type.
- Similarly, if the Rust field has pointer type, then the C++ field has the corresponding C++ pointer type.
- If the field has a user-defined type, such as a struct or enum, then the C++ field uses the bindings for that type.
Unsupported fields
Fields that do not receive bindings are made private, and replaced with an
opaque blob of maybe-uninitialized bytes, as well as a comment in the generated
source code explaining why the field could not receive bindings. For example,
since HashMap is not supported, the space of the object occupied by a
HashMap field will instead be this opaque blob of bytes:
#![allow(unused)]
fn main() {
// Rust: `my_field` is some unsupported type, such as `HashMap`
pub my_field: HashMap<i32, i32>,
}
// C++: `my_field` becomes `private`, and its type is replaced by bytes.
private: ::std::array<unsigned char, 24> my_field;
Specifically, the following subobjects are hidden and replaced with opaque blobs:
- Non-public fields (
privateorpub(...)fields). - Fields whose type does not have bindings.
- Fields that have an unrecognized or unsupported attribute.
Aggregate initialization
Qualifying Rust structs are generated as C++ aggregates (satisfying
std::is_aggregate_v<T>),
which enables C++ aggregate initialization such as designated initializers
(MyStruct{.a = 1, .b = 2}) and braced initialization (MyStruct{1, 2}).
A Rust struct is generated as a C++ aggregate if:
- All of its fields are public (
pub) and supported by Crubit. - It is not marked
#[non_exhaustive]. - It does not implement
Drop. - At most one field requires drop glue, or the struct is annotated with
#[crubit_annotate::field_drop_order_does_not_matter]. - Any field requiring drop glue is movable in C++.
Defaultis not implemented manually (i.e.Defaultis either derived via#[derive(Default)]or not implemented at all).
Field Drop Order and #[crubit_annotate::field_drop_order_does_not_matter]
In Rust, struct fields are dropped in definition order (first to last). In C++, aggregate members are destroyed in reverse definition order (last to first).
When at most one field requires drop glue, destruction order is trivial and matches between Rust and C++. However, when multiple fields require drop glue, the drop order in C++ will be the reverse of Rust’s drop order. To prevent unintended semantic differences, Crubit generates such structs as non-aggregates (with custom C++ destructors calling into Rust) by default.
If the drop order of the fields does not matter, you can annotate the struct
with #[crubit_annotate::field_drop_order_does_not_matter] from
//support:crubit_annotate to opt into C++ aggregate
generation:
#![allow(unused)]
fn main() {
use crubit_annotate::field_drop_order_does_not_matter;
#[field_drop_order_does_not_matter]
pub struct Config {
pub name: MyDropType1,
pub buffer: MyDropType2,
}
}
Manual Default Implementation
If a struct implements Default manually (impl Default for MyStruct { ... }),
it is generated as a non-aggregate. This allows Crubit to generate an explicit
C++ default constructor (MyStruct()) that calls Rust’s Default::default(),
preserving any custom initial values defined in Rust.
Structs using #[derive(Default)] remain C++ aggregates, as derived Default
initializes primitive fields to zero/false, matching C++ value initialization.
Differences between Aggregate and Non-Aggregate Structs
- Direct Member Variables: C++ aggregates expose fields as direct member
variables (
Type field_name;) without anonymous union wrappers or padding fields. - No User-Declared Constructors: C++ aggregates do not have user-declared constructors (such as converting constructors or tuple constructors).
- Value Initialization vs
Default::default(): In C++, value-initializing an aggregate (MyStruct s{};) zero-initializes primitive fields according to C++ rules. If a struct derivesDefaultor does not implementDefault, zero-initialization in C++ matches Rust derived default semantics.
C++ movable
If a struct is not C++ movable, its C++ move constructor
and move assignment operator will be = deleted, and it will be passed by-value
via the rs::Movable wrapper type. See
Movable Types.
Zero-sized types (ZSTs)
C++ does not allow 0-byte types, so Crubit cannot generate bindings for
zero-sized Rust structs (such as pub struct Empty;, pub struct Empty {}, or
structs containing only zero-sized fields) and emits a Zero-sized types (ZSTs) are not supported (b/258259459) comment instead. To expose an empty Rust struct
to C++, add a placeholder non-zero-sized field (for example, _unused: u8,
_unused: bool, or _unused: Option<()>).
CRUBIT_INTERNAL_RUST_TYPE annotation
You may notice that the generated C++ structs are annotated with the
CRUBIT_INTERNAL_RUST_TYPE macro. This annotation instructs Crubit
(specifically rs_bindings_from_cc) to disable automated bindings for this C++
type, and instead map all C++ uses of the type back to the existing Rust type.
This ensures that a Rust struct passed to C++ and then back to Rust resolves to
the original Rust type rather than a newly generated one.
While Crubit generates this annotation automatically for Rust-to-C++ bindings, you can also apply it manually on your own C++ types if you want them to map to an existing Rust type:
struct CRUBIT_INTERNAL_RUST_TYPE("char") char_ {
std::uint32_t c;
};
Template Arguments and Interpolation
For C++ templates, you can use {} interpolation syntax within the Rust type
string to substitute template arguments:
template <typename T>
struct CRUBIT_INTERNAL_RUST_TYPE("RustType<{}>", T) CppType {
T* value;
};
This ensures that a C++ instantiation like CppType<int> maps correctly to
RustType<i32> in Rust.
Importantly, this interpolation syntax allows you to express Rust generic parameters that have no direct C++ equivalent, such as lifetimes or default generic arguments. For example:
template <typename T>
struct CRUBIT_INTERNAL_RUST_TYPE("RustType<'static, {}>", T) CppType {
T* value;
};
Const generics arguments can also be provided with
crubit::rust_type::Const<N>:
template <typename T>
struct CRUBIT_INTERNAL_RUST_TYPE(
"RustType<'static, {}, {}>",
T,
crubit::rust_type::Const<123>,
) CppType {
T* value;
};