Limits of Crubit

Crubit aims to make Rust a viable option for engineering teams with large, pre-existing C++ codebases. There are limits to how well C++ and Rust can interoperate. Tradeoffs still exist, and there can be tension between competing goals. And the tools we have for interop, including Crubit, are many years away from feature-completeness.

Crubit tries to push the envelope with what is possible with C++/Rust interop, automatically supporting cross-language calls with the minimum of developer toil. Where this is not possible, Crubit makes it possible (and easy, in most cases) to work around the mismatch, with documented patterns for the most common deficiencies.

High Level Features

Below is an incomplete overview of features we expect Crubit to support eventually. (This attempts to cover major axes in which interfaces can present challenges for interop.) Included in the overview is the short term estimation of overall difficulty, and long-term confidence levels of feasibility for fully automatic interop. Note that some of these depend on features outside the direct control of the Crubit team, such as in the Rust language and compiler.

We also describe, in abbreviated form, some of the patterns one can employ to work around limitations in Crubit. More detailed information is available in crubit.rs/cpp/cookbook and crubit.rs/errors.

Functions

Feature% functionsExpected Interop Effort (2027)Will Crubit automate this (eventually)?
Call into a non-templated C++ function from Rust, if annotated correctly.xLight{.😊}Planned: 2027{.ga}
Call into a templated C++ functionxDepends, see Generics and Templates.
Call into a non-generic Rust function from C++.xFully automatic{.😊}Confidence: Very High{.😊}
Call into a generic Rust functionxDepends, see Generics and Templates.

Most functions will be callable if Crubit is enabled, but it may require more or different code in the other language. A notable example is functions which return non-Rust-movable types, which must be invoked differently from Rust than functions which do not. Or, for Rust calling C++, while you can call any function that is designed to be looked up by ADL, you cannot use ADL to do so.

In the immediate future, there is a long tail of reasons that functions may not get bindings. For example, any unrecognized function attribute will cause the bindings to be disabled. For the foreseeable future, barring changes to Rust, we will also require annotations if a function is overloaded, to give it a unique Rust name. However, we expect unsupported parameter or return types to be the most common reason that a function does not receive bindings. These reasons are expected to go away over time, and we aim for something like 95%+ of non-templated functions receiving bindings by the end of 2027.

Fortunately, missing support for a function is easy to work around. Sometimes, this can be solved with a simple Crubit annotation (such as crubit.rs/errors/unknown_attribute), or some other minor change to the function declaration. In the case of the type being unsupported, enabling Crubit on the target that defines that type will be a common fix. But, at worst, one can wrap it behind a new function which Crubit does support. The new function can use fewer attributes, fewer language features, hide unsupported types behind wrapper types (or void pointers), and so on.

Using C++ as an example:

// This function will always get bindings
inline void DoStuff(void* untyped_out, const void* untyped_in) {
    auto* out = reinterpret_cast<UnsupportedType*>(out);
    const auto* in = reinterpret_cast<const OtherUnsupportedType*>(untyped_in);
    // arbitrary C++ logic here
    out->UnsupportedMethod(*in);
}

Any C++ feature or type that is not supported in the automatically generated Rust bindings can be hidden in this way. (crubit.rs/errors/unsupported_type describes other solutions for the unsupported type case.)

A similar strategy can be employed for Rust.

Non-templated C++ Types

Feature% functions% classesExpected Interop Effort (2027)Will Crubit automate this (eventually)?
Pass any Rust-movable C++ type by pointer or by value, including inside Rust structs.xxFully automatic{.😊}Planned: 2027{.ga}
Pass any non-Rust-movable C++ type by pointer, including inside Rust structs.xxFully automatic{.😊}Planned: 2027{.ga}
Pass any non-Rust-movable C++ type by value, including inside Rust structs.xxFully automatic{.😊}Planned: 2027{.ga}
A lightweight syntax can be used to perform C++ move construction or assignmentN/AN/AFully automatic{.😊}Planned: 2027{.ga}

