Rust bindings for C++ references and lifetimes
Rust references have lifetimes, which let the Rust compiler check that a
reference doesn’t outlive the value it refers to. C++ references don’t have
lifetimes. To generate Rust bindings for C++ functions and methods that take or
return C++ references (T& or const T&), Crubit needs to know their
lifetimes. Crubit gets them from:
- Rust’s lifetime elision rules, or
- Explicit lifetime annotations in the C++ code.
This page also explains what happens if lifetime elision doesn’t
apply or gives wrong lifetimes, and describes
the CRef and CMut types that Crubit uses for references
returned from C++.
NOTE: C++ pointers don’t have lifetimes in the generated bindings. They map to Rust raw pointers. See Pointer types.
Lifetime elision
Crubit applies Rust’s lifetime elision rules to C++ references. For example, given the following C++ header:
struct Counter final {
int Get() const { return value; }
void Increment() { value++; }
const int& GetRef() const { return value; }
int& GetMutRef() { return value; }
int value;
};
Crubit will generate the following bindings (notice the inferred '__this
lifetime):
impl Counter {
#[inline(always)]
pub fn Get<'__this>(&'__this self) -> ::ffi_11::c_int {
unsafe { self::counter::Get(self) }
}
#[inline(always)]
pub fn Increment<'__this>(&'__this mut self) {
unsafe { self::counter::Increment(self) }
}
#[inline(always)]
pub fn GetRef<'__this>(&'__this self) -> ::cref::CRef<'__this, ::ffi_11::c_int> {
unsafe { self::counter::GetRef(self) }
}
#[inline(always)]
pub fn GetMutRef<'__this>(&'__this mut self) -> ::cref::CMut<'__this, ::ffi_11::c_int> {
unsafe { self::counter::GetMutRef(self) }
}
}
Reference parameters become Rust references, and each of them gets its own
lifetime (e.g. see 'x below):
#[inline(always)]
pub fn AddOne<'x>(x: &'x mut ::ffi_11::c_int) {
unsafe { crate::detail::__rust_thunk___ZN7example6AddOneERi(x) }
}
When lifetime elision doesn’t apply
Lifetime elision doesn’t apply to some functions. For example, in Rust, fn smaller(x: &i32, y: &i32) -> &i32 doesn’t compile (error E0106), because the
result can refer to x or to y. See also “Generic Lifetimes in
Functions”
in the Rust Book.
For such C++ functions, Crubit generates bindings that return a raw pointer instead of a reference. For example, given the following C++ function:
// Lifetime elision doesn't apply: there are two input references and no `this`.
inline const int& Smaller(const int& x, const int& y) { return x < y ? x : y; }
Crubit will generate the following bindings:
/// Lifetime elision doesn't apply: there are two input references and no `this`.
#[inline(always)]
pub fn Smaller<'x, 'y>(
x: &'x ::ffi_11::c_int,
y: &'y ::ffi_11::c_int,
) -> *const ::ffi_11::c_int {
unsafe { crate::detail::__rust_thunk___ZN7example7SmallerERKiS1_(x, y) }
}
Rust code needs unsafe to dereference the returned pointer. To bind the result
as a reference, add explicit lifetime annotations to the C++
function.
Explicit lifetime annotations
C++ code can use explicit lifetime annotations (e.g. $a) to specify lifetimes
that lifetime elision can’t infer. For example, given the following C++
function:
inline const int& $a SmallerWithLifetimes(const int& $a x, const int& $a y) {
return x < y ? x : y;
}
Crubit will generate the following bindings:
/// Explicit lifetime annotations: The result may refer to `x` or to `y`.
#[inline(always)]
pub fn SmallerWithLifetimes<'a>(
x: &'a ::ffi_11::c_int,
y: &'a ::ffi_11::c_int,
) -> ::cref::CRef<'a, ::ffi_11::c_int> {
unsafe { crate::detail::__rust_thunk___ZN7example20SmallerWithLifetimesERKiS1_(x, y) }
}
Notes:
- The
$a,$b, etc. macros come from"support/lifetime_annotations.h". $aannotates the lifetime of the reference that precedes it.- In a method,
$aafter the parameter list (and afterconst, if present) annotates the lifetime ofthis. See the example below. $staticannotates a'staticlifetime.
See Lifetime Annotations for C++ for more details and design discussions.
When lifetime elision gives wrong lifetimes
Lifetime elision is a good default for most C++ APIs, but neither the C++ compiler nor Crubit checks that the lifetimes match what the C++ code does. For example, given the following C++ struct:
struct Number final {
// The result may refer to `this` or to `other`, but lifetime elision ties the
// result only to `this`.
const int& GetLarger(const int& other) const {
return value > other ? value : other;
}
// The result may refer to `this` or to `other`.
const int& $a GetLargerWithLifetimes(const int& $a other) const $a {
return value > other ? value : other;
}
int value;
};
Crubit will generate the following bindings:
impl Number {
/// The result may refer to `this` or to `other`, but lifetime elision ties the
/// result only to `this`.
#[inline(always)]
pub fn GetLarger<'__this, 'other>(
&'__this self,
other: &'other ::ffi_11::c_int,
) -> ::cref::CRef<'__this, ::ffi_11::c_int> {
unsafe { self::number::GetLarger(self, other) }
}
/// The result may refer to `this` or to `other`.
#[inline(always)]
pub fn GetLargerWithLifetimes<'a>(
&'a self,
other: &'a ::ffi_11::c_int,
) -> ::cref::CRef<'a, ::ffi_11::c_int> {
unsafe { self::number::GetLargerWithLifetimes(self, other) }
}
}
Lifetime elision ties the result of GetLarger only to self. This is wrong,
because the result can refer to other. Rust code can keep the result after
other is dropped, and then the result is a dangling reference. This can cause
undefined behavior.
GetLargerWithLifetimes uses explicit lifetime annotations to tie the result to
both self and other. If lifetime elision gives wrong lifetimes, then please
add explicit lifetime annotations to the C++ API.
CRef and CMut
C++ references returned from C++ functions and methods become CRef<'a, T> (for
const T&) or CMut<'a, T> (for T&). CRef and CMut are defined in the
cref crate (//support:cref).
Crubit uses CRef and CMut (rather than Rust references) to accurately
represent that C++ references don’t offer the same guarantees as Rust
references. For example:
- A Rust shared reference (
&T) promises that nothing mutates the value while the reference is in use. On the other hand,const T&in C++ allows mutating the referenced value (e.g. through amutablefield, or through aconst_castif the referenced object itself is notconst). - A Rust mutable reference (
&mut T) promises that the reference is the only way to access the value while it is in use. On the other hand, C++ allows multipleT&(and/orconst T&) to co-exist.
See the documentation of the cref crate for more details.
Methods
Today Crubit binds this as &self or &mut self (rather than as self: CRef<Self> or self: CMut<Self>).
Future Rust language features may make CRef and CMut more ergonomic to use
as method receivers (see arbitrary self
types and
autoref in the “Beyond
Rust References” initiative). Once
that happens, Crubit may start binding this as CRef<Self> or CMut<Self>
(at least for inherent methods - trait methods may still require &self or
&mut self).