Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.


  1. This feature should add ifdef support. ↩