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.
To have bindings, the class must be “trivially relocatable”. For example, any trivial or “POD” class is trivially relocatable.
Given the following C++ header:
cs/file:examples/cpp/trivial_struct/example.h class:Position
Crubit will generate a struct with the same layout, as well as some debug assertions to ensure the bindings are correct.
cs/file:examples/cpp/trivial_struct/example_generated.rs content:^([^/\n])([^!\n]|$)[^\n]*
For an example of a trivially-relocatable class with a destructor, see examples/cpp/trivial_abi_struct/.
The fields on the Rust struct type are the corresponding Rust types:
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, since inheritance is not supported, the space of the object occupied by a base class will instead be this opaque blob of bytes.
Specifically, the following subobjects are hidden and replaced with opaque blobs:
public
fields (private
or protected
fields)no_unique_address
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.
To receive Rust bindings, a type must be “trivially relocatable”. This is the C++ version of saying a class is “Rust-movable”: the class can be “teleported” in memory during its lifetime, as if by using memcpy
and then discarding the old location without running any destruction logic. This means that it can be present in Rust using normal objects and pointers and references, without using Pin
.
For example, a string_view
is trivially relocatable. In fact, every trivially copyable type is trivially relocatable.
However, unlike Rust, many types in C++ are not trivially relocatable. For example, a std::string
might be implemented using the “short string optimization”, in a fashion similar to this:
class String { union { size_t length; char inline_data[sizeof(length)]; }; char* data; // either points to `inline_data`, or the heap. public: size_t size() { if (data == (char*)this) { return strlen(data); } else { return length; } } // ... };
This class is self-referential: the data
pointer may point to inline_data
, which is inside the object itself. If we bitwise copy the object to a new location, as in a “Rust move” or as with memcpy
, then the data
pointer will remain bitwise identical, and point into the old object. It becomes a dangling pointer!
C++ allows self-referential types. In C++, fields can and often do point at other fields, because assignment is overloadable: the assignment operator can be modified to, when copying or moving the string, also “fix up” the data
pointer so that it points to the new location in the new object, instead of dangling.
Rust does not do this. In Rust, assignment is always a “trivial relocation” -- assignment runs no code when copying or moving an object, and copies the bytes as they are. This would break on the String
type defined above, or any other self-referential type.
Unfortunately, any class with a user-defined copy/move operation or destructor might be self-referential, and so by default they are not trivially relocatable. If a class has a user-defined destructor or copy/move constructor/assignment operator, and “should be” trivially relocatable, it must explicitly declare that it is safe to trivially relocate, using the attribute ABSL_ATTRIBUTE_TRIVIAL_ABI
. This attribute allows a class to be trivially relocated, even though it defines an operation that would ordinarily disable trivial relocation.
For example, in the unstable libc++ ABI we use within Google, a unique_ptr<T>
is trivially relocatable, because it applies ABSL_ATTRIBUTE_TRIVIAL_ABI
. This is safe to do, for unique_ptr
, because its exact location in memory does not matter, and paired move/destroy operations can be replaced with trivial relocations.
The exact requirements for a class to be trivially relocatable are subject to change, because they are still being defined within Clang and within the C++ standard. But at the least:
class
or struct
type with only trivially relocatable fields and base classes is trivially relocatable, unless:ABSL_ATTRIBUTE_TRIVIAL_ABI
and defines a copy/move constructor, copy/move assignment operator, or destructor, or,virtual
member function.Some examples of trivially relocatable types:
string_view
struct tm
, or any other type in the C standard libraryunique_ptr
, in the Clang unstable ABI.absl::Status
Some examples of types that are not trivially relocatable:
std::string
, std::vector
, and other nontrivial standard library types.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.Crubit does not support most attributes on structs and their fields. If a struct is marked using any attribute other than alignment or ABSL_ATTRIBUTE_TRIVIAL_ABI
, it will not receive bindings. If a field is marked using any attribute, it will be replaced with a private opaque blob.