Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d1f9754
Add initial draft for rules docs
lucic71 Aug 13, 2026
62cd740
Add section about compat layer
lucic71 Aug 13, 2026
5147aa9
Add more details about rule preprocessors
lucic71 Aug 13, 2026
9ecced6
Add more info about rule IR
lucic71 Aug 13, 2026
4475b4a
Add more conventions
lucic71 Aug 13, 2026
10be55e
Add more info in rule rewriting
lucic71 Aug 13, 2026
940915c
Add more info in writing rules
lucic71 Aug 13, 2026
edc2038
Add The Matching Engine section
lucic71 Aug 13, 2026
63ff5a7
Final edits
lucic71 Aug 14, 2026
415138e
Delete test file
lucic71 Aug 14, 2026
b195a25
Format
lucic71 Aug 14, 2026
b21118a
Add plugins TODO in separate file
lucic71 Aug 14, 2026
2a51340
Add markdown formatting check
lucic71 Aug 14, 2026
8f9ed3c
Add libcc2rs compat, io, and libc-shims
lucic71 Aug 14, 2026
c87b654
Move prettierrc in docs
lucic71 Aug 14, 2026
89276e7
Draft rc and reinterpret sub-sections
lucic71 Aug 16, 2026
71791b7
Restructure libcc2rs
lucic71 Aug 16, 2026
0a9863b
Add more libcc2rs sections
lucic71 Aug 16, 2026
ca5ddad
Add increment and decrement section
lucic71 Aug 16, 2026
8591e71
Add iterators section
lucic71 Aug 16, 2026
e9ffa71
Add function pointer section
lucic71 Aug 16, 2026
9d8bf18
Add variadic args section
lucic71 Aug 16, 2026
41dcffd
Add goto and switch macros
lucic71 Aug 16, 2026
0e2ee1b
Delete unused impl for Cursor
lucic71 Aug 16, 2026
fb23d47
Add missing info
lucic71 Aug 16, 2026
f9cff9e
Run prettier
lucic71 Aug 16, 2026
10ddf78
Move CStringIterator in iterator.rs
lucic71 Aug 16, 2026
68a5633
Run prettier
lucic71 Aug 16, 2026
8546074
Fix warning callouts
lucic71 Aug 17, 2026
df0bebf
Final reviews
lucic71 Aug 17, 2026
5cfdcea
Merge branch 'master' into libcc2rs-docs
lucic71 Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,28 @@
- [The Matching Engine](./rules/matching.md)
- [Rule Rewriting](./rules/rewriting.md)

# The Runtime Library

- [Overview](./runtime/overview.md)
- [Reference Counting](./runtime/rc.md)
- [C Strings](./runtime/cstr.md)
- [void Pointers](./runtime/void.md)
- [Virtual Classes](./runtime/ptr-dyn.md)
- [Type Reinterpretation](./runtime/reinterpret.md)
- [Increment and Decrement](./runtime/inc-dec.md)
- [Iterators](./runtime/iterators.md)
- [Function Pointers](./runtime/fn-ptr.md)
- [Variadic Functions](./runtime/va-args.md)
- [Control Flow Macros](./runtime/control-flow.md)
- [I/O and Formatting](./runtime/io.md)
- [libc Shims](./runtime/libc-shims.md)
- [Compat Helpers](./runtime/compat.md)

# Code Generation

- [Overview](./codegen/overview.md)
- [Pointers and References](./codegen/pointers.md)
- [Unions](./codegen/unions.md)
- [Global Variables](./codegen/globals.md)
- [Temporary Materialization](./codegen/temporaries.md)
- [Translation Plugins](./codegen/plugins.md)
8 changes: 8 additions & 0 deletions docs/src/codegen/globals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Global Variables

Global variables are mapped to thread-local storage, because a `Value<T>` cannot
be a true Rust global. A global must be `Sync`, since every thread can reach it,
and both `Rc` and `RefCell` are single-threaded types: the reference counter and
the borrow checks are not atomic. Thread-local storage sidesteps the requirement
by giving each thread its own copy, which matches the original semantics because
`cpp2rust` does not currently support multi-threaded code.
5 changes: 5 additions & 0 deletions docs/src/codegen/unions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Unions

