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

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 example
  • examples - will gradually fill with more complex examples
  • parser - code which parses a single include_cpp! macro. Used by both the macro (which doesn’t do much) and the code generator (which does much more, by means of engine below)
  • 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 from build.rs scripts to generate .cc and .h files from an include_cxx section.
  • 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, where OLD is the released version number and NEW is the new one. It rewrites the manifests only, so run cargo check --workspace after it and commit the moved Cargo.lock alongside 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 vNEW and 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:

FieldValue
Repository ownerChoochmeque
Repository nameautocxx
Workflow filenamerelease.yml
Environmentrelease

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-bindgen is a git submodule of upstream rust-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/*.patch is autocxx’s delta, one file per feature, applied in filename order. Each patch applies to the submodule’s bindgen/ 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.rs copies the submodule sources into OUT_DIR, applies the patches, and rewrites the result so it compiles as a module rather than a crate: bindgen’s cargo features become fixed cfgs, macros reached through #[macro_use] extern crate become absolute paths, and crate:: becomes crate::vendored_bindgen::.
  • engine/src/lib.rs mounts the result as mod 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:

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

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

  3. Sensitivity to bindgen bugs caused by constructor calculations. bindgen isn’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. bindgen might be in a better place to request implicit constructors, then we can solve these problems. This is tracked here.

  4. CppRef and friends. cxx (and therefore autocxx) can be used to create multiple mutable references to the same Rust memory. This is UB. In cxx this problem applies only to types marked Trivial since others are zero-sized. In autocxx, 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 above generate_opaque_type in non_pod_struct.rs). The intended solution here is to cease to use Rust references to represent C++ references - instead using a newtype wrapper called CppRef<T>. autocxx can already work in this mode using unsafe_references_wrapped but 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 because cxx expects to use Rust references, and its key types (such as cxx::UniquePtr) provide Deref implementations 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 on cxx is limiting for us.