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:
- Today: Using Crubit-generated Rust bindings for C++ APIs if the bindings
accept, store, or return Rust references. TODO: In the future Crubit should
use
CppRef<T>in the generated bindings (and this should mitigate some of the memory safety concerns). - Using Crubit-generated C++ bindings for Rust APIs if the bindings wrap Rust APIs that accept, store, or return Rust references.
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:
- Using copy or move assignment operator of C++ value that Rust has a reference to.
- Mutating public fields of a C++ struct that Rust has a reference to.
TODO: Try to succinctly 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.
Spans/slices are particularly error-prone: a Rust empty slice (&[]) is
represented using a dangling pointer with length zero, while a C++ empty span
typically uses nullptr. Using a nullptr with a Rust empty slice would result in
UB, as would using a dangling pointer in C++. In order to address this,
Crubit provides non-native types with conversion operators:
- Rust has
absl::{string_view, span<T>}types which provideFrom-based conversions to&strand&[T]. - C++ has
rs_std::{StrRef, Slice<T>}types which provide implicit conversions to and fromstring_viewandspan<T>.
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:
- Rust
structs,enums, andunions - all the constructors generated by Crubit guarantee proper initialization rs_std::char_when constructed in C++ viachar_::from_u32or when passed from Rust. (char_::from_u32_uncheckedis 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).