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.