The Rust reference documents Undefined Behavior (UB) and says that “it is the programmer's responsibility when writing unsafe code to ensure that any safe code interacting with the unsafe code cannot trigger these [undefined] behaviors”. A programmer using Crubit bindings has the same responsibility: Crubit is implicitly “unsafe”, and incorrect usage can cause UB. The sections below document requirements for safely using Crubit when working with Rust and C++ references and pointers. The requirements are the same as the ones documented in the Rust reference, but are rephrased below from Crubit perspective.
The safety requirements below focus on avoiding UB related to Rust references and therefore matter in scenarios where Crubit-generated code may create Rust references out of C++ references or C++ pointers:
CppRef<T>
in the generated bindings (and this should mitigate some of the memory safety concerns).Incorrect lifetime annotations may lead to UB. Rust‘s borrow checker prevents incorrect lifetime annotations, but lifetime annotations of C++ APIs are not verified by the C++ compiler and Crubit’s optional lifetime analysis can't detect all incorrect annotations. Note that Crubit assumes that lifetime annotations are correct both for explicit annotations (e.g. int& $a f2(int& $a);
) as well as for annotations provided by #pragma clang lifetime_elision
.
Mutating a value in C++ is UB if the mutation happens while Rust holds a references to that value. This applies to Rust shared references (e.g. &T
) and to exclusive references (e.g. &mut T
).
Examples of C++ features that may mutate a value that Rust holds a reference to:
TODO: Try to succintly mention the idea that short-lived / non-retained references are safe from the mutation risk.
All references and NonNull
pointers must not be null, and if they point to a nonzero span of memory, must not be dangling. (The behavior of a program which violates these rules is undefined.)
C++ doesn't share these rules, and care must be taken when converting Rust references to and from C++ pointers. For example, spans/slices are particularly error-prone: a Rust empty slice uses a dangling pointer (which produces UB in C++ when used in pointer arithmetic), and a C++ empty span (often) uses nullptr (which is UB in Rust). To effectively use spans in FFI, one must either use non-native types, or perform a conversion operation which rewrites the pointer values. For that, we recommend using the conversion routines provided by Crubit support library (e.g. impl From<string_view> for &[u8]
).
TODO(b/271016831, b/262580415): Cover rs_std::Slice<T>
and/or rs_std::str
above once these types are provided by Crubit.
Creating a Rust reference that points to uninitialized memory is UB.
Care should be taken when passing C++ references across the FFI boundary to avoid creating Rust references that point to uninitialized memory. This is especially important for references to types that don't enforce proper initialization through their constructors or other factory APIs - primitive types like integers are one example.
Note that Crubit-generated C++ bindings for Rust code won't create C++ references to uninitialized memory for:
struct
s, enum
s, and union
s - all the constructors generated by Crubit guarantee proper initializationrs_std::rs_char
when constructed in C++ via rs_char::from_u32
or when passed from Rust. (rs_char::from_u32_unchecked
is unsafe in Rust sense)TODO: b/296287315: Support MaybeUninit<T>
.
Constructing a Rust references that aliases the same address as an already existing exclusive reference &mut T
is UB.
TODO: Provide FFI-related examples.
TODO: Document what runtime checks are provided by Crubit (and link to a separate md document that explains why general checks are infeasible).