Skip to content
4 changes: 4 additions & 0 deletions .github/workflows/format.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ jobs:

- name: Check format
run: git diff --exit-code

- name: Check markdown formatting
run: npx prettier@3.6.2 --check "src/**/*.md"
working-directory: docs
4 changes: 4 additions & 0 deletions docs/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"proseWrap": "always",
"printWidth": 80
}
24 changes: 18 additions & 6 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,27 @@

# The Project

* [Introduction](./project/introduction.md)
* [Building](./project/building.md)
* [Usage](./project/usage.md)
* [Test Suite](./project/test-suite.md)
- [Introduction](./project/introduction.md)
- [Building](./project/building.md)
- [Usage](./project/usage.md)
- [Test Suite](./project/test-suite.md)

# Translation Rules

* [Overview](./rules/overview.md)
- [Overview](./rules/overview.md)
- [Rule Format](./rules/format.md)
- [Writing Rules](./rules/writing-rules.md)
- [Compat Shims](./rules/compat.md)
- [Conventions](./rules/conventions.md)
- [The Rule Preprocessors](./rules/preprocessors.md)
- [The Rules IR](./rules/ir.md)
- [Loading and Matching](./rules/loading.md)
- [The Matching Engine](./rules/matching.md)
- [Rule Rewriting](./rules/rewriting.md)

# Code Generation

* [Overview](./codegen/overview.md)
- [Overview](./codegen/overview.md)
- [Pointers and References](./codegen/pointers.md)
- [Temporary Materialization](./codegen/temporaries.md)
- [Translation Plugins](./codegen/plugins.md)
3 changes: 3 additions & 0 deletions docs/src/codegen/plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
> TODO: document the converter plugin mechanism (`cpp2rust/converter/plugins/`),
> which intercepts constructs ahead of the translation rules (currently
> `emplace_back`).
5 changes: 5 additions & 0 deletions docs/src/codegen/pointers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Pointers and References

> TODO: explain how the two models translate C++ pointers and references, in
> particular why the refcount model maps both to `Ptr<T>`, and the role of
> `StrongPtr` for reading through a pointer.
5 changes: 5 additions & 0 deletions docs/src/codegen/temporaries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Temporary Materialization

> TODO: explain how the converter materializes temporaries for expressions that
> need an address but have none (e.g. literals passed where a pointer is
> expected).
20 changes: 9 additions & 11 deletions docs/src/project/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,19 @@ published at PLDI 2026.

## Overview

Cpp2Rust first parses the input C++ file(s) with clang and produces an AST.
It then traverses the AST and emits Rust code as strings, inserting
calls to the `libcc2rs` runtime library where needed (e.g., for raw pointer
semantics).
Cpp2Rust first parses the input C++ file(s) with clang and produces an AST. It
then traverses the AST and emits Rust code as strings, inserting calls to the
`libcc2rs` runtime library where needed (e.g., for raw pointer semantics).
Finally, the Rust code is pretty-printed using `rustfmt` to a single `.rs` file.

By default the *reference counting model* is used, which produces fully safe
Rust.
A generator of unsafe Rust is also available through the `--model=unsafe`
By default the _reference counting model_ is used, which produces fully safe
Rust. A generator of unsafe Rust is also available through the `--model=unsafe`
command line argument for debugging and performance comparisons.

## Runtime library (`libcc2rs`)

The generated code relies on a runtime library designed to simplify the
translation process.
C pointers are converted into the `Ptr<T>` type provided by `libcc2rs`.
`Ptr<T>` models C pointer semantics, including null, arithmetic, and aliasing,
while satisfying Rust's borrow checker through checked run-time operations.
translation process. C pointers are converted into the `Ptr<T>` type provided by
`libcc2rs`. `Ptr<T>` models C pointer semantics, including null, arithmetic, and
aliasing, while satisfying Rust's borrow checker through checked run-time
operations.
4 changes: 2 additions & 2 deletions docs/src/project/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
./build/cpp2rust/cpp2rust --file=<file>.cpp -o=<file>.rs
```

By default, the reference counting model is used (fully safe output).
To generate unsafe Rust instead:
By default, the reference counting model is used (fully safe output). To
generate unsafe Rust instead:

```bash
./build/cpp2rust/cpp2rust --file=<file>.cpp -o=<file>.rs --model=unsafe
Expand Down
145 changes: 145 additions & 0 deletions docs/src/rules/compat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Compat Shims

