Rust bindings for C++ libraries
When a C++ library enables Crubit, that library can be used directly from Rust. This page documents roughly what that entails, and additional subpages (available in the left-hand navigation) document specific aspects of the generated bindings.
Tip: The code examples below are pulled straight from examples/cpp/function/. The other examples in examples/cpp/ are also useful. If you prefer just copy-pasting something, start there.
How to use Crubit
Crubit allows you to call some C++ interfaces from Rust. It supports functions, rust-movable classes and structs, and enums. Crubit does not support advanced features like templates or virtual inheritance.
The rest of this document goes over how to create a C++ library that can be called from Rust, and how to actually call it from Rust. The quick summary is:
-
A
cc_librarygets (nonempty) Rust bindings if it specifiesaspect_hints = ["//features:supported"]. -
Any Rust build target can depend on the bindings for a
cc_library, by specifyingcc_deps=["//path/to:target"]. -
The bindings can be previewed using the following command:
$ bazel build --config=crubit-genfiles //path/to:target
Write a cc_library target
The first part of creating a library that can be used by Crubit is to write a
cc_library target. For example:
cs/file:examples/cpp/function/example.h
If you write a BUILD target as normal, it will not actually get Crubit bindings, but we'll start from there:
cs/file:examples/cpp/function/BUILD symbol:example_lib_broken
Look at the generated bindings
Bindings can be generated for any C++ target, anywhere in the build graph. (Crubit is an aspect1 on all C++ targets.) However, that is not to say that the generated bindings will be useful: by default, Crubit doesn't generate any bindings. Try it!
To examine the generated C++ bindings for the target, you can run the following command:
$ bazel build --config=crubit-genfiles //examples/cpp/function:example_lib_broken
This is the best way to preview the generated bindings for a given C++ target right now. You might end up using this a lot, so keep it in your shell history.
If you run the above command, you should see some output like the following:
Aspect //rs_bindings_from_cc/bazel_support:rust_bindings_from_cc_aspect.bzl%rust_bindings_from_cc_aspect of //examples/cpp/function:example_lib_broken up-to-date:
bazel-bin/examples/cpp/function/example_lib_broken_rust_api_impl.cc
bazel-bin/examples/cpp/function/example_lib_broken_rust_api.rs
bazel-bin/examples/cpp/function/example_lib_broken_namespaces.json
These files are the generated bindings which are used under the hood when depending on a C++ target from Rust. They consist of:
- The supporting C++ code to glue Rust and C++ together. (The
.ccfile.) - The public Rust interface. (The
.rsfile.) - Supporting information that is used by bindings that depend on these
bindings. (The
.jsonfile.)
You don't need to check them in, as they are regenerated automatically whenever you build a Rust build target which depends on C++.
The .rs file is the interesting one for end users. For a library like
:example_lib_broken, which does not enable Crubit, the .rs file will be
essentially empty, only consisting of comments describing the bindings it did
not generate:
#![allow(unused)] fn main() { // Generated from: examples/cpp/function/example.h;l=11 // Error while generating bindings for item 'crubit_add_two_integers': // Can't generate bindings for crubit_add_two_integers, because of missing required features (<internal link>): // //examples/cpp/function:example_lib_broken needs [//features:supported] for crubit_add_two_integers (return type) // //examples/cpp/function:example_lib_broken needs [//features:supported] for crubit_add_two_integers (the type of x (parameter #0)) // //examples/cpp/function:example_lib_broken needs [//features:supported] for crubit_add_two_integers (the type of y (parameter #1)) // //examples/cpp/function:example_lib_broken needs [//features:supported] for crubit_add_two_integers (extern \"C\" function) }
This error is saying something important. It was trying to generate bindings for
the function crubit_add_two_integers, but it couldn't, because four different
things about the function require the supported feature to be enabled on the
target. The parameter and return types require supported, as does the function
itself in the abstract.
supported indicates that a library target supports Rust callers via Crubit,
using the stable features. Other functions and classes might require
experimental, for experimental features of Crubit. For example, if we had
defined anoperator+. For more on this, see
Enable Crubit on a target
To enable Crubit on a C++ target, one must pass an argument, via aspect_hints.
Specifically, as mentioned in the comments, the target must enable the
supported feature:
cs/file:examples/cpp/function/BUILD symbol:\bexample_lib\b
This tells Crubit that it can generate bindings for this target, for any part of
the library that uses features from supported. Now, if we look at a preview of
the automatically generated bindings:
$ bazel build --config=crubit-genfiles //examples/cpp/function:example_lib
We can see the fully-fledged bindings for the library:
cs/file:examples/cpp/function/example_generated.rs
Use a C++ library from Rust
To depend on a C++ library from Rust, add it to cc_deps:
cs/file:examples/cpp/function/BUILD symbol:main
At that point, the bindings are directly usable from Rust. The interface is
identical to the .rs file previewed earlier, but can be used directly:
cs/file:examples/cpp/function/main.rs
Common Errors
Unsupported features
Some features are either unsupported, or else only supported with experimental
feature flags (
For a particularly notable example, a class cannot have a std::string field,
because std::string has properties around move semantics that Crubit does not
yet support. In turn, this means the class containing the std::string has
semantics that Crubit doesn't yet support.
The way to work around this kind of problem, in all cases, is to wrap or hide the problematic interface behind an interface Crubit can handle:
- Move nontrivial types behind a
unique_ptr<T>. Astd::stringfield is not rust-movable, but aunique_ptr<std::string>field is. - Hide unsupported types, in general, behind a wrapper. For example, a
std::vector<T>is not supported, but a struct which wraps aunique_ptr<std::vector<int32_t>>is. - Wrap unsupported functions behind wrappers. For example, methods are not yet supported, but top-level functions are, and can invoke methods.
Crubit is an aspect: an automatically generated entity that exists on every build target. It is disabled by default, so that Rust callers don't accidentally impose on C++ libraries that weren't expecting them.
Aspects allow Crubit to fully understand the dependency graph: the
bindings for X are in the Crubit aspect of X. This allows Crubit to
generate bindings which themselves rely on bindings: if a function
in target `A` returns a struct from target `B`, we know that the
bindings for `A` will depend on the bindings for `B`. Because Crubit
is an aspect, it already knows the name of the bindings for `B`:
it's simply the Crubit aspect on `B`!
Without aspects, or something like aspects, you would need to write
down, for every library, the location of its Rust bindings. There is
no need for that kind of boilerplate when aspects are involved, and
that is why most things shaped like Crubit use aspects. For example,
protocol buffers use aspects for their generated implementations in
multiple languages. (They *also* use named rules, but the rules
simply re-export the aspect, and the underlying aspect is what is
used within the rule for referring to transitive dependencies.)
Thanks to aspects, the `proto_library` doesn't need to re-specify
"ah, and the Go proto is named `'x'`".
Be not afraid! Aspects are what make transitive dependencies work
seamlessly, without boilerplate. So when you see aspect this, or
aspect that, remember: this is a Good Thing.