Note: This is the actively maintained continuation of google/autocxx, which is no longer maintained by Google (see google/autocxx#1507). Bug reports, feature requests and pull requests are welcome here.
autocxx — automatic safe interop between Rust and C++
Welcome to autocxx and thank you for reading!
Use autocxx if you have a large existing C++ codebase and you want to use its types and functions from Rust with maximal safety and minimal fuss.
autocxx is like bindgen, in that it enables you to use C++ functions and types from within Rust. But it automates a lot of the fiddly things you need to do with bindgen bindings: calling destructors, converting strings, unsafely handling raw pointers. C++ functions and types within autocxx bindings should behave naturally and ergonomically, almost as if they were safe Rust functions and types themselves. These ergonomics and safety improvements come from the cxx project - hence the name of this tool, autocxx.
autocxx combines the safety and ergonomics of cxx with the automatic bindings generation of bindgen. It stands on the shoulders of those giants!
When is autocxx the right tool?
Not always:
- If you are making bindings to C code, as opposed to C++, use
bindgeninstead. - If you can make unrestricted changes to the C++ code, use
cxxinstead. - If your C++ to Rust interface is just a few functions or types, use
cxxinstead.
But sometimes:
- If you need to call arbitrary functions and use arbitrary types within an existing C++ codebase, use
autocxx. You’re in the right place! - Like
cxx, but unlikebindgen,autocxxhelps with calls from C++ to Rust, too.
Examples to give you a feel for autocxx
Here’s a code example:
C++ header:
#include <stdint.h>
inline uint32_t do_math(uint32_t a, uint32_t b) { return a+b; }
Rust:
// Use all the autocxx types which might be handy.
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("do_math") // allowlist a function
}
fn main() {
assert_eq!(ffi::do_math(12, 13), 25);
}
A more complex example:
C++ header:
#include <cstdint>
#include <string>
class Goat {
public:
Goat();
~Goat();
void add_a_horn();
std::string describe() const;
private:
uint32_t horns;
};
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("Goat") // allowlist a type and all its methods
}
fn main() {
let mut goat = ffi::Goat::new().within_unique_ptr(); // returns a cxx::UniquePtr, i.e. a std::unique_ptr
goat.pin_mut().add_a_horn();
goat.pin_mut().add_a_horn();
assert_eq!(goat.describe().as_ref().unwrap().to_string_lossy(), "This goat has 2 horns.");
}
This is typical autocxx code: the C++ objects behave much like Rust objects, but
sometimes extra steps are required to handle cases like null pointers or converting strings that may not be UTF-8.
Still, fundamentally, you can interact with C++ objects without using unsafe in the majority of cases.
cxx takes care of the fundamentals of lifetimes and destructors.
How to read this book
We’d recommend starting with tutorial and then workflow.
Tutorial
If you’re here, you want to call some C++ from Rust, right? (If you want to do things the other way round, read the Rust calls page.)
Let’s assume you’re calling into some existing C++ code.
You will need:
- Some C++ header files (
.hfiles) - The C++ “include path”. That is, the set of directories containing those headers. (That’s not necessarily the directory in which each header file lives; C++ might contain
#include "foo/bar.h"and so your include path would need to include the directory containing thefoodirectory). - A list of the APIs (types and functions) from those header files which you wish to make available in Rust.
- To know how to link the C++ libraries into your Cargo project. This is beyond the scope of what
autocxxhelps with, but one solution is to emit a print from your build script. - LLVM to be installed.
- Some patience. This is not a magic solution. C++/Rust interop is hard. Avoid it if you can!
The rest of this ‘getting started’ section assumes Cargo - if you’re using something else, see the building section.
First, add autocxx and cxx to your dependencies and autocxx-build to your build-dependencies in your Cargo.toml. You must specify both.
[dependencies]
autocxx = "0.30.0"
cxx = "1.0"
[build-dependencies]
autocxx-build = "0.30.0"
miette = { version = "7", features = ["fancy"] } # optional but gives nicer error messages!
Now, add a build.rs next to your Cargo.toml (this is a standard cargo build script). This is where you need your include path:
fn main() -> miette::Result<()> {
let include_path = std::path::PathBuf::from("src");
// This assumes all your C++ bindings are in main.rs
let mut b = autocxx_build::Builder::new("src/main.rs", &[&include_path]).build()?;
b.std("c++14")
.compile("autocxx-demo"); // arbitrary library name, pick anything
// Add instructions to link to any C++ libraries you need.
Ok(())
}
See the standard cargo build script output mechanisms for how you can direct Rust to link against pre-existing libraries, and see the building section of this book for more details about build options - for example, enabling C++17.
Finally, in your main.rs you can use the include_cpp macro which is the heart of autocxx:
use autocxx::prelude::*; // use all the main autocxx functions
include_cpp! {
#include "my_header.h" // your header file name
safety!(unsafe) // see details of unsafety policies described in the 'safety' section of the book
generate!("DeepThought") // add this line for each function or type you wish to generate
}
You should then find you can call the function by referring to an ffi namespace:
fn main() {
println!("The answer to Life, The Universe and Everything is {}", ffi::DeepThought());
}
C++ types such as std::string and std::unique_ptr are represented using the types provided by the marvellous cxx library. This provides good ergonomics and safety norms, so unlike with normal bindgen bindings, you won’t normally need to write unsafe code for every function call.
Next, read the section about workflows.
Workflow
C++ is complex, and autocxx can’t ingest everything.
First tip - use an IDE. Type annotation and autocompletion is incredibly helpful in an autocxx
context, where you may be dealing with UniquePtr<T> and Option<&T> and Pin<&mut T> very often.

As you’ll see, it’s also essential when autocxx can’t produce bindings for some reason.
What if autocxx can’t generate bindings?
This bit is important.
When you use autocxx, you’ll ask it to generate Rust bindings for C++ types or functions using
generate! directives.
If you name something specifically in a generate! or generate_pod! directive - a function
or a type - and autocxx can’t generate bindings for it: the build will fail, and the error
will say what stopped it.
If you ask to generate bindings for an entire type, autocxx will generate bindings for as
many methods as possible. For those methods where it can’t generate bindings, it will instead
generate some placeholder function or struct with documentation explaining what went wrong:

This is why it’s crucial to use an IDE with autocxx.
How can I see what bindings autocxx has generated?
Options:
- Use an IDE. (Did we mention, you should use an IDE?)
- Run
cargo doc --document-private-items. - Use
cargo expand. - Add
pretty!()to yourinclude_cpp!and read the fileAUTOCXX_RS_FILEnames. Without it that file is a single line of tokens.
How to work around cases where autocxx can’t generate bindings
Your options are:
- Write extra C++ functions with simpler parameters or return types, and generate bindings to them, instead.
- Write some manual
#[cxx::bridge]bindings - see below.
Usually, you can solve problems by writing a bit of additional C++ code. For example,
supposing autocxx can’t understand your type Sandwich<Ham>. Instead it will give
you a fairly useless opaque type such as Sandwich_Ham. You can write additional
C++ functions to unpack the opaque type into something useful:
const Ham& get_filling(const Sandwich<Ham>& ham_sandwich);
Withholding one function
Sometimes the trouble is a single method of an otherwise useful class:
autocxx generates something for it which won’t compile. The only way out used
to be taking the whole class out of your generate! directives, because
block! names a type
and there was nothing smaller to name.
block_functions!
names that one function instead. The rest of the class binds as usual:
include_cpp! {
#include "my_header.h"
safety!(unsafe_ffi)
generate!("ns::Engine")
block_functions!("ns::Engine::set_mode")
}
Name the function as C++ names it: a member function with the class it belongs
to in front of it, a free function without a class, with its namespaces, as
generate! names one. The namespaces may be left off, which costs what it
costs in throws! - a name written short claims every function answering to
it, in every class and every namespace. The class may not be left off:
block_functions!("ns::set_mode") names a free function in ns, never a
method of some class in ns.
The whole overload set of that name goes. C++ picks between overloads by their arguments, so a directive naming one name has no way to say which of them it meant.
In place of each binding you get the documentation stub every discarded binding leaves, saying the function was blocked - nothing disappears silently. A blocked member of a base class is not imported into the classes which derive from it. Nor does a Rust subclass get an override for a blocked virtual method - so blocking a pure virtual leaves that subclass’s C++ peer abstract, which the C++ compiler will not accept.
Constructors and the destructor cannot be named this way, and no directive
withholds an explicitly declared one. What there is instead is narrower:
block_constructors!
names a class and stops autocxx synthesizing the special members C++ declares
implicitly for it - constructors and destructors the class declares itself are
bound as usual.
A block_functions! which names no function autocxx met is a build error,
since it withheld nothing.
Mixing manual and automated bindings
autocxx uses cxx underneath, and its build process will happily spot and
process manually-crafted cxx::bridge mods which you include in your
Rust source code. A common pattern could be to use autocxx to generate
all the bindings possible, then hand-craft a cxx::bridge mod for the
remainder where autocxx falls short.
To do this, you’ll need to use the ability of one cxx::bridge mod to refer to types from another, for example:
autocxx::include_cpp! {
#include "foo.h"
safety!(unsafe_ffi)
generate!("take_A")
generate!("A")
}
#[cxx::bridge]
mod ffi2 {
unsafe extern "C++" {
include!("foo.h");
type A = crate::ffi::A;
fn give_A() -> UniquePtr<A>; // in practice, autocxx could happily do this
}
}
fn main() {
let a = ffi2::give_A();
assert_eq!(ffi::take_A(&a), autocxx::c_int(5));
}
In the example above, we’re referring from manual bindings to automated bindings.
You can also do it the other way round using extern_cpp_opaque_type!:
autocxx::include_cpp! {
#hexathorpe include "input.h"
safety!(unsafe_ffi)
generate!("handle_a")
generate!("create_a")
extern_cpp_opaque_type!("A", ffi2::A)
}
#[cxx::bridge]
pub mod ffi2 {
unsafe extern "C++" {
include!("input.h");
type A;
}
impl UniquePtr<A> {}
}
fn main() {
let a = ffi::create_a();
ffi::handle_a(&a);
}
My build entirely failed
autocxx should nearly always successfully parse the C++ codebase and
generate some APIs. It’s reliant on bindgen, but bindgen is excellent
and rarely bails out entirely.
If it does, you may be able to use the block! macro.
We’d appreciate a minimized bug report of the troublesome code - see contributing.
Enabling autocompletion in a rust-analyzer IDE
You’ll need to enable both:
- Rust-analyzer: Proc Macro: Enable
- Rust-analyzer: Experimental: Proc Attr Macros
Next steps
Now you’ve read what can go wrong with autocxx, and how to diagnose problems - the next step is to give it a try!
Treat the rest of this manual as a reference.
The allowlist and include_cpp syntax
To include C++ in your Rust codebase using autocxx, you will need
at least one include_cpp macro.
The simplest is:
use autocxx::prelude::*;
include_cpp! {
#include "my_header.h"
generate!("MyAPIFunction")
}
You need to include generate! directives
for every type or function you wish to access from Rust. You don’t need to specify this for member functions
of types that you’ve added - they’ll be generated automatically. (If a particular member function can’t
be generated, some placeholder item with explanatory documentation will be generated instead).
Various other directives are possible inside this macro, most notably:
- You can ask to generate all the items in a namespace using
generate_ns!. Unlikegenerate!, whole-namespace generation is tolerant: items which can’t be generated get a placeholder, whereas anything you name explicitly and which can’t be generated fails the build. - You might sometimes want to ask that a type is generated as ‘plain old data’ using
generate_pod!instead ofgenerate!- see the chapter on C++ types. - You’ll probaly want to specify a
safety!policy
See the docs.rs documentation for the full list.
Building
Building if you’re using cargo
The basics of building in a cargo environment are explained in the tutorial.
If your build depends on later editions of the C++ standard library, you will need to ensure that both libclang and the compiler are sent the appropriate flag, like this:
fn main() {
let path = std::path::PathBuf::from("src"); // include path
let mut b = autocxx_build::Builder::new("src/main.rs", &[&path])
.extra_clang_args(&["-std=c++17"])
.build()
.unwrap();
b.std("c++17") // cc picks the separator per compiler: `-std=c++17`, or `-std:c++17` for cl
.compile("autocxx-demo"); // arbitrary library name, pick anything
// Add instructions to link to any C++ libraries you need.
}
Rebuilding
autocxx-build tells cargo to rerun your build script when the .rs file it
parsed changes, and when any header the C++ preprocessor opened while reading
it changes - which includes headers reached indirectly. You do not need to
name any of those yourself. Do name anything else your build script reads: the
.cc files you hand to cc, and any file you generate from.
Cargo stops scanning your package directory as a whole once a build script names a single file. Your build script’s own sources are still tracked, but any other file it reads and nobody names is one that can change without rerunning it - leaving both the generated Rust bindings and the C++ built from them as they were.
Keeping cxx and cxx-gen level
autocxx writes the C++ half of each generated function using cxx-gen, while
the Rust half comes from the cxx crate your own crate depends on. Since cxx
1.0.189 the patch level of each is part of the symbol name they agree on, so
cxx-gen 0.7.199 and cxx 1.0.190 produce two halves which never meet and the
link fails naming a symbol you never wrote.
Both are ordinary version requirements which cargo resolves to the newest
release, so they agree unless something pins one and not the other - usually a
lockfile updated in one place. cargo update -p cxx -p cxx-gen puts them back
in step. autocxx-build reports the mismatch before generating anything, as
long as your crate depends on cxx directly (which it needs to anyway, since
the generated code names ::cxx) and both crates come from a registry rather
than a [patch] or a vendor directory.
Building - if you’re not using cargo
See the autocxx-gen crate. You’ll need to:
- Run the
codegenphase. You’ll need to use theautocxx-gentool to process the .rs code into C++ header and implementation files. This will also generate.rsside bindings. - Educate the procedural macro about where to find the generated
.rsbindings. Set theAUTOCXX_RSenvironment variable to a list of directories to search. If you useautocxx-build, this happens automatically. (You can alternatively specifyAUTOCXX_RS_FILEto give a precise filename as opposed to a directory to search, though this isn’t recommended unless your build system specifically requires it because it allows only a singleinclude_cpp!block per.rsfile.) Seegen --helpfor details on the naming of the generated files.
flowchart TB
s(Rust source with include_cpp!)
c(Existing C++ headers)
cg(autocxx-gen or autocxx-build)
genrs(Generated .rs file)
gencpp(Generated .cpp and .h files)
rsb(Rust/Cargo build)
cppb(C++ build)
l(Linker)
s --> cg
c --> cg
cg --> genrs
cg --> gencpp
m(autocxx-macro)
s --> m
genrs-. included .->m
m --> rsb
gencpp --> cppb
cppb --> l
rsb --> l
This interop inevitably involves lots of fiddly small functions. It’s likely to perform far better if you can achieve cross-language link-time-optimization (LTO). This issue may give some useful hints - see also all the build-related help in the cxx manual which all applies here too.
C++ versions and other compiler command-line flags
Two standards are in play and they are set separately.
- The standard your headers are parsed at.
autocxxreads your headers withlibclang, at C++17. Raise or lower it withBuilder::extra_clang_args— see below. - The standard the generated code is compiled at. That is your
cc::Build’s business, andautocxxnever sets it. The codecxxandautocxxgenerate requires C++14, so it’s not possible to use an earlier version of C++ than that.
Keep the two the same where you can. A header read at one standard and compiled at another can differ in more than #if __cplusplus >= 201703L, where a declaration autocxx generated a binding for may simply not be there at compile time. The quieter cases need no conditional compilation at all, because C++17 changed rules the header is already written against:
- Overload resolution.
noexceptis part of a function’s type from C++17, so taking the address of anoexceptfunction picks a different overload than it did — and if the result feeds a constant expression, a valueautocxxrecorded from the parse may not be the value the compiler computes. - Constant evaluation. Guaranteed copy elision means
noexcept(noexcept(T(T{})))can be false at C++14 and true at C++17, so a method’s own exception specification can differ between the two.
Most builds are unaffected. autocxx parsed at C++14 while compilers have defaulted to later standards for years, so the two have rarely agreed on their own; C++17 makes them agree for anyone whose compiler defaults to C++17, and narrows the gap for everyone else. Compiler defaults move, though, so set both sides explicitly if it matters to you — that is the only way to know they agree.
The parse standard is C++17 because that is the first standard in which a function’s exception specification is part of its type. Only there can autocxx tell noexcept(false) from noexcept(true), which it needs in order to reproduce a superclass method’s specification on a subclass override rather than refuse it.
To compile the generated code at a later version, you need to:
- Build the generated code with a later C++ version. If you’re using autocxx’s cargo support, then you would do this by calling
stdon the returnedcc::Buildobject —b.std("c++17")— which names the standard and letsccpick the separator the compiler it chose wants:-std=c++17for gcc and clang,-std:c++17for cl (which takes-for/). Prefer it to passing the flag yourself withflag_if_supported, which hard-codes one compiler’s spelling and, wherever that spelling is not the one the compiler wants, is dropped without a word — leaving whatever standard the build would otherwise have used. Note thatccemits thestdflag before theCXXFLAGSfrom your environment, and a flag you pass yourself after them; so for gcc and clang a-std=inCXXFLAGSoverridesstd, and whichever of the two ends up in force is the one that has to agree withextra_clang_argsbelow. (For cl it takes a/std:or-std:there to have that effect; a-std=is not an option cl recognises.) - Also give similar directives to the C++ parsing which happens within autocxx (specifically, by autocxx’s version of bindgen). To do that, use
Builder::extra_clang_args. Calls to it accumulate in call order, so abuild.rswhich assembles its builder out of several helpers keeps each helper’s flags; what a repeated option then means is clang’s to decide, and it takes the last-std=.
The same applies with the command-line autocxx_gen support - you’ll need to pass such extra compiler options to autocxx_gen and also use them when building the generated C++ code.
If your headers do not parse as C++17
Most headers written for an older standard parse as C++17 unchanged. C++17 deleted a few things, and clang reports three of them as errors by default, which would stop autocxx reading the header at all. autocxx demotes each to a warning — you will still see it — so that a header containing one is still read: dynamic exception specifications (void f() throw(int)), the register storage class, and incrementing a bool. Pass -Werror=dynamic-exception-spec, -Werror=register or -Werror=increment-bool in extra_clang_args to have any of them back as an error, since your arguments come after autocxx’s.
What autocxx does not do is change which declarations the parse sees, so a standard library facility C++17 removed is genuinely gone: std::auto_ptr is the one most likely to come up, and on some standard libraries no macro brings it back. A header which needs one of those has to be parsed at an older standard:
let mut b = autocxx_build::Builder::new("src/main.rs", &[&path])
.extra_clang_args(&["-std=c++14"])
.build()
.unwrap();
b.std("c++14").compile("autocxx-demo");
autocxx puts its own -std= first and yours after it, and clang obeys the last one, so that is all it takes. (BINDGEN_EXTRA_CLANG_ARGS lands after both and overrides both.) The cost is that autocxx can no longer tell which way a noexcept(expr) resolved, so a subclass override of a method declared with one is refused — the behaviour before the parse standard was C++17.
C++ structs, enums and classes
If you add a C++ struct, class or enum to the allowlist, Rust bindings will be generated to that type and to any methods it has. Even if you don’t add it to the allowlist, the type may be generated if it’s required by some other function - but in this case all its methods won’t be generated.
Rust and C++ differ in an important way:
- In Rust, the compiler is free to pick up some data and move it to somewhere else (in a
memcpysense). The object is none the wiser. - In C++, once created, an object stays where it is, until or unless it has its “move constructor” invoked.
This makes a big difference: C++ objects can have self-referential pointers, and any such pointer would be invalidated by Rust doing
a memcpy. Such self-referential pointers are common - even some implementations of std::string do it.
POD and non-POD
When asking autocxx to generate bindings for a type, then, you have to make a choice.
- This C++ type is trivial. It has no destructor or move constructor (or they’re trivial), and thus Rust is free to move it around the stack as it wishes.
autocxxcalls these types POD (“plain old data”). Alternatively, - This C++ type has a non-trivial destructor or move constructor, so we can’t allow Rust to move this around.
autocxxcalls these types non-POD.
POD types are nicer:
- You can just use them as regular Rust types.
- You get direct field access.
- No funny business.
Non-POD types are awkward:
- You can’t just have one as a Rust variable. Normally you hold them in a
cxx::UniquePtr, though there are other options. - Fields are read through generated accessors rather than directly.
autocxxgives every public data member a method of the same name: abfield is read withobj.b(). A field Rust could hold by value comes back by value, anything else comes back as a reference borrowed from the object, and an array or reference member gets a documented refusal instead of an accessor. There are no setters yet. - You can’t even have a
&mutreference to one, because then you might be able to usestd::mem::swapor similar. You can have aPin<&mut>reference, which is more fiddly. - You can’t send one to another thread, or share one between threads: they’re neither
SendnorSync, becauseautocxxcan’t know whether your C++ type tolerates that. See thread safety if you know that yours does.
By default, autocxx generates non-POD types. You can request a POD type using generate_pod!. Don’t worry: you can’t mess this up. If the C++ type doesn’t in fact comply with the requirements for a POD type, your build will fail thanks to some static assertions generated in the C++. (If you’re really sure your type is freely relocatable, because you implemented the move constructor and destructor and you promise they’re trivial, you can override these assertions using the C++ trait IsRelocatable per the instructions in cxx.h).
See the chapter on storage for lots more detail on how you can hold onto non-POD types.
Construction
Constructing a POD object is simple: call its new associated function. Bob’s your uncle!
Multiple constructors (aka constructor overloading) follows the same rules as other functions.
Constructing a non-POD object requires two steps.
- Call the
newassociated function in the same way. This will give you something implementingmoveit::New/ - Use this to make the object on the heap or stack, in any of the following ways:
| Where you want to create it | How to create it | What you get | Example |
|---|---|---|---|
| C++ heap (recommended for simplicity) | Within.within_unique_ptr() or UniquePtr::emplace | cxx::UniquePtr<T> | let mut obj = ffi::Goldfish::new().within_unique_ptr() or let mut obj = UniquePtr::emplace(ffi::Goldfish::new()) |
| Rust heap | Within.within_box() or Box::emplace | Pin<Box<T>> | let mut obj = ffi::Goldfish::new().within_box() or let mut obj = Box::emplace(ffi::Goldfish::new()) |
| Rust stack | moveit macro | Pin<MoveRef<T>> | moveit! { let mut obj = ffi::Goldfish::new() } |
For heap construction, the prefix (emplace) and postfix (.within_...) forms are exactly identical. Choose whichever suits your needs best.
Rust’s own std::pin::pin! macro is not a substitute for moveit! here, even though both produce a pinned stack value. pin! pins a value you already own; moveit::New::new only ever writes into a place you hand it, and there’s no way to obtain a non-POD object as a plain Rust value to hand to pin! in the first place - that’s precisely the point of non-POD types. Writing std::pin::pin!(ffi::Goldfish::new()) compiles, but pins the New recipe itself rather than a Goldfish, and the object never gets constructed.
Should you construct on the Rust heap or the C++ heap?
Use .within_unique_ptr() to create objects on the C++ heap. This gives you a cxx::UniquePtr<T> which works well with other autocxx and cxx APIs.
There is a small disadvantage - cxx::UniquePtr<T> is able to store NULL values. Therefore, each time you use the resulting object, there is an unwrap() (explicit or implicit). If this bothers you, use the Box option instead which can never be NULL.
Construction sounds complicated. Do you have a code example?
C++ header:
#include <stdint.h>
#include <string>
struct A {
A() {}
void set(uint32_t val);
uint32_t get() const;
uint32_t a;
};
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("A")
}
fn main() {
moveit! {
let mut stack_obj = ffi::A::new();
}
stack_obj.as_mut().set(42);
assert_eq!(stack_obj.get(), 42);
let mut heap_obj = ffi::A::new().within_unique_ptr();
heap_obj.pin_mut().set(42);
assert_eq!(heap_obj.get(), 42);
let mut another_heap_obj = ffi::A::new().within_box();
another_heap_obj.as_mut().set(42);
assert_eq!(another_heap_obj.get(), 42);
}
Enums
A C++ enum becomes a native Rust enum by default. That’s the right shape
for an enumeration whose values are a closed set - a colour, a state, an error
code - and it lets you match on it exhaustively.
It is the wrong shape for a flag enum. Writing FLAG_A | FLAG_B in C++ gives
you an int - the operands are promoted, unless the enum has an overloaded
operator| - but converting that back to the enum type is ordinary, deliberate
C++, and the result is a perfectly good enum object. A C++ enum object may hold
values no enumerator names: for an enum with a fixed underlying type, such as
enum Flags : int, every value that type can represent; for one without, every
value in the bit range its enumerators span. A Rust enum may not - a value
which is none of its variants is instant undefined behaviour - so the
combination has no representation at all.
Use enum_style!
to pick a different representation for particular enums:
| Style | What you get | Good for |
|---|---|---|
RustifiedEnum | A native Rust enum. This is the default. | Closed sets of values |
RustifiedNonExhaustiveEnum | The same, marked #[non_exhaustive], so your matches need a catch-all arm | Closed sets which C++ may extend later |
NewtypeEnum | An integer newtype whose enumerators are associated constants | Values which may be arbitrary integers |
BitfieldEnum | The same newtype, plus &, ` | , ^and!` |
The integer the two newtype styles wrap is the enum’s underlying type as the
C++ compiler sees it, so flags.0 is a c_int for enum Flags : int and a
c_uint for enum Flags : unsigned. An unscoped enum with no fixed underlying
type has no portable answer: the compiler picks, and it may pick differently
from one target or set of flags to the next. On the targets autocxx tests, MSVC
gives it int, while gcc and clang give one whose enumerators are all
non-negative unsigned int. Name the underlying type in C++ if you intend to
write .0’s type down.
The directive takes the style first, then any number of enum names, and may be repeated to give different styles to different enums:
enum_style!(BitfieldEnum, "FileFlags", "WindowFlags")
enum_style!(RustifiedNonExhaustiveEnum, "ErrorCode")
Name each enum exactly as you would in generate! - so a namespaced enum is
ns::Thing, and an enum nested inside a class is Outer_Inner, because that
is the name bindgen gives it. Asking for two different styles for the same
enum is an error, as is anything that isn’t a plain name.
NewtypeEnum and BitfieldEnum need generate_pod!
These two styles reach Rust as a struct, not an enum, and their
enumerators are associated constants on it. autocxx only re-exports the
generated type - constants and all - for types it holds by value, so those two
styles have to be asked for with
generate_pod!.
A plain generate!
gives you an opaque type with no constants on it, which is unlikely to be what
you wanted. The two rustified styles work with either, since a Rust enum is
POD regardless.
C++ header:
enum FileFlags : int {
READ = 1 << 0,
WRITE = 1 << 1,
EXECUTE = 1 << 2,
};
inline bool is_writable(FileFlags flags) { return flags & FileFlags::WRITE; }
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
enum_style!(BitfieldEnum, "FileFlags")
generate_pod!("FileFlags")
generate!("is_writable")
}
fn main() {
let flags = ffi::FileFlags::READ | ffi::FileFlags::WRITE;
assert!(ffi::is_writable(flags));
assert!(!ffi::is_writable(ffi::FileFlags::READ));
}
Extra derives
The Rust types autocxx generates carry only the derives it needs itself, so
by default you cannot print a generated struct or compare two of them. Use
derive! to ask
for more:
derive!("Point", "Debug", "PartialEq")
Name the type exactly as you would in generate!, and write each trait as you
would inside #[derive(..)] - a path works too, so a derive macro from another
crate can be asked for as "num_enum::TryFromPrimitive". It is then your job
to have that macro in scope, and to make sure the type can satisfy the trait:
Clone needs every field to be Clone, and so on. autocxx does not check,
so an impossible request comes back from rustc rather than from autocxx.
The trait goes onto the type autocxx re-exports, which means derive! works
for a generate_pod! type or an enum, and is refused for anything else -
everything else is an opaque type with no fields, precisely because Rust must
not look inside it, so there would be nothing for a derive to work from.
Default on an enum is refused too: nothing makes one enumerator of a C++ enum
the default, and a derived Default on an enum with no variant marked
#[default] does not compile.
C++ header:
#include <cstdint>
struct Point {
uint32_t x;
uint32_t y;
};
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate_pod!("Point")
derive!("Point", "Debug", "PartialEq")
}
fn main() {
let a = ffi::Point { x: 1, y: 2 };
let b = ffi::Point { x: 1, y: 2 };
assert_eq!(a, b);
assert_eq!(format!("{:?}", a), "Point { x: 1, y: 2 }");
}
std::array
A std::array<T, N> crosses as a Rust [T; N]: cxx spells a Rust array as
std::array<T, N>, so what the bridge declares is the type the header was
written with, and there is no wrapper in between.
By value it crosses in either direction, as a copy. Behind a reference it
crosses as the array itself, const std::array<T, N>& arriving as &[T; N]
and std::array<T, N>& as Pin<&mut [T; N]> - the same spellings every other
C++ reference gets - so what C++ writes through one is what Rust reads back.
Returning a reference works the same way, with the lifetime taken from the
receiver as usual.
uint32_t sum(const std::array<uint8_t, 4>& a);
void bump(std::array<uint8_t, 4>& a);
let a = [1u8, 2, 3, 4];
assert_eq!(ffi::sum(&a), 5);
let mut b = [10u8, 20, 30, 40];
ffi::bump(std::pin::Pin::new(&mut b));
assert_eq!(b, [11u8, 21, 31, 41]);
The element can be any type which crosses by value itself, and the rule is the same by value and behind a reference:
- one of
cxx’s own atoms -uint8_torint8_t, afloator adouble, abool, achar; - an integer whose width the platform chooses, such as
intorunsignedand the typedefs to them,uint32_tandsize_tamong them. Those arrive as the matchingautocxx::c_*newtype, sostd::array<uint32_t, 4>is[autocxx::c_uint; 4]; - a class or enum
autocxxpasses by value, which for a class means onegenerate_pod!accepts.
Three things do not cross:
- An element
autocxxwill not pass by value, which is a class it can hold only behind a pointer. An array of those is not a run of bytes to be moved whole. - A reference to a C array -
const T (&)[N]rather thanconst std::array<T, N>&. Both reachautocxxas&[T; N], but they are different C++ types andcxxwrites thestd::arrayfor either, so only thestd::arrayis bound. An array parameter written without a reference is unaffected: C++ decays it to a pointer beforeautocxxsees it, and it is bound as that pointer. - Either of them behind a pointer.
Each gets a refusal which says so. std::array<T, 0> gets one too: C++ gives
the empty array a size and Rust’s [T; 0] has none, so they are not the same
object.
Forward declarations
A type which is incomplete in the C++ headers (i.e. represented only by a forward
declaration) can’t be held in a UniquePtr within Rust (because Rust can’t know
if it has a destructor that will need to be called if the object is dropped.)
Naturally, such an object can’t be passed by value either; it can still be
referenced in Rust references.
Generic (templated) types
If you’re using one of the generic types which is supported natively by cxx,
e.g. std::unique_ptr, it should work as you expect. For other generic types,
we synthesize a concrete Rust type, corresponding to a C++ typedef, for each
concrete instantiation of the type. Such generated types are always opaque, so
by default that’s enough to pass them
between return types and parameters of other functions within cxx::UniquePtrs
but not really enough to do anything else with these types yet1.
An
instantiable!
directive naming one of them goes further: it gives the instantiation a new()
and binds the member functions its class template declares, so that an
ffi::Boba can be made and asked to do things. bindgen tells autocxx
nothing at all about a specialization, so that directive is you vouching for
what it generates and your C++ compiler is the arbiter; see its documentation
for what is claimed and for the members it cannot reach. Otherwise,
to make these types more useful, you might have to add extra C++ functions to
extract data or otherwise deal with them.
Usually, such concrete types are synthesized automatically because they’re
parameters or return values from functions. Very rarely, you may
want to synthesize them yourself - you can do this using the
concrete!
directive. As noted, though, these types are currently opaque and fairly
useless without passing them back and forth to C++, so this is not a commonly
used facility. It does, however, allow you to give a more descriptive name
to the type in Rust:
C++ header:
#include <string>
struct Tapioca {
std::string yuck;
};
template<typename Floaters>
struct Tea {
Tea() : floaters(nullptr) {}
Floaters* floaters;
};
inline Tea<Tapioca> prepare() {
Tea<Tapioca> mixture;
// prepare...
return mixture;
}
inline void drink(const Tea<Tapioca>&) {}
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("prepare")
generate!("drink")
concrete!("Tea<Tapioca>", Boba)
}
fn main() {
let nicer_than_it_sounds: cxx::UniquePtr<ffi::Boba> = ffi::prepare();
ffi::drink(&nicer_than_it_sounds);
}
Your own smart pointers
A class template which holds a pointer to its argument - scoped_refptr<T>,
intrusive_ptr<T>, whatever your codebase calls its own - is one of those
opaque instantiations, and so by default there is no way to reach what it points
at. A
smart_pointer!
directive naming the template says that it is a smart pointer, and every
instantiation of it then has a get method answering with a raw pointer to the
payload:
C++ header:
#include <cstdint>
struct Milk {
uint32_t creaminess;
};
template<typename T>
class Jug {
public:
Jug() : contents(nullptr) {}
explicit Jug(T* contents) : contents(contents) {}
Jug(Jug&& other) : contents(other.contents) { other.contents = nullptr; }
~Jug() { delete contents; }
T* get() const { return contents; }
private:
T* contents;
};
inline Jug<Milk> fetch() { return Jug<Milk>(new Milk{7}); }
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("fetch")
generate_pod!("Milk")
smart_pointer!("Jug")
}
fn main() {
let jug = ffi::fetch();
// Safe: `fetch` hands back a jug with milk in it, and the milk lives as
// long as the jug does.
let milk = unsafe { &*jug.get() };
assert_eq!(milk.creaminess, 7);
}
The claim the directive makes is that p.get() on a const instantiation
yields a T*, and your C++ compiler is what checks it: the generated shim is
declared returning T* and calls get(), which does not compile if there is no
such member or if what it returns will not convert. bindgen reports the
members a class template declares, but not an inherited one, not a member
function template, and nothing whatsoever about a specialization - so autocxx
cannot tell a template with no get from one whose get it was not told about,
and does not refuse either. A directive which matches no instantiation is an
error, since the accessor you asked for would otherwise silently not be
there.
Under safety!(unsafe_references_wrapped) the accessor is an unsafe fn
answering with a CppRef instead of a raw pointer, since safe code may
dereference one of those.
autocxx never guesses. A class with a get, or one written to look exactly
like std::unique_ptr, is treated like any other template until you say
otherwise.
An instantiation which appears only as a template argument of something else -
Carton<Jug<Milk>> and nothing more - does not get the accessor, because
autocxx names such an argument without converting it.
What you can do with one of these is otherwise what you could always do with an
opaque instantiation: receive it from C++, hold it, hand it back, and drop it,
which destroys it in C++. get is the whole of what the directive adds - there
is no way to make one from Rust and no way to copy one - so a reference-counted
pointer can be moved from the Rust side but not shared from it.
Implicit member functions
Most of the API of a C++ type is contained within the type, so autocxx can
understand what is available for Rust to call when that type is analyzed.
However, there is an important exception for the so-called special
member functions, which will be implicitly generated by the C++ compiler for
some types. autocxx makes use of these types of special members:
- Default constructor
- Destructor
- Copy constructor
- Move constructor
Explicitly declared versions of these special members are easy: autocxx knows
they exist and uses them.
autocxx currently uses its own analysis to determine when implicit versions of
these exist. This analysis tries to be conservative (avoid generating wrappers
that require the existence of C++ functions that don’t exist), but sometimes
this goes wrong and understanding the details is necessary to get the correct
Rust wrappers generated.
In particular, determing whether an implicit version of any of these exists
requires analyzing the types of all bases and members. autocxx only analyzes
types when requested, because some may be un-analyzable. If the types of any
bases or members are not analyzed, autocxx will assume a public destructor
exists (in the absence of any other destructors), and avoid using any other
implicit special member functions. Notably this includes the default
constructor, so types with un-analyzed bases or members and no explicit
constructors will not get a make_unique or new generated. If autocxx isn’t
generating a make_unique or CopyNew or MoveNew for a type which permits
the corresponding operations in C++, make sure the types of all bases and
members are analyzed or implement it explicitly.
autocxx currently does not take member initializers (const int x = 5) into
account when determining whether a default constructor
exists2. Explicitly declared default destructors still
work though.
Currently, autocxx assumes that an explicitly defaulted (= default) member
function exists, although it is valid C++ for that to be
deleted3. Clang’s
-Wdefaulted-function-deleted
flag (enabled by default) will warn about types like this.
A C++ type whose destructor is inaccessible - private, protected, or
= deleted, whether declared that way or made so by a base or member - is one
Rust could never destroy. autocxx therefore does not generate any way for
Rust to own one4: no new, no make_unique, no
CopyNew or MoveNew. Its methods are still generated, so you can call them
through a reference or pointer that C++ hands you, which is how such types are
normally meant to be used. Asking for the constructor anyway gives a compile
error naming the reason.
Many of the special members may be overloaded in C++. This generally means
adding const or volatile qualifiers or extra arguments with defaults.
autocxx avoids using any overloaded special members because choosing which
one to call from Rust gets tricky.
Inherited member functions
C++ calls an inherited member on the object which inherits it - derived.foo()
- and
bindgenreports nothing of the sort: a base class arrives as a field of the derived class, and the base’s members as functions over the base’s own type. Soautocxxbinds each public member of each public base a second time, against the classes which inherit it, and the Rust method is where C++ puts it.
The call itself is made in C++, on the object cast to the base -
static_cast<const Base&>(d).foo(args). That is what dispatches a virtual
member on the object’s own dynamic type, what adjusts this for a base which
does not sit at the start of the derived class, and what makes the Rust method
call the member whose signature it was generated from rather than whatever the
deriving class might declare under the same name. Adding the base to your
generate! list is not needed for any of it - if you do, you also get the
base’s own type and an AsRef upcast to it, and both routes work.
A member is bound this way only where autocxx can say that
derived.foo(args) would have named it, so several shapes are left alone: a
name the deriving class declares itself, or one a class between it and the base
declares, since either hides the inherited member; a name two bases both
declare, which C++ calls ambiguous rather than choosing; a base reached over
anything but public inheritance, which is not a conversion a caller may make;
a base reached by more than one path, which is more than one base subobject -
autocxx declines these even where C++ would allow the conversion, as it does
for a virtual base, because bindgen says which bases are virtual only for the
class declaring them; a base whose C++ name bindgen never reported, which the
cast could not write; a name the base merges with a using declaration of its
own; and a name the base overloads, since bindgen reports neither the default
arguments nor enough of the parameter types to say which overload C++ would
pick. Static members are not imported, a static call having no receiver to make
it on, and neither are data members.
Three kinds of declaration hide an inherited member in C++ which bindgen does
not describe well enough for autocxx to notice: a member of an anonymous
union, an unnamed enum’s enumerators, and a member function template. Where a
class hides an inherited foo with one of those, autocxx binds foo anyway,
and the binding calls the base’s member - the name reads as C++’s would not,
but it does what it says.
An enum class’s enumerators are members of the enumeration rather than of the
class the enum is nested in, so one named after an inherited member hides
nothing and the member is bound as usual.
A member a class re-exports with using Base::foo; is bound too, by a pass of
its own. That is how C++ reaches a member of a private base and how it widens
the access of a protected one, so its call is made on the object itself - there
being no cast to a private base to make.
Abstract types
autocxx does not allow instantiation of abstract types5 (aka types with pure virtual methods).
An abstract type also needs a virtual destructor before autocxx offers Rust a
way to own one. No object of an abstract class exists, so every pointer to one
points at an object of some derived class; if the destructor is not virtual,
deleteing through that pointer runs the wrong one, which is undefined
behaviour and which clang reports as -Wdelete-abstract-non-virtual-dtor and
MSVC as C5205.
The C++ cxx generates for UniquePtr<T>, and for SharedPtr<T>’s
raw-pointer constructor, deletes through a T* exactly that way.
CxxVector<T> is a different case with the same outcome: no element of an
abstract type could ever be constructed, so nothing is there to delete, but the
destructor which would still gets instantiated and the compiler still reports
it. So for an abstract type with no virtual destructor autocxx adds none of
those, nor WeakPtr<T>, which destroys nothing itself but is of no use without
SharedPtr<T>. The generated type’s documentation says so. As with an inaccessible destructor, the type and its
methods are still generated and can be used through a reference or pointer C++
hands you. Adding virtual ~T(); to the C++ class is what makes it ownable.
What autocxx actually knows is that it found no virtual destructor. If the
class inherits one from a base you did not ask autocxx to generate, adding
that base to your generate! list is what lets autocxx see it.
Only the support autocxx adds of its own accord comes back. A C++ function
which itself names a std::unique_ptr<T> for such a T is still bound, and
cxx still generates the deleting glue for it, so such a header can still fail
to compile. Refusing those signatures with a diagnostic instead is a job for a
future release.
-
Fix for explicitly defaulted special member functions that are deleted is tracked here. ↩
-
Until this was fixed, such a type could be constructed and put in a
Boxor on the stack, and its memory was then freed without running any C++ destructor - leaking whatever resources the C++ implementation tracked. See here. ↩ -
autocxx’s determination of abstract types is a bit approximate and could be improved. ↩
Pointers, references, values
autocxx knows how to deal with C++ APIs which take C++ types:
- By value
- By reference (const or not)
- By raw pointer
- By
std::unique_ptr - By
std::shared_ptr - By
std::weak_ptr - By rvalue reference (that is, as a move parameter)
(all of this is because the underlying cxx crate has such versatility).
Some of these have some quirks in the way they’re exposed in Rust, described below.
Passing between C++ and Rust by value
See the section on C++ types for the distinction between POD and non-POD types. POD types can be passed around however you like. Non-POD types can be passed into functions in various ways - see calling C++ functions for more details.
References and pointers
We follow cxx norms here. Specifically:
- A C++ reference becomes a Rust reference
- A C++ pointer becomes a Rust pointer.
- If a reference is returned with an ambiguous lifetime, we don’t generate code for the function
- Pointers require use of
unsafe, references don’t necessarily.
That last point is key. If your C++ API takes pointers, you’re going
to have to use unsafe. Similarly, if your C++ API returns a pointer,
you’ll have to use unsafe to do anything useful with the pointer in Rust.
This is intentional: a pointer from C++ might be subject to concurrent
mutation, or it might have a lifetime that could disappear at any moment.
As a human, you must promise that you understand the constraints around
use of that pointer and that’s what the unsafe keyword is for.
Exactly the same issues apply to C++ references in theory, but in practice,
they usually don’t. Therefore cxx has taken the view that we can “trust”
a C++ reference to a higher degree than a pointer, and autocxx follows that
lead (in fact we ‘trust’ references even slightly more than cxx).
In practice, of course, references are rarely return values from C++
APIs so we rarely have to navel-gaze about the trustworthiness of a
reference.
(See also the discussion of safety - if you haven’t specified
an unsafety policy, all C++ APIs require unsafe so the discussion is moot.
If you’re given a C++ object by pointer, and you want to interact with it, you’ll need to figure out the guarantees attached to the C++ object - most notably its lifetime. To see some of the decision making process involved see the Steam example.
cxx::UniquePtrs tips
We use cxx::UniquePtr in completely the normal way, but there are a few
quirks which you’re more likely to run into with autocxx.
- You’ll need to use
.pin_mut()a lot - see the example at the bottom of C++ functions. - If you need to pass a raw pointer to a function, lots of unsafety is required - something like this:
This may be simplified in future.let mut a = ffi::A::make_unique(); unsafe { ffi::TakePointerToA(std::pin::Pin::<&mut ffi::A>::into_inner_unchecked(a.pin_mut())) };
Storage - stack and heaps
Ensure you understand the distinction between POD and non-POD types described in the C++ types section before proceeding.
POD types
POD types are just regular Rust types! Store them on the stack, heap, in a Vec, a HashMap, whatever you want.
Non-POD types
Non-POD types can be stored:
- In a
cxx::UniquePtr. This is cxx’s Rust wrapper forstd::unique_ptr- so the object is stored in the C++ heap. Most of the time you handle a C++ object fromautocxx, it will be stored in one of these. - In a
Box- so the object is stored on the Rust heap. This has the advantage that there’s no possibility that the object can be NULL. - On the Rust stack, using the
autocxx::moveitmacro.
If in doubt, use cxx::UniquePtr. It’s simple and ergonomic.
See C++ types for a code example showing a type existing on both the stack and the heap.
Whose heap is it anyway?
Specifically cxx::UniquePtr is a binding to std::unique_ptr<T,std::default_delete<T>> which means the object will be deleted using the C++ delete operator. This will respect any overridden operator delete on the type, and similarly, the functions which autocxx provides to construct types should respect overridden operator new. This means: if your C++ type has code to create itself in some special or unusual heap partition, that should work fine.
The class’s own allocator is found by asking for the two signatures a class
declaring nothing else gets, operator new(std::size_t) and
operator delete(void*); a class which declares only the sized, aligned or
destroying overloads is not seen, and its objects are allocated and freed
globally.
A type which asks for more alignment than operator new promises -
alignas(32) where the default is 16 - reaches the std::align_val_t
overloads C++17 added for exactly that, so it is not handed storage it may not
live in. Where autocxx cannot pair an allocation with a deallocation it says
so at compile time rather than guess: an over-aligned type before C++17, which
has no aligned operator new in the language at all, and an over-aligned type
whose class declares only one half of the pair. Giving the class an
operator new(std::size_t) and an operator delete(void*) of its own settles
every one of those cases.
Built-in types
autocxx relies primarily on the standard cxx types.
In particular you should become familiar with cxx::UniquePtr and cxx::CxxString.
There are a few additional integer types, such as c_int,
which are not yet upstreamed to cxx. These are to support those pesky C/C++ integer types
which do not have a predictable number of bits on different machines.
C++ header:
inline int do_math(int a, int b) { return a+b; }
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("do_math")
}
fn main() {
assert_eq!(ffi::do_math(c_int(12), c_int(13)), c_int(25));
}
Each of these integer newtypes implements Display, printing as the integer it
wraps, so println!("{}", ffi::some_count()) works without reaching for .0.
A character type such as c_uchar prints its value as a number, which is what
the wrapped type is.
Inside a unique_ptr
cxx will not put one of its own integer types inside a UniquePtr, so a
std::unique_ptr<uint32_t> arrives as a UniquePtr<autocxx::c_u32> rather
than a UniquePtr<u32>. There is one of these wrappers per fixed width -
c_u8 through c_u64 and c_i8 through c_i64 - and each is a transparent
newtype over the Rust integer of that width, so .0 or .into() gets the
value back. Anywhere else - by value, in a std::vector, in a
std::shared_ptr - a uint32_t is a plain u32 as before.
Character types
C++’s char16_t, char32_t, char8_t and wchar_t are distinct types - a
char32_t is not a uint32_t, and a C++ compiler asked for the exact type of a
function says so - and Rust has no equivalent of any of them. autocxx gives
each one a transparent newtype:
c_char16_t,
c_char32_t,
c_char8_t and
c_wchar_t, each
wrapping the Rust integer of the same width, so a value crosses with .0 or
From. Like the integer newtypes above, each implements Display and prints as
the integer it wraps - a number, not a character.
wchar_t is the one whose width and signedness are the target’s to choose -
unsigned short on Windows, int on Linux and macOS, unsigned int under
AAPCS - so its payload is
autocxx::wchar_t,
which is picked per target. char8_t only exists from C++20 onwards.
char16_t, char32_t and wchar_t go inside a UniquePtr, a SharedPtr, a
WeakPtr and a CxxVector as their newtypes. char8_t does not: naming it
takes a typedef char8_t ... in C++ which autocxx compiles at C++14, where
the keyword does not exist, so a container of one is refused with the rest of
that function left alone.
long double has no such newtype and is not supported, for a reason which
differs by target: on MSVC and Apple Arm it is a double under another name, so
a Rust f64 has the right layout but is still the wrong C++ type and cxx’s
signature check says so; on x86-64 Linux it is an 80-bit x87 float in 16 bytes,
and on AArch64 Linux an IEEE binary128, and Rust has no type for either. A
function whose signature mentions one is refused with an error saying that. A
long double field is fine - those bytes are carried around, not passed
between the languages - though a struct with one cannot be generate_pod!.
128-bit integers
A C++ __int128 is c_i128
and an unsigned __int128 is
c_u128, both
transparent newtypes. Neither may go inside a UniquePtr or a CxxVector,
because MSVC has neither type and the glue which would make that work is
compiled everywhere.
__float128 is not supported. bindgen renders it as a u128 because that is
the right size and Rust has no 128-bit float, which is also how an
unsigned __int128 arrives - so autocxx has bindgen mark the __float128
and refuses a function which mentions one by name, rather than binding it as the
integer beside it.
Strings
autocxx uses cxx::CxxString. However, as noted above, we can’t
just pass a C++ string by value, so we’ll box and unbox it automatically
such that you’re really dealing with UniquePtr<CxxString> on the Rust
side, even if the API just took or returned a plain old std::string.
However, to ease ergonomics, functions that accept a std::string will
actually accept anything that
implements a trait called ffi::ToCppString. That may either be a
UniquePtr<CxxString> or just a plain old Rust string - which will be
converted transparently to a C++ string.
This trait, and its implementations, are not present in the autocxx
documentation because they’re dynamically generated in your code
so that they can call through to a make_string implementation in
the C++ that we’re injecting into your C++ build system.
(None of that happens if you use exclude_utilities, so don’t do that.)
C++ header:
#include <string>
#include <cstdint>
inline uint32_t take_string(std::string a) { return static_cast<uint32_t>(a.size()); }
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("take_string")
}
fn main() {
assert_eq!(ffi::take_string("hello"), 5)
}
If you need to create a blank UniquePtr<CxxString> in Rust, such that
(for example) you can pass its mutable reference or pointer into some
pre-existing C++ API, call ffi::make_string("") which will return
a blank UniquePtr<CxxString>.
If all you need is a reference to a CxxString, you can alternatively use
cxx::let_cxx_string.
String views
A function which takes a std::string_view accepts anything implementing
ffi::AsCppStringView — a &str, a String, a &[u8], a Vec<u8>, or a
&CxxString. Nothing is copied: the view is built in C++ over the bytes you
lend, for the duration of the call. A const std::string_view& parameter is the
same thing and takes the same argument — note that this is so even under
safety!(unsafe_references_wrapped), where other reference parameters become
CppRef wrappers; a wrapper around a type Rust cannot construct would be no use
to anybody.
The bytes are lent for the call, and nothing promises they outlast it. C++ which
copies the view somewhere that outlives the call may be left holding a dangling
one — whether it is depends on what you passed, and passing a borrowed &[u8]
promises nothing beyond the call. It is the same bargain as letting C++ keep the
const char* out of a const std::string& parameter, except that string_view
is a type people do store. The generator cannot see that happen, so it falls under what
you vouch for with safety!. If the C++ keeps what it is lent, give it an owned
std::string parameter instead.
The bytes are bytes. C++ asks nothing about the encoding of a string_view
and neither does this, which is why &[u8] is accepted alongside &str and
why a view may contain an interior NUL.
C++ header:
#include <string_view>
#include <cstddef>
inline size_t take_view(std::string_view v) { return v.size(); }
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("take_view")
}
fn main() {
assert_eq!(ffi::take_view("hello"), 5);
assert_eq!(ffi::take_view(&b"ab\0c"[..]), 4);
}
A std::string_view is never handed back the other way — not as a return value,
not read out of a C++ variable, not as a unique_ptr or shared_ptr payload.
Rust has no type which is a std::string_view, because a view borrows characters
something else owns and autocxx has nothing to tie that borrow to whose
lifetime it could check. Every such position is refused with an explanation
rather than bound unsafely. Hand over an owned std::string instead, which
arrives as a UniquePtr<CxxString>.
For the same reason, a std::string_view parameter of a virtual method can be
called from Rust but not overridden from Rust with subclass!: the way in
builds the view, and the way out would have to hand Rust an unchecked borrow.
std::string_view is C++17. autocxx is told the C++ standard twice — once
for parsing your headers and once for compiling the code it generates — so
remember to set it in both places, e.g. cc::Build::std("c++17") beside
extra_clang_args(["-std=c++17"]). Generated code which needs C++17 and does
not get it says so by name.
Naming
Namespaces
The C++ namespace structure is reflected in mods within the generated ffi mod. However, at present there is an internal limitation that autocxx can’t handle multiple types with the same identifier, even if they’re in different namespaces. This will be fixed in future.
C++ header:
namespace generations {
void hey_boomer();
}
namespace submarines {
void hey_boomer();
}
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("submarines::hey_boomer")
generate!("generations::hey_boomer")
}
fn main() {
ffi::generations::hey_boomer(); // insults your elders and betters
ffi::submarines::hey_boomer(); // launches missiles
}
Nested types
There is support for generating bindings of nested types, with some
restrictions. Currently the C++ type A::B will be given the Rust name
A_B in the same module as its enclosing namespace.
C++ header:
struct Turkey {
struct Duck {
struct Hen {
int wings;
};
};
};
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate_pod!("Turkey_Duck_Hen")
}
fn main() {
let _turducken = ffi::Turkey_Duck_Hen::new().within_box();
}
Overloads
See the chapter on C++ functions.
C++ functions
Calling C++ functions is largly as you might expect.
Value and rvalue parameters
Functions taking non-POD value parameters can take a cxx::UniquePtr<T>
or a &T. This gives you the choice of Rust semantics - where a parameter
is absorbed and destroyed - or C++ semantics where the parameter is copied.
C++ header:
#include <cstdint>
struct Goat {
Goat();
uint32_t horn_count;
};
void feed_goat(Goat g); // takes goat by value
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("Goat")
generate!("feed_goat")
}
fn main() {
let goat = ffi::Goat::new().within_unique_ptr(); // returns a cxx::UniquePtr, i.e. a std::unique_ptr
// C++-like semantics...
ffi::feed_goat(&goat);
// ... you've still got the goat!
ffi::feed_goat(&goat);
// Or, Rust-like semantics, where the goat is consumed.
ffi::feed_goat(goat);
// No goat any more...
// ffi::feed_goat(&goat); // doesn't compile
}
Specifically, you can pass anything which implements ValueParam<T>.
If you’re keeping non-POD values on the Rust stack, you need to explicitly use as_mov to indicate that you want to consume the object using move semantics:
C++ header:
#include <cstdint>
struct Blimp {
Blimp();
uint32_t tons_of_helium;
};
void burst(Blimp b); // consumes blimp,
// but because C++ is amazing, may copy the blimp first.
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("Blimp")
generate!("burst")
}
fn main() {
moveit! {
let mut blimp = ffi::Blimp::new();
}
ffi::burst(&*blimp); // pass by copy
ffi::burst(as_copy(blimp.as_ref())); // explicitly say you want to pass by copy
ffi::burst(as_mov(blimp)); // consume, using move constructor
}
RValue parameters are a little simpler, because (as you’d hope) they consume the object you’re passing in.
C++ header:
#include <cstdint>
struct Cake {
Cake();
uint32_t tons_of_sugar;
};
void eat(Cake&& c); // consumes the cake. You can't have your cake and eat it.
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("Cake")
generate!("eat")
}
fn main() {
moveit! {
let mut stack_cake = ffi::Cake::new();
}
ffi::eat(stack_cake);
// No more cake.
// Still peckish.
let heap_cake = ffi::Cake::new().within_unique_ptr();
ffi::eat(heap_cake);
// Really no more cake now.
}
Default parameters
Are not yet supported1.
Return values
Any C++ function which returns a non-POD type to Rust in fact gives you an opaque
object implementing moveit::New.
This enables you to “emplace” the resulting object either on the stack or heap,
in exactly the same way as if you’re constructying an object. See the section on construction
for how to turn this opaque object into something useful (spoiler: just append .within_unique_ptr()).
Overloads - and identifiers ending in digits
C++ allows function overloads; Rust doesn’t. autocxx follows the lead
of bindgen here and generating overloads as func, func1, func2 etc.
This is essentially awful without rust-analyzer IDE support - see the
workflows chapter for why you should be using an IDE.
Each numbered function carries a doc comment saying which C++ declaration it
came from - C++: saw, declaration 2 of that name, counting in the order autocxx met them - so the IDE has something to show you. Read the position
rather than the digits: a suffix skips any number already taken by a real
function, so func3 is not necessarily the fourth func. The count is in the
order autocxx met the declarations, which is usually the header’s order but
not guaranteed to be - a member function template, for one, is met after every
ordinary method. The same note names the C++ function behind a name Rust would
not accept as an identifier, where type reaches you as type_.
C++ header:
#include <string>
struct View {
std::string of_what; // go and watch In Bruges, it's great
};
struct Tree {
int dendrochronologically_determined_age;
};
void saw(const View&);
void saw(const Tree&);
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("Tree")
generate!("View")
generate!("saw")
generate!("saw1")
}
fn main() {
let view = ffi::View::new().within_unique_ptr();
ffi::saw(&view);
let tree = ffi::Tree::new().within_unique_ptr();
ffi::saw1(&tree); // yuck, overload
}
autocxx doesn’t yet support default parameters.
It’s fairly likely we’ll change the model here in the future, such that we can pass tuples of different parameter types into a single function implementation.
Methods
Calling a const method is simple:
C++ header:
class Sloth {
public:
void sleep() const {} // sloths unchanged by sleep
};
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("Sloth")
}
fn main() {
let sloth = ffi::Sloth::new().within_unique_ptr();
sloth.sleep();
sloth.sleep();
}
Calling a non-const method is a bit more of a pain. Per cxx norms, all mutable
references to C++ objects must be pinned.
In practice, this means you must call .pin_mut()
every time you call a method:
C++ header:
class Sloth {
public:
void unpeel_from_tree() {} // sloths get agitated when removed from
// trees, probably shouldn't be const
};
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("Sloth")
}
fn main() {
let mut sloth = ffi::Sloth::new().within_unique_ptr();
sloth.pin_mut().unpeel_from_tree();
sloth.pin_mut().unpeel_from_tree();
}
Ref-qualified methods
A method qualified with & is called just like any other method - autocxx
always has an lvalue to call it on, so the qualifier makes no difference to
how you use it.
C++ header:
#include <cstdint>
class Sloth {
public:
uint32_t claws() const & { return 3; }
};
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("Sloth")
}
fn main() {
let sloth = ffi::Sloth::new().within_unique_ptr();
assert_eq!(sloth.claws(), 3);
}
A method qualified with && is a different matter: it can only be called on an
object which is about to be discarded, and autocxx only ever holds a C++ object
behind a reference or a smart pointer. There’s no way to express that, so no
bindings are generated for such methods. The rest of the type is unaffected,
and the generated code carries an explanation where the method would have been.
Member function templates
class Sloth {
public:
template<typename T> void hang_from(T branch);
};
Calling hang_from means choosing what T is, and nothing autocxx generates
can say so: a cxx::bridge names C++ functions, and the function you want here
does not exist until something names the arguments which would instantiate it.
There is no turbofish for a C++ template. So no bindings are generated for a
member function template. The rest of the type is unaffected, and the generated
code carries an explanation where the method would have been - which is the
point of this, because such a member used to be simply missing, with nothing in
the bindings to say it had ever been declared.
To call one, give C++ a name for the instantiation you want and bind that:
inline void hang_from_branch(Sloth& sloth, Branch branch) {
sloth.hang_from(branch);
}
An explicit specialization written in the header is not enough on its own: C++ does provide a callable function that way, but libclang reports it outside the class rather than as a member of it, so autocxx does not reach it. The wrapper is the reliable route.
The same applies to the member functions of a class template, which are
described under generic types:
those are bound for an instantiation you declare instantiable!, and a member
function template among them is the one kind which is not.
-
the work is planned here. ↩
C++ Exceptions
C++ exceptions are supported via the throws! directive. When a function is marked
with throws!, its Rust binding returns Result<T, cxx::Exception> instead of T,
allowing you to handle C++ exceptions that propagate across the FFI boundary.
Constructors take a slightly different shape, for reasons the
Constructors section explains.
Basic usage
C++ header:
#include <stdexcept>
void do_risky_thing();
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("do_risky_thing")
throws!("do_risky_thing")
}
fn main() {
let result = ffi::do_risky_thing();
assert!(result.is_err());
// You can access the exception message:
// println!("Error: {}", result.unwrap_err());
}
Functions with return values
Functions that return values work the same way - the return type becomes
Result<T, cxx::Exception>:
C++ header:
#include <stdexcept>
#include <cstdint>
uint32_t parse_number(const char* s);
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("parse_number")
throws!("parse_number")
}
fn main() {
// Successful call - unwrap the Result.
// The function takes a raw pointer, so it stays unsafe
// even under safety!(unsafe_ffi).
let value = unsafe { ffi::parse_number(c"42".as_ptr()) }.unwrap();
assert_eq!(value, 42);
// Exception is caught and converted to Err
let result = unsafe { ffi::parse_number(std::ptr::null()) };
assert!(result.is_err());
}
Qualified names
The throws! directive supports qualified names for precise control over which
functions are marked as throwing:
| Pattern | Matches |
|---|---|
throws!("do_something") | Any function named do_something |
throws!("MyClass::method") | Method method on class MyClass |
throws!("ns::do_something") | Free function do_something in namespace ns |
throws!("ns::MyClass::method") | Method on a namespaced class |
throws!("Outer::Inner::method") | Method on a nested class |
A nested class may also be named by the flattened Outer_Inner spelling
autocxx knows it by, exactly as in generate!.
The class may not be left off, exactly as in block_functions!:
throws!("ns::do_something") names a free function in ns, never a method of
some class in ns. A designation which names only the namespace of a method
therefore matches nothing and is refused, rather than making an unrelated
function of that name fallible.
Partial matching
Partial matching is supported: a shorter pattern will match functions in any
namespace. For example, throws!("do_something") will match both a top-level
do_something and my_namespace::do_something - and, since what is dropped
there is the whole qualification rather than the class alone, a
MyClass::do_something as well.
C++ header:
#include <stdexcept>
namespace utils {
void validate();
}
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("utils::validate")
throws!("validate") // matches utils::validate
}
fn main() {
let result = ffi::utils::validate();
assert!(result.is_err());
}
A designation which matches nothing
A throws! which names no function autocxx met is a build error. There is
nothing useful it could mean: the function it was written for stays declared as
never throwing, cxx puts noexcept on the boundary, and the first exception to
escape it terminates the process rather than arriving as an Err. A
misspelling, a name written with a namespace the item does not have, or a
designation left behind after the C++ it named was renamed all land here.
A designation autocxx matched and then could not honour counts as matched: a
copy constructor becomes a moveit trait method with nowhere to put a
Result, and naming one is not a mistake - see below.
The same is true of block!, block_constructors!, block_functions! and
instantiable!, each of which quietly did nothing when it matched nothing. The
first two name a type, and a type they name is a match whether or not autocxx
could act on it; what they catch is a name which fits nothing autocxx saw at
all. instantiable! names an alias or a concrete! type, and a name which is
neither is refused. Write those three as bindgen does - namespaces, and any
enclosing class flattened into the type, so ns::Outer_Inner - which is the
spelling they accept. block_functions! names a function rather than a type,
exactly as throws! does, reading the same spellings; throws! and
smart_pointer! take the C++ spelling as well.
Constructors
A constructor is named in throws! the way C++ names it - Class::Class:
C++ header:
#include <stdexcept>
#include <cstdint>
class Goat {
public:
Goat(uint32_t horns) {
if (horns > 2) throw std::runtime_error("too many horns");
horns_ = horns;
}
uint32_t horns() const { return horns_; }
private:
uint32_t horns_;
};
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("Goat")
throws!("Goat::Goat")
}
fn main() {
let goat = ffi::Goat::new(2).try_within_unique_ptr().ok().unwrap();
assert_eq!(goat.horns(), 2);
let complaint = ffi::Goat::new(9).try_within_unique_ptr().err().unwrap();
assert_eq!(complaint.what(), "too many horns");
}
An ordinary constructor hands back an impl New, a recipe you finish with
within_unique_ptr(), within_box() or the moveit! macro. One named by
throws! hands back an impl TryNew instead, and each of those finishers has
a fallible counterpart:
| Infallible | Fallible | Gives you |
|---|---|---|
.within_unique_ptr() | .try_within_unique_ptr() | Result<UniquePtr<T>, cxx::Exception> |
.within_box() | .try_within_box() | Result<Pin<Box<T>>, cxx::Exception> |
.within_cpp_pin() | .try_within_cpp_pin() | Result<CppPin<T>, cxx::Exception> |
moveit! { let x = ...; } | stack_slot!(storage); then storage.try_emplace(...) | Result<Pin<MoveRef<T>>, cxx::Exception> |
There is deliberately no Result<impl New, _>: that would decide whether
construction failed before the constructor had run, whereas a C++ constructor
only throws once it is under way. impl TryNew says the right thing - the
place stays uninitialized when construction fails - and it is why the method
names differ rather than just the return types.
On the stack
moveit! reserves stack storage and constructs into it in a single let,
which leaves nowhere for a failure to go. So the two halves are written
separately, and the half which can fail is an ordinary expression which takes
?:
C++ header:
#include <stdexcept>
#include <cstdint>
class Goat {
public:
Goat(uint32_t horns) {
if (horns > 2) throw std::runtime_error("too many horns");
horns_ = horns;
}
uint32_t horns() const { return horns_; }
private:
uint32_t horns_;
};
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("Goat")
throws!("Goat::Goat")
}
fn graze() -> Result<u32, cxx::Exception> {
autocxx::stack_slot!(storage);
let goat = storage.try_emplace(ffi::Goat::new(1))?;
Ok(goat.horns())
}
fn main() {
assert_eq!(graze().unwrap(), 1);
}
When a constructor throws, the storage which had been set aside for the object is released without any destructor running on it - the object never existed. Whichever of its members had already been constructed are destroyed by C++ itself as the exception unwinds, exactly as they would be in a C++ program.
What a designation covers
throws!("Goat::Goat") marks every constructor of Goat, because all of them
share that C++ name; there is no spelling which picks out one overload. A
constructor which is marked but which never actually throws is fallible all the
same - throws! describes what C++ is allowed to do, not what it did - and its
Result is simply always Ok. Constructors nobody has named are untouched:
they keep the infallible impl New they always had.
Naming the class alone - throws!("Goat") - also marks its constructors, since
throws! matches a trailing qualified name. Prefer Goat::Goat: the short form
would equally match a free function called Goat.
One kind of constructor the designation cannot reach is a copy or move
constructor the class declares for itself. Those become moveit’s CopyNew
and MoveNew, whose methods return nothing and so have nowhere to put an
exception; throws!("Goat::Goat") names them along with the rest, but leaves
them as they were, and a copy or move constructor which throws still terminates
the process. The same goes for a destructor, which becomes Drop - and which
should not throw in C++ either. Only the value constructors of the class become
fallible.
Functions which return a class by value
A throws! function which returns a non-POD type by value is built the same
way as a constructor - C++ writes the result into a place Rust provides - so it
hands back an impl TryNew and is finished with the same try_ methods:
let made = ffi::make_goat(2).try_within_unique_ptr()?;
Subclasses
If the C++ superclass’s constructor can throw, so can the constructor of the
peer object autocxx generates for your subclass, and that peer constructor is
the one to name: throws!("MySubclassCpp::MySubclassCpp"). Implement
CppPeerConstructor::try_make_peer
as well as make_peer, and construct the subclass with try_new_rust_owned,
try_new_cpp_owned or try_new_self_owned. Designating the peer’s constructor
also withholds the implementation autocxx writes for itself where the
superclass has a single no-argument constructor, so the implementation below is
what a subclass of any superclass needs:
impl CppPeerConstructor<ffi::MySubclassCpp> for MySubclass {
fn make_peer(&mut self, holder: CppSubclassRustPeerHolder<Self>)
-> cxx::UniquePtr<ffi::MySubclassCpp> {
self.try_make_peer(holder).expect("the superclass constructor threw")
}
fn try_make_peer(&mut self, holder: CppSubclassRustPeerHolder<Self>)
-> Result<cxx::UniquePtr<ffi::MySubclassCpp>, cxx::Exception> {
ffi::MySubclassCpp::new(holder, self.arg)
}
}
Note that there is no finisher on that last line. A peer’s new hands back the
cxx::UniquePtr itself rather than something to place, so the fallible one
hands back the Result and there is nothing left to do to it.
The subclass chapter says
why. The constructor of a concrete template instantiation behaves the same way,
for the same reason - see instantiable!.
How it works
Under the hood, autocxx leverages cxx’s native exception handling.
When a function is marked with throws!, the generated cxx bridge declaration
uses Result<T> as the return type. This causes cxx to automatically wrap
the C++ call in a try-catch block and convert any caught std::exception
(or derived types) to cxx::Exception. The exception is caught on the C++ side
of the boundary and never unwinds into Rust.
The cxx::Exception type provides:
Displayimplementation to get the exception message (what())- Conversion to
std::io::ErrorviaFromtrait
Limitations
-
Non-std::exception types: only exceptions derived from
std::exceptionare caught and converted, because that is what cxx’scatchclause names. Something else - anint, a bare string literal, a class of your own which derives from nothing - passes straight through it and out of thenoexceptshim around it, which callsstd::terminate. The process dies; it is not undefined behaviour, but theResultnever arrives either. -
Undesignated throwing functions: if C++ throws out of a function no
throws!names, the exception reaches anoexceptboundary in the same way and the process terminates. That is true of constructors too, so a constructor which can throw needs designating even if you intend to treat the exception as fatal. -
as_newwith a throwing constructor:as_newtakes aNew, so athrows!constructor cannot be handed straight to a C++ function which takes its argument by value - the call would have nowhere to report a failure that happened while assembling its arguments. Construct the object first, withtry_within_unique_ptr()or astack_slot!, and pass what you get withas_movoras_copy. -
Performance: Exception handling adds minimal runtime overhead - the cost is only incurred when an exception actually occurs.
-
MSVC: an exception model must be selected, or cl.exe emits no unwind tables: exceptions still propagate and get caught, but destructors in intervening frames are skipped, so every throwing call leaks its owning temporaries. The
cc::Buildreturned byautocxx-build’sBuilderpasses/EHscfor the code it compiles. Two things stay outside its reach: C++ you compile separately (viaautocxx-gen, CMake, or anothercc::Build) needs a compatible exception model of its own, and thecxxcrate’s own runtime (cxx.cc) is compiled by cxx’s build script, which currently passes no/EHflag — setCXXFLAGS=/EHscin your build environment to cover it until that is fixed upstream.
Callbacks into Rust
autocxx is primarily to allow calls from Rust to C++, but like cxx it also allows you to expose Rust APIs to C++.
You can:
- Declare that Rust types should be available to C++ using
extern_rust_type - Make Rust functions available to C++ using
extern_rust_function. - Allow Rust subclasses of C++ classes.
This latter option is most commonly used for implementing “listeners” or “observers”, so is often in practice how C++ will call into Rust. More details below.
Subclasses
There is limited and experimental support for creating Rust subclasses of
C++ classes. (Yes, even more experimental than all the rest of this!)
See subclass::CppSubclass for information about how you do this.
This is useful primarily if you want to listen out for messages broadcast
using the C++ observer/listener pattern.
C++ header:
class GoatObserver {
public:
virtual void goat_full() const = 0;
virtual ~GoatObserver() {}
};
void register_observer(const GoatObserver& observer);
void deregister_observer();
void feed_goat();
Rust:
use autocxx::prelude::*;
use autocxx::subclass::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("register_observer")
generate!("deregister_observer")
generate!("feed_goat")
subclass!("GoatObserver", MyGoatObserver)
}
use ffi::*;
#[subclass]
#[derive(Default)]
pub struct MyGoatObserver;
impl GoatObserver_methods for MyGoatObserver {
fn goat_full(&self) {
println!("BURP!");
}
}
impl Drop for MyGoatObserver {
fn drop(&mut self) {
deregister_observer();
}
}
fn main() {
let goat_obs = MyGoatObserver::default_rust_owned();
// Register a reference to the superclass &ffi::GoatObserver
register_observer(goat_obs.as_ref().borrow().as_ref());
feed_goat();
feed_goat();
feed_goat(); // prints BURP!
}
Subclass ownership
See subclass::CppSubclass
for full details, but you must decide who owns your subclass:
- C++ owns it
- Rust owns it
- It’s self-owned, and only ever frees itself (using
delete_self).
Please be careful: the observer pattern is a minefield for use-after-free bugs. It’s recommended that you wrap any such subclass in some sort of Rust newtype wrapper which enforces any ownership invariants so that users of your types literally can’t make any mistakes.
Subclasses without a safety! policy
Where the C++ superclass has a single trivial constructor, autocxx normally
implements
CppPeerConstructor
for your subclass. It stops doing so if you leave out the
safety! directive, because then every generated function is
unsafe - the peer’s constructor included - and make_peer is a safe method,
so the generated body wouldn’t compile.
Subclasses still work; you write that one impl yourself, putting the unsafe
block around the call:
use autocxx::prelude::*;
use autocxx::subclass::{CppPeerConstructor, CppSubclass, CppSubclassRustPeerHolder};
use cxx::UniquePtr;
impl CppPeerConstructor<ffi::MyObserverCpp> for MyObserver {
fn make_peer(
&mut self,
peer_holder: CppSubclassRustPeerHolder<Self>,
) -> UniquePtr<ffi::MyObserverCpp> {
unsafe { ffi::MyObserverCpp::new(peer_holder) }
}
}
This is the same impl you would write under a safety! policy for a
superclass with several constructors, or one which takes arguments - see the
trait’s documentation. Nothing else about the subclass changes: the ownership
constructors, the _methods trait and the casts all behave as they do above.
The peer’s new hands back the UniquePtr itself, so the body is just the
call - no .within_unique_ptr() and nothing to emplace. Most constructors
autocxx generates hand back a moveit::new::New instead, letting the caller
put the object where it likes, including in Rust storage. That cannot be
offered here: autocxx writes the peer class itself, after bindgen has run, so
nothing ever measures it, and its Rust side is cxx’s zero-sized opaque type.
Only C++ can allocate a peer.
Calling superclass methods
Each subclass also implements a trait called <superclass name>_supers which
includes all superclass methods. You can call methods on that, and if you
don’t implement a particular method, that will be used as the default.
C++ header:
#include <iostream>
class Dinosaur {
public:
Dinosaur() {}
virtual void eat() const {
std::cout << "Roarrr!! I ate you!\n";
}
virtual ~Dinosaur() {}
};
Rust:
use autocxx::prelude::*;
use autocxx::subclass::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
subclass!("Dinosaur", TRex)
subclass!("Dinosaur", Diplodocus)
}
use ffi::*;
#[subclass]
#[derive(Default)]
pub struct TRex;
#[subclass]
#[derive(Default)]
pub struct Diplodocus;
impl Dinosaur_methods for TRex {
// TRex does NOT implement the 'eat' method
// so C++ behavior will be used
}
impl Dinosaur_methods for Diplodocus {
fn eat(&self) {
println!("Ahh, some nice juicy leaves.");
// Could call self.eat_super() if we
// developed unexpected carnivorous cravings.
}
}
fn main() {
let trex = TRex::default_rust_owned();
trex.borrow().as_ref().eat(); // eats human
let diplo = Diplodocus::default_rust_owned();
diplo.borrow().as_ref().eat(); // eats shoots and leaves
}
Subclass casting
Subclasses implement AsRef to enable casting to superclasses.
The superclass implements its own _methods trait too, so a function written
as fn feed(d: &impl Dinosaur_methods) takes a TRex, a Diplodocus, or a
C++ Dinosaur alike. (The generator can’t do this where one of the superclass’s
virtual methods has no binding of its own - a private or protected one, for
instance - because there would be nothing for the implementation to call.)
Older releases did not write that impl. If you filled the gap with an
impl Dinosaur_methods for Dinosaur of your own, delete it when you upgrade:
Rust rejects the two together with E0119, conflicting implementations.
Other C++ features
You can make Rust subclasses of C++ classes - as these are mostly used to implement the Observer pattern, they’re documented under calls from C++ to Rust.
Preprocessor symbols
#define and other preprocessor symbols will appear as constants.
At present there is no way to do compile-time disablement of code
(equivalent of #ifdef)1.
Variables and constants
A C++ variable whose value the compiler knows - const int kMax = 4;,
constexpr double kPi = 3.14;, or a static const int member of a class -
appears as a Rust const, so you can use it anywhere.
A variable which instead lives at some address, such as one of
POD struct type, appears as a Rust static. Reading it is
unsafe, because C++ may be changing it at the same time:
C++ header:
#include <cstdint>
struct Point {
uint32_t x;
uint32_t y;
};
extern const Point ORIGIN;
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate_pod!("Point")
generate!("ORIGIN")
}
fn main() {
assert_eq!(unsafe { ffi::ORIGIN.x }, 0);
}
Two restrictions apply.
The variable must have external linkage, which is why the header above says
extern and the definition lives in a .cc file. A namespace-scope variable
which is static, or which is const without extern, or which sits in an
anonymous namespace, is a different object in every translation unit that
includes the header - and in a translation unit which never uses it, the
compiler emits nothing at all. There is no single symbol for Rust to link
against, so autocxx reports the problem instead of generating code which
fails to link. (On MSVC, where the decorated name is the same either way,
autocxx can’t tell, and you get the link error.)
A variable of non-POD type is reached differently, because there is no
non-POD value autocxx can hand Rust at all. Such a variable appears as a
function of its own name - ffi::BOB() - which returns an opaque holder
standing for a const reference to it. The holder’s get() gives you the
address of the variable itself; nothing is copied, so the type need not be
copy-constructible and what you read is whatever C++ has most recently written
there.
let bob = ffi::BOB();
let bob = unsafe { &*bob.as_ref().unwrap().get() };
assert_eq!(bob.get().as_ref().unwrap().to_str().unwrap(), "hello");
A static data member of a class works too, and needs no extern because
defining it gives it external linkage anyway. Its name is flattened into the
enclosing namespace, so struct Anna { static Point ORIGIN; }; asks for
generate!("Anna_ORIGIN") and appears as ffi::Anna_ORIGIN. It must be POD:
the flattened name is not one C++ has, so there is nothing for the getter a
non-POD variable needs to call it by, and autocxx says so rather than
generating C++ which does not compile.
One assumption comes with the unsafe. Rust requires that an object does not
change while Rust holds a reference to it, and autocxx has no way to enforce
that on the C++ side. Reading a variable which C++ mutates concurrently is
undefined behaviour, and so is reading one whose value C++ changes through a
mutable member of an otherwise const object - legal C++, but not something
you can expose this way. Use a getter function for anything C++ writes to.
volatile
Rust has no volatile type. It has volatile accesses -
read_volatile and
write_volatile -
and nothing in a type or a signature carries the promise that a read may not be
cached, reordered or elided. So a C++ declaration mapped straight onto a Rust
type loses it: autocxx would have to generate a volatile access, and where
it cannot, the binding does not keep what the qualifier says.
The dividing line is where the read happens. C++ performing the access keeps the promise; an ordinary Rust load of a plainly mapped type does not. Rust can keep it too, but only through a type which says so - which is what the handle below is for, in the one position where Rust is the side doing the reading.
Refused, by name. A volatile variable, a field of a generate_pod!
struct, and a template argument. In each of these Rust would end up doing the
access itself - a re-exported static is read by ordinary Rust loads, a POD
struct’s fields are ordinary Rust fields - or, for a template argument, the
qualifier would simply be dropped from the C++ autocxx writes, naming a
different specialization. Each refusal says what to do instead, and it is
usually the same thing: write a C++ function which performs the volatile access,
and bind that.
The variable case is the one worth knowing about, because binding it looks
harmless and is not. extern const volatile int status; is how a read-only
hardware register is declared, and the const alone would make it an immutable
Rust static - which the compiler may constant-fold reads of, the exact
opposite of what volatile asked for.
Bound, and honestly - where the value is a scalar. A volatile data member
of built-in, enumeration or pointer type keeps its getter. The C++ autocxx
generates for it is obj.member, and reading a volatile glvalue is a
volatile access - so the read happens in C++, once per call, and what crosses to
Rust is the copy it produced.
A member of class type is refused instead, whichever shape its getter would
take. C++ copies a class by calling a constructor, and an implicitly declared
copy constructor takes const T& or T& - neither of which a volatile T
binds to - so obj.member does not compile for one however copyable it
otherwise is. A borrowed getter is no better: it hands Rust a reference and lets
Rust do the reading.
A volatile return of scalar type is bound through a wrapper. A
cv-qualified return type is part of a function’s type and cxx declares a
function by taking its address, so int (*f$)() = ::f; does not compile for a
volatile int f(). autocxx already meets this for a const return and
answers it the same way: its own wrapper returns the unqualified type and calls
through.
A class-type volatile return is refused. The wrapper would have to
copy-initialize a T from a volatile T, which needs a constructor C++ does
not implicitly declare, and so does not compile at C++14 - the standard
autocxx generates its C++ against. C++17 initializes the result directly and
would accept it, but the refusal is pinned to that floor rather than to whichever
standard you happen to compile with.
A by-value parameter binds as if the qualifier were not there, because as
far as C++ is concerned it is not: a top-level cv-qualifier on a parameter is
no part of a function’s type, so void f(volatile int) and void f(int) are
one function.
A struct with a volatile member is still usable, whichever of these applies;
it simply cannot be generate_pod!.
Its constructors depend on what the member is, though, because C++’s do. A
volatile member of scalar type changes nothing: C++ copies a scalar by reading
it. A volatile member of class type deletes the enclosing class’s
implicitly declared copy and move constructors unless some constructor of the
member’s type accepts the qualified source, because copying the member means
direct-initializing it from a const volatile source and an implicitly declared
M(const M&) will not bind to one. autocxx reports that rather than offering
constructors whose generated C++ could not compile, and says which member is
responsible.
The copy can be had back: declare a constructor on the member’s own type taking
const volatile M&, which is the one reference the source of a copy binds to.
That constructor does not bring the move back with it. A move reads a volatile M rvalue, and an lvalue reference binds to an rvalue only where it is const
and not volatile, so M(const volatile M&) cannot take one.
autocxx withdraws the move in every case, which is more conservative than C++:
a member type declaring M(volatile M&&), or a constructor template which would
accept the source, does keep the enclosing move constructor in C++, and neither
is something autocxx can see. The reason for withdrawing anyway is that
claiming the move is not the safe error. Writing std::move on a class whose
move constructor C++ has deleted is not itself an error - a defaulted move
constructor defined as deleted is skipped during overload resolution, and the
copy constructor runs instead where the class has a usable one - so a wrongly
claimed move compiles
quietly, performs a copy, and stops compiling as soon as another member has no
copy constructor to fall back on.
Bound through a handle - a pointer or reference to something volatile.
volatile int* and volatile int& are where the qualifier is most often used
in real headers, and they are the one position in which Rust performs the
access. There is no Rust pointer type which carries the promise, so autocxx
hands over one which does: autocxx::VolatilePtr<T>, whose read and write
are read_volatile and write_volatile.
volatile uint32_t* uart_status();
void configure(volatile uint32_t* reg);
let reg = unsafe { ffi::uart_status() };
let flags = unsafe { reg.read() };
unsafe { reg.write(flags | 1) };
unsafe { ffi::configure(reg) };
The handle is the currency in both directions, so what one function returns
goes straight into another which takes it. autocxx::VolatilePtr::new makes
one from an address Rust already knows - 0x4000_0000 as *mut u32 - and
as_raw gets the address back out.
A const volatile pointee - how a status register the program may read but not
write is declared - becomes autocxx::VolatileConstPtr<T>, which has no
write.
Three things are worth knowing about the handle. It is deliberately not &T:
a Rust shared reference promises the referent does not change while it lives,
which is the one thing a hardware register is guaranteed to do, so a reference
here would be a false statement to the compiler rather than merely a lost
guarantee. read and write are unsafe, because whether the address is
valid is C++’s business and autocxx cannot check it. And a volatile access is
not an atomic one and carries no ordering relative to anything but other
volatile accesses: concurrent access from another thread is a data race exactly
as it would be otherwise, and
core::sync::atomic is what to
reach for when synchronization is what you want.
The handle appears only where the pointee is of built-in type, because that is
where Rust is the side reading it. read is a read_volatile, which needs a
Copy type: a class is not, and neither is a C++ enumeration, whose generated
Rust counterpart does not implement Copy. (A volatile enumeration member
is bound, because there C++ does the copying.) A pointer-valued pointee is left
out for a second reason - a qualifier written on a pointer binds to the
declarator rather than reading left to right, so T* const volatile is not
const volatile T*, and the generated C++ cannot name it by putting the
qualifiers in front.
Any other pointee is left exactly as it was rather than turned down, because
where C++ performs the access, handing it the address is already right - a copy
constructor taking const volatile T& binds as it always did.
Not yet handled. A volatile-qualified method (void f() volatile).
libclang exposes no way to ask, so where a class has both a plain and a
volatile-qualified overload of one name, both bind to the plain one.
A qualifier which reaches a signature through a type alias - typedef volatile uint32_t vu32;, or using P = volatile uint32_t*; - is also not seen.
bindgen resolves an alias to its target before recording the qualifier, so
nothing downstream knows the type was volatile, and the same is true of
const. Such a signature fails to build inside the generated C++ rather than
being reported against the function you asked for; it is never bound as though
the qualifier were absent. Writing the qualifier at the point of use rather
than in the alias is the way round it.
volatile nested deeper than the immediately pointed-at type - volatile uint32_t*&, a reference to a pointer to something volatile - is likewise not
handled, and fails the same loud way. The handle covers one level, which is the
level register code is written at.
String constants
Whether from a preprocessor symbol or from a C++ char* constant,
strings appear as [u8] with a null terminator. To get a Rust string,
do this:
#define BOB "Hello"
# mod ffi { pub static BOB: [u8; 6] = [72u8, 101u8, 108u8, 108u8, 111u8, 0u8]; }
assert_eq!(std::str::from_utf8(&ffi::BOB).unwrap().trim_end_matches(char::from(0)), "Hello");
std::function
bindgen has no way to describe a std::function in Rust, and cxx cannot
bind one either; its function support stops at
function pointers. autocxx therefore
generates nothing which takes or returns one. Only the members which mention
std::function are lost: the rest of the enclosing class is generated as usual.
A shim which takes a plain C function pointer does not help either: cxx has
no function pointer type, so autocxx has nothing to declare to it, and a
function with one in its signature is refused saying so. A struct field of
function pointer type is a different matter and does work: a field is data
whose layout autocxx copies rather than a type crossing the language
boundary, so a struct holding one can even be POD, and Rust can put one of its
own extern "C" functions there for C++ to call. It is passing or returning
one that has nowhere to go.
To have C++ call into Rust without that, either subclass a C++ observer class from Rust or hand C++ a named Rust function; both are described under callbacks into Rust.
The explanation reaches you through the doc comment of the stub standing in for
whatever could not be generated, whichever standard library you build against,
though the two put std::function beyond bindgen differently. With libstdc++
and libc++ it is reduced to an opaque blob of bytes, and the stub stands in for
that blob - as it does for any other type bindgen reduces the same way, which
in practice means templated types whose parameters it cannot model. With MSVC’s
standard library the type keeps its name, and the explanation is attached to
std::function itself; a class-scoped using alias of it is reported as an
alias to something which could not be generated, and repeats that thing’s own
explanation.
-
This feature should add ifdef support. ↩
Safety
Unsafety policies
By default, every autocxx function is unsafe. That means you can only call C++ functions from unsafe blocks, and it’s up to you to be sure that the C++ code upholds the invariants the Rust compiler expects.
You can optionally specify:
safety!(unsafe)
within your include_cpp! macro invocation. If you do this, you are promising the Rust compiler that all your C++ function calls are upholding the invariants which rustc expects, and thus each individual function is no longer unsafe.
See safety! in the documentation for more details.
Examples with and without safety!(unsafe)
Without a safety! directive:
C++ header:
#include <cstdint>
inline uint32_t do_math(uint32_t a, uint32_t b) { return a+b; }
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
generate!("do_math")
}
fn main() {
assert_eq!(unsafe { ffi::do_math(12, 13) }, 25);
}
With a safety! directive:
C++ header:
#include <cstdint>
inline uint32_t do_math(uint32_t a, uint32_t b) { return a+b; }
Rust:
use autocxx::prelude::*;
include_cpp! {
#include "input.h"
safety!(unsafe)
generate!("do_math")
}
fn main() {
assert_eq!(ffi::do_math(12, 13), 25);
}
Pragmatism in a complex C++ codebase
This crate mostly intends to follow the lead of the cxx crate in where and when unsafe is required. But, this crate is opinionated. It believes some unsafety requires more careful review than other bits, along the following spectrum:
- Rust unsafe code (requires most review)
- Rust code calling C++ with raw pointers
- Rust code calling C++ with shared pointers, or anything else where there can be concurrent mutation
- Rust code calling C++ with unique pointers, where the Rust single-owner model nearly always applies (but we can’t prove that the C++ developer isn’t doing something weird)
- Rust safe code (requires least review)
If your project is 90% Rust code, with small bits of C++, don’t use this crate. You need something where all C++ interaction is marked with big red “this is terrifying” flags. This crate is aimed at cases where there’s 90% C++ and small bits of Rust, and so we want the Rust code to be pragmatically reviewable without the signal:noise ratio of unsafe in the Rust code becoming so bad that unsafe loses all value.
Worked example
Imagine you have this C++:
struct Thing;
void print_thing(const Thing& thing);
By using autocxx (or cxx), you’re promising the Rust compiler that the print_thing function does sensible things with that
reference:
- It doesn’t store a pointer to the thing anywhere and pass it back to Rust later.
- It doesn’t mutate it.
- It doesn’t delete it.
- or any of the other things that you’re not permitted to do in unsafe Rust.
Soundness
This crate shares the general approach to safety and soundness pioneered by cxx, but has two important differences:
- cxx requires you to specify your interface in detail, and thus think through all aspects of the language boundary; autocxx doesn’t, and may autogenerate footguns.
- cxx may allow multiple conflicting Rust references to exist to ‘trivial’ data types (“plain old data” or POD in autocxx parlance), but they’re rare; autocxx may allow conflicting Rust references to exist even to ‘opaque’ (non-POD) data, and they’re more common. This difference exists because opaque data is zero-sized in cxx, and zero-sized references cannot conflict. (In autocxx, we tell Rust about the size in order that we can allocate such types on the stack.)
There are preliminary explorations to avoid this problem by using a C++ reference wrapper type. See examples/reference-wrappers.
Thread safety
autocxx assumes nothing about the thread safety of your C++ types, so the non-POD types it generates are neither Send nor Sync. This follows cxx, which says the same of its opaque types.
Nothing in a C++ class declaration reveals whether its objects may cross a thread boundary. A class might hold a lock it has taken, an index into a thread-local pool, or a destructor which has to run on the thread that constructed the object. Send is exactly the claim that moving a value to another thread is allowed, and autocxx is not in a position to make that claim on your behalf.
This extends to the pointers you hold them in: cxx grants UniquePtr<T>: Send only where T: Send, so a UniquePtr to a non-POD type won’t cross a thread boundary either.
If you know a particular C++ type really is thread safe, you can say so. include_cpp! expands into the crate that invokes it, so in that crate the generated type is local and the impl is yours to write:
// SAFETY: a MyType may be used and destroyed on any one thread.
unsafe impl Send for ffi::MyType {}
// SAFETY: MyType's const methods may additionally be called concurrently
// through shared references.
unsafe impl Sync for ffi::MyType {}
The unsafe is the point: those claims are yours to justify, and they are two different claims. Send says an object may be handed from one thread to another - so audit the destructor as well as the methods, since it runs wherever the value is finally dropped. Sync says two threads may use one object at the same time through &, which is a stronger thing to promise: a C++ const method is free to update a mutable cache without synchronising, and that races even though nothing in Rust looks mutable.
Audit the whole safe API the impl exposes, not merely the calls you have in mind today. Once written, the impl licenses every caller, including your own downstream users.
Generated types with type or lifetime parameters can have the impl too, but there the claim must hold for every instantiation it admits, which usually means a bound:
// SAFETY: MyContainer owns its element and shares nothing else, so it may go
// wherever the element may go.
unsafe impl<T: Send> Send for ffi::MyContainer<T> {}
T: Send is not a universal answer - it suits a container which owns its elements, whereas one handing out shared access to them would want T: Sync, and a container which is thread-affine for reasons of its own is unsuitable whatever T is. Work out which the C++ actually is.
If the bindings come from someone else’s crate
Only the crate containing the include_cpp! can write that impl. If you depend on a library which generates bindings and re-exports them, the type is foreign to you and Rust’s orphan rule refuses the impl - re-exporting or aliasing it doesn’t change that:
use their_bindings::MyType;
unsafe impl Send for MyType {} // error[E0117]
Ask that library to make the claim, since its author is the one who knows the C++ type. Failing that, wrap it in a type of your own and make the claim about the wrapper:
pub struct SendMyType(cxx::UniquePtr<their_bindings::MyType>);
// SAFETY: audited - MyType may be used and destroyed on any thread.
unsafe impl Send for SendMyType {}
impl SendMyType {
pub fn count(&self) -> u32 { self.0.count() }
}
Note this doesn’t make their_bindings::MyType itself Send, so it won’t satisfy an API which demands that bound, and you’ll be forwarding any methods you need.
POD types
POD types are unaffected: they’re plain data, and they’re Send and Sync on the same terms as any other Rust struct with the same fields. Be aware that this is a statement about the data, not a promise about the C++ methods. A POD type which is merely an integer handle into thread-local state on the C++ side is Send as far as Rust is concerned, and keeping its methods on the right thread is still yours to arrange.
Using the generated bindings
Congratulations, you’ve built some bindings using autocxx!
But are they Rustic? How can you ensure that users of the bindings get Rust-like safety?
The C++ API may have documented usage invariants. Your ideal is to encode as many as possible of those into compile-time checks in Rust.
Some options to consider:
- Wrap the bindings in a newtype wrapper which enforces compile-time variants in its APIs; for example, taking a mutable reference to enforce exclusive access.
- Add extra
implblocks to add methods with a more Rustic API. - Read the C++ to Rust design FAQ.
Structuring a large codebase
If you have multiple modules, it may be inconvenient to generate all bindings in one include_cpp! invocation.
There is limited support to refer from one set of bindings to another, using the extern_cpp_type! directive.
C++ header:
#include <cstdint>
struct A {
A() : a(0) {}
int a;
};
enum B {
VARIANT,
};
void handle_a(const A& a);
A create_a(B);
Rust:
use autocxx::prelude::*;
pub mod base {
autocxx::include_cpp! {
#include "input.h"
name!(ffi2)
safety!(unsafe_ffi)
generate!("A")
generate!("B")
}
pub use ffi2::*;
}
pub mod dependent {
autocxx::include_cpp! {
#include "input.h"
safety!(unsafe_ffi)
generate!("handle_a")
generate!("create_a")
extern_cpp_type!("A", crate::base::A)
extern_cpp_type!("B", super::super::base::B)
pod!("B")
}
pub use ffi::*;
}
fn main() {
let a = dependent::create_a(base::B::VARIANT).within_unique_ptr();
dependent::handle_a(&a);
}
Examples
- Demo - simplest possible demo
- POD example - plain-old-data types, passed by value
- Non-trivial type on the stack -
moveit!and C++ objects on the Rust stack - S2 example - example using S2 geometry library
- Steam example - example using (something like) the Steam client library
- LLVM example - wrapping part of LLVM, a large real-world header set
- Subclass example - example using subclasses
- Chromium RenderFrameHost example - a memory-safe handle to an object C++ owns
- C++ calling Rust - the other direction, plus C++ exceptions as
Result - Reference wrappers - the experimental
unsafe_references_wrappedsafety policy; needs nightly - Integration tests
- hundreds of small snippets
Contributions of more examples to the examples directory are much appreciated!
Credits
David Tolnay did much of the hard work here, by inventing the underlying cxx crate, and in fact nearly all of the parsing infrastructure on which this crate depends. bindgen is also awesome. This crate stands on the shoulders of giants!
Miguel Young also did all the hard thinking about whether non-trivial C++ objects can safely exist on the Rust stack. They can! He also draws nifty cartoons.
Thanks to all the other contributors to cxx, bindgen and autocxx.
And thanks to all in the Chromium community for inspiring this tool.
Reporting bugs
Reporting bugs
If you’ve found a problem, and you’re reading this, thank you! Your diligence
in reporting the bug is much appreciated and will make autocxx better.
However, we get a lot of incoming bugs, and we are limited by our ability to triage them. Please help us by making a good bug report.
Much of this is about minimizing the size of your test case so it does not
depend on your system’s C++ headers or any copyrighted code. To minimize the
test case, you can use tools/reduce, something like this:
cd tools/reduce; cargo run -- file -d "safety!(unsafe_ffi)" -d 'generate_pod!("A")' -I ~/my-include-dir -h my-header.h -p problem-error-message -- --remove-pass pass_line_markers
This is a wrapper for the amazing creduce which will take thousands of lines
of C++, preprocess it, and then identify the minimum required lines to
reproduce the same problem. This can sometimes take days to run - that’s
normal - if this is a problem, try to minimize the test case manually, or
do a combination.
Once you have a minimal test case:
- First see if it triggers a problem when you’re using pure
bindgen, since autocxx is very dependent onbindgengenerating correct code. To do this, fetch thebindgencode then runcargo run -- XYZ.hpp --no-layout-tests --enable-cxx-namespaces --allowlist-type ABC-- -std=c++14XYZ.hppis your header (the extension is important) andABCis the type you wish to generate. Assuming you minimized your test case in advance, this should generate just a few lines of Rust code and you can inspect it (or even build it) to see if it seems to be problematic. - If the problem is in
bindgen, report it tobindgen. - If the problem is in
autocxx, please report it at https://github.com/Choochmeque/autocxx/issues. In order of preference:- Raise a pull request adding a new minimized failing integration test to
integration_test.rs - Simply raise an issue with the minimized code.
- Raise a pull request adding a new minimized failing integration test to
If you can’t get minimization to work:
- Use the C++ preprocessor to give a single complete C++ file which demonstrates
the problem, along with the
include_cpp!directive you use. Alternatively, run your build usingAUTOCXX_REPRO_CASE=repro.jsonwhich should put everything we need intooutput.h. If necessary, you can use theCLANG_PATHorCXXenvironment variables to specify the path to the Clang compiler to use. - Failing all else, build using
cargo clean -p <your package name> && RUST_LOG=autocxx_engine=info cargo build -vvvand send the entire log to us. This will include two key bits of logging: the C++ bindings as distilled bybindgen, and then the version which we’ve converted and moulded to be suitable for use bycxx.
Bugs related to linking problems
Unfortunately, linking C++ binaries is a complex area subject in itself, and
we won’t be able to debug your linking issues by means of an autocxx bug report.
Assuming you’re using autocxx’s build.rs support, the actual C++ build and
managed by the cc crate. You can find
many of its options on its Build type.
If you need to bring in an external library, you may also need to emit certain
print statements from your build.rs
to instruct cargo to link against that library.
How to Contribute
We’d love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow.
Licensing
There is no contributor licence agreement. You keep the copyright in what you write; opening a pull request offers that work under the terms autocxx’s own code carries, Apache-2.0 or MIT.
Code reviews
All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult GitHub Help for more information on using pull requests.
Tests
A bug fix comes with a test that fails without it; a new feature comes with tests covering it. Documentation, comments and pure refactors don’t need new tests.
Use of AI
Use whatever tooling you like, AI assistance included; contributions are judged on the result. You are the author of what you submit: you must understand the change, be able to defend it in review and have run the tests yourself. The same standard applies to project members. AI slop is rejected: pull requests their submitter cannot explain and bug reports with no way to reproduce the problem are closed without further investigation.
Community guidelines
This project has a code of conduct.
Directory structure
book- you’re reading it!demo- a very simple demo exampleexamples- will gradually fill with more complex examplesparser- code which parses a singleinclude_cpp!macro. Used by both the macro (which doesn’t do much) and the code generator (which does much more, by means ofenginebelow)engine- all the core code for actual code generation.macro- the procedural macro which expands the Rust code.gen/build- a library to be used frombuild.rsscripts to generate .cc and .h files from aninclude_cxxsection.gen/cmd- a command-line tool which does the same.src(outermost project) - a wrapper crate which imports the procedural macro and a few other things.
Where to start reading
The main algorithm is in engine/src/lib.rs, in the function generate(). This asks
bindgen to generate a heap of Rust code and then passes it into
engine/src/conversion to convert it to be a format suitable for input
to cxx.
However, most of the actual code is in engine/src/conversion/mod.rs.
bindgen is vendored rather than depended upon: engine/third_party/rust-bindgen is a
submodule of upstream rust-lang/rust-bindgen pinned at a release tag, and
engine/third_party/patches holds autocxx’s delta as a patch series which engine/build.rs
applies into OUT_DIR. See “Rolling bindgen” below.
How to develop
If you’re making a change, here’s what you need to do to get useful diagnostics etc.
First of all, cargo run in the demo directory. If it breaks, you don’t get much
in the way of useful diagnostics, because stdout is swallowed by cargo build scripts.
So, practically speaking, you would almost always move onto running one of the tests
in the test suite. With suitable options, you can get plenty of output. For instance:
RUST_BACKTRACE=1 RUST_LOG=autocxx_engine=info cargo test --workspace integration_test::test_cycle_string -- --exact --nocapture
This is especially valuable to see the bindgen output Rust code, and then the converted Rust code which we pass into cxx. Usually, most problems are due to some mis-conversion somewhere
in engine/src/conversion. See here for documentation and diagrams on how the engine works.
You may also wish to set AUTOCXX_ASAN=1 on Linux when running tests. To exercise all
the code paths related to generating both C++ and Rust side shims, you can set
AUTOCXX_FORCE_WRAPPER_GENERATION=1. The test suite doesn’t do this by default because
we also want to test the normal code paths. (In the future we might want to
parameterize the test suite to do both.)
Reporting bugs
Moved to its own document.
How to contribute to this manual
More examples in this manual are very welcome!
Because autocxx examples require both Rust and C++ code to be linked together,
a custom preprocessor is used for this manual. See one of the existing examples
such as in index.md to see how to do this.
Maintenance responsibilities
The project is under active development: C++ constructs it could not bind get
bound, bugs get fixed, and the existing bindings are kept working as Rust,
bindgen and cxx change underneath them.
Work also arrives from outside:
- Changes to Rust
- Changes to
bindgen - Pull requests raised against autocxx
- Issues reported against autocxx
Here’s what to do. If issues are reported, encourage the reporter to raise a PR with a minimized test case by pointing them at the “reporting bugs” page. This resolves all concerns about reproducibility: autocxx is quite sensitive to the environment in which it runs, e.g. standard library header files in use, and it’s rare to be able to reproduce a reporter’s bug without them raising a reproducible test case like this.
If CI starts to fail due to a Rust change (or, if we manage to unfork bindgen, a bindgen change) then raise a pull request to fix it. (Most commonly CI starts to fail because of more aggressive clippy lints).
For user-contributed pull requests, merge them if you can! For reproducible bug reports, try to investigate them. A fair proportion of the bug reports boil down to certain key known areas of technical debt or limitations, described below, and you can mark them as a duplicate or similar.
Releases typically happen every month or two, dependent on what changes have been made.
Release process
A release is a tag push. .github/workflows/release.yml answers a tag matching
v* by publishing to crates.io and creating the github release. It authenticates
by OIDC through crates.io trusted publishing, so there is no API token in this
repository’s secrets and nothing to rotate.
Seven crates are published, and cargo works out the order from the
dependencies: autocxx-parser, autocxx-engine, autocxx-macro, autocxx,
autocxx-build, autocxx-integration-tests, autocxx-gen. The last is in the
set because autocxx-gen dev-depends on it by exact version, so a release
leaving it out cannot resolve. The other workspace members - demo,
tools/reduce, tools/mdbook-preprocessor, tools/patch-check and
integration-tests/fixture-deps - are not published, and the first three hold
no crates.io name at all.
To make a new release:
- Ensure there’s green CI on main.
- Make a branch and run
tools/upgrade-version.sh OLD NEW, whereOLDis the released version number andNEWis the new one. It rewrites the manifests only, so runcargo check --workspaceafter it and commit the movedCargo.lockalongside them - CI builds--locked, and the release refuses a working tree carrying anything the tag does not. The version bump is an ordinary pull request; the workflow never edits a manifest, and refuses a tag which disagrees with one. - Merge that pull request.
- Tag the merged commit
vNEWand push the tag. The workflow refuses a tag pointing anywhere but main.
The workflow runs three jobs. gate holds no publishing credential: it checks
the tag’s commit and versions, then packages and builds all seven crates from
their packaged form, which is everything the publish does except the upload. It
hands on the flattened bindgen sources it built, so that what reaches crates.io
is what it checked. publish mints a crates.io token, valid for thirty minutes,
as its last step before uploading; it compiles nothing, because the OIDC claim a
token is minted from is reachable by every step of the job that holds it, build
scripts included. github-release creates the release with generated notes.
publish runs in a github environment named release, which is where required
reviewers go if you want a manual gate between the tag and the upload.
crates.io setup, once per crate
The workflow cannot publish anything until this is done, and it is done in the crates.io UI rather than here. For each of the seven crates, under Settings → Trusted Publishing → Add → GitHub:
| Field | Value |
|---|---|
| Repository owner | Choochmeque |
| Repository name | autocxx |
| Workflow filename | release.yml |
| Environment | release |
You must own the crate on crates.io to add this, and the crate must already exist there - a name’s first publish still needs an API token. Renaming the workflow file, the repository or the environment invalidates all seven configurations.
Do all seven before the first tag. The dry run needs no credential, so it cannot tell you that the sixth crate’s configuration is missing; that shows up as a rejected upload after five crates have already been published.
When a publish fails partway
cargo’s multi-crate publish is not atomic, and a crates.io publish cannot be
undone or repeated. A run which uploads three crates and then fails leaves those
three published; cargo warns that a version already exists rather than skipping
it, so re-pushing the tag is not a clean retry. Publish what remains by hand -
tools/publish-all.sh is the same sequence, run locally against an API token -
and then move to a new version number rather than trying to complete the old
one from CI.
Rolling bindgen
The engine needs facts about C++ which bindgen does not report: access specifiers, special
members, virtualness, the original C++ spelling behind a flattened nested name, and markers
telling opaque types and references apart from what they are generated as. It used to get
them from autocxx-bindgen, a fork republished under its own name, which had to be rebased
by hand and fell far enough behind upstream to miss the clang-22 fix for
clang_getTypeDeclaration (see google/autocxx#1509
and rust-lang/rust-bindgen#3278).
Instead:
engine/third_party/rust-bindgenis a git submodule of upstreamrust-lang/rust-bindgen, checked out at a release tag. Nothing in it is edited, and no upstream code is committed to this repository.engine/third_party/patches/*.patchis autocxx’s delta, one file per feature, applied in filename order. Each patch applies to the submodule’sbindgen/directory. A patch’s number is its position in that order and carries nothing else, so two patches landing in parallel take the next two free numbers rather than sharing one under a letter suffix. Relabelling the series is therefore safe where reordering it is not: a new number may not move a patch relative to the patches whose output it applies to. Comments in the engine and in the tests cite patches by filename, so a relabelling has to update those as well.engine/build.rscopies the submodule sources intoOUT_DIR, applies the patches, and rewrites the result so it compiles as a module rather than a crate: bindgen’s cargo features become fixedcfgs, macros reached through#[macro_use] extern cratebecome absolute paths, andcrate::becomescrate::vendored_bindgen::.engine/src/lib.rsmounts the result asmod vendored_bindgen.
A checkout therefore needs git submodule update --init --recursive, and CI jobs which
compile the engine check out with submodules: recursive.
Publishing needs one more step, which the release workflow and
tools/publish-all.sh both run. cargo package prunes every directory below the package
root which contains a Cargo.toml, reading it as a separate package; neither include nor
exclude lifts that, so the submodule can never reach crates.io as it stands.
AUTOCXX_VENDOR_BINDGEN=1 cargo build -p autocxx-engine therefore copies the sources to
engine/third_party/bindgen-src with those manifests renamed, and build.rs prefers the
submodule where there is one - which in a published crate there never is. The copy is left
untracked, so the publish is --allow-dirty and no bindgen source is committed here. The
variable belongs on that one command: a packaged crate has no submodule to flatten, so a
build script which saw it set there aborts the verification build.
To move to a new bindgen release:
cd engine/third_party/rust-bindgen && git fetch && git checkout <new tag>cargo check -p autocxx-engine. Every patch that no longer applies fails the build naming the patch and the file, so there is no silent mis-patching.- Rebase the failing patches. The delta is small and each patch is one feature, so this is a matter of re-reading the hunk against the new upstream code rather than a merge.
- Run the integration suite, and diff generated output against the previous commit for anything the release changed on its own.
- Commit the submodule bump and the rebased patches together.
Anything in the patch series which upstream would take is worth sending upstream: the series is the list of reasons autocxx cannot use bindgen as published, and every patch removed from it is one less thing to rebase.
Major areas of tech debt
The core of autocxx is the Api enum. That’s solid. It’s created by parsing
bindgen output, and exists all the way through to Rust and C++ codegen.
It is annotated by various analysis phases; it’s parameterized by
the analysis phase so it’s literally impossible to access those annotations
before they exist.
Because this core is solid, most of the areas of tech debt are discrete and don’t really overlap with each other. If you wish to tackle them, they can be tackled fairly independently.
Without further ado, they are:
-
Naming. The codebase deals with lots of names - the original C++ name, the name chosen by bindgen, the name we want to give to cxx, etc. When one kind of name is used for another purpose, certain invariants need to be applied, e.g. avoiding conflicting overloads, or avoiding built-in cxx names such as
UniquePtr. So, these different kinds of names really need to be encapsulated in newtype wrappers which enforce these invariants. This has been started here. The other tricky aspect here is that we deal with name conflicts in two different ways. What are name conflicts? bindgen gives us a hierarchical namespace which we have to map to a flat namespace to give to cxx. For types, we just reject any duplicate names. For funtions, though, we go to some efforts to rename them to be non-conflicting. We should become uniform here by abstracting the name deconfliction stuff from the function analysis. -
Patched bindgen. As noted under rolling bindgen, above, autocxx patches bindgen rather than using it as published. The patch series is small and each patch is one feature, but every one of them is a hunk to rebase at each bindgen release, and a feature upstream would take is a feature nobody has to rebase again. Tracked here.
-
Sensitivity to bindgen bugs caused by constructor calculations.
bindgenisn’t perfect. When it mis-generates a type, users can ask bindgen to mark that type as “opaque”, in which case it’s represented as just an array of bytes. This is especially useful for complex templated C++ types where bindgen still has a number of bugs. Unfortunately, users of autocxx don’t get that luxury. In order to calculate whether types are movable, autocxx requires visibility into all the fields. That means that any types marked opaque by bindgen are pretty useless, and we never currently ask bindgen for opaque types.bindgenmight be in a better place to request implicit constructors, then we can solve these problems. This is tracked here. -
CppRefand friends.cxx(and thereforeautocxx) can be used to create multiple mutable references to the same Rust memory. This is UB. In cxx this problem applies only to types markedTrivialsince others are zero-sized. Inautocxx, we give all types a size so they can be allocated on the stack, and so the problem is worse. (This is explained more in the comment abovegenerate_opaque_typeinnon_pod_struct.rs). The intended solution here is to cease to use Rust references to represent C++ references - instead using a newtype wrapper calledCppRef<T>.autocxxcan already work in this mode usingunsafe_references_wrappedbut for now it works well only on nightly Rust, pending the merge of the arbitrary self types v2 RFC. Even then, this isn’t quite perfect becausecxxexpects to use Rust references, and its key types (such ascxx::UniquePtr) provideDerefimplementations which point in that direction. This isn’t a huge problem but I mention it here because it’s one of the very few occasions in which our dependence oncxxis limiting for us.
Code of Conduct
This project is a place to make Rust and C++ work together, and its issues, pull requests and reviews are for that work.
What is expected of everyone taking part:
- Be considerate, kind, constructive and helpful.
- Criticise code, designs and decisions, not the people behind them.
- No demeaning, discriminatory, harassing, hateful or sexualised behaviour, speech or imagery, and no personal attacks or threats.
Disagreement is welcome - technical arguments are how this project decides things. Disrespect is not.
Reporting problems
If you see someone behaving disrespectfully, and you are comfortable doing so, address it directly with those involved. If that does not work, or the behaviour is threatening or harassing, report it by opening an issue at https://github.com/Choochmeque/autocxx/issues. A report which should not be public can go to the maintainer instead, through the contact details on https://github.com/Choochmeque.
Reports are read. What follows depends on the case, and may range from nothing to removal from the project’s spaces. Where action is taken the person it concerns is told of the report and given a chance to respond first, except where someone’s safety is at stake.