Skip to content

Commit 06685fb

Browse files
authored
Add documentation for rules (#302)
It covers: * how to write a rule * how cpp-rule-preprocessor and rule-preprocessor work * the format of Rules IR * etc See docs/src/SUMMARY.md for a complete list.
1 parent 5cf9f0f commit 06685fb

18 files changed

Lines changed: 1282 additions & 24 deletions

‎.github/workflows/format.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,7 @@ jobs:
3737

3838
- name: Check format
3939
run: git diff --exit-code
40+
41+
- name: Check markdown formatting
42+
run: npx prettier@3.6.2 --check "src/**/*.md"
43+
working-directory: docs

‎docs/.prettierrc‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"proseWrap": "always",
3+
"printWidth": 80
4+
}

‎docs/src/SUMMARY.md‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,27 @@
22

33
# The Project
44

5-
* [Introduction](./project/introduction.md)
6-
* [Building](./project/building.md)
7-
* [Usage](./project/usage.md)
8-
* [Test Suite](./project/test-suite.md)
5+
- [Introduction](./project/introduction.md)
6+
- [Building](./project/building.md)
7+
- [Usage](./project/usage.md)
8+
- [Test Suite](./project/test-suite.md)
99

1010
# Translation Rules
1111

12-
* [Overview](./rules/overview.md)
12+
- [Overview](./rules/overview.md)
13+
- [Rule Format](./rules/format.md)
14+
- [Writing Rules](./rules/writing-rules.md)
15+
- [Compat Shims](./rules/compat.md)
16+
- [Conventions](./rules/conventions.md)
17+
- [The Rule Preprocessors](./rules/preprocessors.md)
18+
- [The Rules IR](./rules/ir.md)
19+
- [Loading and Matching](./rules/loading.md)
20+
- [The Matching Engine](./rules/matching.md)
21+
- [Rule Rewriting](./rules/rewriting.md)
1322

1423
# Code Generation
1524

16-
* [Overview](./codegen/overview.md)
25+
- [Overview](./codegen/overview.md)
26+
- [Pointers and References](./codegen/pointers.md)
27+
- [Temporary Materialization](./codegen/temporaries.md)
28+
- [Translation Plugins](./codegen/plugins.md)

‎docs/src/codegen/plugins.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
> TODO: document the converter plugin mechanism (`cpp2rust/converter/plugins/`),
2+
> which intercepts constructs ahead of the translation rules (currently
3+
> `emplace_back`).

‎docs/src/codegen/pointers.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Pointers and References
2+
3+
> TODO: explain how the two models translate C++ pointers and references, in
4+
> particular why the refcount model maps both to `Ptr<T>`, and the role of
5+
> `StrongPtr` for reading through a pointer.

‎docs/src/codegen/temporaries.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Temporary Materialization
2+
3+
> TODO: explain how the converter materializes temporaries for expressions that
4+
> need an address but have none (e.g. literals passed where a pointer is
5+
> expected).

‎docs/src/project/introduction.md‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,19 @@ published at PLDI 2026.
99

1010
## Overview
1111

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

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

2321
## Runtime library (`libcc2rs`)
2422

2523
The generated code relies on a runtime library designed to simplify the
26-
translation process.
27-
C pointers are converted into the `Ptr<T>` type provided by `libcc2rs`.
28-
`Ptr<T>` models C pointer semantics, including null, arithmetic, and aliasing,
29-
while satisfying Rust's borrow checker through checked run-time operations.
24+
translation process. C pointers are converted into the `Ptr<T>` type provided by
25+
`libcc2rs`. `Ptr<T>` models C pointer semantics, including null, arithmetic, and
26+
aliasing, while satisfying Rust's borrow checker through checked run-time
27+
operations.

‎docs/src/project/usage.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
./build/cpp2rust/cpp2rust --file=<file>.cpp -o=<file>.rs
77
```
88

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

1212
```bash
1313
./build/cpp2rust/cpp2rust --file=<file>.cpp -o=<file>.rs --model=unsafe

