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 memcpy sense). 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. autocxx calls 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. autocxx calls 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.
  • There is no access to fields (yet).
  • You can't even have a &mut reference to one, because then you might be able to use std::mem::swap or similar. You can have a Pin<&mut> reference, which is more fiddly.

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 new associated function in the same way. This will give you something implementing moveit::New/
  • Use this to make the object on the heap or stack, in any of the following ways:
Where you want to create itHow to create itWhat you getExample
C++ heap (recommended for simplicity)Within.within_unique_ptr() or UniquePtr::emplacecxx::UniquePtr<T>let mut obj = ffi::Goldfish::new().within_unique_ptr() or let mut obj = UniquePtr::emplace(ffi::Goldfish::new())
Rust heapWithin.within_box() or Box::emplacePin<Box<T>>let mut obj = ffi::Goldfish::new().within_box() or let mut obj = Box::emplace(ffi::Goldfish::new())
Rust stackmoveit macro&mut T (more or less)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.

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);
}

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, and never have methods attached. That's therefore 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.

1

Future improvements tracked here

To make them 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);
}

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 which can be instantiated but has an inaccessible constructor will be leaked by Rust4. The object's memory itself will be freed without calling any C++ destructor, which will leak any resources tracked by the C++ implementation.

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.

2

Handling of member initializers is tracked here.

3

Fix for explicitly defaulted special member functions that are deleted is tracked here.

4

Discussion around what to do about inaccessible or deleted destructors here.

Abstract types

autocxx does not allow instantiation of abstract types5 (aka types with pure virtual methods).

5

autocxx's determination of abstract types is a bit approximate and could be improved.