> TODO: explain how the refcount model translates a C union as a `__bytes`
> buffer with one accessor per member, each returning a `reinterpret_cast` view
> over that buffer.
10 changes: 5 additions & 5 deletions docs/src/project/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ 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.
The generated code relies on a [runtime library](../runtime/overview.md)
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.
9 changes: 6 additions & 3 deletions docs/src/rules/compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,12 @@ fn f1() -> Ptr<i32> {
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)`.
`libcc2rs`. Nothing else writes that cell, so it is a discipline of the rules:
every refcount rule that translates a call that can fail must write the error
code into it on the failure path, as
`libcc2rs::cpp2rust_errno().write(__e as i32)` in the
[`stat` rule](./writing-rules.md); a rule that skips the write breaks programs
that check `errno`.

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
Expand Down
4 changes: 3 additions & 1 deletion docs/src/rules/preprocessors.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ The environment is load-bearing:
crate list is hardcoded, so a new dependency in `rules/Cargo.toml` also needs
an entry in `rule-preprocessor/src/semantic.rs`.

> [!WARNING] Stale rlibs from an earlier build can be picked up silently. Run
> [!WARNING]
>
> Stale rlibs from an earlier build can be picked up silently. Run
> `ninja clean` to fix this.

- The sysroot comes from running `rustc --print=sysroot`, so the `rustc` on
Expand Down
33 changes: 33 additions & 0 deletions docs/src/runtime/compat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Compat Helpers

Some C interfaces are macros or platform-specific symbols rather than plain
functions. On the source side, `cpp2rust` rewrites them into ordinary calls (see
[Compat Shims](../rules/compat.md)); the `compat` module is the runtime side of
that rewrite.

`errno` expands to a platform-specific function call (`__errno_location` on
Linux, `__error` on macOS).

In the unsafe model, `cpp2rust_errno_unsafe` binds both platform symbols under
one name and returns the real libc `errno` location:

```rust
pub unsafe fn cpp2rust_errno_unsafe() -> *mut i32;
```

In the refcount model, `errno` is a thread-local refcounted `i32` that the
runtime maintains itself:

```rust
pub fn cpp2rust_errno() -> Ptr<i32>;
```

Refcount code reaches the operating system through the libc shims and nix, so
libc's `errno` is never read by this model. Keeping the cell current is a
discipline of the rules: every rule that translates a call that can fail must
write the error code into `cpp2rust_errno()` on the failure path (see
[Compat Shims](../rules/compat.md)); nothing enforces this, and a rule that
skips the write breaks programs that check `errno`.

`malloc_usable_size` is bound under one name for both platforms (the symbol is
`malloc_size` on macOS).
188 changes: 188 additions & 0 deletions docs/src/runtime/control-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# Control Flow Macros

Rust has no `goto`, and a `match` arm never falls into the next one. The
`libcc2rs-macros` crate provides two procedural macros, re-exported by
`libcc2rs`, that express these C constructs as a state machine. Both models use
them.

## goto_block

`goto_block!` takes a sequence of labeled blocks. Execution starts in the first
block and falls through from each block into the next; `goto!('label)` jumps to
the block with that label, forwards or backwards:

```c
int retry(int n) {
int count = 0;
int acc = 0;
again:
count += 1;
acc += n;
if (count < 3)
goto again;
return acc;
}
```

```rust
pub fn retry_0(n: i32) -> i32 {
let n: Value<i32> = Rc::new(RefCell::new(n));
let count: Value<i32> = <Value<i32>>::default();
let acc: Value<i32> = <Value<i32>>::default();
goto_block!({
'entry: {
*count.borrow_mut() = 0;
*acc.borrow_mut() = 0;
}
'again: {
(*count.borrow_mut()) += 1;
(*acc.borrow_mut()) += (*n.borrow());
if *count.borrow() < 3 {
goto!('again);
}
return (*acc.borrow());
}
});
panic!("ub: non-void function does not return a value")
}
```

The code generator puts the statements that precede the first C label in an
`'entry` block. The `panic!` after the block is there for the Rust compiler: the
function returns from inside the state machine, but the compiler cannot see that
every path does, so without a final diverging statement it rejects the function
for not returning a value.

The macro expands to a `loop` over a `match` on a state variable, one arm per
block. Each arm ends by setting the next state and continuing the loop, and
`goto!('label)` sets the target state instead. In outline, the block above
becomes:

```rust
let mut state: u32 = 0;
'sm: loop {
match state {
0 => {
/* entry body */
state = 1;
continue 'sm;
}
1 => {
/* again body, with goto!('again) as */
{
state = 1;
continue 'sm;
}
break 'sm;
}
_ => break 'sm,
}
}
```

`break` and `continue` written inside a block (outside any loop nested in it)
still refer to the loop enclosing the `goto_block!`: the macro records them in a
flag, leaves the state machine loop, and re-issues them after it. `goto!`
outside a `goto_block!` is a compile error.

Supported `goto` patterns:

- labels at the top level of a block: a function body, a loop body, or a
compound statement;
- a `goto` anywhere inside that block, including in nested `if`s, loops, and
`switch` cases;
- forward and backward jumps.

Not supported yet:

- a jump to a label that is not at the top level of a block enclosing the
`goto`, such as from outside a loop to a label in its body;
- a jump to a label inside an `if` branch.

## switch

A `switch` without fallthrough is translated as a plain `match` inside a labeled
block, where `break` becomes a `break` out of that block. When some case falls
into the next, the code generator uses `switch!` instead. It is written like a
`match`, but an arm whose body does not end in `break` continues into the body
of the following arm, as C does:

```c
switch (x) {
case 1:
r += 10;
case 2:
r += 20;
break;
default:
r = -1;
break;
}
```

```rust
switch!(match (*x.borrow()) {
v if v == 1 => {
(*r.borrow_mut()) += 10;
}
v if v == 2 => {
(*r.borrow_mut()) += 20;
break;
}
_ => {
(*r.borrow_mut()) = -1;
break;
}
});
```

`switch!` desugars to a `goto_block!` whose first block dispatches on the
condition to the block of the matching case; the case bodies follow as
consecutive blocks, so falling off the end of one enters the next, and `break`
leaves the whole `switch!`. A `continue` in a case is not captured by the
`switch!`: as in C, it continues the loop enclosing the `switch`, and is a
compile error when there is none. `goto` and `switch` mix freely: a `switch!`
can be nested in a `goto_block!`, a `goto!` inside a case can target a label of
the enclosing block, and a label attached to a `case` is supported. Statements
between the `switch` and its first `case` are not supported yet.

## Hoisted declarations

In C a variable declared in one case is visible in the cases after it, because
they all belong to the same block. Each `switch!` arm is a separate Rust block,
so the code generator hoists such declarations above the macro and leaves an
assignment in the case:

```c
switch (x) {
case 1:
r = 1;
int y;
y = 10;
r += y;
case 2:
y = 20;
r = y;
break;
}
```

```rust
let y: Value<i32> = <Value<i32>>::default();
switch!(match (*x.borrow()) {
v if v == 1 => {
(*r.borrow_mut()) = 1;
*y.borrow_mut() = 10;
(*r.borrow_mut()) += *y.borrow();
}
v if v == 2 => {
*y.borrow_mut() = 20;
(*r.borrow_mut()) = *y.borrow();
break;
}
_ => {}
});
```

The same hoisting applies to variables used across the labeled blocks of a
`goto_block!`, as `count` and `acc` above show.
18 changes: 18 additions & 0 deletions docs/src/runtime/cstr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# C Strings

C and C++ strings are byte strings: programs manipulate individual bytes and the
contents need not be valid UTF-8, so strings are translated as `u8` buffers
rather than Rust `String` values. A string literal becomes a per-thread interned
buffer with a trailing zero byte, handed out as a `Ptr<u8>` by
`Ptr::from_string_literal`. `Ptr<u8>` also carries the memory functions C
strings rely on: `memcpy` (with `memmove` semantics for overlapping buffers
instead of undefined behavior), `memset`, `memcmp`, and `to_rust_string` for
crossing into Rust APIs.

There are two ways to hand C bytes to Rust code. `CStringIterator`, returned by
`to_c_string_iterator`, walks the bytes of a `Ptr<u8>` up to the null
terminator; the `string.h` rules are built on it, `to_rust_string` collects it
into a `String`, and `Display` for `Ptr<u8>` prints it, so a C string can be
formatted directly. `with_slice` and `with_slice_mut` instead expose a bounded
byte range of the buffer as a Rust slice for the duration of a closure, which is
how a C buffer is passed to Rust and nix functions such as `read` and `write`.
Loading
Loading