blob: 8cb09edaa097461e68bc6a36c281f38949b94908 [file] [view] [edit]
<!--* css: "//depot/docs/includes/featuretable.css" *-->
# 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** | % functions | Expected Interop Effort (2027) | Will Crubit automate this (eventually)?
:------------------------------------------------------------------------ | ----------- | --------------------------------- | ---------------------------------------
Call into a non-templated C++ function from Rust, if annotated correctly. | x | [Light](#light){.😊} | [**Planned: 2027**](#ga){.ga}
Call into a templated C++ function | x | | Depends, see [Generics and Templates](#generics).
<!-- blank separator --> | | |
Call into a non-generic Rust function from C++. | x | [Fully automatic](#automatic){.😊} | [**Confidence: Very High**](#veryhigh){.😊}
Call into a generic Rust function | x | | Depends, see [Generics and Templates](#generics).
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](https://en.cppreference.com/w/cpp/language/adl.html), 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.
<!-- TODO(jeanpierreda): gather statistics -->
Fortunately, missing support for a function is easy to work around. Sometimes,
this can be solved with a simple
[Crubit annotation](http://crubit.rs/cpp/customizing) (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:
```c++
// 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 | % classes | Expected Interop Effort (2027) | Will Crubit automate this (eventually)?
:------------------------------------------------------------------------------------ | ----------- | --------- | --------------------------------- | ---------------------------------------
Pass any Rust-movable C++ type by pointer or by value, including inside Rust structs. | x | x | [Fully automatic](#automatic){.😊} | [**Planned: 2027**](#ga){.ga}
Pass any non-Rust-movable C++ type by pointer, including inside Rust structs. | x | x | [Fully automatic](#automatic){.😊} | [**Planned: 2027**](#ga){.ga}
Pass any non-Rust-movable C++ type by value, including inside Rust structs. | x | x | [Fully automatic](#automatic){.😊} | [**Planned: 2027**](#ga){.ga}
A lightweight syntax can be used to perform C++ move construction or assignment | N/A | N/A | [Fully automatic](#automatic){.😊} | [**Planned: 2027**](#ga){.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** | % functions | Expected Interop Effort (2027) | Will Crubit automate this (eventually)?
:------------------------------------------------------------ | ----------- | --------------------------------- | ---------------------------------------
Pass any non-Generic Rust type by pointer or by value in C++. | x | [Fully automatic](#automatic){.😊} | [**Confidence: Very High**](#veryhigh){.😊}
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](#medium){.😑} | [**Confidence: |
: containers, smart : : : High**](#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:
```c++ {.no-copy}
template <typename T> struct Span {T* ptr; size_t len;};
```
Has a Rust equivalent:
```rust {.no-copy}
#[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 {#generics}
| **Feature** | % functions | Expected Interop Effort | Will Crubit |
: : : (2027) : automate this :
: : : : (eventually)? :
| :----------- | ----------- | -------------------------- | ----------------- |
| Instantiate | x | [Fully | [**Planned: |
: C++ class : : automatic](#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. | x | [Medium](#medium){.😑} | [**Confidence: Speculative**](#speculative){.🙁}
Instantiate C++ function and class templates using types defined in the current crate. | x | [Heavy](#heavy){.🙁} | [**Confidence: Uncertain**](#uncertain){.🙁}
Instantiate C++ function and class templates with **generic** type parameters. Checking of function calls will be deferred to monomorphization time. | x | [Heavy](#heavy){.🙁} | [**Confidence: Speculative**](#speculative){.🙁}
Instantiate generic Rust types and functions from C++. | x | [Medium](#mediun){.😑} | [**Confidence: Uncertain**](#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 classes | [Heavy](#heavy){.🙁} | [**Confidence: High**](#high){.😊}
Implement a trait defined in Rust from C++ statically. | N/A | [Heavy](#heavy){.🙁} | [**Confidence: High**](#high){.😊}
Implement a dyn-compatible trait defined in Rust using an abstract base class and virtual dispatch in C++. | N/A | [Heavy](#heavy){.🙁} | [**Confidence: Uncertain**](#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/](https://github.com/google/crubit/tree/main/examples/cpp/virtual/).
### Safety
**Feature** | %functions | Expected Interop Effort (2027) | Will Crubit automate this (eventually)?
:-------------------------------------------------------------------------------------------------- | ---------- | -------------------------------------------------- | ---------------------------------------
Safely call C++ functions from Rust, with lifetime preconditions checked by the Rust borrow checker | x | [Light](#light){.😊} (where possible) | Crubit gives x% of functions safe bindings
Check lifetime constraints on Rust APIs used from C++. | x | [Fully automatic](#automatic){.😊} (where possible) | [**Confidence: Speculative**](#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:
<a id="ga" class="ga">**Planned: 2027**</a> We plan on finishing this before the
end of 2027, as part of our next major milestone.
<a id="veryhigh" class="😊">**Confidence: Very High**</a> We know we can achieve
this. There may still be caveats for edge cases that require language or
compiler work to fix.
<a id="high" class="😊">**Confidence: High**</a> 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.
<a id="uncertain" class="🙁">**Confidence: Uncertain**</a> 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.
<a id="speculative" class="🙁">**Confidence: Speculative**</a> 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)
<a id="automatic" class="😊">**Fully Automatic Interop**</a> Crubit must be
enabled on targets.
<a id="light" class="😊">**Light**</a> Some declarations need to be annotated.
For example, overloaded functions must have a Rust name specified.
<a id="medium" class="😑">**Medium**</a> 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.
<a id="heavy" class="🙁">**Heavy**</a> 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.