blob: 427b43dc69f4faf405b3898b7023fdf65214c90f [file] [view] [edit]
# Best Practices Writing Rust Bindings for Existing C++ Libraries
## Introduction
This document is an attempt at guidance for how Rust changes can be made to
existing C++ libraries, including core foundational libraries.
For an introduction, see [Rust Bindings for C++ Libraries](index.md).
## Code Organization {#organization}
A C++ library and its Rust bindings are defined as separate targets in the same
`BUILD` file: the `cc_library` target itself, and a corresponding
`rust_api_from_cpp` target.
```python
cc_library(
name = "my_lib",
hdrs = ["my_lib.h"],
aspect_hints = [":my_lib_rust.hint"],
)
rust_api_from_cpp(
name = "my_lib_rust",
cpp_target = ":my_lib",
)
```
The `rust_api_from_cpp` target acts like a `rust_library` and can be depended on
by other Rust targets via `deps`. The `cc_library` rule receives an
`aspect_hint` called `:my_lib_rust.hint`. This aspect hint is generated by
`rust_api_from_cpp`, and tells Crubit how to generate bindings for the given
`cc_library`.
The `rust_api_from_cpp` target should be defined in the same `BUILD` file as the
original `cc_library` target. This helps ensure that there is a single
easily-discoverable Rust API for a given C++ library.
### Adding custom Rust source files (`srcs`)
Handwritten Rust code can be added to the generated bindings by specifying the
`srcs` attribute on `rust_api_from_cpp`:
```python
rust_api_from_cpp(
name = "my_lib_rust",
srcs = ["my_lib_custom.rs"],
cpp_target = ":my_lib",
)
```
To allow Crubit bindings to be `#![no_std]` by default, the generated crate does
not include the regular `std` Rust prelude. Consequently, standard library types
like `Box` have to be imported manually:
```rust
extern crate std;
use std::boxed::Box;
```
<section class="zippy" markdown="1">
Read on only if you're curious about *why* Rust bindings targets are structured this way.
#### Technical Justification
Crubit generates bindings using Bazel
[**aspects**](https://bazel.build/extending/aspects): given an arbitrary C++
Bazel target, Crubit generates, in an aspect, the Rust library which wraps it.
This is necessary for the same reason that it's necessary for
[protocol buffers](https://bazel.build/extending/aspects): to support transitive
dependencies (if A depends on B, then bindings(A) must depend on bindings(B) so
that bindings(A) can use types from B).
However, unlike protocol buffers which use a single target for both languages,
Crubit uses a separate `rust_api_from_cpp` target to represent the Rust
bindings. This target is where library owners configure the bindings (for
example, by adding custom Rust code which should be added to the generated
target).
To configure the Crubit code generation for a C++ target, we use `aspect_hints`.
The `rust_api_from_cpp` macro automatically generates a `.hint` target (named
`<name>.hint`) which contains the configuration. This hint target is then
attached to the C++ target's `aspect_hints`.
### Example
To enable Crubit on a C++ target, define a `rust_api_from_cpp` target and add
its `.hint` target to the C++ target's `aspect_hints`:
```python
cc_library(
name = "my_lib",
hdrs = ["my_lib.h"],
aspect_hints = [":my_lib_rust.hint"],
)
rust_api_from_cpp(
name = "my_lib_rust",
cpp_target = ":my_lib",
)
```
### FAQ: How do separate rules work without cycles? {#faq_separate_rules}
A library `A`, and its bindings `bindings(A)`, must be linked together in the
build graph: if `B` uses a type from `A`, then `bindings(B)` uses a type from
`bindings(A)`.
If we were to point `my_lib` directly to `my_lib_rust` via `aspect_hints`, and
`my_lib_rust` depended on `my_lib` (to trigger bindings generation), we would
create a dependency cycle: `my_lib_rust` -> `my_lib` -> `my_lib_rust`.
To avoid this, the `rust_api_from_cpp` macro splits the definition into two
parts:
1. A configuration target (`my_lib_rust.hint`) which contains the configuration
but does *not* depend on `my_lib`.
2. A bindings target (`my_lib_rust`) which depends on `my_lib`.
The `cc_library` target (`my_lib`) then references the configuration target
(`my_lib_rust.hint`) in its `aspect_hints`.
The dependency flow is: `my_lib_rust` -> `my_lib` -> `my_lib_rust.hint`
Since `my_lib_rust.hint` does not depend on `my_lib_rust` or `my_lib`, the cycle
is broken.
### FAQ: Why are there extra dependencies in `deps(target)`? {#faq_dependency_edges}
Because the Rust bindings are created using an **aspect** on the C++ target,
everything that the Rust bindings need to depend on will appear in a Bazel query
/ depserver query for `deps(target)`.
For example, if you wanted to add some extra source file to the Rust bindings,
you might specify them in `aspect_hints`. This file will show up in
`deps(target)`.
These Rust-only deps are not used at all in pure-C++ builds (the Bazel actions
registered by them won't be executed), but they will show up in the dependency
graph anyway, due to how Bazel query and depserver track dependencies.
NOTE: In particular, if your project has tests that count/limit the transitive
dependencies of a C++ binary, they will overcount the dependencies, and the
overcounting will get worse as Rust support is rolled out through the C++ build
graph.
</section>
## Wrapping and type bridging vs direct use of types {#bridging}
Crubit automatically generates layout-compatible Rust equivalents of C++ types.
When the C++ type is [Rust-movable](classes_and_structs.md#rust_movable), the
Crubit-generated Rust type is Rust-movable, these can be used by value, by
pointer, in struct fields, arrays, and any other compound data type. A C++
pointer `const T*` can become a Rust `*const T`, and a C++ `T` field can become
a Rust `T` field, and so on, with few restrictions.
For example, the following C++ type:
```c++
struct Vec2d {
float x;
float y;
};
```
Becomes (roughly) the following Rust type:
```rust
#[repr(C)]
struct Vec2d {
pub x: f32,
pub y: f32,
}
```
These have an identical layout, and so a C++ pointer or field containing a C++
`Vec2d` is exactly equivalent to a Rust pointer or field containing a Rust
`Vec2d`.
(See [Types](../types/) for more information about layout-compatibility.)
Because of this, it is often not required to manually write any new types. The
bindings generated by Crubit will produce a working type automatically.
### When to wrap a type
There are, still, a handful of reasons to manually write "wrapper" types which
encapsulate or replace the original C++ type (or its Crubit-generated Rust
type).
* If the type is **not** naturally Rust-movable, but it's important for the
Rust type to be Rust-movable. It may be possible to make changes to the C++
code to make the type Rust-movable using some of the strategies described in
[the cookbook](cookbook.md#rust_movable). This allows the greatest
flexibility, as the type becomes usable in almost every context. But if that
is not possible, writing a new "wrapper" type can keep Rust programmers
productive.
* Some Rust types have very special semantics, which are impossible to
implement in the bindings for a C++ type. For example, Rust has special
support for `Result` and `Option` in error handling via the `?` operator,
which cannot yet be implemented by `Status` or `std::optional` using stable
Rust features. These privileged Rust types can be used instead of the
equivalent C++ types, as a wrapper type.
* The type is simply not supported/supportable in Rust, and needs a wrapper as
a workaround. (See also: crubit.rs/errors/unsupported_type.)
In these cases, Crubit may bridge to a wrapper type as a workaround, while we
hopefully fix the underlying issues that mean we cannot directly use the
underlying type. This offers us a subset of the API we want, and allows
continued progress.
### Why not to wrap a type
Wrapper types work best when passed by value: if you return a `T` in C++, the
corresponding Rust function can automatically convert it to and return a
`WrappedT`.
However, no conversion is possible for references or fields, which really are
the original type, with its size and alignment and address in memory - to make
this work transparently requires an ever-expanding network of wrapper types, one
for every compound data type that might contain `T`:
* `T` must become `WrappedT`
* `const T&`, if it is supported at all, must become something like
`TRef<'a>`, or a dynamically sized `&TView`.
* `std::vector<T>`, if it is supported at all, must become something like
`TVector`.
* `struct MyStruct {T x;}` must become a wrapped `WrappedMyStruct`.
* ...
The problems introduced by wrapper types can easily outweigh the benefits that
they bring. Crubit aims to reduce their necessity to zero over time.
#### *Bad reasons to wrap a type*
In most other circumstances where one might *want* to reach for wrapper types,
alternatives exist:
* If we want to use a wrapper type in order to give the type a nicer Rust API,
then, as an alternative, one can customize the Rust API of the wrapped type
using an aspect hint. You can define new methods and trait implementations
to the side, without altering any C++ code.
* If we want to use a wrapper type in order to change the type invariants to
make them stricter or looser this is fine, as long as it doesn't *replace*
the not-as-nice type. For example, if a C++ API returns `std::string`
(bytes, "probably" UTF-8), the Rust equivalent should not return a Rust
`String` (Unicode, definitely UTF-8). Changing type invariants in-place
causes some APIs to become impossible to call, and causes the Rust and C++
ecosystems to diverge and become incompatible. The bindings should be high
fidelity. Wrapper types of this form should be optional, and available
equally to both C++ and Rust to avoid fragmenting the ecosystem.
</section>
## Fidelity {#fidelity}
Anything possible in C++ should be possible in Rust. See
[<internal link>](http://goto.google.com/no-hitchhikers).
The Rust API for a given C++ API should not try to make the interface "better"
at more than a superficial level, because it can compromise the ability of other
teams to write new Rust code, or port existing C++ code to Rust.
**Good changes:**
* Changing method names, especially to names that Rust callers might expect.
For example, changing `Status::ok()` (C++) to `Status::is_ok()` (Rust) –
Rust callers expect many of these boolean functions to be prefixed with
`is_`.
* Adding new APIs that Rust users expect. For example, trait implementations
that allow the type to better interoperate with the Rust ecosystem, or
functions which accept a `Path` or `&str` in *addition* to a raw C++
`string_view`.
* Reifying C++ comments around lifetime or safety as actual lifetime
annotations or `unsafe` declarations.
If the Rust type is outright unnatural to use, people won't use it, and it's
worse for the ecosystem to have two APIs than one API.
**Bad changes:**
* Removing deprecated APIs which still have C++ callers.
* Placing new requirements on Rust callers that were not placed on C++
callers, such as requiring UTF-8 when C++ does not.