‎docs/src/rules/compat.md‎

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Compat Shims
2+
3+
Rule matching needs a resolvable callee. The preprocessor keys every expression
4+
rule on the function, method, constructor, constant, or global that the
5+
pattern's `return` expression resolves to, and the only macros it can record are
6+
those that
7+
[expand to an integer literal](./writing-rules.md#enum-values-constants-and-macros),
8+
which match by macro name. Any other macro is invisible to the rule system: by
9+
the time clang has built the AST, the macro is gone and only its expansion
10+
remains.
11+
12+
That is a problem for a small set of libc APIs that are specified as macros over
13+
platform internals:
14+
15+
- `errno` is an object-like macro; glibc expands it to `(*__errno_location())`,
16+
macOS to `(*__error())`.
17+
- `assert` expands to a conditional that stringifies the condition and calls a
18+
platform-specific failure handler with file and line arguments.
19+
- `FD_SET`, `FD_CLR`, `FD_ISSET`, and `FD_ZERO` expand to bit manipulation on
20+
the `fd_set` representation, through helpers that differ per platform.
21+
- `ntohl`, `ntohs`, `htonl`, and `htons` expand to byte-swap builtins or to
22+
nothing at all, depending on endianness.
23+
24+
There is no stable, platform-independent callee here to key a rule on. The
25+
_compat headers_ in `cpp2rust/compat/` fix this by rewriting each such macro
26+
into a call to a synthetic, well-known function before matching happens.
27+
28+
## How the shims work
29+
30+
`cpp2rust/compat` is injected as a system include directory ahead of the
31+
platform headers in every clang invocation the project makes: both when
32+
`cpp2rust` parses the input program and when `cpp-rule-preprocessor` compiles
33+
rule sources. The shared flag list lives in `cpp2rust/compat/platform_flags.h`
34+
(`getPlatformClangBeginFlags`), and the directory path is baked in at build time
35+
via the `COMPAT_INCLUDE_DIR` definition.
36+
37+
A shim header sits at the same relative path as the real header it shadows
38+
(`errno.h`, `sys/select.h`, `arpa/inet.h`, ...), so an ordinary
39+
`#include <errno.h>` finds the shim first. The header then:
40+
41+
1. pulls in the real platform header with `#include_next` (a GNU extension; the
42+
shared flags pass `-Wno-gnu-include-next` for it),
43+
2. `#undef`s the macro,
44+
3. declares a `cpp2rust_*` shim function,
45+
4. redefines the macro to call the shim.
46+
47+
`cpp2rust/compat/errno.h` in full:
48+
49+
```c
50+
#include_next <errno.h>
51+
52+
#undef errno
53+
54+
int *cpp2rust_errno(void);
55+
56+
#define errno (*cpp2rust_errno())
57+
```
58+
59+
The redefinition keeps `errno` an lvalue by dereferencing the returned pointer,
60+
so both reads and assignments like `errno = 0` still parse; what the matcher
61+
sees in either case is a call to `int *cpp2rust_errno()`.
62+
63+
Because the input program and the rule sources are compiled with the same shim
64+
headers, both sides canonicalize to the same signature, and an ordinary
65+
[expression rule](./writing-rules.md) matches it:
66+
67+
```c
68+
// rules/errno/src.c
69+
#include <errno.h>
70+
71+
int *f1(void) { return cpp2rust_errno(); }
72+
```
73+
74+
The shim functions are declared but never defined on the C side. They only exist
75+
so that the callee resolves; translation replaces the call with the rule body,
76+
so no C implementation is ever linked. Whatever the shim is supposed to _do_ is
77+
supplied by the Rust targets:
78+
79+
```rust
80+
// rules/errno/tgt_unsafe.rs
81+
unsafe fn f1() -> *mut i32 {
82+
libcc2rs::cpp2rust_errno_unsafe()
83+
}
84+
```
85+
86+
```rust
87+
// rules/errno/tgt_refcount.rs
88+
fn f1() -> Ptr<i32> {
89+
libcc2rs::cpp2rust_errno()
90+
}
91+
```
92+
93+
In the unsafe model `libcc2rs::cpp2rust_errno_unsafe` wraps the real platform
94+
errno location (`__errno_location` on Linux, `__error` on macOS). The refcount
95+
model instead _virtualizes_ errno as a thread-local `Value<i32>` inside
96+
`libcc2rs`; this is the same cell that other refcount rules write when they
97+
translate a failing libc call into
98+
`libcc2rs::cpp2rust_errno().write(__e as i32)`.
99+
100+
A rule pattern may spell either the macro or the shim directly; the two are
101+
identical after expansion. `rules/errno` and `rules/assert` call the shim by
102+
name, while `rules/arpa_inet` and `rules/select` write the macro form:
103+
104+
```c
105+
// rules/select/src.cpp
106+
void f2(int fd, fd_set *set) { return FD_SET(fd, set); }
107+
```
108+
109+
## The current shims
110+
111+
| Header | Macros | Shim functions | Rules |
112+
| -------------- | ----------------------------------------- | ------------------------------- | --------------------------------------------------------------------------------------- |
113+
| `assert.h` | `assert` | `cpp2rust_assert_fail(bool)` | `rules/assert` maps it to `assert!(a0)` |
114+
| `errno.h` | `errno` | `cpp2rust_errno()` | `rules/errno`, see above |
115+
| `arpa/inet.h` | `ntohl`, `ntohs`, `htonl`, `htons` | `cpp2rust_ntohl(x)`, ... | `rules/arpa_inet` maps them to `u32::from_be`, `u16::to_be`, ... |
116+
| `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) |
117+
118+
Note how the shim also normalizes the _shape_ of the API. C's `assert` is a
119+
macro precisely so it can stringify its condition and capture file and line; the
120+
shim reduces it to a plain `void(bool)` function, and the Rust side regains the
121+
diagnostics by mapping it to the `assert!` macro.
122+
123+
## Adding a new shim
124+
125+
To make another macro-based API matchable:
126+
127+
1. Create the header in `cpp2rust/compat/` at the same relative path as the
128+
platform header that defines the macro.
129+
2. Follow the pattern above: `#include_next` the real header, `#undef` the
130+
macro, declare a `cpp2rust_<name>` function with the macro's effective
131+
signature, and redefine the macro to call it.
132+
3. Write rules for the shim in a `rules/` module as for any other function,
133+
including the corresponding header in `src.c`/`src.cpp`.
134+
4. If a model needs runtime support (as refcount errno does), implement it in
135+
`libcc2rs` and call it from the rule target.
136+
137+
Keep the shim's signature platform-independent; the whole point is that both
138+
sides of every rule see one canonical declaration on every platform.
139+
140+
## Related normalization
141+
142+
The same shared flag list also passes `-D_FORTIFY_SOURCE=0`, which keeps glibc
143+
from substituting fortified variants (`__printf_chk` and friends) for standard
144+
calls. Like the shims, this ensures that calls in the input program resolve to
145+
the standard declarations the rules are written against.

‎docs/src/rules/conventions.md‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Conventions
2+
3+
Most of these conventions are enforced by the preprocessors, and violating them
4+
fails the build; the notes below call out the ones that are not checked.
5+
6+
## Naming
7+
8+
| Element | C++ side | Rust side |
9+
| --------------------- | --------------------------------------------- | --------------------------------------------------- |
10+
| Expression rule | `f1`, `f2`, ... | same name |
11+
| Type rule | `t1`, `t2`, ... via `using`/`typedef` | `fn tN() -> RustType` with no arguments |
12+
| Parameters | free-form (`o`, `it`, `key`, `dst`, `n`, ...) | must be `a0`, `a1`, ... consecutive from 0 |
13+
| Generics | `T1`, `T2`, ... (type and non-type params) | `T1`, `T2`, ... consecutive from 1 |
14+
| Variadic pack | `typename... Args` | trailing `va: &[VaArg]` |
15+
| Locals in Rust bodies | | double-underscore prefix: `__v`, `__fd`, `__e`, ... |
16+
17+
Notes:
18+
19+
- Rule numbering is per module, and gaps are currently allowed (e.g. `rules/map`
20+
has no `f4`), though this might change in the future. Names must be unique
21+
across `src.c` and `src.cpp` combined.
22+
- On the C++ side parameter names are free, but the _order_ defines the
23+
placeholder indices: the first parameter is `a0` on the Rust side, the second
24+
is `a1`, and so on. The receiver of a method rule is always the first
25+
parameter, hence `a0`.
26+
- Generic parameters are matched positionally between the two sides, so `T1` in
27+
the Rust target means "whatever bound to `T1` in the C++ pattern".
28+
- Locals introduced inside Rust rule bodies use a `__` prefix. This is not
29+
checked by the build, but it is needed: rule bodies are spliced inline into
30+
the generated code, so an unprefixed local could collide with a variable name
31+
from the translated program.
32+
33+
## Function qualifiers
34+
35+
- In `tgt_unsafe.rs`, expression rules are `unsafe fn`; type rules (`tN`) are
36+
plain `fn`.
37+
- In `tgt_refcount.rs`, all rules are safe `fn`. The refcount model produces
38+
fully safe Rust, so a refcount rule body must not need `unsafe`.
39+
40+
The build does not check the qualifiers themselves; only rustc's usual rules
41+
apply when the `rules` crate compiles. In particular, nothing stops an `unsafe`
42+
block inside a refcount rule body from being spliced into the output, so keeping
43+
refcount rules safe is what upholds the model's safety guarantee.
44+
45+
## C++ pattern shape
46+
47+
- An `fN` body must be exactly one `return` statement. The preprocessor rejects
48+
anything else.
49+
- `return` statements are not allowed inside Rust rule bodies; write the result
50+
as a tail expression instead.
51+
- Exercise exactly one construct per rule. If an API has several overloads,
52+
write one rule per overload (including separate rules for `const T &` versus
53+
`T &&` parameters).
54+
55+
## Argument accesses
56+
57+
Every use of an `aN` parameter in a rule body is classified as a read, write, or
58+
move by the [rule preprocessor](./preprocessors.md#rule-preprocessor). Passing
59+
an argument by value counts as a read, not a move; the only way to record a move
60+
is `std::mem::take(&mut aN)`.
61+
62+
## Type checking
63+
64+
All `tgt_*.rs` files are compiled as part of the `rules` crate, so a rule body
65+
that does not type-check against `libcc2rs`, `libc`, `nix`, etc. breaks the
66+
build. If a rule needs a new crate dependency, add it to `rules/Cargo.toml` and
67+
to the hardcoded crate list in `rule-preprocessor/src/semantic.rs` (see
68+
[The Rule Preprocessors](./preprocessors.md#rule-preprocessor)).

0 commit comments

Comments
 (0)