Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

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".
  • $a annotates the lifetime of the reference that precedes it.
  • In a method, $a after the parameter list (and after const, if present) annotates the lifetime of this. See the example below.
  • $static annotates a 'static lifetime.

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 a mutable field, or through a const_cast if the referenced object itself is not const).
  • 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 multiple T& (and/or const 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).