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