C++ types are more complicated than Rust types, and can maintain invariants Rust does not. For example, they can enforce non-destructibility (via a deleted destructor), or automatically maintain pinnedness (via implicitly invoked copy and move constructors). These types will not be as easy to use from Rust, but we are aiming for near 100% coverage, so that functions can typically receive bindings.

Bindings can be generated for a type, even if the fields or other subobjects inside of it do not have bindings. What matters are the high level properties: the type's layout/ABI, what operations it supports, and so on. So when a type is not supported by Crubit, it almost always works to wrap it in a new type. However, more than for functions, we want to make this necessary as little as possible. Types have a wide blast radius: if a type does not get bindings, then no function using that type gets bindings. It is more important to support all types than to support all functions, and we plan to entirely cover the space of types and fully characterize any cases where they cannot receive bindings.

Non-Generic Rust Types

Feature% functionsExpected Interop Effort (2027)Will Crubit automate this (eventually)?
Pass any non-Generic Rust type by pointer or by value in C++.xFully automatic{.😊}Confidence: Very High{.😊}

There are currently no known cases where we cannot in principle support using a (non-generic) Rust type from C++.

Containers and Smart Pointers

| Feature | % functions | Expected Interop | Will Crubit | : : : Effort (2027) : automate this : : : : : (eventually)? : | :---------------- | ----------- | --------------------- | ------------------ | | Pass all standard | x | Medium{.😑} | Confidence: | : containers, smart : : : High{.😊} : : pointers, and : : : : : idiomatic : : : : : vocabulary types : : : : : between : : : : : languages, with : : : : : zero or : : : : : constant-time : : : : : overhead bridging : : : : : between : : : : : corresponding : : : : : types. : : : :

Every type gets some baseline level of support (and the wording around C++ types above applies), but these vocabulary types are valuable to special-case to receive a better Rust API. We do this by rewriting the types in pure Rust. For example, the C++ template class:

template <typename T> struct Span {T* ptr; size_t len;};

Has a Rust equivalent:

#[repr(C)] pub struct Span<T>(*mut T, usize);

If the layouts match, then these can be used interchangeably in most places, even with otherwise no template/generic support.

However, there is a long tail of vocabulary types, and types in the standard library often have differing implementations, some of which have incompatible Rust semantics. (GCC‘s std::string is pinned, Clang’s is Rust-movable.)

We expect users of Crubit to gravitate towards a set of types that works well in both languages, especially on multiplatform codebases.

Generics and Templates