Rule matching needs a resolvable callee. The preprocessor keys every expression
rule on the function, method, constructor, constant, or global that the
pattern's `return` expression resolves to, and the only macros it can record are
those that
[expand to an integer literal](./writing-rules.md#enum-values-constants-and-macros),
which match by macro name. Any other macro is invisible to the rule system: by
the time clang has built the AST, the macro is gone and only its expansion
remains.

That is a problem for a small set of libc APIs that are specified as macros over
platform internals:

- `errno` is an object-like macro; glibc expands it to `(*__errno_location())`,
macOS to `(*__error())`.
- `assert` expands to a conditional that stringifies the condition and calls a
platform-specific failure handler with file and line arguments.
- `FD_SET`, `FD_CLR`, `FD_ISSET`, and `FD_ZERO` expand to bit manipulation on
the `fd_set` representation, through helpers that differ per platform.
- `ntohl`, `ntohs`, `htonl`, and `htons` expand to byte-swap builtins or to
nothing at all, depending on endianness.

There is no stable, platform-independent callee here to key a rule on. The
_compat headers_ in `cpp2rust/compat/` fix this by rewriting each such macro
into a call to a synthetic, well-known function before matching happens.

## How the shims work

`cpp2rust/compat` is injected as a system include directory ahead of the
platform headers in every clang invocation the project makes: both when
`cpp2rust` parses the input program and when `cpp-rule-preprocessor` compiles
rule sources. The shared flag list lives in `cpp2rust/compat/platform_flags.h`
(`getPlatformClangBeginFlags`), and the directory path is baked in at build time
via the `COMPAT_INCLUDE_DIR` definition.

A shim header sits at the same relative path as the real header it shadows
(`errno.h`, `sys/select.h`, `arpa/inet.h`, ...), so an ordinary
`#include <errno.h>` finds the shim first. The header then:

1. pulls in the real platform header with `#include_next` (a GNU extension; the
shared flags pass `-Wno-gnu-include-next` for it),
2. `#undef`s the macro,
3. declares a `cpp2rust_*` shim function,
4. redefines the macro to call the shim.

`cpp2rust/compat/errno.h` in full:

```c
#include_next <errno.h>

#undef errno

int *cpp2rust_errno(void);

#define errno (*cpp2rust_errno())
```

The redefinition keeps `errno` an lvalue by dereferencing the returned pointer,
so both reads and assignments like `errno = 0` still parse; what the matcher
sees in either case is a call to `int *cpp2rust_errno()`.

Because the input program and the rule sources are compiled with the same shim
headers, both sides canonicalize to the same signature, and an ordinary
[expression rule](./writing-rules.md) matches it:

```c
// rules/errno/src.c
#include <errno.h>

int *f1(void) { return cpp2rust_errno(); }
```

The shim functions are declared but never defined on the C side. They only exist
so that the callee resolves; translation replaces the call with the rule body,
so no C implementation is ever linked. Whatever the shim is supposed to _do_ is
supplied by the Rust targets:

```rust
// rules/errno/tgt_unsafe.rs
unsafe fn f1() -> *mut i32 {
libcc2rs::cpp2rust_errno_unsafe()
}
```

```rust
// rules/errno/tgt_refcount.rs
fn f1() -> Ptr<i32> {
libcc2rs::cpp2rust_errno()
}
```

In the unsafe model `libcc2rs::cpp2rust_errno_unsafe` wraps the real platform
errno location (`__errno_location` on Linux, `__error` on macOS). The refcount
model instead _virtualizes_ errno as a thread-local `Value<i32>` inside
`libcc2rs`; this is the same cell that other refcount rules write when they
translate a failing libc call into
`libcc2rs::cpp2rust_errno().write(__e as i32)`.

A rule pattern may spell either the macro or the shim directly; the two are
identical after expansion. `rules/errno` and `rules/assert` call the shim by
name, while `rules/arpa_inet` and `rules/select` write the macro form:

```c
// rules/select/src.cpp
void f2(int fd, fd_set *set) { return FD_SET(fd, set); }
```

## The current shims

| Header | Macros | Shim functions | Rules |
| -------------- | ----------------------------------------- | ------------------------------- | --------------------------------------------------------------------------------------- |
| `assert.h` | `assert` | `cpp2rust_assert_fail(bool)` | `rules/assert` maps it to `assert!(a0)` |
| `errno.h` | `errno` | `cpp2rust_errno()` | `rules/errno`, see above |
| `arpa/inet.h` | `ntohl`, `ntohs`, `htonl`, `htons` | `cpp2rust_ntohl(x)`, ... | `rules/arpa_inet` maps them to `u32::from_be`, `u16::to_be`, ... |
| `sys/select.h` | `FD_SET`, `FD_CLR`, `FD_ISSET`, `FD_ZERO` | `cpp2rust_fd_set(fd, set)`, ... | `rules/select` maps them to `libc::FD_SET(...)` (unsafe) or `CFdSet` methods (refcount) |

Note how the shim also normalizes the _shape_ of the API. C's `assert` is a
macro precisely so it can stringify its condition and capture file and line; the
shim reduces it to a plain `void(bool)` function, and the Rust side regains the
diagnostics by mapping it to the `assert!` macro.

## Adding a new shim

To make another macro-based API matchable:

1. Create the header in `cpp2rust/compat/` at the same relative path as the
platform header that defines the macro.
2. Follow the pattern above: `#include_next` the real header, `#undef` the
macro, declare a `cpp2rust_<name>` function with the macro's effective
signature, and redefine the macro to call it.
3. Write rules for the shim in a `rules/` module as for any other function,
including the corresponding header in `src.c`/`src.cpp`.
4. If a model needs runtime support (as refcount errno does), implement it in
`libcc2rs` and call it from the rule target.

Keep the shim's signature platform-independent; the whole point is that both
sides of every rule see one canonical declaration on every platform.

## Related normalization

The same shared flag list also passes `-D_FORTIFY_SOURCE=0`, which keeps glibc
from substituting fortified variants (`__printf_chk` and friends) for standard
calls. Like the shims, this ensures that calls in the input program resolve to
the standard declarations the rules are written against.
68 changes: 68 additions & 0 deletions docs/src/rules/conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Conventions

Most of these conventions are enforced by the preprocessors, and violating them
fails the build; the notes below call out the ones that are not checked.

## Naming

| Element | C++ side | Rust side |
| --------------------- | --------------------------------------------- | --------------------------------------------------- |
| Expression rule | `f1`, `f2`, ... | same name |
| Type rule | `t1`, `t2`, ... via `using`/`typedef` | `fn tN() -> RustType` with no arguments |
| Parameters | free-form (`o`, `it`, `key`, `dst`, `n`, ...) | must be `a0`, `a1`, ... consecutive from 0 |
| Generics | `T1`, `T2`, ... (type and non-type params) | `T1`, `T2`, ... consecutive from 1 |
| Variadic pack | `typename... Args` | trailing `va: &[VaArg]` |
| Locals in Rust bodies | | double-underscore prefix: `__v`, `__fd`, `__e`, ... |

Notes:

- Rule numbering is per module, and gaps are currently allowed (e.g. `rules/map`
has no `f4`), though this might change in the future. Names must be unique
across `src.c` and `src.cpp` combined.
- On the C++ side parameter names are free, but the _order_ defines the
placeholder indices: the first parameter is `a0` on the Rust side, the second
is `a1`, and so on. The receiver of a method rule is always the first
parameter, hence `a0`.
- Generic parameters are matched positionally between the two sides, so `T1` in
the Rust target means "whatever bound to `T1` in the C++ pattern".
- Locals introduced inside Rust rule bodies use a `__` prefix. This is not
checked by the build, but it is needed: rule bodies are spliced inline into
the generated code, so an unprefixed local could collide with a variable name
from the translated program.

## Function qualifiers

- In `tgt_unsafe.rs`, expression rules are `unsafe fn`; type rules (`tN`) are
plain `fn`.
- In `tgt_refcount.rs`, all rules are safe `fn`. The refcount model produces
fully safe Rust, so a refcount rule body must not need `unsafe`.

The build does not check the qualifiers themselves; only rustc's usual rules
apply when the `rules` crate compiles. In particular, nothing stops an `unsafe`
block inside a refcount rule body from being spliced into the output, so keeping
refcount rules safe is what upholds the model's safety guarantee.

## C++ pattern shape

- An `fN` body must be exactly one `return` statement. The preprocessor rejects
anything else.
- `return` statements are not allowed inside Rust rule bodies; write the result
as a tail expression instead.
- Exercise exactly one construct per rule. If an API has several overloads,
write one rule per overload (including separate rules for `const T &` versus
`T &&` parameters).

## Argument accesses

Every use of an `aN` parameter in a rule body is classified as a read, write, or
move by the [rule preprocessor](./preprocessors.md#rule-preprocessor). Passing
an argument by value counts as a read, not a move; the only way to record a move
is `std::mem::take(&mut aN)`.

## Type checking

All `tgt_*.rs` files are compiled as part of the `rules` crate, so a rule body
that does not type-check against `libcc2rs`, `libc`, `nix`, etc. breaks the
build. If a rule needs a new crate dependency, add it to `rules/Cargo.toml` and
to the hardcoded crate list in `rule-preprocessor/src/semantic.rs` (see
[The Rule Preprocessors](./preprocessors.md#rule-preprocessor)).
Loading
Loading