| # 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. |