| Feature | % functions | Expected Interop Effort | Will Crubit | : : : (2027) : automate this : : : : : (eventually)? : | :----------- | ----------- | -------------------------- | ----------------- | | Instantiate | x | [Fully | **Planned: | : C++ class : : automatic{.😊} : 2027**](#ga){.ga} : : templates : : : : : from Rust : : : : : with : : : : : concrete : : : : : types and : : : : : constants as : : : : : template : : : : : parameters. : : : :

Feature%Expected Interop Effort (2027)Will Crubit automate this (eventually)?
Instantiate C++ function templates from Rust with concrete types and constants as template parameters.xMedium{.😑}Confidence: Speculative{.🙁}
Instantiate C++ function and class templates using types defined in the current crate.xHeavy{.🙁}Confidence: Uncertain{.🙁}
Instantiate C++ function and class templates with generic type parameters. Checking of function calls will be deferred to monomorphization time.xHeavy{.🙁}Confidence: Speculative{.🙁}
Instantiate generic Rust types and functions from C++.xMedium{.😑}Confidence: Uncertain{.🙁}

Templates (and generics) are instantiated lazily on use, while Crubit is designed as an ahead-of-time interface/type transpiler. Ultimately, this approach hits its limit here, and a new design involving more directly hooking the compilers together will be necessary to fully support templates and generics in every circumstance.

Crubit plans to (by the end of 2027) support fully instantiated templates or generic types: functions which return or accept a Foo<Bar>, where Bar is not a template type parameter. Other uses of templates will need to be decomposed into building blocks that can receive bindings, or else reimplemented in the other language (as we do with types like Span).

Abstraction and interfaces

Feature%Expected Interop Effort (2027)Will Crubit automate this (eventually)?
Implement an interface defined by a C++ base class from Rust code.x% of classesHeavy{.🙁}Confidence: High{.😊}
Implement a trait defined in Rust from C++ statically.N/AHeavy{.🙁}Confidence: High{.😊}
Implement a dyn-compatible trait defined in Rust using an abstract base class and virtual dispatch in C++.N/AHeavy{.🙁}Confidence: Uncertain{.🙁}

Rust and C++ have many features that are not just “a function can be called”, or “a type can be used”. The way in which you define or dispatch to functions can be a load bearing part of the interface, and doesn't always have a natural equivalent on the other side, and Crubit will not support all of these for some time yet.

The recommended workaround, in most places, is to define a part of your logic in one language, and then call into the other. For example, write a derived class in C++, but implement all of its virtual methods by calling into Rust functions.

There is an example of this in crubit/examples/cpp/virtual/.

Safety

Feature%functionsExpected Interop Effort (2027)Will Crubit automate this (eventually)?
Safely call C++ functions from Rust, with lifetime preconditions checked by the Rust borrow checkerxLight{.😊} (where possible)Crubit gives x% of functions safe bindings
Check lifetime constraints on Rust APIs used from C++.xFully automatic{.😊} (where possible)Confidence: Speculative{.🙁}

Rust and C++ have very different stances on memory safety, but both languages do share a goal to, when reasonable in that language, make it safe to call given functions. In Rust, this is embedded in the type system. In C++, this often takes the form of implementation-specific attributes which the compiler can read to inform what warnings to give.

It is possible to lose safety unless FFI correctly propagates the relevant information to the other side. And in Rust, incorrectly marking functions as unsafe can lead to safety fatigue, where most calls are trivially correct, and the few that aren't have insufficient review.

Confidence Levels

Confidence levels:

Planned: 2027 We plan on finishing this before the end of 2027, as part of our next major milestone.

Confidence: Very High We know we can achieve this. There may still be caveats for edge cases that require language or compiler work to fix.

Confidence: High We think we can achieve this, but some of the details are not fully designed, and there is always a risk of “unknown unknowns”. May depend on getting features upstream, but we are reasonably confident we can do so.

Confidence: Uncertain There are substantial unknowns in the “how” that will take some dedicated engineering effort to resolve. We think there is a path that looks feasible.

Confidence: Speculative At a high level it seems like this should be possible, but there are major technical, social, or resourcing risks that could prevent it from happening.

Expected Interop Effort (2027)

Fully Automatic Interop Crubit must be enabled on targets.

Light Some declarations need to be annotated. For example, overloaded functions must have a Rust name specified.

Medium A layer of FFI glue code is required, such as new function or type definitions. The patterns involved are straightforward, and documented. Expertise is helpful, but not required.

Heavy Multiple layers of ad-hoc FFI glue are required, even with Crubit, or else the task may require reimplementing significant chunks of code in the other language. Some expertise may be required.

Irreconcilable differences

Expressiveness

C++ is fundamentally a more expressive language, in terms of accepted code patterns, than Rust. While Rust will continue to evolve and gain some forms of expressiveness, many of its strengths draw from its relatively limited expressiveness in certain areas. Therefore, differences between C++ and Rust will always exist, and show up in the source code.

Examples of these include:

  • Safety annotations
  • Implicit invocation of user-defined constructors, conversion operators, and assignment operators
  • Overloading operators in C++ that are not overloadable in Rust
  • Text-based macro substitution

Typically, this means that Rust code ends up more verbose than C++ code, though it can also require adding additional annotations to the C++ to better inform how to match C++‘s very general semantics with Rust’s more narrow interfaces.

Diverging patterns

Idiomatic patterns are not the same between the two languages, and will continue to diverge further over time. For example, many more objects are pinned in C++ than are pinned in Rust, and this will present an ongoing impedance mismatch.

Ecosystem considerations

The Rust and C++ languages are limited in how far they can evolve to accommodate interoperability. Within each community there are priorities besides interop at play.