| # C++ Bindings Cookbook |
| |
| This document presents a collection of techniques for creating Rust bindings for |
| C++ libraries. |
| |
| These techniques are often *workarounds* for gaps in what Crubit can do. Expect |
| the recommended practices to evolve over time, as Crubit's capabilities expand! |
| |
| > BEST PRACTICE: The tips below describe deviations from typical C++ style. (If |
| > typical C++ style worked, you wouldn't need a cookbook.) When you deviate from |
| > typical C++ style, **document why**, and try to keep changes limited in scope, |
| > close to the interop boundary. |
| > |
| > If possible, solve the same problem while staying within more typical C++ |
| > style. For example: you may be able to |
| > [add `ABSL_ATTRIBUTE_TRIVIAL_ABI`](#trivial_abi) to a type you control, |
| > instead of [boxing the type in a pointer](#boxing). |
| |
| ## Making types Rust-movable {#rust_movable} |
| |
| As described in crubit.rs/cpp/classes_and_structs#rust_movable, types have a |
| more pleasant API when they are Rust-movable. |
| |
| This can happen for a couple of easily fixable reasons, described in |
| subsections: |
| |
| * The type defines a destructor or copy/move constructor / assignment |
| operator. If it is in-principle still Rust-movable, and these functions do |
| not care about the address of the object in memory, then the type can be |
| [annotated with `ABSL_ATTRIBUTE_TRIVIAL_ABI`](#trivial_abi) |
| * The type has a field which is not rust-movable. In that case, the field can |
| be [boxed in a pointer](#boxing). |
| |
| There are *other* reasons a type can become non-Rust-movable, which do not have |
| these easy fixes described below. For example, virtual methods, or |
| non-Rust-movable base classes. For those, your only option is the hard option of |
| more radically restructuring your code to avoid those patterns. |
| |
| ### `ABSL_ATTRIBUTE_TRIVIAL_ABI` {#trivial_abi} |
| |
| crubit.rs/cpp/cookbook#trivial_abi |
| |
| One of the ways a type can become non-Rust-movable is if it has a copy/move |
| constructor / assignment operator, or a destructor. In that case, Clang will |
| assume that it cannot be trivially relocated, **unless** it is annotated with |
| [`ABSL_ATTRIBUTE_TRIVIAL_ABI`](https://github.com/abseil/abseil-cpp/blob/master/absl/base/attributes.h#:~:text=ABSL_ATTRIBUTE_TRIVIAL_ABI). |
| |
| ```c++ {.bad} |
| struct LogWhenDestroyed { |
| ~LogWhenDestroyed() { |
| std::cerr << "I was destroyed!\n"; |
| } |
| }; |
| ``` |
| |
| ```c++ {.good} |
| struct ABSL_ATTRIBUTE_TRIVIAL_ABI LogWhenDestroyed { |
| ~LogWhenDestroyed() { |
| std::cerr << "I was destroyed!\n"; |
| } |
| }; |
| ``` |
| |
| > WARNING: Only use `ABSL_ATTRIBUTE_TRIVIAL_ABI` if changing the location of an |
| > object in memory is safe. In particular, if the object is self-referential, |
| > using `ABSL_ATTRIBUTE_TRIVIAL_ABI` will result in Undefined Behavior (UB). |
| > |
| > ```c++ {.bad} |
| > class SelfReferential { |
| > public: |
| > SelfReferential(const SelfReferential& other) : x(other.x), x_ptr(&x) {} |
| > private: |
| > int x = 0; |
| > int* x_ptr = &x; |
| > } |
| > ``` |
| > |
| > Types like this, if Rust-moved, will contain invalid pointers. Carefully |
| > review any code adding `ABSL_ATTRIBUTE_TRIVIAL_ABI`. |
| |
| ### Boxing in a pointer {#boxing} |
| |
| crubit.rs/cpp/cookbook#boxing |
| |
| One of the ways a type can become non-Rust-movable is if it has a field, where |
| the type of that field is not Rust-movable. There is no way to override this: |
| there is nothing a type can do to make itself Rust-movable if one subobject is |
| not. |
| |
| For example, consider a field like `std::string name;`. `std::string` defines a |
| custom destructor and copy / move constructor/assignment operator, in order to |
| correctly manage owned heap memory for the string. Because of this, it also is |
| not Rust-movable. And, at the time of writing, `std::string` currently cannot |
| use `ABSL_ATTRIBUTE_TRIVIAL_ABI` in any STL implementation. In the case of |
| libstdc++, for example, `std::string` contains a self-referential pointer: when |
| the string is small enough, the `data()` pointer refers to the inside of the |
| string. Rust-moving it would cause the pointer to refer back to the *old* |
| object, which would cause undefined behavior. |
| |
| If a struct or class contains a `std::string` as a subobject by value, or any |
| other non-Rust-movable object, then that struct or class is itself also not |
| Rust-movable. (If you somehow were able to Rust-move the parent object, this |
| would also Rust-move the `string`, causing the very same issues.) |
| |
| Instead, what you can do is change the type of the field, so that it doesn't |
| contain the problematic type *by value*. Instead, it can hold the |
| non-Rust-movable type by pointer. |
| |
| BEST PRACTICE: Except where necessary for better Rust interop, this is **not** |
| good C++ style. When you use this trick, document why, and try to limit it to |
| types close to the interop boundary. If possible, instead of boxing `T`, make |
| `T` itself rust-movable. (This is not easy for standard library types, but if |
| the type is under your control, it *may* be as easy as adding |
| `ABSL_ATTRIBUTE_TRIVIAL_ABI`.) |
| |
| #### `unique_ptr` {#unique_ptr} |
| |
| NOTE: The following is non-portable, and only works in libc++ with the unstable |
| ABI. If you aren't sure about whether you are using the unstable ABI, it is likely that you are not, but you might want to check in with your local toolchain maintainer. |
| |
| If you tightly control your dependencies, you might be using |
| libc++'s unstable ABI. The unstable ABI, among other things, makes |
| `unique_ptr<T>` (and `shared_ptr<T>`) Rust-movable. In fact, it is Rust-movable |
| even if `T` itself is not. |
| |
| This means that if a particular field is making its parent type |
| non-Rust-movable, one fix is to wrap it in a `unique_ptr`: |
| |
| ```c++ {.bad} |
| struct Person { |
| std::string name; |
| int age; |
| } |
| ``` |
| |
| ```c++ {.good} |
| struct Person { |
| // boxed to make Person rust-movable: crubit.rs/cpp/cookbook#boxing |
| std::unique_ptr<std::string> name; |
| int age; |
| } |
| ``` |
| |
| #### Raw pointers {#raw_ptr} |
| |
| BEST PRACTICE: This should only be used in codebases that do not use a |
| Rust-movable `unique_ptr` or `unique_ptr` equivalent. Consider wrapping this in |
| an `ABSL_ATTRIBUTE_TRIVIAL_ABI` type which resembles `unique_ptr`, instead. |
| |
| <section class="zippy" markdown="1"> |
| |
| When not using libc++'s unstable ABI, the most straightforward way to make a |
| field Rust-movable is to instead use a **raw** pointer, and delete it in the |
| destructor (as if it were held by a `unique_ptr`). |
| |
| ```c++ {.bad} |
| struct Person { |
| std::string name; |
| int age; |
| } |
| ``` |
| |
| ```c++ {.good} |
| struct ABSL_ATTRIBUTE_TRIVIAL_ABI Person { |
| // Owned, boxed to make Person rust-movable: crubit.rs/cpp/cookbook#boxing |
| std::string* name; |
| int age; |
| |
| ~Person() { |
| delete name; |
| } |
| } |
| ``` |
| |
| (Note the use of `ABSL_ATTRIBUTE_TRIVIAL_ABI`: because we added a destructor, we |
| also need to add `ABSL_ATTRIBUTE_TRIVIAL_ABI` to indicate that the destructor |
| does not care about the address of `Person`.) |
| |
| </section> |
| |
| ## Renaming functions for Rust {#renaming} |
| |
| crubit.rs/cpp/cookbook#renaming |
| |
| Overloaded functions cannot be called from Rust (yet: b/213280424). To make them |
| available anyway, you can use the `CRUBIT_RUST_NAME` attribute macro to specify |
| a unique Rust name for each overload: |
| |
| ```c++ {.bad} |
| void Foo(int x); |
| void Foo(float x); |
| ``` |
| |
| ```c++ {.good} |
| #include "support/annotations.h" |
| |
| CRUBIT_RUST_NAME("FooInt") |
| void Foo(int x); |
| CRUBIT_RUST_NAME("FooFloat") |
| void Foo(float x); |
| ``` |
| |
| ## Thread-safety {#thread_safety} |
| |
| WARNING: Thread-safety in interop is subtle. Ensure you understand the |
| implications before exposing mutating methods to Rust. |
| |
| SUMMARY: The preferred way to make a thread-safe C++ type available to Rust is |
| to annotate it with `CRUBIT_THREAD_SAFE`. This automatically implements `Send + |
| Sync` in Rust, wraps the type in `UnsafeCell`, and allows non-const methods to |
| be called via shared references (`&self`). However, this requires all mutated |
| fields to be marked `mutable`, or else for the object to never be stored in a |
| C++ `const`. |
| |
| ### `CRUBIT_THREAD_SAFE` {#crubit_thread_safe} |
| |
| WARNING: This is an unsafe operation. Only use this annotation on thread-safe |
| types which are never created as `const`, or else which mark all fields that can |
| be mutated as `mutable`, even if those fields can only be mutated from a |
| non-const method. |
| |
| To make a C++ class that internally synchronizes access (e.g., using mutexes or |
| atomics) safe to use concurrently in Rust, annotate its definition with |
| `CRUBIT_THREAD_SAFE`: |
| |
| ```c++ |
| #include "support/annotations.h" |
| |
| class CRUBIT_THREAD_SAFE ThreadSafeCounter { |
| public: |
| void Increment(); // Can be called via `&self` in Rust. |
| int Get() const; // Can be called via `&self` in Rust. |
| private: |
| mutable std::atomic<int> counter_; |
| }; |
| ``` |
| |
| This annotation tells Crubit to: |
| |
| 1. **Implement `Send` and `Sync`** for the type in Rust. |
| 2. **Wrap the Rust representation in `UnsafeCell`**, which permits shared mutation |
| without undefined behavior in Rust. |
| 3. **Expose non-const C++ methods as taking `&self`** (shared reference) instead of |
| `&mut self` or raw pointers. |
| |
| This allows Rust callers to use the type concurrently across threads using |
| standard shared references. |
| |
| Because `CRUBIT_THREAD_SAFE` makes the type internally mutable in Rust, the type |
| must either also be internally mutable in C++, by making all mutated fields |
| `mutable`, or else it must never be stored as a `const` object. |
| |
| To help prevent accidentally passing in references to C++ objects with `const` |
| storage when calling back into Rust, Crubit automatically annotates generated |
| Rust bindings for `CRUBIT_THREAD_SAFE` types with a Rust-side attribute |
| (`#[crubit_annotate::cpp_thread_safe]`). When generating C++ bindings from Rust |
| (`cc_bindings_from_rs`) for functions that accept or return shared references |
| (`&MyThreadSafeType`) to these types, Crubit generates non-const references in |
| C++ (`MyThreadSafeType&` instead of `const MyThreadSafeType&`). This prevents |
| C++ callers from passing immutable `const` objects into Rust functions that |
| might internally mutate them through a shared reference. |
| |
| ### Manual Approach (Legacy) {#manual_thread_safety} |
| |
| <section class="zippy" markdown="1"> |
| |
| Before `CRUBIT_THREAD_SAFE` was available, or if you cannot annotate the class |
| directly, making a mutating C++ method safe to call concurrently in Rust required |
| a manual workaround: |
| |
| 1. The class must be documented as thread-safe. |
| 2. The field being mutated must be marked as `mutable`, even if it is only |
| mutated in non-const methods, so that internal mutation in Rust does not |
| cause UB. |
| 3. The method must be manually wrapped in a C++ `const` method (or wrapped in |
| Rust with `unsafe` casts). |
| |
| Due to b/481398972, the mutated field cannot be public. Due to b/481405536, the |
| class itself must not be trivially copyable. |
| |
| Example of manual wrapping: |
| |
| ```c++ |
| class FakeClock { |
| public: |
| // The destructor is only here to make the type non-trivially copyable. |
| // TODO(b/481405536): Remove the destructor once this bug is fixed. |
| ~FakeClock() {} |
| CRUBIT_RUST_NAME("advance_mut") |
| void Advance(int n) { |
| DoNotUseForRustOnlyAdvance(n); |
| } |
| |
| CRUBIT_RUST_NAME("advance") |
| void InternalDoNotUseForRustOnlyAdvance(int n) const { |
| mytime_ += n; |
| } |
| private: |
| // Mutable for Rust. |
| mutable int mytime_; |
| }; |
| ``` |
| |
| You can also use the `CRUBIT_THREAD_SAFE` annotation, but this is more dangerous, as |
| there is no enforcement that every method only modifies `mutable` fields, and |
| requires more extensive review: |
| |
| ```c++ |
| class CRUBIT_THREAD_SAFE FakeClock { |
| ... |
| }; |
| ``` |
| |
| Using `const` methods, while less idiomatic in C++, structurally forces |
| soundness, so that there is no UB. |
| |
| </section> |
| |
| ## Working around blocking bugs in Crubit {#blockers} |
| |
| Crubit is still in development, and has bugs which can completely stop your work |
| if Crubit was in the critical path. These can take the form of parsing errors or |
| crashes when Crubit runs, or else generated bindings which do not compile. |
| |
| The following workarounds can help get you moving again: |
| |
| ### Disable Crubit on a declaration {#disable-declaration} |
| |
| If a declaration causes hard failures within Crubit, that declaration alone can |
| be disabled using the `CRUBIT_DO_NOT_BIND` attribute macro: |
| |
| ```c++ |
| #include "support/annotations.h" |
| |
| CRUBIT_DO_NOT_BIND // b/123: Crubit crashes on std::crash_rust_plz_t |
| void foo(std::crash_rust_plz_t*); |
| ``` |
| |
| > NOTE: By disabling Crubit on this declaration, items which depend on it may |
| > also, in turn, not receive bindings. For example, if it declares a type, then |
| > functions which accept or return that type will also not receive bindings. |
| > |
| > For that reason, except for functions, `CRUBIT_DO_NOT_BIND` requires modifying |
| > an allowlist at |
| > rs_bindings_from_cc/bazel_support/generate_bindings.bzl. |
| > Changes to that file are most easily mailed using <internal link>_manage: add |
| > `AUTO_MANAGE=testing:TGP` to the CL description. |
| |
| ### Disable Crubit on a header {#disable-header} |
| |
| If an entire header is giving problems (e.g. is unparseable), then it can be |
| removed from consideration by Crubit. Once disabled, Crubit will avoid reading |
| the header directly, although it is still included via `#include` preprocessor |
| directives. |
| |
| Add the target name and header name to `public_headers_to_remove` in |
| rs_bindings_from_cc/bazel_support/rust_bindings_from_cc_aspect.bzl. |
| See the example in |
| rs_bindings_from_cc/test/disable/disable_header/. |
| |
| To mail the CL performing this change, use <internal link>_manage: add |
| `AUTO_MANAGE=testing:TGP` to the CL description. |
| |
| > NOTE: By disabling Crubit on this header, items which are defined in that |
| > header will not receive bindings. For example, this means that functions which |
| > use types defined in that header will also not get bindings, even if the |
| > function was defined in a header that was not disabled. |
| > |
| > When possible, it's preferable to use a smaller fix. For example, if the same |
| > header is owned by two targets, it's preferable to move the header into a |
| > third target, depended on by both. That way, functions which use types defined |
| > in that header will still get bindings, in both targets. |
| |
| ## Making backwards-compatible changes to C++ code with Rust callers {#compatibility} |
| |
| Because Rust does not have all the features C++ does, some changes that do not |
| *usually* break a C++ caller can break a Rust caller. For example, most C++ |
| callers will not be broken by adding a new overload to an overload set, but that |
| does break Rust callers, because Rust doesn't have overloading. |
| |
| Most of the time, you can think of Rust users (and Crubit) as the most |
| pathological possible C++ caller, relying on all the details of your API. |
| |
| The following sections attempt to be an exhaustive list of the things you might |
| expect to be able to do if you only have C++ callers, but cannot do with Rust |
| callers. |
| |
| ### Making a type non-Rust-movable {#compatibility-non_movable} |
| |
| If a type is made non-Rust-movable, when it was previously Rust-movable, this |
| will break all but the most trivial of Rust callers. |
| |
| Specifically, the following operations are compatibility-breaking if the type |
| was Rust-movable: |
| |
| * Adding a virtual method. |
| * Adding a user-defined copy constructor, move constructor, or destructor, |
| without specifying `ABSL_ATTRIBUTE_TRIVIAL_ABI`. |
| * Adding a non-Rust-movable field or base class (such as `std::string`). |
| |
| ```c++ {.no-copy} |
| struct Before { |
| int x; |
| }; |
| struct After { |
| ~After() {} |
| |
| int x; |
| std::string y; |
| }; |
| |
| Before ReturnBefore(); |
| After ReturnAfter(); |
| ``` |
| |
| ```rust {.no-copy} |
| #[derive(Copy, Clone)] |
| struct Before {...} |
| struct After {...} |
| |
| pub fn ReturnBefore() -> Before {...} |
| pub fn ReturnAfter() -> Ctor![After] {...} |
| ``` |
| |
| **Fix:** Preserve Rust-movability, as described above in |
| [Making types Rust-movable](#rust_movable) |
| |
| ```c++ {.good, .no-copy} |
| class ABSL_ATTRIBUTE_TRIVIAL_ABI CompatibleAfter { |
| ~After() {} |
| |
| int x; |
| std::unique_ptr<std::string> y; |
| }; |
| ``` |
| |
| ### Adding or removing virtual destructors {#compatibility-polymorphic} |
| |
| If a type's destructor is made virtual, or was previously virtual and now |
| becomes nonvirtual, this changes how Rust callers invoke destruction. For |
| example, `unique_ptr<T>` becomes `virtual_unique_ptr<T>` if `T` has a virtual |
| destructor. |
| |
| ```c++ {.no-copy} |
| class Before {}; |
| class After {~After(){}}; |
| |
| std::unique_ptr<Before> ReturnBefore(); |
| std::unique_ptr<After> ReturnAfter(); |
| ``` |
| |
| ```rust {.no-copy} |
| pub fn ReturnBefore() -> cc_std::std::unique_ptr<Before> {...} |
| pub fn ReturnAfter() -> cc_std::std::virtual_unique_ptr<After> {...} |
| |
| let _: cc_std::std::unique_ptr<_> = ReturnAfter(); // error[E0308]: mismatched types |
| ``` |
| |
| **Fix:** Migrate callers that use heap allocated types such as `unique_ptr`. |
| |
| ```rust {.good .no-copy} |
| let _: cc_std::std::virtual_unique_ptr<_> = ReturnAfter(); |
| ``` |
| |
| ### Adding an overload {#compatibility-overload} |
| |
| Except for constructors and operators, adding an overload to a C++ function will |
| cause it to no longer receive bindings, unless the overloads are given distinct |
| names in Rust (via `CRUBIT_RUST_NAME`). |
| |
| ```c++ {.no-copy} |
| void Before(); |
| |
| void After(); |
| void After(int); |
| ``` |
| |
| ```rust {.no-copy} |
| pub fn Before() {...} |
| // No bindings for After. |
| ``` |
| |
| **Fix:** Use `CRUBIT_RUST_NAME("NewName")` to specify a per-overload name. |
| |
| ```c++ {.good .no-copy} |
| void CompatibleAfter(); |
| |
| CRUBIT_RUST_NAME("CompatibleAfterInt") |
| void CompatibleAfter(int); |
| ``` |
| |
| ```rust {.good .no-copy} |
| pub fn CompatibleAfter() {...} |
| pub fn CompatibleAfterInt(_: c_int) {...} |
| ``` |
| |
| ### Changing types with implicit conversions {#compatibility-conversions} |
| |
| In C++, it is often compatible to change from, for example, accepting an |
| `int32_t` to a `int64_t`, or similar. This is not a compatible change in Rust, |
| because implicit conversions do not exist. |
| |
| ```c++ {.no-copy} |
| void Before(int32_t); |
| void After(int64_t); |
| ``` |
| |
| ```rust {.no-copy} |
| pub fn Before(_: i32) {...} |
| pub fn After(_: i64) {...} |
| |
| let x: i32 = ...; |
| After(x); // error[E0308]: mismatched types |
| ``` |
| |
| **Fix:** Migrate callers. |
| |
| ```rust {.good .no-copy} |
| After(x.into()) |
| ``` |
| |
| ### Unsupported features {#compatibility-unsupported} |
| |
| Use of other unsupported features of C++, such as `decltype` or various |
| unsupported clang attributes, will also cause breakages for Rust callers. |
| |
| This is an open-ended failure category, and includes, at minimum: |
| |
| * Changing from a concrete function to a template function |
| * Changing from an ordinary class to a type alias to a template specialization |
| * Unrecognized attributes |
| * Types that do not have Crubit support yet |
| |
| For instance: |
| |
| ```c++ {.no-copy} |
| int Before(int x) {...} |
| |
| [[clang::very_interesting_advanced_attribute]] |
| auto After(int x) -> decltype(x + std::declval<int>()) {...} |
| ``` |
| |
| ```rust {.no-copy} |
| pub fn Before(_: c_int) -> c_int {...} |
| // No bindings for After |
| ``` |
| |
| **Fix:** Avoid the use of these features, or wrap with Rust-only functions that |
| avoid the use of these features. |
| |
| ```c++ {.good .no-copy} |
| int CompatibleAfter(int x) {return After(x);} |
| ``` |
| |
| ```rust {.good .no-copy} |
| pub fn After(_: c_int) -> c_int {...} |
| ``` |