From d1f975415e472450dc38c2d8d4e19bd04a3692c8 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Thu, 13 Aug 2026 16:49:10 +0100 Subject: [PATCH 01/30] Add initial draft for rules docs --- docs/src/SUMMARY.md | 9 ++ docs/src/codegen/pointers.md | 5 + docs/src/codegen/temporaries.md | 5 + docs/src/rules/conventions.md | 62 ++++++++ docs/src/rules/format.md | 154 ++++++++++++++++++++ docs/src/rules/ir.md | 116 +++++++++++++++ docs/src/rules/loading.md | 93 ++++++++++++ docs/src/rules/math-test.md | 48 +++++++ docs/src/rules/overview.md | 45 +++++- docs/src/rules/preprocessors.md | 88 ++++++++++++ docs/src/rules/rewriting.md | 85 +++++++++++ docs/src/rules/writing-rules.md | 241 ++++++++++++++++++++++++++++++++ 12 files changed, 948 insertions(+), 3 deletions(-) create mode 100644 docs/src/codegen/pointers.md create mode 100644 docs/src/codegen/temporaries.md create mode 100644 docs/src/rules/conventions.md create mode 100644 docs/src/rules/format.md create mode 100644 docs/src/rules/ir.md create mode 100644 docs/src/rules/loading.md create mode 100644 docs/src/rules/math-test.md create mode 100644 docs/src/rules/preprocessors.md create mode 100644 docs/src/rules/rewriting.md create mode 100644 docs/src/rules/writing-rules.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 101b03d7..d20cd664 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -10,7 +10,16 @@ # Translation Rules * [Overview](./rules/overview.md) +* [Rule Format](./rules/format.md) +* [Writing Rules](./rules/writing-rules.md) +* [Conventions](./rules/conventions.md) +* [The Rule Preprocessors](./rules/preprocessors.md) +* [The Rules IR](./rules/ir.md) +* [Loading and Matching](./rules/loading.md) +* [Rule Rewriting](./rules/rewriting.md) # Code Generation * [Overview](./codegen/overview.md) +* [Pointers and References](./codegen/pointers.md) +* [Temporary Materialization](./codegen/temporaries.md) diff --git a/docs/src/codegen/pointers.md b/docs/src/codegen/pointers.md new file mode 100644 index 00000000..6020022f --- /dev/null +++ b/docs/src/codegen/pointers.md @@ -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`, and the role of +> `StrongPtr` for reading through a pointer. diff --git a/docs/src/codegen/temporaries.md b/docs/src/codegen/temporaries.md new file mode 100644 index 00000000..a4a86303 --- /dev/null +++ b/docs/src/codegen/temporaries.md @@ -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). diff --git a/docs/src/rules/conventions.md b/docs/src/rules/conventions.md new file mode 100644 index 00000000..2d1b70f4 --- /dev/null +++ b/docs/src/rules/conventions.md @@ -0,0 +1,62 @@ +# 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). + +## 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 teach `rule-preprocessor` to locate its rlib. diff --git a/docs/src/rules/format.md b/docs/src/rules/format.md new file mode 100644 index 00000000..8a858a26 --- /dev/null +++ b/docs/src/rules/format.md @@ -0,0 +1,154 @@ +# Rule Format + +A rule module is a directory under `rules/`, usually named after the header or +library it covers (`rules/unistd`, `rules/vector`, `rules/string`, ...). It +contains: + +* `src.cpp` and/or `src.c`: the C++ (or C) side of each rule. +* `tgt_unsafe.rs`: the Rust targets for the unsafe model. +* `tgt_refcount.rs`: the Rust targets for the reference counting model + (optional, see below). + +A rule is a pair of same-named functions on the two sides. Names determine the +rule kind: + +* `f1`, `f2`, ... are *expression rules*: they map a C++ call, member access, + constructor, or constant to a Rust expression. +* `t1`, `t2`, ... are *type rules*: they map a C++ type to a Rust type. + +## Expression rules + +On the C++ side, an `fN` function must have a body that is exactly one +`return` statement. The returned expression is the pattern: the preprocessor +resolves the *callee* of that expression (the function, method, constructor, +enum constant, or macro being used) and that becomes the rule's matching key. +The function parameters stand for the arguments at the call site. + +```cpp +// rules/unistd/src.cpp +int f4(const char *pathname) { return unlink(pathname); } +``` + +On the Rust side, the same-named function gives the replacement expression. +Parameters must be named `a0`, `a1`, ... and correspond positionally to the +C++ parameters: + +```rust +// rules/unistd/tgt_unsafe.rs +unsafe fn f4(a0: *const libc::c_char) -> i32 { + libc::unlink(a0) +} +``` + +```rust +// rules/unistd/tgt_refcount.rs +fn f4(a0: Ptr) -> i32 { + match nix::unistd::unlink(a0.to_rust_string().as_str()) { + Ok(()) => 0, + Err(__e) => { + libcc2rs::cpp2rust_errno().write(__e as i32); + -1 + } + } +} +``` + +When the converter encounters `unlink(x)` in the input, it emits the rule +body with the translated `x` substituted for `a0`. + +## Type rules + +On the C++ side, a `tN` rule is a type alias (`using` or `typedef`). On the +Rust side, it is a *zero-argument function* whose return type is the mapped +Rust type and whose body is the default initializer for that type: + +```cpp +// rules/vector/src.cpp +template using t1 = std::vector; +``` + +```rust +// rules/vector/tgt_unsafe.rs +fn t1() -> Vec { + Default::default() +} +``` + +## Model layering + +The loader always reads `ir_unsafe.json` first. When translating with the +reference counting model, it then overlays `ir_refcount.json` on top: entries +with the same rule name replace the unsafe ones. + +This means `tgt_refcount.rs` only needs to contain the rules that *differ* +from the unsafe model. For example, the `__builtin_mul_overflow` rule in +`rules/builtin` has a pointer out-parameter (`a2` below), so the two models +translate it differently: in the unsafe model `a2` is a raw `*mut i64` +written through a deref, while in the refcount model it is a `Ptr` +written through `Ptr::write`. The other two arguments are identical in both +models: + +```rust +// rules/builtin/tgt_unsafe.rs +unsafe fn f9(a0: i64, a1: i64, a2: *mut i64) -> bool { + let (val, ovf) = a0.overflowing_mul(a1); + *a2 = val; + ovf +} +``` + +```rust +// rules/builtin/tgt_refcount.rs +fn f9(a0: i64, a1: i64, a2: Ptr) -> bool { + let (val, ovf) = a0.overflowing_mul(a1); + a2.write(val); + ovf +} +``` + +The module's other rules (byte swaps, `__builtin_expect`, ...) translate +identically in both models, so they appear only in `tgt_unsafe.rs` and the +refcount model inherits them. A module where no rule needs a refcount-specific +translation can omit `tgt_refcount.rs` entirely. + +## C and C++ sources + +A module may have both `src.c` and `src.cpp`; both are preprocessed and merged +into one `ir_src.json`. Defining the same rule name in both files is a hard +error, so numbering must not collide. + +This split is necessary because rules match on the exact canonical signature +of the callee, and some libc functions have *different signatures in C and +C++*. For example, C has a single +`char *strchr(const char *, int)`, while C++ replaces it with const-correct +overloads such as `const char *strchr(const char *, int)`. Since the +signatures differ, `rules/cstring` defines one rule per language: + +```cpp +// rules/cstring/src.cpp +const char *f6(const char *a0, int a1) { return strchr(a0, a1); } +``` + +```c +// rules/cstring/src.c +char *f5(const char *a0, int a1) { return (strchr)(a0, a1); } +``` + +The C++ rule matches `strchr` calls in code translated as C++, the C rule +matches them in code translated as C. + +`rules/builtin` uses this to cover both languages: `src.cpp` defines +`f9`/`f10` for the C++ `__builtin_mul_overflow` (returning `bool`) while +`src.c` defines `f12`/`f13` for the C version (returning `int`); their Rust +bodies are identical. + +## The `rules` crate + +The whole `rules/` tree is a single Rust crate. `rules/build.rs` walks the +tree, collects every `tgt_*.rs`, and generates `rules/src/modules.rs` with one +`#[path = ...]` module per file. Building the crate therefore type-checks +every rule body against the crates the rule targets call into, which are +declared as dependencies in `rules/Cargo.toml` (`libcc2rs`, `libc`, `nix`, +...). The +Rust rule preprocessor compiles exactly this crate to resolve types in rule +bodies. `rules/src/` is the only subdirectory that is not a rule module. diff --git a/docs/src/rules/ir.md b/docs/src/rules/ir.md new file mode 100644 index 00000000..29a6933b --- /dev/null +++ b/docs/src/rules/ir.md @@ -0,0 +1,116 @@ +# The Rules IR + +Each rule module compiles to up to three JSON files in +`/rules//`: + +* `ir_src.json`: the C++ side, from + [`cpp-rule-preprocessor`](./preprocessors.md#cpp-rule-preprocessor). +* `ir_unsafe.json`: the Rust side for the unsafe model, from + [`rule-preprocessor`](./preprocessors.md#rule-preprocessor). +* `ir_refcount.json`: the Rust side for the refcount model, also from + `rule-preprocessor` (only if the module has a `tgt_refcount.rs`). + +All three are objects keyed by rule name (`f1`, `t1`, ...), and the loader +joins them by name. + +## Source IR (`ir_src.json`) + +A flat map from rule name to the canonical signature of the C++ construct the +rule matches. For `rules/vector`: + +```json +{ + "t1": "std::vector", + "f3": "_Bool std::vector::empty() const" +} +``` + +This signature string is the lookup key for the whole rule: the converter +prints C++ constructs from the input AST with the same printer and compares +the strings. + +## Target IR (`ir_unsafe.json` / `ir_refcount.json`) + +An expression rule serializes as an `ExprRule` object: the rule's signature +plus its body as a list of *fragments*. For +`unsafe fn f6(a0: &mut Vec) -> *mut T1 { a0.as_mut_ptr() }`: + +```json +"f6": { + "body": [ + { "method_call": { + "receiver": [ { "placeholder": { "arg": 0, "access": "read" } } ], + "body": [ { "text": ".as_mut_ptr()" } ] } } + ], + "generics": { "T1": [] }, + "params": { "a0": { "type": "&mut Vec" } }, + "return_type": { "type": "*mut T1", "is_unsafe_pointer": true } +} +``` + +The fragment kinds are: + +* `text`: literal Rust source, emitted verbatim. +* `placeholder`: a use of one of the rule's `aN` parameters in the body (not + an argument of whatever the body calls); the converter substitutes the + translated call-site argument here. Its fields: + * `arg`: the parameter index N. + * `access`: how the body uses the argument: `read`, `write`, or `move`. + * `is_index_base`: the placeholder is the base of an index expression. +* `generic`: a `TN` slot, replaced with the instantiated Rust type. +* `method_call`: a method call split into `receiver` and `body` fragment + lists, so the code generator can rewrite the pair (see + [Rule Rewriting](./rewriting.md)). +* `va_args`: the expansion point for a variadic tail. + +Every type in the IR (in `params`, `return_type`, and type rules) is a +`TypeInfo` object, the type text plus a set of flags: + +* `is_refcount_pointer`: the type is a `Ptr<...>`. +* `is_unsafe_pointer`: the type is a raw `*mut`/`*const` pointer. +* `derives` (type rules only): the standard traits the mapped type + implements (`Copy`, `Clone`, `Default`, ...). + +An `ExprRule` carries two flags of its own: + +* `multi_statement`: the body has more than one statement and must be + wrapped in a block to stay a single expression. +* `is_extern`: the rule is an extern passthrough declaration and has no + body. + +Fields that are false, empty, or unset are omitted from the JSON. + +A type rule serializes as a `TypeRule` object: its `TypeInfo` plus the +`init` initializer expression, merged into one object: + +```json +"t1": { "type": "Vec", "init": "Default::default()" } +``` + +## In-memory form + +`cpp2rust` mirrors this JSON in C++ structs of the same names, defined in +`cpp2rust/converter/translation_rule.h`. `TranslationRule::Load` reads one +module directory (the `ir_*.json` files described above) and produces two +maps keyed by rule name, one holding `ExprRule`s and one holding `TypeRule`s: + +* An `ExprRule` holds the body + [fragments](#target-ir-ir_unsafejson--ir_refcountjson), the parameter and + return `TypeInfo`s, and the two rule-level flags. The name-keyed JSON maps become + positional vectors: parameter `aN` is entry N of `params`, generic `TN` is + entry N-1 of `generics` (each entry being the bound list). Rules support + at most 9 generic parameters (`kMaxGenerics`). +* A `TypeRule` holds the mapped type's `TypeInfo` and the `initializer` + expression. The same struct also represents the built-in type mappings + (scalars, pointers, ...) that the loader registers directly in code, + without any JSON behind them: for example, `int` maps to `i32`, and + `int *` to `*mut i32` in the unsafe model or `Ptr` in the refcount + model. + +Both also carry `src`, the canonical C++ signature attached from +[`ir_src.json`](#source-ir-ir_srcjson); it is the key the rule is matched +by. + +How the loader finds the IR directory, overlays the refcount model on the +unsafe one, and indexes the loaded rules for matching is covered in +[Loading and Matching](./loading.md). diff --git a/docs/src/rules/loading.md b/docs/src/rules/loading.md new file mode 100644 index 00000000..83e0beee --- /dev/null +++ b/docs/src/rules/loading.md @@ -0,0 +1,93 @@ +# Loading and Matching + +## Finding the rules directory + +`cpp2rust` takes the IR directory via `--rules `. If the flag is omitted +it tries `./rules` and then `/../rules`, accepting the first +candidate that contains (recursively) a subdirectory with `ir_src.json` plus +`ir_unsafe.json` or `ir_refcount.json`. Since the build writes the IR to +`/rules` and the binary lands in `/bin`, the default resolution +picks up the generated IR without any flags. + +## Loading + +Rules are loaded once per process by `Mapper::LoadTranslationRules`: + +1. Built-in type mappings are registered first: scalars, `void *`, `size_t`, + and the per-model pointer forms (`*mut T` for unsafe, `Ptr` for + refcount). +2. Every subdirectory of the rules directory is loaded with + `TranslationRule::Load`, which reads `ir_unsafe.json`, overlays + `ir_refcount.json` when translating with the refcount model, and then + attaches the C++ signature from `ir_src.json` to each rule by name. + +Loading is strict: an `ir_src.json` entry with no matching target rule is a +fatal error (this is what catches mismatched `#if`/`#[cfg]` gating), every +generic declared by a rule must appear in its C++ signature, and two type +rules for the same C++ type are rejected. + +## Matching + +Loaded rules are indexed in two multimaps, one for expression rules and one +for type rules. The multimap key is only a coarse bucket for collecting +candidate rules; whether a candidate actually matches is decided by the +unification step described below. The bucket key is derived from the C++ +signature: + +* For expressions, the qualified function name with the return type, the + parameter list, and all template arguments stripped, so the rule for + `_Bool std::vector::empty() const` lands in the `std::vector::empty` + bucket. +* For types, the text up to the first `<`, so all `std::vector<...>` rules + land in the `std::vector` bucket. + +During translation, the converter prints the construct it encounters with the +same canonical printer used by `cpp-rule-preprocessor`, which is what makes +the two sides comparable: + +* Functions and methods print as + ` ()[ const][ &|&&]`. +* Enum constants and global variables print as their qualified name. +* Integer literals expanded from a macro print as the macro name. + +All rules in the matching bucket are then unified against this string with a +template matcher that binds `T1`...`T9` to the concrete types at the use +site. If several rules match, the one with the longest source signature wins, +so more specific rules take precedence. Type lookups first try the +sugar-preserving spelling (so a rule can match `size_t` as written) and retry +with the desugared type on failure. + +## Application + +When a rule matches, the converter walks its body fragments and emits: + +* `text` fragments verbatim, +* `placeholder` fragments as the translated call-site argument. How the + argument is emitted depends on the placeholder's access and on whether the + argument and the declared parameter type are pointers: + * Read access emits the argument as a plain value, with an implicit + numeric cast when the parameter type asks for one. + * Write access emits the argument as an lvalue. + * Move access wraps the argument in `std::mem::take(&mut ...)`; + temporaries are moved as-is. + * If the rule declares a pointer parameter but the argument is not a + pointer, the converter takes a fresh pointer to it, + [materializing a temporary](../codegen/temporaries.md) when the + argument has no address of its own. For example, + the refcount `std::max` rule declares `Ptr` parameters, since the + C++ side takes `const T1 &` and the refcount model + [translates references as `Ptr`](../codegen/pointers.md), so + `std::max(x1, x2)` on plain locals + substitutes `x1.as_pointer()` and `x2.as_pointer()` for the + placeholders, while `std::max(30, 40)` first materializes `__tmp_0` + and `__tmp_1` values for the literals and points into those. + * If the argument is a pointer but the rule expects a value, the + converter dereferences it. +* `generic` fragments as the Rust mapping of the bound C++ type, +* `va_args` fragments as the converted variadic tail, +* `method_call` fragments as receiver followed by body, possibly rewritten + (see [Rule Rewriting](./rewriting.md)). + +Multi-statement bodies are wrapped in `{ }` so they remain a single +expression. Rules for user-defined C++ types are injected through the same +mechanism at translation time. diff --git a/docs/src/rules/math-test.md b/docs/src/rules/math-test.md new file mode 100644 index 00000000..2fbc5079 --- /dev/null +++ b/docs/src/rules/math-test.md @@ -0,0 +1,48 @@ +# Formula Rendering Test + +This page is a rendering test for hand-translating the inference rules from +`latex/translating.tex` (Fig. 2, the $\mathcal{T}$ function) into the book +via the `mdbook-katex` preprocessor. Only a few representative rules are +shown. + +The macros from `latex/macros.tex` are ported once into +`docs/katex-macros.txt` and are available on every page, so the rule bodies +below stay nearly identical to the paper's source. + +## Axioms + +$$ +\begin{array}{cc} +\text{Int} & \text{String} \\[4pt] +\dfrac{}{\typcomp{\cint} \defeq \rint} +& +\dfrac{}{\typcomp{\cstring} \defeq \rstring} +\end{array} +$$ + +## Rule with premises + +$$ +\begin{array}{c}\text{Unique Pointer}\\[4pt] +\dfrac{ + \typcomp{\typ} = \rtyp + \qquad + \boxTy(\rtyp) = \rtyp' +}{\typcomp{\uniqueptr{\typ}} \defeq \roption{\rtyp'}} +\end{array} +$$ + +## Rule with premises on two lines + +$$ +\begin{array}{c}\text{Ptr - Non-Virtual Class}\\[4pt] +\dfrac + {\begin{array}{c} + \typcomp{\typ} = \rtyp + \qquad + \typ \text{ is not a virtual class} + \\ + \converttoptr(\rtyp) = \rtyp' + \end{array}}{\typcomp{\typ*} \defeq \ptr{\rtyp'}} +\end{array} +$$ diff --git a/docs/src/rules/overview.md b/docs/src/rules/overview.md index dbdacce6..ccf9172e 100644 --- a/docs/src/rules/overview.md +++ b/docs/src/rules/overview.md @@ -2,7 +2,46 @@ Translation rules describe how C++ library APIs are mapped to Rust. Each rule module lives in the `rules/` directory and pairs a C++ source file -(`src.cpp`) with its Rust translation for each model (`tgt_refcount.rs` and -`tgt_unsafe.rs`). +(`src.cpp`) with its Rust translation for each model (`tgt_unsafe.rs` and +`tgt_refcount.rs`). -This part of the book explains how rules work and how to write new ones. +The central idea is that every rule is expressed as ordinary, compilable +C++ and Rust source code, free of anything platform dependent. Both sides are +run through real compilers at build time, so a rule that does not compile +fails the build, and the platform-specific spellings a rule needs to match +against (for example `bool` canonicalizing to `_Bool`) are derived by the +compiler on the host rather than written by hand. The same rule sources work +on every platform `cpp2rust` builds on. + +Rules go through a build-time compilation pipeline before `cpp2rust` can use +them: + +1. You author a rule module: C++ patterns in `src.cpp` and Rust targets in + `tgt_unsafe.rs` / `tgt_refcount.rs`. +2. At build time, two preprocessors compile the module into JSON IR under + `/rules//`: + `cpp-rule-preprocessor` compiles the C++ side into `ir_src.json`, and + `rule-preprocessor` compiles the Rust side into `ir_unsafe.json` and + `ir_refcount.json`. +3. At startup, `cpp2rust` loads the IR files and indexes the rules by the + canonical signature of the C++ construct they match. +4. During translation, the converter looks up rules by signature and splices + their Rust bodies into the output, substituting arguments for placeholders + and rewriting the body when needed (e.g. wrapping method calls in + `with_mut` when the receiver is a `Ptr`). + +The rest of this part covers each stage: + +* [Rule Format](./format.md): the files that make up a rule module and how + the two models are layered. +* [Writing Rules](./writing-rules.md): how to write rules for functions, + methods, operators, types, constants, and variadics. +* [Conventions](./conventions.md): naming and style conventions rule authors + must follow. +* [The Rule Preprocessors](./preprocessors.md): the two build-time tools that + compile rules to IR. +* [The Rules IR](./ir.md): the JSON format the preprocessors emit. +* [Loading and Matching](./loading.md): how `cpp2rust` loads the IR and + matches rules against the input AST. +* [Rule Rewriting](./rewriting.md): how rule bodies are adapted at + application time, in particular the `with_mut` rewrite. diff --git a/docs/src/rules/preprocessors.md b/docs/src/rules/preprocessors.md new file mode 100644 index 00000000..ab4c714c --- /dev/null +++ b/docs/src/rules/preprocessors.md @@ -0,0 +1,88 @@ +# The Rule Preprocessors + +Two build-time tools compile rule modules into the [JSON IR](./ir.md) that +`cpp2rust` loads at runtime. Both write into `/rules//`: + +* `cpp-rule-preprocessor` compiles `src.cpp`/`src.c` into `ir_src.json`. +* `rule-preprocessor` compiles `tgt_unsafe.rs`/`tgt_refcount.rs` into + `ir_unsafe.json`/`ir_refcount.json`. + +The C++ side is keyed by resolved callee signatures; the Rust side by rule +names. The two are joined by rule name when `cpp2rust` loads them. + +## cpp-rule-preprocessor + +A clang LibTooling executable (`cpp2rust/cpp_rule_preprocessor.cpp`) that runs +once per rule directory: + +```bash +cpp-rule-preprocessor --dir rules/string --out /rules/string/ir_src.json +``` + +Extra compiler flags can be passed with repeated `--cxxflags` options. CMake +invokes it for every rule module via the `preprocess-cpp-rules` target. + +For each rule it: + +1. Validates that every `fN` body is exactly one `return` statement. +2. Resolves the *callee* of the returned expression. For non-template rules + this is just the called declaration. For template rules the callee is + unresolved, so the tool instantiates the rule's template parameters with + synthesized dummy types and runs overload resolution to find the function + the rule refers to. +3. Prints the resolved declaration as a canonical signature string: + ` ()[ const][ volatile][ &|&&]`. + For `tN` aliases it prints the underlying type. + +The output is a flat JSON object mapping rule names to these signature +strings. + +Two details of the printer matter for matching. Typedefs that resolve to +builtin types are kept as written instead of being desugared: `size_t` prints +as `size_t`, not `unsigned long`, which is what lets it map to `usize` while +plain `unsigned long` maps to `u64`. And integer literals expanded from a +macro are recorded as the macro *name*, which is how +[constant rules](./writing-rules.md#enum-values-constants-and-macros) like the +`O_CREAT` one match by name. + +## rule-preprocessor + +A Rust binary crate built with the nightly toolchain because it links the +compiler's own libraries (`rustc_driver`, `rustc_middle`, ...). It processes +the whole rules tree in one invocation: + +```bash +CARGO_TARGET_DIR= cargo +nightly run --release \ + --manifest-path rule-preprocessor/Cargo.toml -- /rules [rules-dir] +``` + +CMake first builds the `rules` crate (which also regenerates +`rules/src/modules.rs`) and then runs the preprocessor via the +`preprocess-rust-rules` target. It works in two phases. + +**Phase 1, syntactic.** Each `tgt_*.rs` file is parsed with rust-analyzer's +parser, and functions whose `#[cfg]` does not match the host are dropped. +Every function body is then turned into a list of *fragments*, whose kinds +are described in +[The Rules IR](./ir.md#target-ir-ir_unsafejson--ir_refcountjson). The +fragmentation is mainly concerned with how the rule's arguments are used: +references to parameters and generics become placeholder and generic +fragments, while source text that does not involve an argument is kept +as-is. + +Each placeholder is tagged with an *access*: read, write, or move. Some uses +give the access away syntactically (`&mut a0` is a write); those that do not, +typically method-call receivers and arguments, are left as `unknown` for +phase 2. This phase also applies the two +[preprocessor-side rewrites](./rewriting.md#preprocessor-side-rewrites) that +support rule rewriting. + +**Phase 2, semantic.** The preprocessor compiles the `rules` crate in-process +with `rustc` and walks the typed HIR. This gives it the real signature of +every callee, which resolves the `unknown` accesses: passing to a +`&mut`/`*mut` parameter is a write, to a `&`/`*const` parameter a read, and +to `std::mem::take` a move. For type rules it also records which standard +traits (`Copy`, `Clone`, `Default`, ...) the mapped type implements. A +placeholder still `unknown` after this phase fails the build. + +The result is one `ir_.json` per input file, keyed by rule name. diff --git a/docs/src/rules/rewriting.md b/docs/src/rules/rewriting.md new file mode 100644 index 00000000..48bce900 --- /dev/null +++ b/docs/src/rules/rewriting.md @@ -0,0 +1,85 @@ +# Rule Rewriting + +A rule body is written against idiomatic Rust types: a rule that mutates a +vector declares its parameter as `&mut Vec`. But in the refcount model the +call-site argument is usually a `Ptr>`, and a `Ptr` +[cannot produce a long-lived `&mut`](../codegen/pointers.md). Instead of forcing every rule to handle pointers, the code +generator *rewrites* the rule body at application time. + +## The `with_mut` rewrite + +`libcc2rs` provides + +```rust +impl Ptr { + pub fn with_mut(&self, f: impl FnOnce(&mut T) -> R) -> R { ... } +} +``` + +which checks the pointer, borrows the pointee mutably, and runs the closure +on it (with an immutable sibling `Ptr::with`). The refcount converter uses it +to bridge the gap. The rewrite fires when all three hold: + +1. The rule body fragment is a method call whose receiver contains a + placeholder (the preprocessor splits every method call into receiver and + body fragments precisely to enable this). +2. The receiver placeholder's access is write or move, i.e. the method takes + `&mut self` or the rule mutates the parameter. Read access does not need + the rewrite, since a read can go through a + [`StrongPtr`](../codegen/pointers.md) obtained with `Ptr::upgrade`, or + through a `read()` copy. +3. The call-site argument is a pointer or a reference reached through a + pointer. + +The rule's method call `a0.method(...)` is then emitted as + +```rust +ptr.with_mut(|__v: | __v.method(...)) +``` + +For example, the `push_back` rule is written as an ordinary `&mut` method +call: + +```rust +fn f21(a0: &mut Vec, a1: T1) { ... a0.push(...) } +``` + +Given the C++ input `v.push_back(20);` where `v` is reached through a +`Ptr>`, the generated code is: + +```rust +v.with_mut(|__v: &mut Vec| __v.push(20)); +``` + +When the receiver is a plain local value rather than a pointer, condition 3 +fails and no closure is emitted; the same rule produces a direct call like +`(*v2.borrow_mut()).push(0);`. + +When the pointee is itself a boxed value (`Value`, i.e. +`Rc>`), the closure takes `&mut Value` and an extra borrow is +inserted: + +```rust +ptr.with_mut(|__v: &mut Value>| (*__v.borrow_mut()).push(20)) +``` + +## The read-access counterpart + +For read access the converter does not emit a closure. A pointer receiver +whose rule parameter is a value or `&` type is simply dereferenced +(`p.read()` or `(*p.upgrade().deref())`); conversely, if the rule declares a +`Ptr` parameter but the argument is not a pointer, the converter inserts an +`as_pointer()` cast or +[materializes a temporary](../codegen/temporaries.md). + +## Preprocessor-side rewrites + +Two rewrites in `rule-preprocessor` exist to make the `with_mut` rewrite +possible: + +* A `*` deref in front of a `&mut` parameter is dropped from the body, since + the substituted argument is already an lvalue or pointer expression. +* `std::mem::take(&mut aN)` collapses to a bare placeholder marked as a move, + so the converter can re-express the move against the actual argument (for a + pointer that becomes `std::mem::take(&mut )` on the borrowed + pointee). diff --git a/docs/src/rules/writing-rules.md b/docs/src/rules/writing-rules.md new file mode 100644 index 00000000..1d7300a2 --- /dev/null +++ b/docs/src/rules/writing-rules.md @@ -0,0 +1,241 @@ +# Writing Rules + +This page shows how to write rules for each kind of C++ construct. In every +case the recipe is the same: write an `fN` (or `tN`) function on the C++ side +whose single `return` statement exercises the construct, and a same-named +function on the Rust side giving the translation. + +## Free functions + +```cpp +// rules/stat/src.cpp +int f1(const char *pathname, struct stat *statbuf) { + return stat(pathname, statbuf); +} +``` + +```rust +// rules/stat/tgt_refcount.rs +fn f1(a0: Ptr, a1: Ptr) -> i32 { + match nix::sys::stat::stat(a0.to_rust_string().as_str()) { + Ok(__s) => { + a1.with_mut(|__st| *__st = Stat::from_libc(&__s)); + 0 + } + Err(__e) => { + libcc2rs::cpp2rust_errno().write(__e as i32); + -1 + } + } +} +``` + +Rule bodies may be arbitrarily complex; multi-statement bodies are wrapped in +a block when spliced into the output. + +`return` statements are prohibited in Rust rule bodies (the preprocessor +rejects them); produce the result as a tail expression instead. The body is +not emitted as a function of its own: it is spliced inline into the generated +code as a block expression, so a `return` would not end the rule, it would +return from whatever generated function the rule happens to be expanded in. + +## Methods + +There is no special syntax for member functions: write a free function that +takes the receiver as its first parameter and calls the method on it. On the +Rust side the receiver is `a0`. + +```cpp +// rules/vector/src.cpp +template std::size_t f2(const std::vector &o) { + return o.size(); +} +``` + +```rust +// rules/vector/tgt_unsafe.rs +unsafe fn f2(a0: Vec) -> usize { + a0.len() +} +``` + +Template rules use generic parameters named `T1`, `T2`, ... on both sides, +matched positionally. The rule is written against the open template +`std::vector`, with `T1` left as a placeholder, so a single rule covers +every instantiation: when the input program calls `size()` on, say, a +`std::vector`, the matcher binds `T1 = int`. + +## Constructors + +Constructors are functions returning the type by value, one rule per overload: + +```cpp +// rules/string/src.cpp +std::string f7(const char *s, std::size_t n) { return std::string(s, n); } +std::string f9(std::size_t n, char ch) { return std::string(n, ch); } +``` + +Overloads that differ in value category are distinct rules too: `rules/vector` +has separate rules for `push_back(const T1 &)` and `push_back(T1 &&)`. + +No destructor rules exist so far: the STL and libc APIs covered by the +current rules have not needed any, since their types map to Rust types whose +`Drop` implementations already do the right thing. + +## Operators + +Write operators with explicit `operator` call syntax, in member form +(`x.operator@(...)`) or free form (`operator@(a, b)`): + +```cpp +// rules/map/src.cpp +template +T2 &f1(std::map &o, const T1 &key) { return o.operator[](key); } + +template +bool f11(typename std::map::iterator a, + typename std::map::iterator b) { + return operator!=(a, b); +} +``` + +Post-increment is distinguished from pre-increment by the usual dummy `int` +parameter: `a0.operator++(a1)` versus `it.operator++()`. Member accesses +through iterators are also rules (e.g. `it->first`, `it->second`, `o.second` +in `rules/map` and `rules/pair`). + +## Types + +A type rule has two halves. On the C++ side, declare a type alias named `tN` +for the C++ type being mapped. On the Rust side, write a function with the +same name that takes no arguments: its *return type* is the Rust type that +the C++ type maps to, and its *body* is the default value the generated code +uses when it needs to construct one (e.g. for an uninitialized variable). +Reference and pointer variants of a type each get their own rule: + +```cpp +// rules/iostream/src.cpp +using t1 = std::ostream; +using t2 = std::ostream &; +using t3 = std::ostream *; +``` + +C structs use `typedef` instead of `using`: + +```cpp +// rules/stat/src.cpp +typedef struct stat t1; +``` + +```rust +// rules/stat/tgt_unsafe.rs +fn t1() -> ::libc::stat { unsafe { std::mem::zeroed() } } +``` + +```rust +// rules/stat/tgt_refcount.rs +fn t1() -> libcc2rs::Stat { Default::default() } +``` + +A type rule may map to the sentinel type `libcc2rs::IgnoreRule`, meaning +"this model has no special mapping for the type"; the converter then falls +back to its normal type conversion. This is useful when only one model needs +a custom mapping: `rules/carray` maps multi-dimensional C arrays to nested +boxed slices in the refcount model, while its `tgt_unsafe.rs` targets are +`IgnoreRule` so the unsafe model keeps the default array conversion. + +## Enum values, constants, and macros + +Constants are `fN` functions that take no arguments and return the constant, +one rule per value: + +```cpp +// rules/fcntl/src.cpp +int f3(void) { return O_CREAT; } +int f4(void) { return O_TRUNC; } +``` + +```rust +// rules/fcntl/tgt_unsafe.rs +unsafe fn f3() -> i32 { ::libc::O_CREAT } +``` + +For macros that expand to integer literals, the preprocessor records the +*macro name* rather than the value, so `O_CREAT` in the input matches this +rule by name. Enum constants and global variables (e.g. `std::cout`) are +matched by their qualified name. + +## Variadic functions + +The C++ side uses a template parameter pack rather than a C-style `...` +parameter, out of necessity: a function that takes `...` cannot forward its +variadic arguments to another call, so a rule like + +```cpp +int f1(int a0, int a1, ...) { return fcntl(a0, a1, ...); } +``` + +is not +expressible. A parameter pack can be forwarded (`args...`), which is exactly +what the rule body needs to do. The Rust side takes a trailing parameter that +must be typed `&[VaArg]` and named `va`: + +```cpp +// rules/fcntl/src.cpp +template +int f1(int a0, int a1, Args... args) { + return fcntl(a0, a1, args...); +} +``` + +```rust +// rules/fcntl/tgt_refcount.rs +fn f1(a0: i32, a1: i32, va: &[VaArg]) -> i32 { ... } +``` + +## Passthrough rules + +When a call should be forwarded verbatim to the same-named function in Rust's +`libc` crate, the Rust target can be an `extern` declaration instead of a +body: + +```cpp +// rules/fcntl/src.cpp +template +int f1(int a0, int a1, Args... args) { + return fcntl(a0, a1, args...); +} +``` + +```rust +// rules/fcntl/tgt_unsafe.rs +unsafe extern "C" { + fn f1(a0: i32, a1: i32, ...) -> i32; +} +``` + +The converter then emits a direct `libc::fcntl(...)` call at the call site. + +## Platform-specific rules + +Gate the C++ side with the usual preprocessor conditionals and the Rust side +with `#[cfg(...)]`; the two must agree so that the rule name sets line up: + +```cpp +// rules/socket/src.c +#ifdef __linux__ +int f4(void) { return SOCK_CLOEXEC; } +#endif +``` + +```rust +// rules/socket/tgt_unsafe.rs +#[cfg(target_os = "linux")] +unsafe fn f4() -> i32 { + libc::SOCK_CLOEXEC +} +``` + +The Rust preprocessor evaluates `#[cfg]` attributes against the host target +(only `target_os = linux|macos` and `target_arch = x86_64|x86` are accepted) +and drops non-matching rules. From 62cd7401d7a2454556b65f21b7f51c5dcdc3b9b5 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Thu, 13 Aug 2026 17:33:22 +0100 Subject: [PATCH 02/30] Add section about compat layer --- docs/src/SUMMARY.md | 1 + docs/src/rules/compat.md | 146 ++++++++++++++++++++++++++++++++ docs/src/rules/overview.md | 2 + docs/src/rules/writing-rules.md | 5 ++ 4 files changed, 154 insertions(+) create mode 100644 docs/src/rules/compat.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index d20cd664..38511e53 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -12,6 +12,7 @@ * [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) diff --git a/docs/src/rules/compat.md b/docs/src/rules/compat.md new file mode 100644 index 00000000..e456b551 --- /dev/null +++ b/docs/src/rules/compat.md @@ -0,0 +1,146 @@ +# 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 ` 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 + +#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 + +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 { + 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` 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_` 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. diff --git a/docs/src/rules/overview.md b/docs/src/rules/overview.md index ccf9172e..d36aeebf 100644 --- a/docs/src/rules/overview.md +++ b/docs/src/rules/overview.md @@ -36,6 +36,8 @@ The rest of this part covers each stage: the two models are layered. * [Writing Rules](./writing-rules.md): how to write rules for functions, methods, operators, types, constants, and variadics. +* [Compat Shims](./compat.md): how macro-based libc APIs like `errno` and + `FD_SET` are rewritten into matchable function calls. * [Conventions](./conventions.md): naming and style conventions rule authors must follow. * [The Rule Preprocessors](./preprocessors.md): the two build-time tools that diff --git a/docs/src/rules/writing-rules.md b/docs/src/rules/writing-rules.md index 1d7300a2..dbc94078 100644 --- a/docs/src/rules/writing-rules.md +++ b/docs/src/rules/writing-rules.md @@ -165,6 +165,11 @@ For macros that expand to integer literals, the preprocessor records the rule by name. Enum constants and global variables (e.g. `std::cout`) are matched by their qualified name. +Integer-literal macros are the only macros matchable directly. Macros whose +expansions are platform internals with no stable callee, such as `errno` or +`FD_SET`, are first rewritten into calls to synthetic `cpp2rust_*` functions +by the [compat shims](./compat.md); rules then match the shim call. + ## Variadic functions The C++ side uses a template parameter pack rather than a C-style `...` From 5147aa95fce8522ac958d67ce1ae61238aba871b Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Thu, 13 Aug 2026 17:57:30 +0100 Subject: [PATCH 03/30] Add more details about rule preprocessors --- docs/src/rules/preprocessors.md | 75 +++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/docs/src/rules/preprocessors.md b/docs/src/rules/preprocessors.md index ab4c714c..ccd7ffc0 100644 --- a/docs/src/rules/preprocessors.md +++ b/docs/src/rules/preprocessors.md @@ -19,8 +19,17 @@ once per rule directory: cpp-rule-preprocessor --dir rules/string --out /rules/string/ir_src.json ``` -Extra compiler flags can be passed with repeated `--cxxflags` options. CMake -invokes it for every rule module via the `preprocess-cpp-rules` target. +Extra compiler flags can be passed with repeated `--cxxflags` options, +though CMake, which invokes the tool for every rule module via the +`preprocess-cpp-rules` target, passes none. Note that the parent directory +of `--out` must already exist; CMake creates it before each invocation, so +a manual run must do the same. + +Rule sources are always compiled with the fixed flag set from +`cpp2rust/compat/platform_flags.h`, the same one used to parse input +programs (see [Compat Shims](./compat.md)). There is no compilation +database and no `-std=` flag: the language is chosen by clang from the +file extension, and `src.c` is processed before `src.cpp`. For each rule it: @@ -31,17 +40,20 @@ For each rule it: synthesized dummy types and runs overload resolution to find the function the rule refers to. 3. Prints the resolved declaration as a canonical signature string: - ` ()[ const][ volatile][ &|&&]`. - For `tN` aliases it prints the underlying type. + ` ([, ...])[ const][ volatile][ &|&&]`, + where `, ...` appears for C-variadic functions and the trailing + qualifiers only for methods. For `tN` aliases it prints the underlying + type. The output is a flat JSON object mapping rule names to these signature strings. -Two details of the printer matter for matching. Typedefs that resolve to -builtin types are kept as written instead of being desugared: `size_t` prints -as `size_t`, not `unsigned long`, which is what lets it map to `usize` while -plain `unsigned long` maps to `u64`. And integer literals expanded from a -macro are recorded as the macro *name*, which is how +The printer preserves typedef sugar instead of desugaring it: `size_t` +prints as `size_t`, not `unsigned long`, which is what lets it map to +`usize` while plain `unsigned long` maps to `u64` (for `tN` aliases this +preservation is explicit; inside function signatures the spelling survives +through the printing policy). Integer literals expanded from a macro are +recorded as the macro *name*, which is how [constant rules](./writing-rules.md#enum-values-constants-and-macros) like the `O_CREAT` one match by name. @@ -56,9 +68,30 @@ CARGO_TARGET_DIR= cargo +nightly run --release \ --manifest-path rule-preprocessor/Cargo.toml -- /rules [rules-dir] ``` -CMake first builds the `rules` crate (which also regenerates -`rules/src/modules.rs`) and then runs the preprocessor via the -`preprocess-rust-rules` target. It works in two phases. +The environment is load-bearing: + +* `CARGO_TARGET_DIR` must be set (the tool aborts otherwise): the rlibs of + the rule dependencies (`libcc2rs`, `libc`, `nix`, ...) are looked up in + `$CARGO_TARGET_DIR//deps`, which the `cargo run` above + populates. The 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 `ninja + > clean` to fix this. +* The sysroot comes from running `rustc --print=sysroot`, so the `rustc` on + `PATH` must be the same nightly the preprocessor was built with (running + through `cargo +nightly run` guarantees this). +* `rules-dir` is optional and defaults to the relative path `../rules`, + resolved against the *current working directory* of the process. + +CMake drives all of this via the `preprocess-rust-rules` target: it first +builds the `rules` crate with the stable toolchain (which also regenerates +`rules/src/modules.rs`), then runs the preprocessor with +`CARGO_TARGET_DIR=/target_preprocessor`. That initial `cargo build` +of the `rules` crate is what actually gates the build on rule bodies +type-checking (see below). The preprocessor works in two phases. **Phase 1, syntactic.** Each `tgt_*.rs` file is parsed with rust-analyzer's parser, and functions whose `#[cfg]` does not match the host are dropped. @@ -81,8 +114,16 @@ support rule rewriting. with `rustc` and walks the typed HIR. This gives it the real signature of every callee, which resolves the `unknown` accesses: passing to a `&mut`/`*mut` parameter is a write, to a `&`/`*const` parameter a read, and -to `std::mem::take` a move. For type rules it also records which standard -traits (`Copy`, `Clone`, `Default`, ...) the mapped type implements. A -placeholder still `unknown` after this phase fails the build. - -The result is one `ir_.json` per input file, keyed by rule name. +to `std::mem::take` a move. For type rules it also records which of the +nine derivable standard traits (`Copy`, `Clone`, `Debug`, `Default`, +`PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`) the mapped type implements. +A placeholder still `unknown` after this phase fails the build. + +The preprocessor assumes the `rules` crate is buildable, which the earlier +`cargo build` of the crate ensures; errors from the in-process compilation +are therefore only reported as a warning. + +The result is one `ir_.json` per input file, keyed by rule name. The +output file name is derived from the input file name (`tgt_unsafe.rs` → +`ir_unsafe.json`) and the module directory is the direct parent of the +`tgt_*.rs` file. From 9ecced651819291ca6f2dcec213b3740503b1d1d Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Thu, 13 Aug 2026 18:00:05 +0100 Subject: [PATCH 04/30] Add more info about rule IR --- docs/src/rules/ir.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/src/rules/ir.md b/docs/src/rules/ir.md index 29a6933b..17e88c56 100644 --- a/docs/src/rules/ir.md +++ b/docs/src/rules/ir.md @@ -57,7 +57,8 @@ The fragment kinds are: * `arg`: the parameter index N. * `access`: how the body uses the argument: `read`, `write`, or `move`. * `is_index_base`: the placeholder is the base of an index expression. -* `generic`: a `TN` slot, replaced with the instantiated Rust type. +* `generic`: a `TN` slot, replaced with the instantiated Rust type; + serialized as the 1-based index N. * `method_call`: a method call split into `receiver` and `body` fragment lists, so the code generator can rewrite the pair (see [Rule Rewriting](./rewriting.md)). @@ -71,14 +72,19 @@ Every type in the IR (in `params`, `return_type`, and type rules) is a * `derives` (type rules only): the standard traits the mapped type implements (`Copy`, `Clone`, `Default`, ...). +The two pointer flags are mutually exclusive; the loader rejects a type +with both set. + An `ExprRule` carries two flags of its own: -* `multi_statement`: the body has more than one statement and must be - wrapped in a block to stay a single expression. +* `multi_statement`: the body has more than one statement, or a statement + followed by a tail expression, and must be wrapped in a block to stay a + single expression. * `is_extern`: the rule is an extern passthrough declaration and has no body. -Fields that are false, empty, or unset are omitted from the JSON. +Fields that are false, empty, or unset are omitted from the JSON. A `va` +parameter is never listed in `params`, and a `()` return type is omitted. A type rule serializes as a `TypeRule` object: its `TypeInfo` plus the `init` initializer expression, merged into one object: @@ -87,6 +93,9 @@ A type rule serializes as a `TypeRule` object: its `TypeInfo` plus the "t1": { "type": "Vec", "init": "Default::default()" } ``` +There is no explicit tag distinguishing the two rule kinds: an entry with +`body` is an expression rule, one with `type` and `init` a type rule. + ## In-memory form `cpp2rust` mirrors this JSON in C++ structs of the same names, defined in From 4475b4abc68656aea0f76526d7f3605d817de701 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Thu, 13 Aug 2026 18:03:52 +0100 Subject: [PATCH 05/30] Add more conventions --- docs/src/rules/conventions.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/src/rules/conventions.md b/docs/src/rules/conventions.md index 2d1b70f4..38cb25d3 100644 --- a/docs/src/rules/conventions.md +++ b/docs/src/rules/conventions.md @@ -54,9 +54,18 @@ guarantee. 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 teach `rule-preprocessor` to locate its rlib. +and to the hardcoded crate list in `rule-preprocessor/src/semantic.rs` +(see [The Rule Preprocessors](./preprocessors.md#rule-preprocessor)). From 10be55ed8092cefe0b5b370ca8a688c4e8e38b9d Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Thu, 13 Aug 2026 18:12:26 +0100 Subject: [PATCH 06/30] Add more info in rule rewriting --- docs/src/codegen/overview.md | 4 ++++ docs/src/rules/rewriting.md | 36 ++++++++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/docs/src/codegen/overview.md b/docs/src/codegen/overview.md index fb3916d3..ca97a4f7 100644 --- a/docs/src/codegen/overview.md +++ b/docs/src/codegen/overview.md @@ -2,3 +2,7 @@ This part of the book documents the internals of the code generator: how the clang AST is traversed and how Rust code is emitted. + +TODO: document the converter plugin mechanism +(`cpp2rust/converter/plugins/`), which intercepts constructs ahead of the +translation rules (currently `emplace_back`). diff --git a/docs/src/rules/rewriting.md b/docs/src/rules/rewriting.md index 48bce900..33bba679 100644 --- a/docs/src/rules/rewriting.md +++ b/docs/src/rules/rewriting.md @@ -18,18 +18,20 @@ impl Ptr { which checks the pointer, borrows the pointee mutably, and runs the closure on it (with an immutable sibling `Ptr::with`). The refcount converter uses it -to bridge the gap. The rewrite fires when all three hold: +to bridge the gap; the unsafe converter never rewrites and simply emits +receiver followed by body. The rewrite fires when all three hold: 1. The rule body fragment is a method call whose receiver contains a placeholder (the preprocessor splits every method call into receiver and - body fragments precisely to enable this). + body fragments precisely to enable this). If the receiver contains + several placeholders, the first one is used. 2. The receiver placeholder's access is write or move, i.e. the method takes `&mut self` or the rule mutates the parameter. Read access does not need the rewrite, since a read can go through a [`StrongPtr`](../codegen/pointers.md) obtained with `Ptr::upgrade`, or through a `read()` copy. -3. The call-site argument is a pointer or a reference reached through a - pointer. +3. The call-site argument is a pointer, or an expression of reference type + (which includes an operator call returning a reference). The rule's method call `a0.method(...)` is then emitted as @@ -55,14 +57,25 @@ When the receiver is a plain local value rather than a pointer, condition 3 fails and no closure is emitted; the same rule produces a direct call like `(*v2.borrow_mut()).push(0);`. +The rewrite applies to pointer dereferences (`p->push_back(20)`) and to +reference usages (`r.push_back(20)` with `std::vector &r = *p`); both +are [translated as a `Ptr`](../codegen/pointers.md), and that `Ptr` is what +`with_mut` is called on. + When the pointee is itself a boxed value (`Value`, i.e. `Rc>`), the closure takes `&mut Value` and an extra borrow is -inserted: +inserted. This is the case for nested containers: the refcount model +translates `std::vector>` as `Vec>>` so +that each element has interior mutability of its own, and a `Ptr` to an +inner vector therefore points at a `Value>`, not a `Vec`: ```rust ptr.with_mut(|__v: &mut Value>| (*__v.borrow_mut()).push(20)) ``` +The closure type is built from the C++ argument's type, not the rule's +declared parameter type. + ## The read-access counterpart For read access the converter does not emit a closure. A pointer receiver @@ -75,11 +88,14 @@ whose rule parameter is a value or `&` type is simply dereferenced ## Preprocessor-side rewrites Two rewrites in `rule-preprocessor` exist to make the `with_mut` rewrite -possible: +possible. Both apply only to `&mut` parameters: -* A `*` deref in front of a `&mut` parameter is dropped from the body, since +* A `*` deref in front of the parameter is dropped from the body, since the substituted argument is already an lvalue or pointer expression. -* `std::mem::take(&mut aN)` collapses to a bare placeholder marked as a move, - so the converter can re-express the move against the actual argument (for a +* `std::mem::take(&mut aN)` collapses to a bare placeholder, so the + converter can re-express the move against the actual argument (for a pointer that becomes `std::mem::take(&mut )` on the borrowed - pointee). + pointee). The spelling must be exactly this fully qualified form: + `mem::take` or an imported `take` is not rewritten. The collapsed + placeholder's access is left `unknown` in phase 1; phase 2 resolves the + `std::mem::take` call to a move. From 940915c6c9c53dd291042756d14c1bfb8f32c5f3 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Thu, 13 Aug 2026 18:23:42 +0100 Subject: [PATCH 07/30] Add more info in writing rules --- docs/src/rules/writing-rules.md | 106 ++++++++++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 4 deletions(-) diff --git a/docs/src/rules/writing-rules.md b/docs/src/rules/writing-rules.md index dbc94078..172f25af 100644 --- a/docs/src/rules/writing-rules.md +++ b/docs/src/rules/writing-rules.md @@ -39,6 +39,10 @@ not emitted as a function of its own: it is spliced inline into the generated code as a block expression, so a `return` would not end the rule, it would return from whatever generated function the rule happens to be expanded in. +When the pattern's type cannot be named, the rule uses an `auto` return +type: `rules/iomanip` writes `auto f1(int n) { return std::setw(n); }` +because `std::setw` returns an unspecified type. + ## Methods There is no special syntax for member functions: write a free function that @@ -65,6 +69,28 @@ matched positionally. The rule is written against the open template every instantiation: when the input program calls `size()` on, say, a `std::vector`, the matcher binds `T1 = int`. +## Static member functions + +A static member function is also written with a receiver parameter, which +exists only to name the class. The call site has no receiver argument, so +the Rust side drops it and numbers the remaining parameters from `a0`; here +there are none: + +```cpp +// rules/limits/src.cpp +template T1 f1(std::numeric_limits &a0) { return a0.max(); } +``` + +```rust +// rules/limits/tgt_unsafe.rs +unsafe fn f1() -> T1 { + ::MAX +} +``` + +(`HasMinMax` is a helper trait defined alongside the rules in the same +file.) + ## Constructors Constructors are functions returning the type by value, one rule per overload: @@ -100,9 +126,55 @@ bool f11(typename std::map::iterator a, ``` Post-increment is distinguished from pre-increment by the usual dummy `int` -parameter: `a0.operator++(a1)` versus `it.operator++()`. Member accesses -through iterators are also rules (e.g. `it->first`, `it->second`, `o.second` -in `rules/map` and `rules/pair`). +parameter: `a0.operator++(a1)` versus `it.operator++()`. Conversion +operators use the same explicit syntax: `a0.operator T1 &()` in +`rules/functional` matches the conversion of a `std::reference_wrapper` +back to a reference. Field accesses are rules of their own, matched by the +field: `it->first` and `it->second` through iterators, plain `o.second` on +a pair (`rules/map`, `rules/pair`). + +## Callable arguments + +A rule parameter may be a callable. Function pointers are spelled directly; +for a lambda, whose type cannot be written, the rule declares a file-scope +lambda and takes `decltype(lambda)`: + +```cpp +// rules/algorithm/src.cpp +auto lambda = [](const T2 &a, const T2 &b) { return false; }; +void f6(T1 first, T1 last, decltype(lambda) comp) { + return std::stable_sort(first, last, comp); +} +``` + +```rust +// rules/algorithm/tgt_unsafe.rs +unsafe fn f6(a0: *mut T1, a1: *mut T1, a2: &mut T2) +where + T2: FnMut(&T1, &T1) -> bool, +{ ... } +``` + +`T1` and `T2` are not template parameters here but file-scope helper +structs modelling an iterator and its value type; being named like +generics, they bind as `T1`/`T2` at the use site. The function pointer +version of the comparator is a separate rule (`f7`). + +## Iterators + +There is no iterator abstraction: an iterator type gets a type rule, and +every operation on it its own expression rule (`operator*`, `operator++`, +`operator!=`, ...). What the type maps to is up to the rule: +`std::string::iterator` becomes a plain pointer (`*mut libc::c_char` +unsafe, `Ptr` refcount), while `std::map` iterators become the runtime +types `libcc2rs::UnsafeMapIterator`/`MapIterator`. Dependent iterator +types are named with `typename`: + +```cpp +// rules/map/src.cpp +template +using t2 = typename std::map::const_iterator; +``` ## Types @@ -163,7 +235,9 @@ unsafe fn f3() -> i32 { ::libc::O_CREAT } For macros that expand to integer literals, the preprocessor records the *macro name* rather than the value, so `O_CREAT` in the input matches this rule by name. Enum constants and global variables (e.g. `std::cout`) are -matched by their qualified name. +matched by their qualified name. A global and its address are separate +rules: `rules/iostream` maps both `std::cout` (`f1`) and `&std::cout` +(`f3`). Integer-literal macros are the only macros matchable directly. Macros whose expansions are platform internals with no stable callee, such as `errno` or @@ -198,6 +272,9 @@ int f1(int a0, int a1, Args... args) { fn f1(a0: i32, a1: i32, va: &[VaArg]) -> i32 { ... } ``` +Bodies read the arguments through the va-args API in `libcc2rs` +(`VaArg`, `VaList`, the `VaArgGet` accessors, `format_c`). + ## Passthrough rules When a call should be forwarded verbatim to the same-named function in Rust's @@ -244,3 +321,24 @@ unsafe fn f4() -> i32 { The Rust preprocessor evaluates `#[cfg]` attributes against the host target (only `target_os = linux|macos` and `target_arch = x86_64|x86` are accepted) and drops non-matching rules. + +Mutually exclusive platform branches use `#elif` with disjoint rule +numbers: `rules/errno` defines `f91`–`f135` under `__linux__` and +`f136`–`f153` under `__APPLE__`. Feature-test macros a pattern needs must +come before the includes, as with `#define _GNU_SOURCE` in +`rules/socket/src.c`. + +## Pattern resolution limits + +The preprocessor resolves a template pattern by instantiating its template +parameters with synthesized types +([The Rule Preprocessors](./preprocessors.md#cpp-rule-preprocessor)): + +* A bare `T1` becomes an empty struct, so the pattern cannot use members, + operators, or nested types of `T1`. +* A parameter pack instantiates to the empty pack. +* A non-type parameter is pinned to the value `1`. + +Unqualified callees are looked up in `namespace std` first and in the +global scope only when `std` has no match, so an unqualified name that +exists in both resolves to the `std` one. From edc20386f1d7a97d70f3fc39adc0b9743b7e972d Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Thu, 13 Aug 2026 18:34:00 +0100 Subject: [PATCH 08/30] Add The Matching Engine section --- docs/src/SUMMARY.md | 1 + docs/src/rules/loading.md | 38 +++++++++++++++++++------------ docs/src/rules/matching.md | 40 +++++++++++++++++++++++++++++++++ docs/src/rules/overview.md | 2 ++ docs/src/rules/preprocessors.md | 2 +- docs/src/rules/writing-rules.md | 4 ++-- 6 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 docs/src/rules/matching.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 38511e53..65814ddf 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -17,6 +17,7 @@ * [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 diff --git a/docs/src/rules/loading.md b/docs/src/rules/loading.md index 83e0beee..d1556145 100644 --- a/docs/src/rules/loading.md +++ b/docs/src/rules/loading.md @@ -13,9 +13,14 @@ picks up the generated IR without any flags. Rules are loaded once per process by `Mapper::LoadTranslationRules`: -1. Built-in type mappings are registered first: scalars, `void *`, `size_t`, - and the per-model pointer forms (`*mut T` for unsafe, `Ptr` for - refcount). +1. Built-in type mappings are registered first. Every scalar is mapped + with its width taken from the host (`int` maps to `i32`, + `unsigned long` to `u64`), together with its `const` form and its + pointer forms: `*mut`/`*const` in the unsafe model, `Ptr` in the + refcount model, where constness is dropped. `char` maps to + `libc::c_char` in the unsafe model and to `u8` in refcount; + `size_t`/`ssize_t` map to `usize`/`isize`; `void *` maps to + `*mut ::libc::c_void`, or in refcount to `AnyPtr`. 2. Every subdirectory of the rules directory is loaded with `TranslationRule::Load`, which reads `ir_unsafe.json`, overlays `ir_refcount.json` when translating with the refcount model, and then @@ -30,9 +35,9 @@ rules for the same C++ type are rejected. Loaded rules are indexed in two multimaps, one for expression rules and one for type rules. The multimap key is only a coarse bucket for collecting -candidate rules; whether a candidate actually matches is decided by the -unification step described below. The bucket key is derived from the C++ -signature: +candidate rules; whether a candidate actually matches is decided by +[the matching engine](./matching.md). The bucket key is derived from the +C++ signature: * For expressions, the qualified function name with the return type, the parameter list, and all template arguments stripped, so the rule for @@ -50,12 +55,16 @@ the two sides comparable: * Enum constants and global variables print as their qualified name. * Integer literals expanded from a macro print as the macro name. -All rules in the matching bucket are then unified against this string with a -template matcher that binds `T1`...`T9` to the concrete types at the use -site. If several rules match, the one with the longest source signature wins, -so more specific rules take precedence. Type lookups first try the -sugar-preserving spelling (so a rule can match `size_t` as written) and retry -with the desugared type on failure. +All rules in the matching bucket are then unified against this string by +[the matching engine](./matching.md), which binds `T1`...`T9` to the +concrete types at the use site and picks the most specific rule when +several match. Type lookups first try the sugar-preserving spelling (so a +rule can match `size_t` as written) and retry with the desugared type on +failure. + +Running `cpp2rust` with `--verbose` logs every lookup and the rule it +matched, which is the quickest way to see why a rule does or does not +fire. ## Application @@ -81,8 +90,9 @@ When a rule matches, the converter walks its body fragments and emits: substitutes `x1.as_pointer()` and `x2.as_pointer()` for the placeholders, while `std::max(30, 40)` first materializes `__tmp_0` and `__tmp_1` values for the literals and points into those. - * If the argument is a pointer but the rule expects a value, the - converter dereferences it. + * If the receiver argument is a pointer but the rule expects a value, + the converter dereferences it; this only happens for receivers, not + for ordinary arguments. * `generic` fragments as the Rust mapping of the bound C++ type, * `va_args` fragments as the converted variadic tail, * `method_call` fragments as receiver followed by body, possibly rewritten diff --git a/docs/src/rules/matching.md b/docs/src/rules/matching.md new file mode 100644 index 00000000..d31c522e --- /dev/null +++ b/docs/src/rules/matching.md @@ -0,0 +1,40 @@ +# The Matching Engine + +[Loading and Matching](./loading.md) collects the candidate rules for a +construct from a bucket; each candidate's source signature is then +*unified* against the printed construct. The signature is treated as a +template whose `T1`...`T9` slots capture concrete types: +`std::vector::vector()` unifies with `std::vector::vector()` by +binding `T1 = int`. + +Unification works on the two strings: + +* Whitespace differences are ignored. +* A `TN` slot captures up to the next literal text of the pattern, found + at the same `<>`/`()`/`[]` nesting depth. This is how `T1` captures all + of `std::map` in `std::vector>` without + stopping at the inner comma. +* A `TN` that appears again must match its first capture exactly. +* The whole printed string must be consumed; trailing text fails the + match. +* Slots may stay unbound (a pattern can use `T2` without `T1`). + +A rule matches if unification succeeds. When several rules in the bucket +match, the one with the longest source signature wins, so more specific +rules take precedence; between equally long signatures the choice is +unspecified. + +## Bucket keys + +The bucket keys described in +[Loading and Matching](./loading.md#matching) have two special cases: +array types bucket by the text after the first `[` rather than the text +before a `<`, and `operator()` rules are cut at the operator's own +parentheses, so their key ends at `...::operator`. + +## Instantiating the target + +Captures are C++ spellings. Before being substituted into the rule's Rust +fragments, each capture is itself mapped through the type rules, +recursively, so `T1 = std::vector` substitutes as `Vec`. A +captured type with no type rule of its own is an error. diff --git a/docs/src/rules/overview.md b/docs/src/rules/overview.md index d36aeebf..0f74ed68 100644 --- a/docs/src/rules/overview.md +++ b/docs/src/rules/overview.md @@ -45,5 +45,7 @@ The rest of this part covers each stage: * [The Rules IR](./ir.md): the JSON format the preprocessors emit. * [Loading and Matching](./loading.md): how `cpp2rust` loads the IR and matches rules against the input AST. +* [The Matching Engine](./matching.md): how a candidate rule's signature + is unified against the input. * [Rule Rewriting](./rewriting.md): how rule bodies are adapted at application time, in particular the `with_mut` rewrite. diff --git a/docs/src/rules/preprocessors.md b/docs/src/rules/preprocessors.md index ccd7ffc0..7ef6f538 100644 --- a/docs/src/rules/preprocessors.md +++ b/docs/src/rules/preprocessors.md @@ -124,6 +124,6 @@ The preprocessor assumes the `rules` crate is buildable, which the earlier are therefore only reported as a warning. The result is one `ir_.json` per input file, keyed by rule name. The -output file name is derived from the input file name (`tgt_unsafe.rs` → +output file name is derived from the input file name (`tgt_unsafe.rs` becomes `ir_unsafe.json`) and the module directory is the direct parent of the `tgt_*.rs` file. diff --git a/docs/src/rules/writing-rules.md b/docs/src/rules/writing-rules.md index 172f25af..842f43c6 100644 --- a/docs/src/rules/writing-rules.md +++ b/docs/src/rules/writing-rules.md @@ -323,8 +323,8 @@ The Rust preprocessor evaluates `#[cfg]` attributes against the host target and drops non-matching rules. Mutually exclusive platform branches use `#elif` with disjoint rule -numbers: `rules/errno` defines `f91`–`f135` under `__linux__` and -`f136`–`f153` under `__APPLE__`. Feature-test macros a pattern needs must +numbers: `rules/errno` defines `f91` to `f135` under `__linux__` and +`f136` to `f153` under `__APPLE__`. Feature-test macros a pattern needs must come before the includes, as with `#define _GNU_SOURCE` in `rules/socket/src.c`. From 63ff5a74028131e4260e04e9a062455888bc8d86 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Fri, 14 Aug 2026 11:59:46 +0100 Subject: [PATCH 09/30] Final edits --- docs/src/rules/format.md | 2 +- docs/src/rules/ir.md | 23 +++++++++++++++-------- docs/src/rules/loading.md | 8 ++++---- docs/src/rules/overview.md | 25 ++++++++++--------------- docs/src/rules/preprocessors.md | 2 +- 5 files changed, 31 insertions(+), 29 deletions(-) diff --git a/docs/src/rules/format.md b/docs/src/rules/format.md index 8a858a26..f9b6a641 100644 --- a/docs/src/rules/format.md +++ b/docs/src/rules/format.md @@ -70,7 +70,7 @@ template using t1 = std::vector; ```rust // rules/vector/tgt_unsafe.rs fn t1() -> Vec { - Default::default() + Vec::new() } ``` diff --git a/docs/src/rules/ir.md b/docs/src/rules/ir.md index 17e88c56..85234c9a 100644 --- a/docs/src/rules/ir.md +++ b/docs/src/rules/ir.md @@ -64,7 +64,7 @@ The fragment kinds are: [Rule Rewriting](./rewriting.md)). * `va_args`: the expansion point for a variadic tail. -Every type in the IR (in `params`, `return_type`, and type rules) is a +Every type in the Rules IR (in `params`, `return_type`, and type rules) is a `TypeInfo` object, the type text plus a set of flags: * `is_refcount_pointer`: the type is a `Ptr<...>`. @@ -83,7 +83,7 @@ An `ExprRule` carries two flags of its own: * `is_extern`: the rule is an extern passthrough declaration and has no body. -Fields that are false, empty, or unset are omitted from the JSON. A `va` +Fields that are false, empty, or unset are omitted from the Rules IR. A `va` parameter is never listed in `params`, and a `()` return type is omitted. A type rule serializes as a `TypeRule` object: its `TypeInfo` plus the @@ -98,21 +98,28 @@ There is no explicit tag distinguishing the two rule kinds: an entry with ## In-memory form -`cpp2rust` mirrors this JSON in C++ structs of the same names, defined in -`cpp2rust/converter/translation_rule.h`. `TranslationRule::Load` reads one +`cpp2rust` mirrors the Rules IR in C++ structs of the same names, defined in +[`cpp2rust/converter/translation_rule.h`][translation-rule-h]. +`TranslationRule::Load` reads one module directory (the `ir_*.json` files described above) and produces two -maps keyed by rule name, one holding `ExprRule`s and one holding `TypeRule`s: +maps keyed by rule name, one holding [`ExprRule`]s and one holding +[`TypeRule`]s: + +[translation-rule-h]: https://github.com/cpp2rust/cpp2rust/blob/master/cpp2rust/converter/translation_rule.h +[`ExprRule`]: https://github.com/cpp2rust/cpp2rust/blob/master/cpp2rust/converter/translation_rule.h#L71 +[`TypeRule`]: https://github.com/cpp2rust/cpp2rust/blob/master/cpp2rust/converter/translation_rule.h#L84 * An `ExprRule` holds the body [fragments](#target-ir-ir_unsafejson--ir_refcountjson), the parameter and - return `TypeInfo`s, and the two rule-level flags. The name-keyed JSON maps become + return `TypeInfo`s, and the two rule-level flags (`multi_statement` and `is_extern`). The + name-keyed Rules IR maps become positional vectors: parameter `aN` is entry N of `params`, generic `TN` is entry N-1 of `generics` (each entry being the bound list). Rules support at most 9 generic parameters (`kMaxGenerics`). * A `TypeRule` holds the mapped type's `TypeInfo` and the `initializer` expression. The same struct also represents the built-in type mappings (scalars, pointers, ...) that the loader registers directly in code, - without any JSON behind them: for example, `int` maps to `i32`, and + without any Rules IR behind them: for example, `int` maps to `i32`, and `int *` to `*mut i32` in the unsafe model or `Ptr` in the refcount model. @@ -120,6 +127,6 @@ Both also carry `src`, the canonical C++ signature attached from [`ir_src.json`](#source-ir-ir_srcjson); it is the key the rule is matched by. -How the loader finds the IR directory, overlays the refcount model on the +How the loader finds the Rules IR directory, overlays the refcount model on the unsafe one, and indexes the loaded rules for matching is covered in [Loading and Matching](./loading.md). diff --git a/docs/src/rules/loading.md b/docs/src/rules/loading.md index d1556145..2e0cd18b 100644 --- a/docs/src/rules/loading.md +++ b/docs/src/rules/loading.md @@ -2,12 +2,12 @@ ## Finding the rules directory -`cpp2rust` takes the IR directory via `--rules `. If the flag is omitted +`cpp2rust` takes the Rules IR directory via `--rules `. If the flag is omitted it tries `./rules` and then `/../rules`, accepting the first candidate that contains (recursively) a subdirectory with `ir_src.json` plus -`ir_unsafe.json` or `ir_refcount.json`. Since the build writes the IR to +`ir_unsafe.json` or `ir_refcount.json`. Since the build writes the Rules IR to `/rules` and the binary lands in `/bin`, the default resolution -picks up the generated IR without any flags. +picks up the generated Rules IR without any flags. ## Loading @@ -51,7 +51,7 @@ same canonical printer used by `cpp-rule-preprocessor`, which is what makes the two sides comparable: * Functions and methods print as - ` ()[ const][ &|&&]`. + ` ([, ...])[ const][ volatile][ &|&&]`. * Enum constants and global variables print as their qualified name. * Integer literals expanded from a macro print as the macro name. diff --git a/docs/src/rules/overview.md b/docs/src/rules/overview.md index 0f74ed68..dd321667 100644 --- a/docs/src/rules/overview.md +++ b/docs/src/rules/overview.md @@ -5,30 +5,25 @@ Each rule module lives in the `rules/` directory and pairs a C++ source file (`src.cpp`) with its Rust translation for each model (`tgt_unsafe.rs` and `tgt_refcount.rs`). -The central idea is that every rule is expressed as ordinary, compilable -C++ and Rust source code, free of anything platform dependent. Both sides are -run through real compilers at build time, so a rule that does not compile -fails the build, and the platform-specific spellings a rule needs to match -against (for example `bool` canonicalizing to `_Bool`) are derived by the -compiler on the host rather than written by hand. The same rule sources work -on every platform `cpp2rust` builds on. +Every rule is expressed as ordinary, compilable C++ and Rust source code, +free of anything platform dependent. Both sides are run through real +compilers at build time, so a rule that does not compile fails the build, and +the platform-specific spellings (for example `bool` canonicalizing to +`_Bool`) are derived by the compiler on the host rather than written by hand. +The same rule sources work on every platform `cpp2rust` builds on. Rules go through a build-time compilation pipeline before `cpp2rust` can use them: 1. You author a rule module: C++ patterns in `src.cpp` and Rust targets in `tgt_unsafe.rs` / `tgt_refcount.rs`. -2. At build time, two preprocessors compile the module into JSON IR under +2. At build time, two preprocessors compile the module into Rules IR under `/rules//`: `cpp-rule-preprocessor` compiles the C++ side into `ir_src.json`, and `rule-preprocessor` compiles the Rust side into `ir_unsafe.json` and `ir_refcount.json`. -3. At startup, `cpp2rust` loads the IR files and indexes the rules by the +3. At startup, `cpp2rust` loads the Rules IR files and indexes the rules by the canonical signature of the C++ construct they match. -4. During translation, the converter looks up rules by signature and splices - their Rust bodies into the output, substituting arguments for placeholders - and rewriting the body when needed (e.g. wrapping method calls in - `with_mut` when the receiver is a `Ptr`). The rest of this part covers each stage: @@ -41,9 +36,9 @@ The rest of this part covers each stage: * [Conventions](./conventions.md): naming and style conventions rule authors must follow. * [The Rule Preprocessors](./preprocessors.md): the two build-time tools that - compile rules to IR. + compile rules to the Rules IR. * [The Rules IR](./ir.md): the JSON format the preprocessors emit. -* [Loading and Matching](./loading.md): how `cpp2rust` loads the IR and +* [Loading and Matching](./loading.md): how `cpp2rust` loads the Rules IR and matches rules against the input AST. * [The Matching Engine](./matching.md): how a candidate rule's signature is unified against the input. diff --git a/docs/src/rules/preprocessors.md b/docs/src/rules/preprocessors.md index 7ef6f538..ad18ed3c 100644 --- a/docs/src/rules/preprocessors.md +++ b/docs/src/rules/preprocessors.md @@ -1,6 +1,6 @@ # The Rule Preprocessors -Two build-time tools compile rule modules into the [JSON IR](./ir.md) that +Two build-time tools compile rule modules into the [Rules IR](./ir.md) that `cpp2rust` loads at runtime. Both write into `/rules//`: * `cpp-rule-preprocessor` compiles `src.cpp`/`src.c` into `ir_src.json`. From 415138ede01078d7691139a52e38ee99e11fdc6e Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Fri, 14 Aug 2026 12:15:54 +0100 Subject: [PATCH 10/30] Delete test file --- docs/src/rules/math-test.md | 48 ------------------------------------- 1 file changed, 48 deletions(-) delete mode 100644 docs/src/rules/math-test.md diff --git a/docs/src/rules/math-test.md b/docs/src/rules/math-test.md deleted file mode 100644 index 2fbc5079..00000000 --- a/docs/src/rules/math-test.md +++ /dev/null @@ -1,48 +0,0 @@ -# Formula Rendering Test - -This page is a rendering test for hand-translating the inference rules from -`latex/translating.tex` (Fig. 2, the $\mathcal{T}$ function) into the book -via the `mdbook-katex` preprocessor. Only a few representative rules are -shown. - -The macros from `latex/macros.tex` are ported once into -`docs/katex-macros.txt` and are available on every page, so the rule bodies -below stay nearly identical to the paper's source. - -## Axioms - -$$ -\begin{array}{cc} -\text{Int} & \text{String} \\[4pt] -\dfrac{}{\typcomp{\cint} \defeq \rint} -& -\dfrac{}{\typcomp{\cstring} \defeq \rstring} -\end{array} -$$ - -## Rule with premises - -$$ -\begin{array}{c}\text{Unique Pointer}\\[4pt] -\dfrac{ - \typcomp{\typ} = \rtyp - \qquad - \boxTy(\rtyp) = \rtyp' -}{\typcomp{\uniqueptr{\typ}} \defeq \roption{\rtyp'}} -\end{array} -$$ - -## Rule with premises on two lines - -$$ -\begin{array}{c}\text{Ptr - Non-Virtual Class}\\[4pt] -\dfrac - {\begin{array}{c} - \typcomp{\typ} = \rtyp - \qquad - \typ \text{ is not a virtual class} - \\ - \converttoptr(\rtyp) = \rtyp' - \end{array}}{\typcomp{\typ*} \defeq \ptr{\rtyp'}} -\end{array} -$$ From b195a255137dd99aed9b7a25bcb4c898decb9277 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Fri, 14 Aug 2026 12:32:43 +0100 Subject: [PATCH 11/30] Format --- docs/src/SUMMARY.md | 34 +++---- docs/src/codegen/overview.md | 6 +- docs/src/codegen/temporaries.md | 4 +- docs/src/project/introduction.md | 20 ++-- docs/src/project/usage.md | 4 +- docs/src/rules/compat.md | 105 ++++++++++---------- docs/src/rules/conventions.md | 85 ++++++++-------- docs/src/rules/format.md | 80 ++++++++------- docs/src/rules/ir.md | 117 +++++++++++----------- docs/src/rules/loading.md | 126 ++++++++++++------------ docs/src/rules/matching.md | 48 +++++---- docs/src/rules/overview.md | 49 +++++---- docs/src/rules/preprocessors.md | 134 ++++++++++++------------- docs/src/rules/rewriting.md | 79 +++++++-------- docs/src/rules/writing-rules.md | 164 +++++++++++++++---------------- 15 files changed, 511 insertions(+), 544 deletions(-) diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 65814ddf..b935edd1 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -2,26 +2,26 @@ # 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) -* [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) +- [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) -* [Pointers and References](./codegen/pointers.md) -* [Temporary Materialization](./codegen/temporaries.md) +- [Overview](./codegen/overview.md) +- [Pointers and References](./codegen/pointers.md) +- [Temporary Materialization](./codegen/temporaries.md) diff --git a/docs/src/codegen/overview.md b/docs/src/codegen/overview.md index ca97a4f7..908d56c5 100644 --- a/docs/src/codegen/overview.md +++ b/docs/src/codegen/overview.md @@ -3,6 +3,6 @@ This part of the book documents the internals of the code generator: how the clang AST is traversed and how Rust code is emitted. -TODO: document the converter plugin mechanism -(`cpp2rust/converter/plugins/`), which intercepts constructs ahead of the -translation rules (currently `emplace_back`). +TODO: document the converter plugin mechanism (`cpp2rust/converter/plugins/`), +which intercepts constructs ahead of the translation rules (currently +`emplace_back`). diff --git a/docs/src/codegen/temporaries.md b/docs/src/codegen/temporaries.md index a4a86303..4b2b5520 100644 --- a/docs/src/codegen/temporaries.md +++ b/docs/src/codegen/temporaries.md @@ -1,5 +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 +> 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). diff --git a/docs/src/project/introduction.md b/docs/src/project/introduction.md index 404920d7..ff987afb 100644 --- a/docs/src/project/introduction.md +++ b/docs/src/project/introduction.md @@ -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` type provided by `libcc2rs`. -`Ptr` 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` type provided by +`libcc2rs`. `Ptr` models C pointer semantics, including null, arithmetic, and +aliasing, while satisfying Rust's borrow checker through checked run-time +operations. diff --git a/docs/src/project/usage.md b/docs/src/project/usage.md index 64a110cc..deb413d9 100644 --- a/docs/src/project/usage.md +++ b/docs/src/project/usage.md @@ -6,8 +6,8 @@ ./build/cpp2rust/cpp2rust --file=.cpp -o=.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=.cpp -o=.rs --model=unsafe diff --git a/docs/src/rules/compat.md b/docs/src/rules/compat.md index e456b551..5c45492b 100644 --- a/docs/src/rules/compat.md +++ b/docs/src/rules/compat.md @@ -1,28 +1,28 @@ # 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 +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 +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: +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 +- `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 +_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 @@ -30,17 +30,16 @@ into a call to a synthetic, well-known function before matching happens. `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. +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 ` 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), +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. @@ -57,12 +56,12 @@ 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()`. +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 +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 @@ -72,10 +71,10 @@ shim headers, both sides canonicalize to the same signature, and an ordinary 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: +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 @@ -91,11 +90,11 @@ fn f1() -> Ptr { } ``` -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` inside `libcc2rs`; this is the same cell that other refcount -rules write when they translate a failing libc call into +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` 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 @@ -109,17 +108,17 @@ 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`, ... | +| 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. +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 @@ -132,15 +131,15 @@ To make another macro-based API matchable: 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. +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. +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. +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. diff --git a/docs/src/rules/conventions.md b/docs/src/rules/conventions.md index 38cb25d3..476ed08a 100644 --- a/docs/src/rules/conventions.md +++ b/docs/src/rules/conventions.md @@ -1,71 +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. +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`, ... | +| 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 +- 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. + 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 +- 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 +- 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. +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). +- 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)`. +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)). +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)). diff --git a/docs/src/rules/format.md b/docs/src/rules/format.md index f9b6a641..199c4481 100644 --- a/docs/src/rules/format.md +++ b/docs/src/rules/format.md @@ -4,25 +4,25 @@ A rule module is a directory under `rules/`, usually named after the header or library it covers (`rules/unistd`, `rules/vector`, `rules/string`, ...). It contains: -* `src.cpp` and/or `src.c`: the C++ (or C) side of each rule. -* `tgt_unsafe.rs`: the Rust targets for the unsafe model. -* `tgt_refcount.rs`: the Rust targets for the reference counting model +- `src.cpp` and/or `src.c`: the C++ (or C) side of each rule. +- `tgt_unsafe.rs`: the Rust targets for the unsafe model. +- `tgt_refcount.rs`: the Rust targets for the reference counting model (optional, see below). A rule is a pair of same-named functions on the two sides. Names determine the rule kind: -* `f1`, `f2`, ... are *expression rules*: they map a C++ call, member access, +- `f1`, `f2`, ... are _expression rules_: they map a C++ call, member access, constructor, or constant to a Rust expression. -* `t1`, `t2`, ... are *type rules*: they map a C++ type to a Rust type. +- `t1`, `t2`, ... are _type rules_: they map a C++ type to a Rust type. ## Expression rules -On the C++ side, an `fN` function must have a body that is exactly one -`return` statement. The returned expression is the pattern: the preprocessor -resolves the *callee* of that expression (the function, method, constructor, -enum constant, or macro being used) and that becomes the rule's matching key. -The function parameters stand for the arguments at the call site. +On the C++ side, an `fN` function must have a body that is exactly one `return` +statement. The returned expression is the pattern: the preprocessor resolves the +_callee_ of that expression (the function, method, constructor, enum constant, +or macro being used) and that becomes the rule's matching key. The function +parameters stand for the arguments at the call site. ```cpp // rules/unistd/src.cpp @@ -30,8 +30,8 @@ int f4(const char *pathname) { return unlink(pathname); } ``` On the Rust side, the same-named function gives the replacement expression. -Parameters must be named `a0`, `a1`, ... and correspond positionally to the -C++ parameters: +Parameters must be named `a0`, `a1`, ... and correspond positionally to the C++ +parameters: ```rust // rules/unistd/tgt_unsafe.rs @@ -53,14 +53,14 @@ fn f4(a0: Ptr) -> i32 { } ``` -When the converter encounters `unlink(x)` in the input, it emits the rule -body with the translated `x` substituted for `a0`. +When the converter encounters `unlink(x)` in the input, it emits the rule body +with the translated `x` substituted for `a0`. ## Type rules -On the C++ side, a `tN` rule is a type alias (`using` or `typedef`). On the -Rust side, it is a *zero-argument function* whose return type is the mapped -Rust type and whose body is the default initializer for that type: +On the C++ side, a `tN` rule is a type alias (`using` or `typedef`). On the Rust +side, it is a _zero-argument function_ whose return type is the mapped Rust type +and whose body is the default initializer for that type: ```cpp // rules/vector/src.cpp @@ -80,13 +80,12 @@ The loader always reads `ir_unsafe.json` first. When translating with the reference counting model, it then overlays `ir_refcount.json` on top: entries with the same rule name replace the unsafe ones. -This means `tgt_refcount.rs` only needs to contain the rules that *differ* -from the unsafe model. For example, the `__builtin_mul_overflow` rule in +This means `tgt_refcount.rs` only needs to contain the rules that _differ_ from +the unsafe model. For example, the `__builtin_mul_overflow` rule in `rules/builtin` has a pointer out-parameter (`a2` below), so the two models -translate it differently: in the unsafe model `a2` is a raw `*mut i64` -written through a deref, while in the refcount model it is a `Ptr` -written through `Ptr::write`. The other two arguments are identical in both -models: +translate it differently: in the unsafe model `a2` is a raw `*mut i64` written +through a deref, while in the refcount model it is a `Ptr` written through +`Ptr::write`. The other two arguments are identical in both models: ```rust // rules/builtin/tgt_unsafe.rs @@ -117,12 +116,12 @@ A module may have both `src.c` and `src.cpp`; both are preprocessed and merged into one `ir_src.json`. Defining the same rule name in both files is a hard error, so numbering must not collide. -This split is necessary because rules match on the exact canonical signature -of the callee, and some libc functions have *different signatures in C and -C++*. For example, C has a single -`char *strchr(const char *, int)`, while C++ replaces it with const-correct -overloads such as `const char *strchr(const char *, int)`. Since the -signatures differ, `rules/cstring` defines one rule per language: +This split is necessary because rules match on the exact canonical signature of +the callee, and some libc functions have _different signatures in C and C++_. +For example, C has a single `char *strchr(const char *, int)`, while C++ +replaces it with const-correct overloads such as +`const char *strchr(const char *, int)`. Since the signatures differ, +`rules/cstring` defines one rule per language: ```cpp // rules/cstring/src.cpp @@ -137,18 +136,17 @@ char *f5(const char *a0, int a1) { return (strchr)(a0, a1); } The C++ rule matches `strchr` calls in code translated as C++, the C rule matches them in code translated as C. -`rules/builtin` uses this to cover both languages: `src.cpp` defines -`f9`/`f10` for the C++ `__builtin_mul_overflow` (returning `bool`) while -`src.c` defines `f12`/`f13` for the C version (returning `int`); their Rust -bodies are identical. +`rules/builtin` uses this to cover both languages: `src.cpp` defines `f9`/`f10` +for the C++ `__builtin_mul_overflow` (returning `bool`) while `src.c` defines +`f12`/`f13` for the C version (returning `int`); their Rust bodies are +identical. ## The `rules` crate -The whole `rules/` tree is a single Rust crate. `rules/build.rs` walks the -tree, collects every `tgt_*.rs`, and generates `rules/src/modules.rs` with one -`#[path = ...]` module per file. Building the crate therefore type-checks -every rule body against the crates the rule targets call into, which are -declared as dependencies in `rules/Cargo.toml` (`libcc2rs`, `libc`, `nix`, -...). The -Rust rule preprocessor compiles exactly this crate to resolve types in rule -bodies. `rules/src/` is the only subdirectory that is not a rule module. +The whole `rules/` tree is a single Rust crate. `rules/build.rs` walks the tree, +collects every `tgt_*.rs`, and generates `rules/src/modules.rs` with one +`#[path = ...]` module per file. Building the crate therefore type-checks every +rule body against the crates the rule targets call into, which are declared as +dependencies in `rules/Cargo.toml` (`libcc2rs`, `libc`, `nix`, ...). The Rust +rule preprocessor compiles exactly this crate to resolve types in rule bodies. +`rules/src/` is the only subdirectory that is not a rule module. diff --git a/docs/src/rules/ir.md b/docs/src/rules/ir.md index 85234c9a..60201d34 100644 --- a/docs/src/rules/ir.md +++ b/docs/src/rules/ir.md @@ -3,15 +3,15 @@ Each rule module compiles to up to three JSON files in `/rules//`: -* `ir_src.json`: the C++ side, from +- `ir_src.json`: the C++ side, from [`cpp-rule-preprocessor`](./preprocessors.md#cpp-rule-preprocessor). -* `ir_unsafe.json`: the Rust side for the unsafe model, from +- `ir_unsafe.json`: the Rust side for the unsafe model, from [`rule-preprocessor`](./preprocessors.md#rule-preprocessor). -* `ir_refcount.json`: the Rust side for the refcount model, also from +- `ir_refcount.json`: the Rust side for the refcount model, also from `rule-preprocessor` (only if the module has a `tgt_refcount.rs`). -All three are objects keyed by rule name (`f1`, `t1`, ...), and the loader -joins them by name. +All three are objects keyed by rule name (`f1`, `t1`, ...), and the loader joins +them by name. ## Source IR (`ir_src.json`) @@ -25,14 +25,14 @@ rule matches. For `rules/vector`: } ``` -This signature string is the lookup key for the whole rule: the converter -prints C++ constructs from the input AST with the same printer and compares -the strings. +This signature string is the lookup key for the whole rule: the converter prints +C++ constructs from the input AST with the same printer and compares the +strings. ## Target IR (`ir_unsafe.json` / `ir_refcount.json`) -An expression rule serializes as an `ExprRule` object: the rule's signature -plus its body as a list of *fragments*. For +An expression rule serializes as an `ExprRule` object: the rule's signature plus +its body as a list of _fragments_. For `unsafe fn f6(a0: &mut Vec) -> *mut T1 { a0.as_mut_ptr() }`: ```json @@ -50,82 +50,81 @@ plus its body as a list of *fragments*. For The fragment kinds are: -* `text`: literal Rust source, emitted verbatim. -* `placeholder`: a use of one of the rule's `aN` parameters in the body (not - an argument of whatever the body calls); the converter substitutes the - translated call-site argument here. Its fields: - * `arg`: the parameter index N. - * `access`: how the body uses the argument: `read`, `write`, or `move`. - * `is_index_base`: the placeholder is the base of an index expression. -* `generic`: a `TN` slot, replaced with the instantiated Rust type; - serialized as the 1-based index N. -* `method_call`: a method call split into `receiver` and `body` fragment - lists, so the code generator can rewrite the pair (see +- `text`: literal Rust source, emitted verbatim. +- `placeholder`: a use of one of the rule's `aN` parameters in the body (not an + argument of whatever the body calls); the converter substitutes the translated + call-site argument here. Its fields: + - `arg`: the parameter index N. + - `access`: how the body uses the argument: `read`, `write`, or `move`. + - `is_index_base`: the placeholder is the base of an index expression. +- `generic`: a `TN` slot, replaced with the instantiated Rust type; serialized + as the 1-based index N. +- `method_call`: a method call split into `receiver` and `body` fragment lists, + so the code generator can rewrite the pair (see [Rule Rewriting](./rewriting.md)). -* `va_args`: the expansion point for a variadic tail. +- `va_args`: the expansion point for a variadic tail. Every type in the Rules IR (in `params`, `return_type`, and type rules) is a `TypeInfo` object, the type text plus a set of flags: -* `is_refcount_pointer`: the type is a `Ptr<...>`. -* `is_unsafe_pointer`: the type is a raw `*mut`/`*const` pointer. -* `derives` (type rules only): the standard traits the mapped type - implements (`Copy`, `Clone`, `Default`, ...). +- `is_refcount_pointer`: the type is a `Ptr<...>`. +- `is_unsafe_pointer`: the type is a raw `*mut`/`*const` pointer. +- `derives` (type rules only): the standard traits the mapped type implements + (`Copy`, `Clone`, `Default`, ...). -The two pointer flags are mutually exclusive; the loader rejects a type -with both set. +The two pointer flags are mutually exclusive; the loader rejects a type with +both set. An `ExprRule` carries two flags of its own: -* `multi_statement`: the body has more than one statement, or a statement - followed by a tail expression, and must be wrapped in a block to stay a - single expression. -* `is_extern`: the rule is an extern passthrough declaration and has no - body. +- `multi_statement`: the body has more than one statement, or a statement + followed by a tail expression, and must be wrapped in a block to stay a single + expression. +- `is_extern`: the rule is an extern passthrough declaration and has no body. Fields that are false, empty, or unset are omitted from the Rules IR. A `va` parameter is never listed in `params`, and a `()` return type is omitted. -A type rule serializes as a `TypeRule` object: its `TypeInfo` plus the -`init` initializer expression, merged into one object: +A type rule serializes as a `TypeRule` object: its `TypeInfo` plus the `init` +initializer expression, merged into one object: ```json "t1": { "type": "Vec", "init": "Default::default()" } ``` -There is no explicit tag distinguishing the two rule kinds: an entry with -`body` is an expression rule, one with `type` and `init` a type rule. +There is no explicit tag distinguishing the two rule kinds: an entry with `body` +is an expression rule, one with `type` and `init` a type rule. ## In-memory form `cpp2rust` mirrors the Rules IR in C++ structs of the same names, defined in [`cpp2rust/converter/translation_rule.h`][translation-rule-h]. -`TranslationRule::Load` reads one -module directory (the `ir_*.json` files described above) and produces two -maps keyed by rule name, one holding [`ExprRule`]s and one holding -[`TypeRule`]s: - -[translation-rule-h]: https://github.com/cpp2rust/cpp2rust/blob/master/cpp2rust/converter/translation_rule.h -[`ExprRule`]: https://github.com/cpp2rust/cpp2rust/blob/master/cpp2rust/converter/translation_rule.h#L71 -[`TypeRule`]: https://github.com/cpp2rust/cpp2rust/blob/master/cpp2rust/converter/translation_rule.h#L84 - -* An `ExprRule` holds the body +`TranslationRule::Load` reads one module directory (the `ir_*.json` files +described above) and produces two maps keyed by rule name, one holding +[`ExprRule`]s and one holding [`TypeRule`]s: + +[translation-rule-h]: + https://github.com/cpp2rust/cpp2rust/blob/master/cpp2rust/converter/translation_rule.h +[`ExprRule`]: + https://github.com/cpp2rust/cpp2rust/blob/master/cpp2rust/converter/translation_rule.h#L71 +[`TypeRule`]: + https://github.com/cpp2rust/cpp2rust/blob/master/cpp2rust/converter/translation_rule.h#L84 + +- An `ExprRule` holds the body [fragments](#target-ir-ir_unsafejson--ir_refcountjson), the parameter and - return `TypeInfo`s, and the two rule-level flags (`multi_statement` and `is_extern`). The - name-keyed Rules IR maps become - positional vectors: parameter `aN` is entry N of `params`, generic `TN` is - entry N-1 of `generics` (each entry being the bound list). Rules support - at most 9 generic parameters (`kMaxGenerics`). -* A `TypeRule` holds the mapped type's `TypeInfo` and the `initializer` + return `TypeInfo`s, and the two rule-level flags (`multi_statement` and + `is_extern`). The name-keyed Rules IR maps become positional vectors: + parameter `aN` is entry N of `params`, generic `TN` is entry N-1 of `generics` + (each entry being the bound list). Rules support at most 9 generic parameters + (`kMaxGenerics`). +- A `TypeRule` holds the mapped type's `TypeInfo` and the `initializer` expression. The same struct also represents the built-in type mappings - (scalars, pointers, ...) that the loader registers directly in code, - without any Rules IR behind them: for example, `int` maps to `i32`, and - `int *` to `*mut i32` in the unsafe model or `Ptr` in the refcount - model. + (scalars, pointers, ...) that the loader registers directly in code, without + any Rules IR behind them: for example, `int` maps to `i32`, and `int *` to + `*mut i32` in the unsafe model or `Ptr` in the refcount model. Both also carry `src`, the canonical C++ signature attached from -[`ir_src.json`](#source-ir-ir_srcjson); it is the key the rule is matched -by. +[`ir_src.json`](#source-ir-ir_srcjson); it is the key the rule is matched by. How the loader finds the Rules IR directory, overlays the refcount model on the unsafe one, and indexes the loaded rules for matching is covered in diff --git a/docs/src/rules/loading.md b/docs/src/rules/loading.md index 2e0cd18b..8865a8af 100644 --- a/docs/src/rules/loading.md +++ b/docs/src/rules/loading.md @@ -2,23 +2,22 @@ ## Finding the rules directory -`cpp2rust` takes the Rules IR directory via `--rules `. If the flag is omitted -it tries `./rules` and then `/../rules`, accepting the first -candidate that contains (recursively) a subdirectory with `ir_src.json` plus -`ir_unsafe.json` or `ir_refcount.json`. Since the build writes the Rules IR to -`/rules` and the binary lands in `/bin`, the default resolution +`cpp2rust` takes the Rules IR directory via `--rules `. If the flag is +omitted it tries `./rules` and then `/../rules`, accepting the +first candidate that contains (recursively) a subdirectory with `ir_src.json` +plus `ir_unsafe.json` or `ir_refcount.json`. Since the build writes the Rules IR +to `/rules` and the binary lands in `/bin`, the default resolution picks up the generated Rules IR without any flags. ## Loading Rules are loaded once per process by `Mapper::LoadTranslationRules`: -1. Built-in type mappings are registered first. Every scalar is mapped - with its width taken from the host (`int` maps to `i32`, - `unsigned long` to `u64`), together with its `const` form and its - pointer forms: `*mut`/`*const` in the unsafe model, `Ptr` in the - refcount model, where constness is dropped. `char` maps to - `libc::c_char` in the unsafe model and to `u8` in refcount; +1. Built-in type mappings are registered first. Every scalar is mapped with its + width taken from the host (`int` maps to `i32`, `unsigned long` to `u64`), + together with its `const` form and its pointer forms: `*mut`/`*const` in the + unsafe model, `Ptr` in the refcount model, where constness is dropped. + `char` maps to `libc::c_char` in the unsafe model and to `u8` in refcount; `size_t`/`ssize_t` map to `usize`/`isize`; `void *` maps to `*mut ::libc::c_void`, or in refcount to `AnyPtr`. 2. Every subdirectory of the rules directory is loaded with @@ -28,76 +27,73 @@ Rules are loaded once per process by `Mapper::LoadTranslationRules`: Loading is strict: an `ir_src.json` entry with no matching target rule is a fatal error (this is what catches mismatched `#if`/`#[cfg]` gating), every -generic declared by a rule must appear in its C++ signature, and two type -rules for the same C++ type are rejected. +generic declared by a rule must appear in its C++ signature, and two type rules +for the same C++ type are rejected. ## Matching -Loaded rules are indexed in two multimaps, one for expression rules and one -for type rules. The multimap key is only a coarse bucket for collecting -candidate rules; whether a candidate actually matches is decided by -[the matching engine](./matching.md). The bucket key is derived from the -C++ signature: +Loaded rules are indexed in two multimaps, one for expression rules and one for +type rules. The multimap key is only a coarse bucket for collecting candidate +rules; whether a candidate actually matches is decided by +[the matching engine](./matching.md). The bucket key is derived from the C++ +signature: -* For expressions, the qualified function name with the return type, the +- For expressions, the qualified function name with the return type, the parameter list, and all template arguments stripped, so the rule for `_Bool std::vector::empty() const` lands in the `std::vector::empty` bucket. -* For types, the text up to the first `<`, so all `std::vector<...>` rules - land in the `std::vector` bucket. +- For types, the text up to the first `<`, so all `std::vector<...>` rules land + in the `std::vector` bucket. During translation, the converter prints the construct it encounters with the -same canonical printer used by `cpp-rule-preprocessor`, which is what makes -the two sides comparable: +same canonical printer used by `cpp-rule-preprocessor`, which is what makes the +two sides comparable: -* Functions and methods print as +- Functions and methods print as ` ([, ...])[ const][ volatile][ &|&&]`. -* Enum constants and global variables print as their qualified name. -* Integer literals expanded from a macro print as the macro name. +- Enum constants and global variables print as their qualified name. +- Integer literals expanded from a macro print as the macro name. All rules in the matching bucket are then unified against this string by -[the matching engine](./matching.md), which binds `T1`...`T9` to the -concrete types at the use site and picks the most specific rule when -several match. Type lookups first try the sugar-preserving spelling (so a -rule can match `size_t` as written) and retry with the desugared type on -failure. +[the matching engine](./matching.md), which binds `T1`...`T9` to the concrete +types at the use site and picks the most specific rule when several match. Type +lookups first try the sugar-preserving spelling (so a rule can match `size_t` as +written) and retry with the desugared type on failure. -Running `cpp2rust` with `--verbose` logs every lookup and the rule it -matched, which is the quickest way to see why a rule does or does not -fire. +Running `cpp2rust` with `--verbose` logs every lookup and the rule it matched, +which is the quickest way to see why a rule does or does not fire. ## Application When a rule matches, the converter walks its body fragments and emits: -* `text` fragments verbatim, -* `placeholder` fragments as the translated call-site argument. How the - argument is emitted depends on the placeholder's access and on whether the - argument and the declared parameter type are pointers: - * Read access emits the argument as a plain value, with an implicit - numeric cast when the parameter type asks for one. - * Write access emits the argument as an lvalue. - * Move access wraps the argument in `std::mem::take(&mut ...)`; - temporaries are moved as-is. - * If the rule declares a pointer parameter but the argument is not a - pointer, the converter takes a fresh pointer to it, - [materializing a temporary](../codegen/temporaries.md) when the - argument has no address of its own. For example, - the refcount `std::max` rule declares `Ptr` parameters, since the - C++ side takes `const T1 &` and the refcount model - [translates references as `Ptr`](../codegen/pointers.md), so - `std::max(x1, x2)` on plain locals - substitutes `x1.as_pointer()` and `x2.as_pointer()` for the - placeholders, while `std::max(30, 40)` first materializes `__tmp_0` - and `__tmp_1` values for the literals and points into those. - * If the receiver argument is a pointer but the rule expects a value, - the converter dereferences it; this only happens for receivers, not - for ordinary arguments. -* `generic` fragments as the Rust mapping of the bound C++ type, -* `va_args` fragments as the converted variadic tail, -* `method_call` fragments as receiver followed by body, possibly rewritten - (see [Rule Rewriting](./rewriting.md)). - -Multi-statement bodies are wrapped in `{ }` so they remain a single -expression. Rules for user-defined C++ types are injected through the same -mechanism at translation time. +- `text` fragments verbatim, +- `placeholder` fragments as the translated call-site argument. How the argument + is emitted depends on the placeholder's access and on whether the argument and + the declared parameter type are pointers: + - Read access emits the argument as a plain value, with an implicit numeric + cast when the parameter type asks for one. + - Write access emits the argument as an lvalue. + - Move access wraps the argument in `std::mem::take(&mut ...)`; temporaries + are moved as-is. + - If the rule declares a pointer parameter but the argument is not a pointer, + the converter takes a fresh pointer to it, + [materializing a temporary](../codegen/temporaries.md) when the argument has + no address of its own. For example, the refcount `std::max` rule declares + `Ptr` parameters, since the C++ side takes `const T1 &` and the refcount + model [translates references as `Ptr`](../codegen/pointers.md), so + `std::max(x1, x2)` on plain locals substitutes `x1.as_pointer()` and + `x2.as_pointer()` for the placeholders, while `std::max(30, 40)` first + materializes `__tmp_0` and `__tmp_1` values for the literals and points into + those. + - If the receiver argument is a pointer but the rule expects a value, the + converter dereferences it; this only happens for receivers, not for ordinary + arguments. +- `generic` fragments as the Rust mapping of the bound C++ type, +- `va_args` fragments as the converted variadic tail, +- `method_call` fragments as receiver followed by body, possibly rewritten (see + [Rule Rewriting](./rewriting.md)). + +Multi-statement bodies are wrapped in `{ }` so they remain a single expression. +Rules for user-defined C++ types are injected through the same mechanism at +translation time. diff --git a/docs/src/rules/matching.md b/docs/src/rules/matching.md index d31c522e..faee2222 100644 --- a/docs/src/rules/matching.md +++ b/docs/src/rules/matching.md @@ -1,40 +1,36 @@ # The Matching Engine [Loading and Matching](./loading.md) collects the candidate rules for a -construct from a bucket; each candidate's source signature is then -*unified* against the printed construct. The signature is treated as a -template whose `T1`...`T9` slots capture concrete types: -`std::vector::vector()` unifies with `std::vector::vector()` by -binding `T1 = int`. +construct from a bucket; each candidate's source signature is then _unified_ +against the printed construct. The signature is treated as a template whose +`T1`...`T9` slots capture concrete types: `std::vector::vector()` unifies +with `std::vector::vector()` by binding `T1 = int`. Unification works on the two strings: -* Whitespace differences are ignored. -* A `TN` slot captures up to the next literal text of the pattern, found - at the same `<>`/`()`/`[]` nesting depth. This is how `T1` captures all - of `std::map` in `std::vector>` without - stopping at the inner comma. -* A `TN` that appears again must match its first capture exactly. -* The whole printed string must be consumed; trailing text fails the - match. -* Slots may stay unbound (a pattern can use `T2` without `T1`). - -A rule matches if unification succeeds. When several rules in the bucket -match, the one with the longest source signature wins, so more specific -rules take precedence; between equally long signatures the choice is -unspecified. +- Whitespace differences are ignored. +- A `TN` slot captures up to the next literal text of the pattern, found at the + same `<>`/`()`/`[]` nesting depth. This is how `T1` captures all of + `std::map` in `std::vector>` without stopping at + the inner comma. +- A `TN` that appears again must match its first capture exactly. +- The whole printed string must be consumed; trailing text fails the match. +- Slots may stay unbound (a pattern can use `T2` without `T1`). + +A rule matches if unification succeeds. When several rules in the bucket match, +the one with the longest source signature wins, so more specific rules take +precedence; between equally long signatures the choice is unspecified. ## Bucket keys -The bucket keys described in -[Loading and Matching](./loading.md#matching) have two special cases: -array types bucket by the text after the first `[` rather than the text -before a `<`, and `operator()` rules are cut at the operator's own +The bucket keys described in [Loading and Matching](./loading.md#matching) have +two special cases: array types bucket by the text after the first `[` rather +than the text before a `<`, and `operator()` rules are cut at the operator's own parentheses, so their key ends at `...::operator`. ## Instantiating the target Captures are C++ spellings. Before being substituted into the rule's Rust -fragments, each capture is itself mapped through the type rules, -recursively, so `T1 = std::vector` substitutes as `Vec`. A -captured type with no type rule of its own is an error. +fragments, each capture is itself mapped through the type rules, recursively, so +`T1 = std::vector` substitutes as `Vec`. A captured type with no type +rule of its own is an error. diff --git a/docs/src/rules/overview.md b/docs/src/rules/overview.md index dd321667..6e3d008e 100644 --- a/docs/src/rules/overview.md +++ b/docs/src/rules/overview.md @@ -1,16 +1,16 @@ # Overview -Translation rules describe how C++ library APIs are mapped to Rust. -Each rule module lives in the `rules/` directory and pairs a C++ source file -(`src.cpp`) with its Rust translation for each model (`tgt_unsafe.rs` and +Translation rules describe how C++ library APIs are mapped to Rust. Each rule +module lives in the `rules/` directory and pairs a C++ source file (`src.cpp`) +with its Rust translation for each model (`tgt_unsafe.rs` and `tgt_refcount.rs`). -Every rule is expressed as ordinary, compilable C++ and Rust source code, -free of anything platform dependent. Both sides are run through real -compilers at build time, so a rule that does not compile fails the build, and -the platform-specific spellings (for example `bool` canonicalizing to -`_Bool`) are derived by the compiler on the host rather than written by hand. -The same rule sources work on every platform `cpp2rust` builds on. +Every rule is expressed as ordinary, compilable C++ and Rust source code, free +of anything platform dependent. Both sides are run through real compilers at +build time, so a rule that does not compile fails the build, and the +platform-specific spellings (for example `bool` canonicalizing to `_Bool`) are +derived by the compiler on the host rather than written by hand. The same rule +sources work on every platform `cpp2rust` builds on. Rules go through a build-time compilation pipeline before `cpp2rust` can use them: @@ -18,29 +18,28 @@ them: 1. You author a rule module: C++ patterns in `src.cpp` and Rust targets in `tgt_unsafe.rs` / `tgt_refcount.rs`. 2. At build time, two preprocessors compile the module into Rules IR under - `/rules//`: - `cpp-rule-preprocessor` compiles the C++ side into `ir_src.json`, and - `rule-preprocessor` compiles the Rust side into `ir_unsafe.json` and - `ir_refcount.json`. + `/rules//`: `cpp-rule-preprocessor` compiles the C++ side into + `ir_src.json`, and `rule-preprocessor` compiles the Rust side into + `ir_unsafe.json` and `ir_refcount.json`. 3. At startup, `cpp2rust` loads the Rules IR files and indexes the rules by the canonical signature of the C++ construct they match. The rest of this part covers each stage: -* [Rule Format](./format.md): the files that make up a rule module and how - the two models are layered. -* [Writing Rules](./writing-rules.md): how to write rules for functions, +- [Rule Format](./format.md): the files that make up a rule module and how the + two models are layered. +- [Writing Rules](./writing-rules.md): how to write rules for functions, methods, operators, types, constants, and variadics. -* [Compat Shims](./compat.md): how macro-based libc APIs like `errno` and +- [Compat Shims](./compat.md): how macro-based libc APIs like `errno` and `FD_SET` are rewritten into matchable function calls. -* [Conventions](./conventions.md): naming and style conventions rule authors +- [Conventions](./conventions.md): naming and style conventions rule authors must follow. -* [The Rule Preprocessors](./preprocessors.md): the two build-time tools that +- [The Rule Preprocessors](./preprocessors.md): the two build-time tools that compile rules to the Rules IR. -* [The Rules IR](./ir.md): the JSON format the preprocessors emit. -* [Loading and Matching](./loading.md): how `cpp2rust` loads the Rules IR and +- [The Rules IR](./ir.md): the JSON format the preprocessors emit. +- [Loading and Matching](./loading.md): how `cpp2rust` loads the Rules IR and matches rules against the input AST. -* [The Matching Engine](./matching.md): how a candidate rule's signature - is unified against the input. -* [Rule Rewriting](./rewriting.md): how rule bodies are adapted at - application time, in particular the `with_mut` rewrite. +- [The Matching Engine](./matching.md): how a candidate rule's signature is + unified against the input. +- [Rule Rewriting](./rewriting.md): how rule bodies are adapted at application + time, in particular the `with_mut` rewrite. diff --git a/docs/src/rules/preprocessors.md b/docs/src/rules/preprocessors.md index ad18ed3c..ffff711b 100644 --- a/docs/src/rules/preprocessors.md +++ b/docs/src/rules/preprocessors.md @@ -3,8 +3,8 @@ Two build-time tools compile rule modules into the [Rules IR](./ir.md) that `cpp2rust` loads at runtime. Both write into `/rules//`: -* `cpp-rule-preprocessor` compiles `src.cpp`/`src.c` into `ir_src.json`. -* `rule-preprocessor` compiles `tgt_unsafe.rs`/`tgt_refcount.rs` into +- `cpp-rule-preprocessor` compiles `src.cpp`/`src.c` into `ir_src.json`. +- `rule-preprocessor` compiles `tgt_unsafe.rs`/`tgt_refcount.rs` into `ir_unsafe.json`/`ir_refcount.json`. The C++ side is keyed by resolved callee signatures; the Rust side by rule @@ -19,49 +19,46 @@ once per rule directory: cpp-rule-preprocessor --dir rules/string --out /rules/string/ir_src.json ``` -Extra compiler flags can be passed with repeated `--cxxflags` options, -though CMake, which invokes the tool for every rule module via the -`preprocess-cpp-rules` target, passes none. Note that the parent directory -of `--out` must already exist; CMake creates it before each invocation, so -a manual run must do the same. +Extra compiler flags can be passed with repeated `--cxxflags` options, though +CMake, which invokes the tool for every rule module via the +`preprocess-cpp-rules` target, passes none. Note that the parent directory of +`--out` must already exist; CMake creates it before each invocation, so a manual +run must do the same. Rule sources are always compiled with the fixed flag set from -`cpp2rust/compat/platform_flags.h`, the same one used to parse input -programs (see [Compat Shims](./compat.md)). There is no compilation -database and no `-std=` flag: the language is chosen by clang from the -file extension, and `src.c` is processed before `src.cpp`. +`cpp2rust/compat/platform_flags.h`, the same one used to parse input programs +(see [Compat Shims](./compat.md)). There is no compilation database and no +`-std=` flag: the language is chosen by clang from the file extension, and +`src.c` is processed before `src.cpp`. For each rule it: 1. Validates that every `fN` body is exactly one `return` statement. -2. Resolves the *callee* of the returned expression. For non-template rules - this is just the called declaration. For template rules the callee is - unresolved, so the tool instantiates the rule's template parameters with - synthesized dummy types and runs overload resolution to find the function - the rule refers to. +2. Resolves the _callee_ of the returned expression. For non-template rules this + is just the called declaration. For template rules the callee is unresolved, + so the tool instantiates the rule's template parameters with synthesized + dummy types and runs overload resolution to find the function the rule refers + to. 3. Prints the resolved declaration as a canonical signature string: ` ([, ...])[ const][ volatile][ &|&&]`, - where `, ...` appears for C-variadic functions and the trailing - qualifiers only for methods. For `tN` aliases it prints the underlying - type. - -The output is a flat JSON object mapping rule names to these signature -strings. - -The printer preserves typedef sugar instead of desugaring it: `size_t` -prints as `size_t`, not `unsigned long`, which is what lets it map to -`usize` while plain `unsigned long` maps to `u64` (for `tN` aliases this -preservation is explicit; inside function signatures the spelling survives -through the printing policy). Integer literals expanded from a macro are -recorded as the macro *name*, which is how -[constant rules](./writing-rules.md#enum-values-constants-and-macros) like the -`O_CREAT` one match by name. + where `, ...` appears for C-variadic functions and the trailing qualifiers + only for methods. For `tN` aliases it prints the underlying type. + +The output is a flat JSON object mapping rule names to these signature strings. + +The printer preserves typedef sugar instead of desugaring it: `size_t` prints as +`size_t`, not `unsigned long`, which is what lets it map to `usize` while plain +`unsigned long` maps to `u64` (for `tN` aliases this preservation is explicit; +inside function signatures the spelling survives through the printing policy). +Integer literals expanded from a macro are recorded as the macro _name_, which +is how [constant rules](./writing-rules.md#enum-values-constants-and-macros) +like the `O_CREAT` one match by name. ## rule-preprocessor A Rust binary crate built with the nightly toolchain because it links the -compiler's own libraries (`rustc_driver`, `rustc_middle`, ...). It processes -the whole rules tree in one invocation: +compiler's own libraries (`rustc_driver`, `rustc_middle`, ...). It processes the +whole rules tree in one invocation: ```bash CARGO_TARGET_DIR= cargo +nightly run --release \ @@ -70,40 +67,37 @@ CARGO_TARGET_DIR= cargo +nightly run --release \ The environment is load-bearing: -* `CARGO_TARGET_DIR` must be set (the tool aborts otherwise): the rlibs of - the rule dependencies (`libcc2rs`, `libc`, `nix`, ...) are looked up in - `$CARGO_TARGET_DIR//deps`, which the `cargo run` above - populates. The 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 `ninja - > clean` to fix this. -* The sysroot comes from running `rustc --print=sysroot`, so the `rustc` on +- `CARGO_TARGET_DIR` must be set (the tool aborts otherwise): the rlibs of the + rule dependencies (`libcc2rs`, `libc`, `nix`, ...) are looked up in + `$CARGO_TARGET_DIR//deps`, which the `cargo run` above populates. The + 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 + > `ninja clean` to fix this. + +- The sysroot comes from running `rustc --print=sysroot`, so the `rustc` on `PATH` must be the same nightly the preprocessor was built with (running through `cargo +nightly run` guarantees this). -* `rules-dir` is optional and defaults to the relative path `../rules`, - resolved against the *current working directory* of the process. +- `rules-dir` is optional and defaults to the relative path `../rules`, resolved + against the _current working directory_ of the process. -CMake drives all of this via the `preprocess-rust-rules` target: it first -builds the `rules` crate with the stable toolchain (which also regenerates +CMake drives all of this via the `preprocess-rust-rules` target: it first builds +the `rules` crate with the stable toolchain (which also regenerates `rules/src/modules.rs`), then runs the preprocessor with -`CARGO_TARGET_DIR=/target_preprocessor`. That initial `cargo build` -of the `rules` crate is what actually gates the build on rule bodies -type-checking (see below). The preprocessor works in two phases. +`CARGO_TARGET_DIR=/target_preprocessor`. That initial `cargo build` of +the `rules` crate is what actually gates the build on rule bodies type-checking +(see below). The preprocessor works in two phases. **Phase 1, syntactic.** Each `tgt_*.rs` file is parsed with rust-analyzer's -parser, and functions whose `#[cfg]` does not match the host are dropped. -Every function body is then turned into a list of *fragments*, whose kinds -are described in -[The Rules IR](./ir.md#target-ir-ir_unsafejson--ir_refcountjson). The -fragmentation is mainly concerned with how the rule's arguments are used: -references to parameters and generics become placeholder and generic -fragments, while source text that does not involve an argument is kept -as-is. - -Each placeholder is tagged with an *access*: read, write, or move. Some uses +parser, and functions whose `#[cfg]` does not match the host are dropped. Every +function body is then turned into a list of _fragments_, whose kinds are +described in [The Rules IR](./ir.md#target-ir-ir_unsafejson--ir_refcountjson). +The fragmentation is mainly concerned with how the rule's arguments are used: +references to parameters and generics become placeholder and generic fragments, +while source text that does not involve an argument is kept as-is. + +Each placeholder is tagged with an _access_: read, write, or move. Some uses give the access away syntactically (`&mut a0` is a write); those that do not, typically method-call receivers and arguments, are left as `unknown` for phase 2. This phase also applies the two @@ -111,17 +105,17 @@ phase 2. This phase also applies the two support rule rewriting. **Phase 2, semantic.** The preprocessor compiles the `rules` crate in-process -with `rustc` and walks the typed HIR. This gives it the real signature of -every callee, which resolves the `unknown` accesses: passing to a -`&mut`/`*mut` parameter is a write, to a `&`/`*const` parameter a read, and -to `std::mem::take` a move. For type rules it also records which of the -nine derivable standard traits (`Copy`, `Clone`, `Debug`, `Default`, -`PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`) the mapped type implements. -A placeholder still `unknown` after this phase fails the build. +with `rustc` and walks the typed HIR. This gives it the real signature of every +callee, which resolves the `unknown` accesses: passing to a `&mut`/`*mut` +parameter is a write, to a `&`/`*const` parameter a read, and to +`std::mem::take` a move. For type rules it also records which of the nine +derivable standard traits (`Copy`, `Clone`, `Debug`, `Default`, `PartialEq`, +`Eq`, `PartialOrd`, `Ord`, `Hash`) the mapped type implements. A placeholder +still `unknown` after this phase fails the build. The preprocessor assumes the `rules` crate is buildable, which the earlier -`cargo build` of the crate ensures; errors from the in-process compilation -are therefore only reported as a warning. +`cargo build` of the crate ensures; errors from the in-process compilation are +therefore only reported as a warning. The result is one `ir_.json` per input file, keyed by rule name. The output file name is derived from the input file name (`tgt_unsafe.rs` becomes diff --git a/docs/src/rules/rewriting.md b/docs/src/rules/rewriting.md index 33bba679..7ce8efda 100644 --- a/docs/src/rules/rewriting.md +++ b/docs/src/rules/rewriting.md @@ -3,8 +3,9 @@ A rule body is written against idiomatic Rust types: a rule that mutates a vector declares its parameter as `&mut Vec`. But in the refcount model the call-site argument is usually a `Ptr>`, and a `Ptr` -[cannot produce a long-lived `&mut`](../codegen/pointers.md). Instead of forcing every rule to handle pointers, the code -generator *rewrites* the rule body at application time. +[cannot produce a long-lived `&mut`](../codegen/pointers.md). Instead of forcing +every rule to handle pointers, the code generator _rewrites_ the rule body at +application time. ## The `with_mut` rewrite @@ -16,20 +17,19 @@ impl Ptr { } ``` -which checks the pointer, borrows the pointee mutably, and runs the closure -on it (with an immutable sibling `Ptr::with`). The refcount converter uses it -to bridge the gap; the unsafe converter never rewrites and simply emits -receiver followed by body. The rewrite fires when all three hold: +which checks the pointer, borrows the pointee mutably, and runs the closure on +it (with an immutable sibling `Ptr::with`). The refcount converter uses it to +bridge the gap; the unsafe converter never rewrites and simply emits receiver +followed by body. The rewrite fires when all three hold: -1. The rule body fragment is a method call whose receiver contains a - placeholder (the preprocessor splits every method call into receiver and - body fragments precisely to enable this). If the receiver contains - several placeholders, the first one is used. +1. The rule body fragment is a method call whose receiver contains a placeholder + (the preprocessor splits every method call into receiver and body fragments + precisely to enable this). If the receiver contains several placeholders, the + first one is used. 2. The receiver placeholder's access is write or move, i.e. the method takes - `&mut self` or the rule mutates the parameter. Read access does not need - the rewrite, since a read can go through a - [`StrongPtr`](../codegen/pointers.md) obtained with `Ptr::upgrade`, or - through a `read()` copy. + `&mut self` or the rule mutates the parameter. Read access does not need the + rewrite, since a read can go through a [`StrongPtr`](../codegen/pointers.md) + obtained with `Ptr::upgrade`, or through a `read()` copy. 3. The call-site argument is a pointer, or an expression of reference type (which includes an operator call returning a reference). @@ -39,8 +39,7 @@ The rule's method call `a0.method(...)` is then emitted as ptr.with_mut(|__v: | __v.method(...)) ``` -For example, the `push_back` rule is written as an ordinary `&mut` method -call: +For example, the `push_back` rule is written as an ordinary `&mut` method call: ```rust fn f21(a0: &mut Vec, a1: T1) { ... a0.push(...) } @@ -58,44 +57,42 @@ fails and no closure is emitted; the same rule produces a direct call like `(*v2.borrow_mut()).push(0);`. The rewrite applies to pointer dereferences (`p->push_back(20)`) and to -reference usages (`r.push_back(20)` with `std::vector &r = *p`); both -are [translated as a `Ptr`](../codegen/pointers.md), and that `Ptr` is what +reference usages (`r.push_back(20)` with `std::vector &r = *p`); both are +[translated as a `Ptr`](../codegen/pointers.md), and that `Ptr` is what `with_mut` is called on. -When the pointee is itself a boxed value (`Value`, i.e. -`Rc>`), the closure takes `&mut Value` and an extra borrow is -inserted. This is the case for nested containers: the refcount model -translates `std::vector>` as `Vec>>` so -that each element has interior mutability of its own, and a `Ptr` to an -inner vector therefore points at a `Value>`, not a `Vec`: +When the pointee is itself a boxed value (`Value`, i.e. `Rc>`), +the closure takes `&mut Value` and an extra borrow is inserted. This is the +case for nested containers: the refcount model translates +`std::vector>` as `Vec>>` so that each element +has interior mutability of its own, and a `Ptr` to an inner vector therefore +points at a `Value>`, not a `Vec`: ```rust ptr.with_mut(|__v: &mut Value>| (*__v.borrow_mut()).push(20)) ``` -The closure type is built from the C++ argument's type, not the rule's -declared parameter type. +The closure type is built from the C++ argument's type, not the rule's declared +parameter type. ## The read-access counterpart -For read access the converter does not emit a closure. A pointer receiver -whose rule parameter is a value or `&` type is simply dereferenced -(`p.read()` or `(*p.upgrade().deref())`); conversely, if the rule declares a -`Ptr` parameter but the argument is not a pointer, the converter inserts an -`as_pointer()` cast or -[materializes a temporary](../codegen/temporaries.md). +For read access the converter does not emit a closure. A pointer receiver whose +rule parameter is a value or `&` type is simply dereferenced (`p.read()` or +`(*p.upgrade().deref())`); conversely, if the rule declares a `Ptr` parameter +but the argument is not a pointer, the converter inserts an `as_pointer()` cast +or [materializes a temporary](../codegen/temporaries.md). ## Preprocessor-side rewrites Two rewrites in `rule-preprocessor` exist to make the `with_mut` rewrite possible. Both apply only to `&mut` parameters: -* A `*` deref in front of the parameter is dropped from the body, since - the substituted argument is already an lvalue or pointer expression. -* `std::mem::take(&mut aN)` collapses to a bare placeholder, so the - converter can re-express the move against the actual argument (for a - pointer that becomes `std::mem::take(&mut )` on the borrowed - pointee). The spelling must be exactly this fully qualified form: - `mem::take` or an imported `take` is not rewritten. The collapsed - placeholder's access is left `unknown` in phase 1; phase 2 resolves the - `std::mem::take` call to a move. +- A `*` deref in front of the parameter is dropped from the body, since the + substituted argument is already an lvalue or pointer expression. +- `std::mem::take(&mut aN)` collapses to a bare placeholder, so the converter + can re-express the move against the actual argument (for a pointer that + becomes `std::mem::take(&mut )` on the borrowed pointee). The spelling + must be exactly this fully qualified form: `mem::take` or an imported `take` + is not rewritten. The collapsed placeholder's access is left `unknown` in + phase 1; phase 2 resolves the `std::mem::take` call to a move. diff --git a/docs/src/rules/writing-rules.md b/docs/src/rules/writing-rules.md index 842f43c6..aa49121c 100644 --- a/docs/src/rules/writing-rules.md +++ b/docs/src/rules/writing-rules.md @@ -1,9 +1,9 @@ # Writing Rules -This page shows how to write rules for each kind of C++ construct. In every -case the recipe is the same: write an `fN` (or `tN`) function on the C++ side -whose single `return` statement exercises the construct, and a same-named -function on the Rust side giving the translation. +This page shows how to write rules for each kind of C++ construct. In every case +the recipe is the same: write an `fN` (or `tN`) function on the C++ side whose +single `return` statement exercises the construct, and a same-named function on +the Rust side giving the translation. ## Free functions @@ -30,18 +30,18 @@ fn f1(a0: Ptr, a1: Ptr) -> i32 { } ``` -Rule bodies may be arbitrarily complex; multi-statement bodies are wrapped in -a block when spliced into the output. +Rule bodies may be arbitrarily complex; multi-statement bodies are wrapped in a +block when spliced into the output. -`return` statements are prohibited in Rust rule bodies (the preprocessor -rejects them); produce the result as a tail expression instead. The body is -not emitted as a function of its own: it is spliced inline into the generated -code as a block expression, so a `return` would not end the rule, it would -return from whatever generated function the rule happens to be expanded in. +`return` statements are prohibited in Rust rule bodies (the preprocessor rejects +them); produce the result as a tail expression instead. The body is not emitted +as a function of its own: it is spliced inline into the generated code as a +block expression, so a `return` would not end the rule, it would return from +whatever generated function the rule happens to be expanded in. -When the pattern's type cannot be named, the rule uses an `auto` return -type: `rules/iomanip` writes `auto f1(int n) { return std::setw(n); }` -because `std::setw` returns an unspecified type. +When the pattern's type cannot be named, the rule uses an `auto` return type: +`rules/iomanip` writes `auto f1(int n) { return std::setw(n); }` because +`std::setw` returns an unspecified type. ## Methods @@ -71,10 +71,9 @@ every instantiation: when the input program calls `size()` on, say, a ## Static member functions -A static member function is also written with a receiver parameter, which -exists only to name the class. The call site has no receiver argument, so -the Rust side drops it and numbers the remaining parameters from `a0`; here -there are none: +A static member function is also written with a receiver parameter, which exists +only to name the class. The call site has no receiver argument, so the Rust side +drops it and numbers the remaining parameters from `a0`; here there are none: ```cpp // rules/limits/src.cpp @@ -88,8 +87,7 @@ unsafe fn f1() -> T1 { } ``` -(`HasMinMax` is a helper trait defined alongside the rules in the same -file.) +(`HasMinMax` is a helper trait defined alongside the rules in the same file.) ## Constructors @@ -104,9 +102,9 @@ std::string f9(std::size_t n, char ch) { return std::string(n, ch); } Overloads that differ in value category are distinct rules too: `rules/vector` has separate rules for `push_back(const T1 &)` and `push_back(T1 &&)`. -No destructor rules exist so far: the STL and libc APIs covered by the -current rules have not needed any, since their types map to Rust types whose -`Drop` implementations already do the right thing. +No destructor rules exist so far: the STL and libc APIs covered by the current +rules have not needed any, since their types map to Rust types whose `Drop` +implementations already do the right thing. ## Operators @@ -126,18 +124,18 @@ bool f11(typename std::map::iterator a, ``` Post-increment is distinguished from pre-increment by the usual dummy `int` -parameter: `a0.operator++(a1)` versus `it.operator++()`. Conversion -operators use the same explicit syntax: `a0.operator T1 &()` in -`rules/functional` matches the conversion of a `std::reference_wrapper` -back to a reference. Field accesses are rules of their own, matched by the -field: `it->first` and `it->second` through iterators, plain `o.second` on -a pair (`rules/map`, `rules/pair`). +parameter: `a0.operator++(a1)` versus `it.operator++()`. Conversion operators +use the same explicit syntax: `a0.operator T1 &()` in `rules/functional` matches +the conversion of a `std::reference_wrapper` back to a reference. Field +accesses are rules of their own, matched by the field: `it->first` and +`it->second` through iterators, plain `o.second` on a pair (`rules/map`, +`rules/pair`). ## Callable arguments -A rule parameter may be a callable. Function pointers are spelled directly; -for a lambda, whose type cannot be written, the rule declares a file-scope -lambda and takes `decltype(lambda)`: +A rule parameter may be a callable. Function pointers are spelled directly; for +a lambda, whose type cannot be written, the rule declares a file-scope lambda +and takes `decltype(lambda)`: ```cpp // rules/algorithm/src.cpp @@ -155,20 +153,20 @@ where { ... } ``` -`T1` and `T2` are not template parameters here but file-scope helper -structs modelling an iterator and its value type; being named like -generics, they bind as `T1`/`T2` at the use site. The function pointer -version of the comparator is a separate rule (`f7`). +`T1` and `T2` are not template parameters here but file-scope helper structs +modelling an iterator and its value type; being named like generics, they bind +as `T1`/`T2` at the use site. The function pointer version of the comparator is +a separate rule (`f7`). ## Iterators -There is no iterator abstraction: an iterator type gets a type rule, and -every operation on it its own expression rule (`operator*`, `operator++`, +There is no iterator abstraction: an iterator type gets a type rule, and every +operation on it its own expression rule (`operator*`, `operator++`, `operator!=`, ...). What the type maps to is up to the rule: -`std::string::iterator` becomes a plain pointer (`*mut libc::c_char` -unsafe, `Ptr` refcount), while `std::map` iterators become the runtime -types `libcc2rs::UnsafeMapIterator`/`MapIterator`. Dependent iterator -types are named with `typename`: +`std::string::iterator` becomes a plain pointer (`*mut libc::c_char` unsafe, +`Ptr` refcount), while `std::map` iterators become the runtime types +`libcc2rs::UnsafeMapIterator`/`MapIterator`. Dependent iterator types are named +with `typename`: ```cpp // rules/map/src.cpp @@ -178,12 +176,12 @@ using t2 = typename std::map::const_iterator; ## Types -A type rule has two halves. On the C++ side, declare a type alias named `tN` -for the C++ type being mapped. On the Rust side, write a function with the -same name that takes no arguments: its *return type* is the Rust type that -the C++ type maps to, and its *body* is the default value the generated code -uses when it needs to construct one (e.g. for an uninitialized variable). -Reference and pointer variants of a type each get their own rule: +A type rule has two halves. On the C++ side, declare a type alias named `tN` for +the C++ type being mapped. On the Rust side, write a function with the same name +that takes no arguments: its _return type_ is the Rust type that the C++ type +maps to, and its _body_ is the default value the generated code uses when it +needs to construct one (e.g. for an uninitialized variable). Reference and +pointer variants of a type each get their own rule: ```cpp // rules/iostream/src.cpp @@ -209,17 +207,17 @@ fn t1() -> ::libc::stat { unsafe { std::mem::zeroed() } } fn t1() -> libcc2rs::Stat { Default::default() } ``` -A type rule may map to the sentinel type `libcc2rs::IgnoreRule`, meaning -"this model has no special mapping for the type"; the converter then falls -back to its normal type conversion. This is useful when only one model needs -a custom mapping: `rules/carray` maps multi-dimensional C arrays to nested -boxed slices in the refcount model, while its `tgt_unsafe.rs` targets are -`IgnoreRule` so the unsafe model keeps the default array conversion. +A type rule may map to the sentinel type `libcc2rs::IgnoreRule`, meaning "this +model has no special mapping for the type"; the converter then falls back to its +normal type conversion. This is useful when only one model needs a custom +mapping: `rules/carray` maps multi-dimensional C arrays to nested boxed slices +in the refcount model, while its `tgt_unsafe.rs` targets are `IgnoreRule` so the +unsafe model keeps the default array conversion. ## Enum values, constants, and macros -Constants are `fN` functions that take no arguments and return the constant, -one rule per value: +Constants are `fN` functions that take no arguments and return the constant, one +rule per value: ```cpp // rules/fcntl/src.cpp @@ -232,17 +230,16 @@ int f4(void) { return O_TRUNC; } unsafe fn f3() -> i32 { ::libc::O_CREAT } ``` -For macros that expand to integer literals, the preprocessor records the -*macro name* rather than the value, so `O_CREAT` in the input matches this -rule by name. Enum constants and global variables (e.g. `std::cout`) are -matched by their qualified name. A global and its address are separate -rules: `rules/iostream` maps both `std::cout` (`f1`) and `&std::cout` -(`f3`). +For macros that expand to integer literals, the preprocessor records the _macro +name_ rather than the value, so `O_CREAT` in the input matches this rule by +name. Enum constants and global variables (e.g. `std::cout`) are matched by +their qualified name. A global and its address are separate rules: +`rules/iostream` maps both `std::cout` (`f1`) and `&std::cout` (`f3`). Integer-literal macros are the only macros matchable directly. Macros whose expansions are platform internals with no stable callee, such as `errno` or -`FD_SET`, are first rewritten into calls to synthetic `cpp2rust_*` functions -by the [compat shims](./compat.md); rules then match the shim call. +`FD_SET`, are first rewritten into calls to synthetic `cpp2rust_*` functions by +the [compat shims](./compat.md); rules then match the shim call. ## Variadic functions @@ -254,10 +251,9 @@ variadic arguments to another call, so a rule like int f1(int a0, int a1, ...) { return fcntl(a0, a1, ...); } ``` -is not -expressible. A parameter pack can be forwarded (`args...`), which is exactly -what the rule body needs to do. The Rust side takes a trailing parameter that -must be typed `&[VaArg]` and named `va`: +is not expressible. A parameter pack can be forwarded (`args...`), which is +exactly what the rule body needs to do. The Rust side takes a trailing parameter +that must be typed `&[VaArg]` and named `va`: ```cpp // rules/fcntl/src.cpp @@ -272,14 +268,13 @@ int f1(int a0, int a1, Args... args) { fn f1(a0: i32, a1: i32, va: &[VaArg]) -> i32 { ... } ``` -Bodies read the arguments through the va-args API in `libcc2rs` -(`VaArg`, `VaList`, the `VaArgGet` accessors, `format_c`). +Bodies read the arguments through the va-args API in `libcc2rs` (`VaArg`, +`VaList`, the `VaArgGet` accessors, `format_c`). ## Passthrough rules When a call should be forwarded verbatim to the same-named function in Rust's -`libc` crate, the Rust target can be an `extern` declaration instead of a -body: +`libc` crate, the Rust target can be an `extern` declaration instead of a body: ```cpp // rules/fcntl/src.cpp @@ -319,14 +314,13 @@ unsafe fn f4() -> i32 { ``` The Rust preprocessor evaluates `#[cfg]` attributes against the host target -(only `target_os = linux|macos` and `target_arch = x86_64|x86` are accepted) -and drops non-matching rules. +(only `target_os = linux|macos` and `target_arch = x86_64|x86` are accepted) and +drops non-matching rules. -Mutually exclusive platform branches use `#elif` with disjoint rule -numbers: `rules/errno` defines `f91` to `f135` under `__linux__` and -`f136` to `f153` under `__APPLE__`. Feature-test macros a pattern needs must -come before the includes, as with `#define _GNU_SOURCE` in -`rules/socket/src.c`. +Mutually exclusive platform branches use `#elif` with disjoint rule numbers: +`rules/errno` defines `f91` to `f135` under `__linux__` and `f136` to `f153` +under `__APPLE__`. Feature-test macros a pattern needs must come before the +includes, as with `#define _GNU_SOURCE` in `rules/socket/src.c`. ## Pattern resolution limits @@ -334,11 +328,11 @@ The preprocessor resolves a template pattern by instantiating its template parameters with synthesized types ([The Rule Preprocessors](./preprocessors.md#cpp-rule-preprocessor)): -* A bare `T1` becomes an empty struct, so the pattern cannot use members, +- A bare `T1` becomes an empty struct, so the pattern cannot use members, operators, or nested types of `T1`. -* A parameter pack instantiates to the empty pack. -* A non-type parameter is pinned to the value `1`. +- A parameter pack instantiates to the empty pack. +- A non-type parameter is pinned to the value `1`. -Unqualified callees are looked up in `namespace std` first and in the -global scope only when `std` has no match, so an unqualified name that -exists in both resolves to the `std` one. +Unqualified callees are looked up in `namespace std` first and in the global +scope only when `std` has no match, so an unqualified name that exists in both +resolves to the `std` one. From b21118a0dc4af3382cdc3d341e37a820670e2d01 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Fri, 14 Aug 2026 12:39:30 +0100 Subject: [PATCH 12/30] Add plugins TODO in separate file --- docs/src/SUMMARY.md | 1 + docs/src/codegen/overview.md | 4 ---- docs/src/codegen/plugins.md | 3 +++ 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 docs/src/codegen/plugins.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index b935edd1..e11a8166 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -25,3 +25,4 @@ - [Overview](./codegen/overview.md) - [Pointers and References](./codegen/pointers.md) - [Temporary Materialization](./codegen/temporaries.md) +- [Translation Plugins](./codegen/plugins.md) diff --git a/docs/src/codegen/overview.md b/docs/src/codegen/overview.md index 908d56c5..fb3916d3 100644 --- a/docs/src/codegen/overview.md +++ b/docs/src/codegen/overview.md @@ -2,7 +2,3 @@ This part of the book documents the internals of the code generator: how the clang AST is traversed and how Rust code is emitted. - -TODO: document the converter plugin mechanism (`cpp2rust/converter/plugins/`), -which intercepts constructs ahead of the translation rules (currently -`emplace_back`). diff --git a/docs/src/codegen/plugins.md b/docs/src/codegen/plugins.md new file mode 100644 index 00000000..dbecbf3b --- /dev/null +++ b/docs/src/codegen/plugins.md @@ -0,0 +1,3 @@ +> TODO: document the converter plugin mechanism (`cpp2rust/converter/plugins/`), +> which intercepts constructs ahead of the translation rules (currently +> `emplace_back`). From 2a513400d45ac403d0bd79ee3603461cb912383b Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Fri, 14 Aug 2026 12:45:46 +0100 Subject: [PATCH 13/30] Add markdown formatting check --- .github/workflows/format.yml | 3 +++ .prettierrc | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 .prettierrc diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index ea243a85..5c6a93de 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -37,3 +37,6 @@ jobs: - name: Check format run: git diff --exit-code + + - name: Check markdown formatting + run: npx prettier@3.6.2 --check "docs/src/**/*.md" diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..cfae7e2e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "proseWrap": "always", + "printWidth": 80 +} From 8f9ed3c2e0daf9b8ac3c8c2ae69921e23e877b1b Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Fri, 14 Aug 2026 14:04:52 +0100 Subject: [PATCH 14/30] Add libcc2rs compat, io, and libc-shims --- docs/src/SUMMARY.md | 7 ++ docs/src/project/introduction.md | 10 +-- docs/src/runtime/compat.md | 32 ++++++++++ docs/src/runtime/io.md | 106 +++++++++++++++++++++++++++++++ docs/src/runtime/libc-shims.md | 102 +++++++++++++++++++++++++++++ docs/src/runtime/overview.md | 73 +++++++++++++++++++++ 6 files changed, 325 insertions(+), 5 deletions(-) create mode 100644 docs/src/runtime/compat.md create mode 100644 docs/src/runtime/io.md create mode 100644 docs/src/runtime/libc-shims.md create mode 100644 docs/src/runtime/overview.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index e11a8166..9ffd1f02 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -20,6 +20,13 @@ - [The Matching Engine](./rules/matching.md) - [Rule Rewriting](./rules/rewriting.md) +# The Runtime Library + +- [Overview](./runtime/overview.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) diff --git a/docs/src/project/introduction.md b/docs/src/project/introduction.md index ff987afb..e637a81a 100644 --- a/docs/src/project/introduction.md +++ b/docs/src/project/introduction.md @@ -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` type provided by -`libcc2rs`. `Ptr` 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` type provided by `libcc2rs`. `Ptr` models C pointer semantics, +including null, arithmetic, and aliasing, while satisfying Rust's borrow checker +through checked run-time operations. diff --git a/docs/src/runtime/compat.md b/docs/src/runtime/compat.md new file mode 100644 index 00000000..c9fb1627 --- /dev/null +++ b/docs/src/runtime/compat.md @@ -0,0 +1,32 @@ +# 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; +``` + +Refcount code reaches the operating system through the libc shims and nix, so +libc's `errno` is never read by this model. Instead, each rule is responsible +for writing the error code of a failed call into the refcounted value. Nothing +enforces this on the rule side, but a rule that skips the write breaks programs +that check `errno`, so it is part of writing a correct rule. + +`malloc_usable_size` is bound under one name for both platforms (the symbol is +`malloc_size` on macOS). diff --git a/docs/src/runtime/io.md b/docs/src/runtime/io.md new file mode 100644 index 00000000..0708850f --- /dev/null +++ b/docs/src/runtime/io.md @@ -0,0 +1,106 @@ +# I/O and Formatting + +The `io`, `format`, and `fd` modules support the stdio stream functions, +`printf`-style formatting, and descriptor-based I/O. + +## C Streams + +A C `FILE` is more than a file handle: it carries sticky end-of-file and error +flags that `feof` and `ferror` report long after the read that set them. +`std::fs::File` keeps no such state, so the refcount model translates `FILE *` +as a `Ptr`, a [libc shim](./libc-shims.md) that holds the file descriptor +together with these two flags. The standard streams are thread-local `CFile` +values over descriptors 0, 1, and 2, returned by `c_stdin`, `c_stdout`, and +`c_stderr`. + +In the unsafe model streams stay raw: `stdin_unsafe`, `stdout_unsafe`, and +`stderr_unsafe` return the process's `*mut libc::FILE` handles, whose symbol +names differ per platform (`stdin` on Linux, `__stdinp` on macOS). + +`fread` and `fwrite` exist in both models as named functions, because translated +programs take their address: + +```rust +pub fn fread_refcount(a0: AnyPtr, a1: usize, a2: usize, a3: Ptr) -> usize; +pub unsafe fn fread_unsafe(a0: *mut c_void, a1: usize, a2: usize, a3: *mut libc::FILE) -> usize; +``` + +The refcount variant reinterprets the destination as a byte array and reads +through the `CFile`; the unsafe variant forwards to `libc::fread`. + +## C++ Streams + +In the refcount model `cin`, `cout`, and `cerr` are translated as +`Ptr` values over duplicates of the standard descriptors. In the +unsafe model `cin_unsafe`, `cout_unsafe`, and `cerr_unsafe` return raw pointers +to thread-local `std::fs::File` values. C++ streams do not map fully onto +`std::fs::File`, so this translation may change in the future. + +## Formatting + +The code generator first translates the `printf` family into the idiomatic +`print!` and `println!` macros. That is not always possible: the target stream +may not be known at translation time, or the format string may be a runtime +value. For those cases, and for functions that format into a buffer such as +`snprintf`, the refcount model falls back to `format_c`; the unsafe model calls +libc directly. + +`format_c` evaluates a C format string against a slice of variadic arguments and +returns the formatted `String`: + +```rust +pub fn format_c(fmt: &str, va: &[VaArg]) -> String; +``` + +Parsing and rendering come from the `sprintf` crate. The integer, character, +string, and floating-point conversions are supported, and `%s` reads the +argument through the refcounted pointer as a Rust string. A malformed format +string or an argument of the wrong kind is a panic. + +## File descriptors + +Rust tracks descriptor ownership in the type system: an `OwnedFd` closes the +descriptor when dropped, and a `BorrowedFd` grants temporary access to one. C +has no such distinction: a descriptor is a plain `int`, mixed freely with +integer arithmetic, so the translator cannot tell which `int` values are +descriptors. The refcount model therefore leaves descriptors as integers in the +translated program and keeps the ownership in one place, the thread-local +`FdRegistry`, a table from each integer to the open descriptor it names. + +The registry follows the descriptor's life. When a rule opens a file, +`FdRegistry::register` stores the resulting `OwnedFd` and hands the program its +raw number. When a rule performs I/O on that number, `FdRegistry::with_fd` looks +the entry up and lends it out as a `BorrowedFd` for the duration of the call. +When the program calls `close`, `FdRegistry::close` removes the entry, which +closes the descriptor. The registry starts out holding the standard descriptors +0, 1, and 2. + +In the `fstat` rule, the descriptor argument goes through `with_fd`: + +```rust +fn f2(a0: i32, a1: Ptr) -> i32 { + match FdRegistry::with_fd(a0, |fd: BorrowedFd<'_>| nix::sys::stat::fstat(fd)) { + // ... + } +} +``` + +`with_fds` borrows several descriptors at once for `select`-style calls. The +`select` rule collects every descriptor set in the `fd_set` arguments and +borrows them all for the duration of the call: + +```rust +let wanted: Vec = /* the descriptors set in the fd_set arguments */; +FdRegistry::with_fds(&wanted, |borrowed: &[BorrowedFd<'_>]| { + let mut read_set = nix::sys::select::FdSet::new(); + for fd in &borrowed[..read_count] { + read_set.insert(*fd); + } + // ... build the write and except sets the same way ... + nix::sys::select::select(nfds, &mut read_set, /* ... */) +}) +``` + +Using a descriptor that was never opened, or using it after it was closed, is a +bug in the original program. The registry turns such a use into a panic (with a +message prefixed `ub:`) so the bug surfaces instead of going unnoticed. diff --git a/docs/src/runtime/libc-shims.md b/docs/src/runtime/libc-shims.md new file mode 100644 index 00000000..d17b6307 --- /dev/null +++ b/docs/src/runtime/libc-shims.md @@ -0,0 +1,102 @@ +# libc Shims + +In the refcount model every struct member is wrapped in a `Value`, so a libc +struct cannot be used directly. Even without that wrapping the layouts would not +meet: libc structs hold raw pointers, which are incompatible with the refcounted +pointers the model uses. The `libc_shims` modules therefore define Rust +counterparts for the libc types translated programs use. A shim struct mirrors +its C struct member by member, with each field a `Value`, and converts to or +from the underlying libc or nix type at the call boundary. + +`Stat` is a typical shim: + +```rust +#[derive(Default)] +pub struct Stat { + pub st_dev: Value, + pub st_ino: Value, + // ... + pub st_size: Value, +} + +impl Stat { + pub fn from_libc(s: &::libc::stat) -> Self { /* ... */ } +} +``` + +A `stat` call in the source program becomes a `nix::sys::stat::stat` call. On +success nix returns a raw `libc::stat`, so the result goes through +`Stat::from_libc` before it is written into the translated struct. + +## The modules + +| Module | C types | +| --------- | ------------------------------------------------------------------------------------------------ | +| `cfile` | `FILE` (`CFile`) | +| `dirent` | `struct dirent`, `DIR` (`Dirent`, `CDir`) | +| `fdset` | `fd_set` (`CFdSet`) | +| `ifaddrs` | `struct ifaddrs` (`Ifaddrs`) | +| `ip` | `struct in_addr`, `struct in6_addr` (`InAddr`, `In6Addr`) | +| `netdb` | `struct addrinfo` (`Addrinfo`) | +| `poll` | `struct pollfd` (`Pollfd`) | +| `pwd` | `struct passwd` (`Passwd`) | +| `socket` | the `sockaddr` family (`Sockaddr`, `SockaddrIn`, `SockaddrIn6`, `SockaddrUn`, `SockaddrStorage`) | +| `stat` | `struct stat` (`Stat`) | +| `termios` | `struct termios`, `struct winsize` (`Termios`, `Winsize`) | +| `time` | `struct tm`, `struct timeval`, `struct timespec` (`Tm`, `Timeval`, `Timespec`) | + +Most shims are plain data plus conversions like `Stat`. `CFile` carries the +stdio stream logic (see [I/O and Formatting](./io.md)), and the `time` shims +convert through the `jiff` crate. `CFdSet` and the `sockaddr` family depart +further from their C counterparts. + +## CFdSet + +nix has its own `FdSet`, but it is stricter than the C one: it ties the set to +the lifetimes of the descriptors it holds. A C `fd_set` is just a set of +integers that accepts anything; whether the descriptors are valid is only +checked by the `select` call that eventually receives the set. `CFdSet` keeps +the C behavior by storing plain integers, and the `select` rule builds the nix +`FdSet` from it at call time. + +## The sockaddr family + +C socket code reinterprets one address struct as another: the program fills in a +`struct sockaddr_in`, passes it to `bind` as a `struct sockaddr *`, and casts +back to the concrete type on the way out of `accept`. The address shims keep +this pattern working by implementing [`ByteRepr`](./reinterpret.md) with the +exact byte layout of their C structs: the family in the first two bytes, the +remaining members at their C offsets. A cast in the source program becomes a +[`reinterpret_cast`](./reinterpret.md) on the refcounted pointer, which reads +the struct through that byte layout as the target type, so any member of the +family can be viewed as any other, exactly as in C. + +The call boundary works the same way. `Sockaddr::decode` reads the family from +the first two bytes and reinterprets the pointer as the concrete type before +handing nix a typed address: + +```rust +pub fn decode(addr: &Ptr, _len: u32) -> Option> { + let family = addr.reinterpret_cast::().read(); + if family == libc::AF_INET as u16 { + let m = addr.reinterpret_cast::().read(); + Some(Box::new(nix::sys::socket::SockaddrIn::from(m.to_libc()))) + } + // ... AF_INET6 and AF_UNIX in the same way ... +} +``` + +`Sockaddr::encode` goes the other way, writing an address returned by nix into +the caller's buffer through the concrete shim. `Ifaddrs` hands out its addresses +as `Ptr` values ready to be reinterpreted. + +## Non-uniform fields + +Some struct fields are not spelled the same on every platform. `struct stat` +keeps the modification time in a nested `struct timespec`, named `st_mtim` on +Linux and `st_mtimespec` on macOS, while the shim exposes a single `st_mtime` +field. `struct in6_addr` hides its bytes behind the internal `__in6_u` union on +Linux, while the shim exposes `s6_addr`. The shims pick one uniform field, and +the code generator meets them halfway: `replaceNonUniformLibcField` in the +converter rewrites the platform-specific member chain in the source, so +`st.st_mtim.tv_sec` becomes `st.st_mtime` in the translated code. diff --git a/docs/src/runtime/overview.md b/docs/src/runtime/overview.md new file mode 100644 index 00000000..6edcf920 --- /dev/null +++ b/docs/src/runtime/overview.md @@ -0,0 +1,73 @@ +# Overview + +`libcc2rs` is the runtime library that translated programs link against. Every +Rust file `cpp2rust` emits begins with the same import: + +```rust +extern crate libcc2rs; +use libcc2rs::*; +``` + +The crate re-exports everything at its root, so this single glob import gives +the generated code access to the whole API without qualified paths. The build is +wired into CMake: the crate is compiled with Cargo into an `rlib`, and +translated programs are compiled against that `rlib` together with +`libcc2rs-macros`, the companion proc-macro crate. + +## The two output models + +The library serves both output models. The refcount model depends on it for its +entire pointer representation: refcounted values, pointers, and byte-level views +all come from the runtime. The unsafe model expresses pointers as raw Rust +pointers and calls libc directly (its output also imports `libc::*`), so it only +uses the runtime for constructs that raw pointers cannot express, such as +`goto`, `switch` fallthrough, and variadic calls. + +Some libc functions exist in the crate as real named functions, such as +`fread_refcount` and `fread_unsafe`, because translated programs can take their +address. A [rule body](../rules/format.md) alone has nothing to take the address +of, so the runtime defines a function with the signature the model expects. + +## Module map + +The modules fall into three groups. + +The refcounted pointer model, the core of the refcount output: + +- `rc`: `Value`, `Ptr`, and `AnyPtr`, the refcounted stand-ins for C + values and pointers. +- `reinterpret`: the `ByteRepr` trait and allocation views that let a refcounted + allocation be reinterpreted at the byte level, as C pointer casts do. +- `alloc`: `malloc`, `free`, `realloc`, and `calloc` over refcounted byte + arrays. + +Language-feature emulation, used by both models: + +- `inc` and `dec`: traits implementing the four `++`/`--` operator forms. +- `iterators`: iteration for C++ containers that need stable iterators, with an + implementation for both refcount and unsafe. +- `fn_ptr`: `FnPtr`, function pointers with C-style address identity. +- `va_args`: `VaArg` and `VaList`, the representation of variadic calls. +- The `goto`, `goto_block`, and `switch` proc macros, re-exported from + `libcc2rs-macros`, which rewrite unstructured control flow into state + machines. + +The OS and libc surface: + +- `io`: `CFile` streams, the standard streams, and read/write helpers. +- `format`: `printf`-style format string evaluation. +- `fd`: a registry tying integer file descriptors to their owning objects. +- `libc_shims`: safe wrappers over libc APIs, one submodule per area (files, + directories, sockets, name resolution, polling, terminal control, time, and so + on). +- `compat`: platform-specific definitions, such as the location of `errno` and + `malloc_usable_size`. + +## Dependencies + +The crate has four dependencies: + +- `libcc2rs-macros` provides the control-flow proc macros. +- `libc` and `nix` provide the raw and safe OS interfaces the shims wrap. +- `jiff` backs the time shims. +- `sprintf` backs `printf`-style formatting. From c87b6545d86db457ff57ddfac5b926b8dda2e16b Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Fri, 14 Aug 2026 15:43:56 +0100 Subject: [PATCH 15/30] Move prettierrc in docs --- .github/workflows/format.yml | 3 ++- .prettierrc => docs/.prettierrc | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename .prettierrc => docs/.prettierrc (100%) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 5c6a93de..361d065e 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -39,4 +39,5 @@ jobs: run: git diff --exit-code - name: Check markdown formatting - run: npx prettier@3.6.2 --check "docs/src/**/*.md" + run: npx prettier@3.6.2 --check "src/**/*.md" + working-directory: docs diff --git a/.prettierrc b/docs/.prettierrc similarity index 100% rename from .prettierrc rename to docs/.prettierrc From 89276e7a2f676079f9bfec9d1229f95e27d98ff1 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Sun, 16 Aug 2026 13:18:01 +0100 Subject: [PATCH 16/30] Draft rc and reinterpret sub-sections --- docs/src/SUMMARY.md | 2 + docs/src/runtime/overview.md | 27 ++--- docs/src/runtime/rc.md | 186 ++++++++++++++++++++++++++++++++ docs/src/runtime/reinterpret.md | 60 +++++++++++ 4 files changed, 263 insertions(+), 12 deletions(-) create mode 100644 docs/src/runtime/rc.md create mode 100644 docs/src/runtime/reinterpret.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 9ffd1f02..53595b51 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -23,6 +23,8 @@ # The Runtime Library - [Overview](./runtime/overview.md) +- [Reference Counting](./runtime/rc.md) +- [Type Reinterpretation](./runtime/reinterpret.md) - [I/O and Formatting](./runtime/io.md) - [libc Shims](./runtime/libc-shims.md) - [Compat Helpers](./runtime/compat.md) diff --git a/docs/src/runtime/overview.md b/docs/src/runtime/overview.md index 6edcf920..3840f56d 100644 --- a/docs/src/runtime/overview.md +++ b/docs/src/runtime/overview.md @@ -34,10 +34,11 @@ The modules fall into three groups. The refcounted pointer model, the core of the refcount output: -- `rc`: `Value`, `Ptr`, and `AnyPtr`, the refcounted stand-ins for C - values and pointers. -- `reinterpret`: the `ByteRepr` trait and allocation views that let a refcounted - allocation be reinterpreted at the byte level, as C pointer casts do. +- [`rc`](./rc.md): `Value`, `Ptr`, and `AnyPtr`, the refcounted stand-ins + for C values and pointers. +- [`reinterpret`](./reinterpret.md): the `ByteRepr` trait and allocation views + that let a refcounted allocation be reinterpreted at the byte level, as C + pointer casts do. - `alloc`: `malloc`, `free`, `realloc`, and `calloc` over refcounted byte arrays. @@ -54,14 +55,16 @@ Language-feature emulation, used by both models: The OS and libc surface: -- `io`: `CFile` streams, the standard streams, and read/write helpers. -- `format`: `printf`-style format string evaluation. -- `fd`: a registry tying integer file descriptors to their owning objects. -- `libc_shims`: safe wrappers over libc APIs, one submodule per area (files, - directories, sockets, name resolution, polling, terminal control, time, and so - on). -- `compat`: platform-specific definitions, such as the location of `errno` and - `malloc_usable_size`. +- [`io`](./io.md): `CFile` streams, the standard streams, and read/write + helpers. +- [`format`](./io.md#formatting): `printf`-style format string evaluation. +- [`fd`](./io.md#file-descriptors): a registry tying integer file descriptors to + their owning objects. +- [`libc_shims`](./libc-shims.md): safe wrappers over libc APIs, one submodule + per area (files, directories, sockets, name resolution, polling, terminal + control, time, and so on). +- [`compat`](./compat.md): platform-specific definitions, such as the location + of `errno` and `malloc_usable_size`. ## Dependencies diff --git a/docs/src/runtime/rc.md b/docs/src/runtime/rc.md new file mode 100644 index 00000000..623f2d33 --- /dev/null +++ b/docs/src/runtime/rc.md @@ -0,0 +1,186 @@ +# Reference Counting + +The refcount model produces safe Rust, and `rc.rs` is where that safety comes +from. It defines the two types every translated program is built on: `Value`, +the translation of a C++ variable, and `Ptr`, the translation of a C++ +pointer. + +## Values and pointers + +Rust requires every value to have a single owner, known at compile time, and +references to follow the borrow rules. C++ promises neither: a variable can be +aliased by any number of pointers, and any of them may write. Proving ownership +in the presence of such unrestricted aliasing is undecidable in general, so the +refcount model does not try. Instead it moves Rust's ownership and mutability +checks from compile time to run time, trading some speed for safety: `Rc` counts +references and checks lifetimes dynamically, and `RefCell` checks at each access +that readers and writers do not overlap. + +A C++ variable is therefore translated as a `Value`, an alias for +`Rc>`. Taking the address of a variable becomes a call to +`as_pointer`, which produces a `Ptr`: + +```c +int b = 2; +int *b_ptr = &b; +*b_ptr = 3; +``` + +```rust +let b: Value = Rc::new(RefCell::new(2)); +let b_ptr: Value> = Rc::new(RefCell::new(b.as_pointer())); +(*b_ptr.borrow()).write(3); +``` + +## Weak references + +A C++ pointer does not own what it points to, and `Ptr` keeps that property: +it holds a `Weak` reference to the allocation plus an element offset. Ownership +stays with the variable binding for stack values and with the allocation itself +for the heap. When the owner goes away, every pointer into it dangles, and the +next access panics instead of reading freed memory. + +The choice of weak over strong references is about destructors. C++ RAII code +relies on destructors running at precise points, such as a mutex being released +at the end of a scope; a strong reference held by a stray pointer could keep the +object alive past that point and run its destructor late. With weak references, +objects die exactly where C++ says they do, and a pointer that outlives its +object dangles. + +This is the central property of the model: memory bugs of the original program, +such as use after free, double free, and null or out-of-bounds dereference, +become panics in the translated one. Their messages carry the `ub:` prefix. + +## Pointer kinds + +A `Ptr` knows what it points into: + +- `Null`: the null pointer, and the default value. +- `StackSingle` and `HeapSingle`: a single value. +- `StackArray` and `HeapArray`: a fixed-size array. +- `Vec`: a growable buffer; `std::vector` contents and string literals live in + one. +- `Reinterpreted`: a byte-level view produced by a cast (see + [Type Reinterpretation](./reinterpret.md)). + +An array carries one reference counter for the whole allocation, not one per +element: the pointer pairs a weak reference to the whole array with the offset +of the element it points to, which keeps the memory and performance overhead of +arrays low. + +Two pointers compare equal when they point into the same allocation at the same +byte offset, and ordering compares allocation addresses, as C++ pointer +comparison does. + +## The heap + +`new` and `new[]` are translated as `Ptr::alloc` and `Ptr::alloc_array`. The +allocation's `Rc` is deliberately leaked so the object outlives the statement +that created it. The leak is legitimate: a Rust program that leaks memory is +still well typed. `delete` and `delete_array` recover the leaked reference and +drop it: + +```c +int *d = new int(0); +*d = 5; +delete d; +``` + +```rust +let d: Value> = Rc::new(RefCell::new(Ptr::alloc(0))); +(*d.borrow()).write(5); +(*d.borrow()).delete(); +``` + +`delete` checks that the pointer still points at the start of a live heap +allocation: freeing twice, freeing through an offset pointer, or freeing a stack +value panics with `ub:`. + +## Dereferences + +A dereference becomes a short-lived borrow. `read` and `write` copy a value out +of or into the allocation: + +```c +*d = 5; +int v = *d; +``` + +```rust +(*d.borrow()).write(5); +let v: Value = Rc::new(RefCell::new((*d.borrow()).read())); +``` + +A `Ptr` cannot simply return a `&T` or `&mut T` to its pointee: the reference +would keep the `RefCell` borrowed with nothing to bound its lifetime. `with` and +`with_mut` invert the control instead: the expression that needs the reference +moves into a closure, and the borrow lasts exactly as long as the closure runs. +They carry the operations that need a reference to the existing value, such as a +`push_back` on a vector reached through a pointer (`write` could only replace +the vector wholesale): + +```rust +v.with_mut(|v| v.push(20)); +``` + +Applied rule bodies are the main producer of these calls (see +[Rule Rewriting](../rules/rewriting.md)). `write` itself is a thin wrapper: it +is defined as `with_mut(|v| *v = value)`. + +In every case the `RefCell` is borrowed only for the duration of the access, +which is what lets freely aliasing C++ pointers coexist with the borrow checker: +no borrow outlives the expression that created it. When an expression needs an +actual Rust reference, the pointer is upgraded to a `StrongPtr`, which holds the +allocation alive and hands out a `Ref`. + +These borrows are the model's mutability checks, moved from compile time to run +time. Rust's rule still holds, any number of readers or one writer, but it is +enforced when the access happens: an expression that writes a variable while +also reading it through an alias, such as `*x.borrow_mut() = *x.borrow() + 1`, +traps. The code generator is responsible for not emitting such expressions: it +stores intermediate results in temporaries, so the reading borrow ends before +the writing borrow starts. + +## Arithmetic + +The offset lives in the pointer, so arithmetic never touches the allocation. +`p + n`, `p - n`, and the `++`/`--` forms move the offset, including past the +end of the allocation, exactly as C++ allows; bounds are checked only when the +pointer is dereferenced. Subtracting two pointers yields their element distance +and requires both to point into the same allocation. + +## 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` by +`Ptr::from_string_literal`. `Ptr` also carries the memory functions C +strings rely on: `memcpy` (which copies backwards when the ranges overlap), +`memset`, `memcmp`, and `to_rust_string` for crossing into Rust APIs. + +## Virtual classes + +A pointer to a virtual class cannot be a `Ptr`. The class is translated as a +Rust trait, and trait objects are unsized, which Rust marks with `dyn`. The +runtime provides a dedicated `PtrDyn` type for these pointers, kept +separate so the generic `Ptr` pays no cost for dynamic dispatch. `to_strong` +upgrades a `Ptr` into a `Value`, and `as_pointer_dyn` turns a +`Value` into a `PtrDyn`; a virtual call upgrades the pointer and +dispatches through the trait. + +## void pointers + +`void *` is translated as `AnyPtr`, a type-erased `Ptr`. `to_any` erases the +element type; `reinterpret_cast` recovers it, returning the original pointer +when the types match and a byte-level view otherwise. The `malloc` family +allocates and frees through `AnyPtr`. + +## Global variables + +Global variables are mapped to thread-local storage, because a `Value` 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. diff --git a/docs/src/runtime/reinterpret.md b/docs/src/runtime/reinterpret.md new file mode 100644 index 00000000..83871a96 --- /dev/null +++ b/docs/src/runtime/reinterpret.md @@ -0,0 +1,60 @@ +# Type Reinterpretation + +C code reads the same memory at different types: a `long` is inspected byte by +byte through a `char *`, a byte buffer from `malloc` is used as an array of +structs, a `struct sockaddr_in` is passed where a `struct sockaddr` is expected. +In the refcount model there are no raw bytes to point at: values are typed Rust +data behind refcounted cells. The `reinterpret` module supplies the byte view +these programs expect. + +## ByteRepr + +`ByteRepr` gives a type its C byte representation: + +```rust +pub trait ByteRepr: 'static { + fn byte_size() -> usize; + fn to_bytes(&self, buf: &mut [u8]); + fn from_bytes(buf: &[u8]) -> Self; +} +``` + +The primitive types serialize to their native-endian bytes, matching what C sees +on the host. The [libc shims](./libc-shims.md#the-sockaddr-family) implement the +trait by hand with the byte layout of their C structs. Types with no meaningful +C layout, such as `std::fs::File` or `Vec`, implement the trait with defaults +that panic, so reinterpreting one is caught at run time. + +## Views over the original allocation + +`reinterpret_cast` copies nothing. It produces a `Ptr` in the `Reinterpreted` +kind: a handle to the original allocation plus a byte offset, stepping by the +target type's size. A read serializes the overlapping elements of the original +into bytes and parses the target value out of them; a write is a +read-modify-write back into the original. The data always lives in the original +allocation, so writes through the original are visible through every view and +writes through a view are visible everywhere else: + +```rust +let p: Ptr = Ptr::alloc(0x0807060504030201); +let bytes: Ptr = p.reinterpret_cast::(); + +assert_eq!(bytes.read(), 0x01); +bytes.offset(7).write(0xAA); +assert_eq!(p.read(), 0xAA07060504030201); +``` + +A reinterpreted pointer counts its offset in bytes, so its arithmetic matches +the C cast exactly. Casting a view again does not stack views: the new pointer +keeps the handle to the original allocation. + +Deleting through a reinterpreted pointer frees the original allocation. That is +how `free` works on a buffer that has been cast around: the pointer is +reinterpreted to bytes and the original allocation is deleted. + +## AnyPtr casts + +`AnyPtr::reinterpret_cast` first tries to recover the pointer as it was erased: +casting a `void *` back to the type it came from returns the original `Ptr`, +with no byte view involved. Only a cast to a different type goes through the +byte representation. From 71791b79f90072aecf3e1d9f16f9de69817f6196 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Sun, 16 Aug 2026 17:01:06 +0100 Subject: [PATCH 17/30] Restructure libcc2rs --- libcc2rs/src/alloc.rs | 3 +- libcc2rs/src/cstr.rs | 221 +++++++++++++++++++ libcc2rs/src/fn_ptr.rs | 3 +- libcc2rs/src/iterators.rs | 24 ++ libcc2rs/src/lib.rs | 9 + libcc2rs/src/ptr_dyn.rs | 78 +++++++ libcc2rs/src/rc.rs | 449 +------------------------------------- libcc2rs/src/va_args.rs | 4 +- libcc2rs/src/void.rs | 142 ++++++++++++ 9 files changed, 486 insertions(+), 447 deletions(-) create mode 100644 libcc2rs/src/cstr.rs create mode 100644 libcc2rs/src/ptr_dyn.rs create mode 100644 libcc2rs/src/void.rs diff --git a/libcc2rs/src/alloc.rs b/libcc2rs/src/alloc.rs index 9c90a63e..bf3846cd 100644 --- a/libcc2rs/src/alloc.rs +++ b/libcc2rs/src/alloc.rs @@ -1,7 +1,8 @@ // Copyright (c) 2022-present INESC-ID. // Distributed under the MIT license that can be found in the LICENSE file. -use crate::rc::{AnyPtr, Ptr}; +use crate::rc::Ptr; +use crate::void::AnyPtr; pub fn malloc_refcount(a0: usize) -> AnyPtr { Ptr::alloc_array(vec![0u8; a0].into_boxed_slice()).to_any() diff --git a/libcc2rs/src/cstr.rs b/libcc2rs/src/cstr.rs new file mode 100644 index 00000000..dc7f21ce --- /dev/null +++ b/libcc2rs/src/cstr.rs @@ -0,0 +1,221 @@ +// Copyright (c) 2022-present INESC-ID. +// Distributed under the MIT license that can be found in the LICENSE file. + +use std::cell::RefCell; +use std::collections::HashMap; +use std::fmt; +use std::rc::Rc; + +use crate::rc::{Ptr, PtrKind}; + +impl fmt::Display for Ptr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.kind { + PtrKind::Null => write!(f, "NULL"), + _ => { + for value in self { + let ch = value.read(); + if ch == 0 { + break; + } + write!(f, "{}", char::from(ch))?; + } + Ok(()) + } + } + } +} + +type StringLiteralMap = HashMap<&'static [u8], Rc>>>; + +thread_local! { + static STRING_LITERALS: RefCell = RefCell::new(HashMap::new()); +} + +impl Ptr { + pub fn with_slice_mut(&self, len: usize, f: impl FnOnce(&mut [u8]) -> R) -> R { + let off = self.offset; + match &self.kind { + PtrKind::Null => panic!("ub: null pointer"), + PtrKind::StackSingle(weak) | PtrKind::HeapSingle(weak) => { + assert!(off == 0 && len <= 1, "ub: with_slice_mut out of bounds"); + let rc = weak.upgrade().expect("ub: dangling pointer"); + let mut b = rc.borrow_mut(); + f(&mut std::slice::from_mut(&mut *b)[..len]) + } + PtrKind::StackArray(weak) | PtrKind::HeapArray(weak) => { + let rc = weak.upgrade().expect("ub: dangling pointer"); + let mut b = rc.borrow_mut(); + f(&mut b[off..off + len]) + } + PtrKind::Vec(weak) => { + let rc = weak.upgrade().expect("ub: dangling pointer"); + let mut b = rc.borrow_mut(); + f(&mut b[off..off + len]) + } + PtrKind::Reinterpreted(data) => { + let mut buf = vec![0u8; len]; + data.alloc.read_bytes(off, &mut buf); + let r = f(&mut buf); + data.alloc.write_bytes(off, &buf); + r + } + } + } + + pub fn with_slice(&self, len: usize, f: impl FnOnce(&[u8]) -> R) -> R { + let off = self.offset; + match &self.kind { + PtrKind::Null => panic!("ub: null pointer"), + PtrKind::StackSingle(weak) | PtrKind::HeapSingle(weak) => { + assert!(off == 0 && len <= 1, "ub: with_slice out of bounds"); + let rc = weak.upgrade().expect("ub: dangling pointer"); + let b = rc.borrow(); + f(&std::slice::from_ref(&*b)[..len]) + } + PtrKind::StackArray(weak) | PtrKind::HeapArray(weak) => { + let rc = weak.upgrade().expect("ub: dangling pointer"); + let b = rc.borrow(); + f(&b[off..off + len]) + } + PtrKind::Vec(weak) => { + let rc = weak.upgrade().expect("ub: dangling pointer"); + let b = rc.borrow(); + f(&b[off..off + len]) + } + PtrKind::Reinterpreted(data) => { + let mut buf = vec![0u8; len]; + data.alloc.read_bytes(off, &mut buf); + f(&buf) + } + } + } + + #[allow(clippy::explicit_counter_loop)] + pub fn memcpy(&self, src: &Self, len: usize) { + if *self > *src { + let mut dst = self.offset(len); + let mut s = src.offset(len); + for _ in 0..len { + dst -= 1; + s -= 1; + dst.write(s.read()); + } + return; + } + let mut dst = self.clone(); + let mut i: usize = 0; + for value in src { + if i >= len { + break; + } + dst.write(value.read()); + dst += 1; + i += 1; + } + assert_eq!(i, len, "ub: memcpy"); + } + + #[allow(clippy::explicit_counter_loop)] + pub fn memset(&self, value: u8, num: usize) { + let mut dst = self.clone(); + for _ in 0..num { + dst.write(value); + dst += 1; + } + } + + #[allow(clippy::explicit_counter_loop)] + pub fn memcmp(&self, other: &Self, len: usize) -> i32 { + let mut a = self.clone(); + let mut b = other.clone(); + for _ in 0..len { + let va = a.read(); + let vb = b.read(); + if va != vb { + return (va as i32).wrapping_sub(vb as i32); + } + a += 1; + b += 1; + } + 0 + } + + pub fn slice_until(&self, end: &Self) -> Vec { + assert!(self.kind == end.kind, "ub: invalid slice"); + let start: usize = self.offset; + let end: usize = end.offset; + assert!(start <= end); + assert!(end <= self.len()); + match self.kind { + PtrKind::Null => panic!("ub: dereference of null pointer"), + PtrKind::StackSingle(_) | PtrKind::HeapSingle(_) => { + if start < end { + vec![self.read()] + } else { + Vec::new() + } + } + PtrKind::Vec(ref weak) => { + let strong = weak.upgrade().expect("ub: dangling pointer"); + let raw = strong.borrow(); + raw[start..end].to_vec() + } + PtrKind::StackArray(ref weak) | PtrKind::HeapArray(ref weak) => { + let strong = weak.upgrade().expect("ub: dangling pointer"); + let raw = strong.borrow(); + raw[start..end].to_vec() + } + PtrKind::Reinterpreted(ref data) => { + let mut buf = vec![0u8; end.wrapping_sub(start)]; + data.alloc.read_bytes(start, &mut buf); + buf + } + } + } + + #[inline] + pub fn from_string_literal(s: &'static [u8]) -> Self { + STRING_LITERALS.with(|literals| { + let mut literals = literals.borrow_mut(); + let weak = Rc::downgrade(literals.entry(s).or_insert_with(|| { + Rc::new(RefCell::new({ + let mut v = s.to_vec(); + v.push(0); + v + })) + })); + Ptr { + offset: 0, + kind: PtrKind::Vec(weak), + } + }) + } + + pub fn to_c_string_iterator(&self) -> CStringIterator { + CStringIterator { ptr: self.clone() } + } + + pub fn to_rust_string(&self) -> String { + let bytes: Vec = self.to_c_string_iterator().collect(); + String::from_utf8_lossy(&bytes).into_owned() + } +} + +pub struct CStringIterator { + ptr: Ptr, +} + +impl Iterator for CStringIterator { + type Item = u8; + fn next(&mut self) -> Option { + // read until the null terminator + match self.ptr.read() { + 0 => None, + ch => { + self.ptr += 1; + Some(ch) + } + } + } +} diff --git a/libcc2rs/src/fn_ptr.rs b/libcc2rs/src/fn_ptr.rs index 12bfced8..861a5b10 100644 --- a/libcc2rs/src/fn_ptr.rs +++ b/libcc2rs/src/fn_ptr.rs @@ -6,8 +6,9 @@ use std::marker::PhantomData; use std::ops::Deref; use std::rc::Rc; -use crate::rc::{AnyPtr, ErasedPtr, Ptr}; +use crate::rc::Ptr; use crate::reinterpret::ByteRepr; +use crate::void::{AnyPtr, ErasedPtr}; pub trait FnAddr { fn fn_addr(&self) -> usize; diff --git a/libcc2rs/src/iterators.rs b/libcc2rs/src/iterators.rs index 623a1110..28d4077a 100644 --- a/libcc2rs/src/iterators.rs +++ b/libcc2rs/src/iterators.rs @@ -225,3 +225,27 @@ impl> PostfixDec for MapIter Ptr { + pub fn to_string_iterator(&self) -> StringIterator { + StringIterator { ptr: self.clone() } + } +} + +pub struct StringIterator { + ptr: Ptr, +} + +impl Iterator for StringIterator { + type Item = Ptr; + fn next(&mut self) -> Option { + // stop before the null terminator at the last position + if self.ptr.get_offset().wrapping_add(1) < self.ptr.len() { + let value = self.ptr.clone(); + self.ptr += 1; + Some(value) + } else { + None + } + } +} diff --git a/libcc2rs/src/lib.rs b/libcc2rs/src/lib.rs index 9071ff50..9e0566cb 100644 --- a/libcc2rs/src/lib.rs +++ b/libcc2rs/src/lib.rs @@ -7,6 +7,15 @@ pub use reinterpret::ByteRepr; mod rc; pub use rc::*; +mod cstr; +pub use cstr::*; + +mod void; +pub use void::*; + +mod ptr_dyn; +pub use ptr_dyn::*; + mod libc_shims; pub use libc_shims::*; diff --git a/libcc2rs/src/ptr_dyn.rs b/libcc2rs/src/ptr_dyn.rs new file mode 100644 index 00000000..34ada33a --- /dev/null +++ b/libcc2rs/src/ptr_dyn.rs @@ -0,0 +1,78 @@ +// Copyright (c) 2022-present INESC-ID. +// Distributed under the MIT license that can be found in the LICENSE file. + +use std::cell::{Ref, RefCell, RefMut}; +use std::rc::{Rc, Weak}; + +pub struct StrongPtrDyn { + rc: Rc>, +} + +impl StrongPtrDyn { + pub fn deref(&self) -> Ref<'_, T> { + self.rc.borrow() + } + + pub fn deref_mut(&self) -> RefMut<'_, T> { + self.rc.borrow_mut() + } +} + +#[derive(Default, Debug)] +enum PtrKindDyn { + #[default] + Null, // TODO: is this useful? + StackSingle(Weak>), +} + +impl Clone for PtrKindDyn { + fn clone(&self) -> Self { + match &self { + PtrKindDyn::Null => PtrKindDyn::Null, + PtrKindDyn::StackSingle(weak) => PtrKindDyn::StackSingle(weak.clone()), + } + } +} + +#[derive(Debug, Default)] +pub struct PtrDyn { + offset: usize, + kind: PtrKindDyn, +} + +impl PtrDyn { + pub fn upgrade(&self) -> StrongPtrDyn { + match &self.kind { + PtrKindDyn::Null => panic!("ub: dereference of null pointer"), + PtrKindDyn::StackSingle(weak) => { + assert_eq!(self.offset, 0, "ub: invalid offset"); + StrongPtrDyn { + rc: weak.upgrade().expect("ub: dangling pointer"), + } + } + } + } +} + +impl Clone for PtrDyn { + fn clone(&self) -> Self { + Self { + offset: self.offset, + kind: self.kind.clone(), + } + } +} + +pub trait AsPointerDyn { + fn as_pointer_dyn(&self) -> PtrDyn; +} + +impl AsPointerDyn for Rc> { + #[inline] + fn as_pointer_dyn(&self) -> PtrDyn { + PtrDyn { + offset: 0, + kind: PtrKindDyn::StackSingle(Rc::downgrade(self)), + } + } +} diff --git a/libcc2rs/src/rc.rs b/libcc2rs/src/rc.rs index 9eb6a1d8..0112dabd 100644 --- a/libcc2rs/src/rc.rs +++ b/libcc2rs/src/rc.rs @@ -3,10 +3,9 @@ use crate::{PostfixDec, PostfixInc, PrefixDec, PrefixInc}; use std::any::{Any, TypeId}; -use std::collections::HashMap; use std::{ - cell::{Ref, RefCell, RefMut}, + cell::{Ref, RefCell}, fmt, ops::Sub, rc::{Rc, Weak}, @@ -16,15 +15,15 @@ use crate::reinterpret::{ByteRepr, OriginalAlloc, SingleOriginalAlloc, SliceOrig pub type Value = Rc>; -struct ReinterpretedView { +pub(crate) struct ReinterpretedView { // Pointer to the source of reinterpret - alloc: Rc, + pub(crate) alloc: Rc, // C++ size of the reinterpreted view elem_byte_size: usize, } #[derive(Default)] -enum PtrKind { +pub(crate) enum PtrKind { #[default] Null, StackSingle(Weak>), @@ -138,8 +137,8 @@ impl PartialOrd for PtrKind { } pub struct Ptr { - offset: usize, - kind: PtrKind, + pub(crate) offset: usize, + pub(crate) kind: PtrKind, } impl Default for Ptr { @@ -337,10 +336,6 @@ impl Ptr { } } - pub fn to_string_iterator(&self) -> StringIterator { - StringIterator { ptr: self.clone() } - } - pub fn upgrade(&self) -> StrongPtr { match &self.kind { PtrKind::Null => panic!("ub: null pointer"), @@ -633,42 +628,6 @@ impl Iterator for PtrValueIter { impl ExactSizeIterator for PtrValueIter {} -pub struct StringIterator { - ptr: Ptr, -} - -impl Iterator for StringIterator { - type Item = Ptr; - fn next(&mut self) -> Option { - // stop before the null terminator at the last position - if self.ptr.get_offset().wrapping_add(1) < self.ptr.len() { - let value = self.ptr.clone(); - self.ptr += 1; - Some(value) - } else { - None - } - } -} - -pub struct CStringIterator { - ptr: Ptr, -} - -impl Iterator for CStringIterator { - type Item = u8; - fn next(&mut self) -> Option { - // read until the null terminator - match self.ptr.read() { - 0 => None, - ch => { - self.ptr += 1; - Some(ch) - } - } - } -} - impl Sub for Ptr { type Output = isize; fn sub(self, other: Self) -> Self::Output { @@ -888,369 +847,7 @@ impl fmt::Debug for Ptr { } } -impl fmt::Display for Ptr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self.kind { - PtrKind::Null => write!(f, "NULL"), - _ => { - for value in self { - let ch = value.read(); - if ch == 0 { - break; - } - write!(f, "{}", char::from(ch))?; - } - Ok(()) - } - } - } -} - -type StringLiteralMap = HashMap<&'static [u8], Rc>>>; - -thread_local! { - static STRING_LITERALS: RefCell = RefCell::new(HashMap::new()); -} - -impl Ptr { - pub fn with_slice_mut(&self, len: usize, f: impl FnOnce(&mut [u8]) -> R) -> R { - let off = self.offset; - match &self.kind { - PtrKind::Null => panic!("ub: null pointer"), - PtrKind::StackSingle(weak) | PtrKind::HeapSingle(weak) => { - assert!(off == 0 && len <= 1, "ub: with_slice_mut out of bounds"); - let rc = weak.upgrade().expect("ub: dangling pointer"); - let mut b = rc.borrow_mut(); - f(&mut std::slice::from_mut(&mut *b)[..len]) - } - PtrKind::StackArray(weak) | PtrKind::HeapArray(weak) => { - let rc = weak.upgrade().expect("ub: dangling pointer"); - let mut b = rc.borrow_mut(); - f(&mut b[off..off + len]) - } - PtrKind::Vec(weak) => { - let rc = weak.upgrade().expect("ub: dangling pointer"); - let mut b = rc.borrow_mut(); - f(&mut b[off..off + len]) - } - PtrKind::Reinterpreted(data) => { - let mut buf = vec![0u8; len]; - data.alloc.read_bytes(off, &mut buf); - let r = f(&mut buf); - data.alloc.write_bytes(off, &buf); - r - } - } - } - - pub fn with_slice(&self, len: usize, f: impl FnOnce(&[u8]) -> R) -> R { - let off = self.offset; - match &self.kind { - PtrKind::Null => panic!("ub: null pointer"), - PtrKind::StackSingle(weak) | PtrKind::HeapSingle(weak) => { - assert!(off == 0 && len <= 1, "ub: with_slice out of bounds"); - let rc = weak.upgrade().expect("ub: dangling pointer"); - let b = rc.borrow(); - f(&std::slice::from_ref(&*b)[..len]) - } - PtrKind::StackArray(weak) | PtrKind::HeapArray(weak) => { - let rc = weak.upgrade().expect("ub: dangling pointer"); - let b = rc.borrow(); - f(&b[off..off + len]) - } - PtrKind::Vec(weak) => { - let rc = weak.upgrade().expect("ub: dangling pointer"); - let b = rc.borrow(); - f(&b[off..off + len]) - } - PtrKind::Reinterpreted(data) => { - let mut buf = vec![0u8; len]; - data.alloc.read_bytes(off, &mut buf); - f(&buf) - } - } - } - - #[allow(clippy::explicit_counter_loop)] - pub fn memcpy(&self, src: &Self, len: usize) { - if *self > *src { - let mut dst = self.offset(len); - let mut s = src.offset(len); - for _ in 0..len { - dst -= 1; - s -= 1; - dst.write(s.read()); - } - return; - } - let mut dst = self.clone(); - let mut i: usize = 0; - for value in src { - if i >= len { - break; - } - dst.write(value.read()); - dst += 1; - i += 1; - } - assert_eq!(i, len, "ub: memcpy"); - } - - #[allow(clippy::explicit_counter_loop)] - pub fn memset(&self, value: u8, num: usize) { - let mut dst = self.clone(); - for _ in 0..num { - dst.write(value); - dst += 1; - } - } - - #[allow(clippy::explicit_counter_loop)] - pub fn memcmp(&self, other: &Self, len: usize) -> i32 { - let mut a = self.clone(); - let mut b = other.clone(); - for _ in 0..len { - let va = a.read(); - let vb = b.read(); - if va != vb { - return (va as i32).wrapping_sub(vb as i32); - } - a += 1; - b += 1; - } - 0 - } - - pub fn slice_until(&self, end: &Self) -> Vec { - assert!(self.kind == end.kind, "ub: invalid slice"); - let start: usize = self.offset; - let end: usize = end.offset; - assert!(start <= end); - assert!(end <= self.len()); - match self.kind { - PtrKind::Null => panic!("ub: dereference of null pointer"), - PtrKind::StackSingle(_) | PtrKind::HeapSingle(_) => { - if start < end { - vec![self.read()] - } else { - Vec::new() - } - } - PtrKind::Vec(ref weak) => { - let strong = weak.upgrade().expect("ub: dangling pointer"); - let raw = strong.borrow(); - raw[start..end].to_vec() - } - PtrKind::StackArray(ref weak) | PtrKind::HeapArray(ref weak) => { - let strong = weak.upgrade().expect("ub: dangling pointer"); - let raw = strong.borrow(); - raw[start..end].to_vec() - } - PtrKind::Reinterpreted(ref data) => { - let mut buf = vec![0u8; end.wrapping_sub(start)]; - data.alloc.read_bytes(start, &mut buf); - buf - } - } - } - - #[inline] - pub fn from_string_literal(s: &'static [u8]) -> Self { - STRING_LITERALS.with(|literals| { - let mut literals = literals.borrow_mut(); - let weak = Rc::downgrade(literals.entry(s).or_insert_with(|| { - Rc::new(RefCell::new({ - let mut v = s.to_vec(); - v.push(0); - v - })) - })); - Ptr { - offset: 0, - kind: PtrKind::Vec(weak), - } - }) - } - - pub fn to_c_string_iterator(&self) -> CStringIterator { - CStringIterator { ptr: self.clone() } - } - - pub fn to_rust_string(&self) -> String { - let bytes: Vec = self.to_c_string_iterator().collect(); - String::from_utf8_lossy(&bytes).into_owned() - } -} - -pub(crate) trait ErasedPtr: std::any::Any { - fn as_bytes(&self) -> Ptr; - fn as_any(&self) -> &dyn std::any::Any; - fn equals(&self, other: &dyn ErasedPtr) -> bool; - fn is_null(&self) -> bool; -} - -impl PartialEq for dyn ErasedPtr { - fn eq(&self, other: &Self) -> bool { - self.equals(other) - } -} - -impl ErasedPtr for Ptr -where - T: ByteRepr + 'static, - Ptr: PartialEq, -{ - fn as_bytes(&self) -> Ptr { - self.reinterpret_cast::() - } - - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn equals(&self, other: &dyn ErasedPtr) -> bool { - other.as_any().downcast_ref::>() == Some(self) - } - - fn is_null(&self) -> bool { - Ptr::is_null(self) - } -} - -#[derive(Clone)] -pub struct AnyPtr { - pub(crate) ptr: Rc, -} - -impl Ptr { - pub fn to_any(&self) -> AnyPtr { - AnyPtr { - ptr: Rc::new(self.clone()), - } - } -} - -impl Default for AnyPtr { - fn default() -> Self { - Ptr::<()>::null().to_any() - } -} - -impl AnyPtr { - pub fn reinterpret_cast(&self) -> Ptr { - if self.ptr.is_null() { - return Ptr::::null(); - } - if let Some(p) = self.ptr.as_any().downcast_ref::>() { - return p.clone(); - } - self.ptr.as_bytes().reinterpret_cast::() - } - - pub fn is_null(&self) -> bool { - self.ptr.is_null() - } -} - -impl PartialEq for AnyPtr { - fn eq(&self, other: &Self) -> bool { - *self.ptr == *other.ptr - } -} - -impl AnyPtr { - pub fn memcpy(&self, src: &AnyPtr, len: usize) { - let dst_u8 = self.ptr.as_bytes(); - let src_u8 = src.ptr.as_bytes(); - dst_u8.memcpy(&src_u8, len); - } - - pub fn memset(&self, value: u8, num: usize) { - self.ptr.as_bytes().memset(value, num); - } - - pub fn memcmp(&self, other: &AnyPtr, len: usize) -> i32 { - let a = self.ptr.as_bytes(); - let b = other.ptr.as_bytes(); - a.memcmp(&b, len) - } -} - -pub struct StrongPtrDyn { - rc: Rc>, -} - -impl StrongPtrDyn { - pub fn deref(&self) -> Ref<'_, T> { - self.rc.borrow() - } - - pub fn deref_mut(&self) -> RefMut<'_, T> { - self.rc.borrow_mut() - } -} - -#[derive(Default, Debug)] -enum PtrKindDyn { - #[default] - Null, // TODO: is this useful? - StackSingle(Weak>), -} - -impl Clone for PtrKindDyn { - fn clone(&self) -> Self { - match &self { - PtrKindDyn::Null => PtrKindDyn::Null, - PtrKindDyn::StackSingle(weak) => PtrKindDyn::StackSingle(weak.clone()), - } - } -} - -#[derive(Debug, Default)] -pub struct PtrDyn { - offset: usize, - kind: PtrKindDyn, -} - -impl PtrDyn { - pub fn upgrade(&self) -> StrongPtrDyn { - match &self.kind { - PtrKindDyn::Null => panic!("ub: dereference of null pointer"), - PtrKindDyn::StackSingle(weak) => { - assert_eq!(self.offset, 0, "ub: invalid offset"); - StrongPtrDyn { - rc: weak.upgrade().expect("ub: dangling pointer"), - } - } - } - } -} - -impl Clone for PtrDyn { - fn clone(&self) -> Self { - Self { - offset: self.offset, - kind: self.kind.clone(), - } - } -} - -pub trait AsPointerDyn { - fn as_pointer_dyn(&self) -> PtrDyn; -} - -impl AsPointerDyn for Rc> { - #[inline] - fn as_pointer_dyn(&self) -> PtrDyn { - PtrDyn { - offset: 0, - kind: PtrKindDyn::StackSingle(Rc::downgrade(self)), - } - } -} - impl ByteRepr for Ptr {} -impl ByteRepr for AnyPtr {} impl Ptr { pub fn to_int(&self) -> usize { @@ -1266,16 +863,6 @@ impl Ptr { } } -impl AnyPtr { - pub fn to_int(&self) -> usize { - self.reinterpret_cast::().to_int() - } - - pub fn from_int(value: usize) -> Self { - Ptr::::from_int(value).to_any() - } -} - #[cfg(test)] mod tests { use super::*; @@ -1328,28 +915,4 @@ mod tests { p.delete(); } - - #[test] - fn anyptr_null_cast() { - // void* nullptr - let any = Ptr::<()>::null().to_any(); - let p = any.reinterpret_cast::(); - assert!(p.is_null()); - - let p2 = any.reinterpret_cast::(); - assert!(p2.is_null()); - - // int* nullptr - let any2 = Ptr::::null().to_any(); - let p3 = any2.reinterpret_cast::(); - assert!(p3.is_null()); - } - - #[test] - fn to_any_without_clone() { - let p: Ptr = Ptr::null(); // std::fs::File is not Clone - let any = p.to_any(); - let recovered = any.reinterpret_cast::(); - assert!(recovered.is_null()); - } } diff --git a/libcc2rs/src/va_args.rs b/libcc2rs/src/va_args.rs index 95693cbc..bc8ae3ab 100644 --- a/libcc2rs/src/va_args.rs +++ b/libcc2rs/src/va_args.rs @@ -3,7 +3,7 @@ use std::ffi::c_void; -use crate::rc::AnyPtr; +use crate::void::AnyPtr; #[derive(Clone)] pub enum VaArg { @@ -128,7 +128,7 @@ impl VaArgGet for crate::rc::Ptr { } } -impl VaArgGet for crate::rc::AnyPtr { +impl VaArgGet for AnyPtr { fn get(v: &VaArg) -> Self { match v { VaArg::Ptr(any) => any.clone(), diff --git a/libcc2rs/src/void.rs b/libcc2rs/src/void.rs new file mode 100644 index 00000000..f8903a3e --- /dev/null +++ b/libcc2rs/src/void.rs @@ -0,0 +1,142 @@ +// Copyright (c) 2022-present INESC-ID. +// Distributed under the MIT license that can be found in the LICENSE file. + +use std::rc::Rc; + +use crate::rc::Ptr; +use crate::reinterpret::ByteRepr; + +pub(crate) trait ErasedPtr: std::any::Any { + fn as_bytes(&self) -> Ptr; + fn as_any(&self) -> &dyn std::any::Any; + fn equals(&self, other: &dyn ErasedPtr) -> bool; + fn is_null(&self) -> bool; +} + +impl PartialEq for dyn ErasedPtr { + fn eq(&self, other: &Self) -> bool { + self.equals(other) + } +} + +impl ErasedPtr for Ptr +where + T: ByteRepr + 'static, + Ptr: PartialEq, +{ + fn as_bytes(&self) -> Ptr { + self.reinterpret_cast::() + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn equals(&self, other: &dyn ErasedPtr) -> bool { + other.as_any().downcast_ref::>() == Some(self) + } + + fn is_null(&self) -> bool { + Ptr::is_null(self) + } +} + +#[derive(Clone)] +pub struct AnyPtr { + pub(crate) ptr: Rc, +} + +impl Ptr { + pub fn to_any(&self) -> AnyPtr { + AnyPtr { + ptr: Rc::new(self.clone()), + } + } +} + +impl Default for AnyPtr { + fn default() -> Self { + Ptr::<()>::null().to_any() + } +} + +impl AnyPtr { + pub fn reinterpret_cast(&self) -> Ptr { + if self.ptr.is_null() { + return Ptr::::null(); + } + if let Some(p) = self.ptr.as_any().downcast_ref::>() { + return p.clone(); + } + self.ptr.as_bytes().reinterpret_cast::() + } + + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } +} + +impl PartialEq for AnyPtr { + fn eq(&self, other: &Self) -> bool { + *self.ptr == *other.ptr + } +} + +impl AnyPtr { + pub fn memcpy(&self, src: &AnyPtr, len: usize) { + let dst_u8 = self.ptr.as_bytes(); + let src_u8 = src.ptr.as_bytes(); + dst_u8.memcpy(&src_u8, len); + } + + pub fn memset(&self, value: u8, num: usize) { + self.ptr.as_bytes().memset(value, num); + } + + pub fn memcmp(&self, other: &AnyPtr, len: usize) -> i32 { + let a = self.ptr.as_bytes(); + let b = other.ptr.as_bytes(); + a.memcmp(&b, len) + } +} + +impl ByteRepr for AnyPtr {} + +impl AnyPtr { + pub fn to_int(&self) -> usize { + self.reinterpret_cast::().to_int() + } + + pub fn from_int(value: usize) -> Self { + Ptr::::from_int(value).to_any() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn anyptr_null_cast() { + // void* nullptr + let any = Ptr::<()>::null().to_any(); + let p = any.reinterpret_cast::(); + assert!(p.is_null()); + + let p2 = any.reinterpret_cast::(); + assert!(p2.is_null()); + + // int* nullptr + let any2 = Ptr::::null().to_any(); + let p3 = any2.reinterpret_cast::(); + assert!(p3.is_null()); + } + + #[test] + fn to_any_without_clone() { + let p: Ptr = Ptr::null(); // std::fs::File is not Clone + let any = p.to_any(); + let recovered = any.reinterpret_cast::(); + assert!(recovered.is_null()); + } +} From 0a9863beb29913318f5dce5744e18140dabb2f2c Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Sun, 16 Aug 2026 17:36:49 +0100 Subject: [PATCH 18/30] Add more libcc2rs sections --- docs/src/SUMMARY.md | 5 +++ docs/src/codegen/globals.md | 8 ++++ docs/src/codegen/unions.md | 5 +++ docs/src/runtime/cstr.md | 16 ++++++++ docs/src/runtime/overview.md | 43 +++++++++---------- docs/src/runtime/ptr-dyn.md | 9 ++++ docs/src/runtime/rc.md | 67 +++++++++++------------------- docs/src/runtime/reinterpret.md | 73 ++++++++++++++++++++++++++++++++- docs/src/runtime/void.md | 44 ++++++++++++++++++++ 9 files changed, 203 insertions(+), 67 deletions(-) create mode 100644 docs/src/codegen/globals.md create mode 100644 docs/src/codegen/unions.md create mode 100644 docs/src/runtime/cstr.md create mode 100644 docs/src/runtime/ptr-dyn.md create mode 100644 docs/src/runtime/void.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 53595b51..802a0081 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -24,6 +24,9 @@ - [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) - [I/O and Formatting](./runtime/io.md) - [libc Shims](./runtime/libc-shims.md) @@ -33,5 +36,7 @@ - [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) diff --git a/docs/src/codegen/globals.md b/docs/src/codegen/globals.md new file mode 100644 index 00000000..7606b407 --- /dev/null +++ b/docs/src/codegen/globals.md @@ -0,0 +1,8 @@ +# Global Variables + +Global variables are mapped to thread-local storage, because a `Value` 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. diff --git a/docs/src/codegen/unions.md b/docs/src/codegen/unions.md new file mode 100644 index 00000000..1df4aa31 --- /dev/null +++ b/docs/src/codegen/unions.md @@ -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. diff --git a/docs/src/runtime/cstr.md b/docs/src/runtime/cstr.md new file mode 100644 index 00000000..0fa199a3 --- /dev/null +++ b/docs/src/runtime/cstr.md @@ -0,0 +1,16 @@ +# 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` by +`Ptr::from_string_literal`. `Ptr` 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. + +`CStringIterator` walks the bytes of a `Ptr` up to the null terminator, and +`Display` for `Ptr` prints them, so a C string can be formatted directly. +`with_slice` and `with_slice_mut` 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`. diff --git a/docs/src/runtime/overview.md b/docs/src/runtime/overview.md index 3840f56d..23b7cd39 100644 --- a/docs/src/runtime/overview.md +++ b/docs/src/runtime/overview.md @@ -1,41 +1,36 @@ # Overview -`libcc2rs` is the runtime library that translated programs link against. Every -Rust file `cpp2rust` emits begins with the same import: +Proving ownership in the presence of C++'s unrestricted aliasing is undecidable +in general, so `cpp2rust` does not try to satisfy Rust's borrow checker +statically. Its default output, the refcount model, moves Rust's ownership and +mutability checks to run time: reference counting replaces static ownership, and +dynamic borrow checks replace static mutability checks. This trades some speed +for safety, and it lets every program be translated. + +`libcc2rs` is the runtime library where those checks live: a small crate of +auxiliary types and functions, such as `Value`, `Ptr`, and `AnyPtr`, that +translated programs link against. Keeping this machinery in one library keeps +the generated refcount code free of `unsafe`. + +Every Rust file `cpp2rust` emits imports the whole crate: ```rust extern crate libcc2rs; use libcc2rs::*; ``` -The crate re-exports everything at its root, so this single glob import gives -the generated code access to the whole API without qualified paths. The build is -wired into CMake: the crate is compiled with Cargo into an `rlib`, and -translated programs are compiled against that `rlib` together with -`libcc2rs-macros`, the companion proc-macro crate. - -## The two output models - -The library serves both output models. The refcount model depends on it for its -entire pointer representation: refcounted values, pointers, and byte-level views -all come from the runtime. The unsafe model expresses pointers as raw Rust -pointers and calls libc directly (its output also imports `libc::*`), so it only -uses the runtime for constructs that raw pointers cannot express, such as -`goto`, `switch` fallthrough, and variadic calls. - -Some libc functions exist in the crate as real named functions, such as -`fread_refcount` and `fread_unsafe`, because translated programs can take their -address. A [rule body](../rules/format.md) alone has nothing to take the address -of, so the runtime defines a function with the signature the model expects. - ## Module map The modules fall into three groups. The refcounted pointer model, the core of the refcount output: -- [`rc`](./rc.md): `Value`, `Ptr`, and `AnyPtr`, the refcounted stand-ins - for C values and pointers. +- [`rc`](./rc.md): `Value` and `Ptr`, the refcounted stand-ins for C + values and pointers. +- [`cstr`](./cstr.md): string literals, the `string.h` memory functions, and + iteration over `Ptr` byte strings. +- [`void`](./void.md): `AnyPtr`, the type-erased pointer for `void *`. +- [`ptr_dyn`](./ptr-dyn.md): `PtrDyn`, pointers to virtual classes. - [`reinterpret`](./reinterpret.md): the `ByteRepr` trait and allocation views that let a refcounted allocation be reinterpreted at the byte level, as C pointer casts do. diff --git a/docs/src/runtime/ptr-dyn.md b/docs/src/runtime/ptr-dyn.md new file mode 100644 index 00000000..d266fc6b --- /dev/null +++ b/docs/src/runtime/ptr-dyn.md @@ -0,0 +1,9 @@ +# Virtual Classes + +A pointer to a virtual class cannot be a `Ptr`. The class is translated as a +Rust trait, and trait objects are unsized, which Rust marks with `dyn`. The +runtime provides a dedicated `PtrDyn` type for these pointers, kept +separate so the generic `Ptr` pays no cost for dynamic dispatch. `to_strong` +upgrades a `Ptr` into a `Value`, and `as_pointer_dyn` turns a +`Value` into a `PtrDyn`; a virtual call upgrades the pointer and +dispatches through the trait. diff --git a/docs/src/runtime/rc.md b/docs/src/runtime/rc.md index 623f2d33..71721793 100644 --- a/docs/src/runtime/rc.md +++ b/docs/src/runtime/rc.md @@ -74,11 +74,12 @@ comparison does. ## The heap -`new` and `new[]` are translated as `Ptr::alloc` and `Ptr::alloc_array`. The -allocation's `Rc` is deliberately leaked so the object outlives the statement -that created it. The leak is legitimate: a Rust program that leaks memory is -still well typed. `delete` and `delete_array` recover the leaked reference and -drop it: +`new` and `new[]` are translated as `Ptr::alloc` and `Ptr::alloc_array`, and +`malloc`, `calloc`, and `realloc` allocate through `Ptr::alloc_array` as well. +The allocation's `Rc` is deliberately leaked so the object outlives the +statement that created it. The leak is legitimate: a Rust program that leaks +memory is still well typed. `delete` and `delete_array` recover the leaked +reference and drop it: ```c int *d = new int(0); @@ -130,8 +131,9 @@ is defined as `with_mut(|v| *v = value)`. In every case the `RefCell` is borrowed only for the duration of the access, which is what lets freely aliasing C++ pointers coexist with the borrow checker: no borrow outlives the expression that created it. When an expression needs an -actual Rust reference, the pointer is upgraded to a `StrongPtr`, which holds the -allocation alive and hands out a `Ref`. +actual Rust reference, the pointer is upgraded to a +[`StrongPtr`](../codegen/pointers.md), which holds the allocation alive and +hands out a `Ref`. These borrows are the model's mutability checks, moved from compile time to run time. Rust's rule still holds, any number of readers or one writer, but it is @@ -149,38 +151,19 @@ end of the allocation, exactly as C++ allows; bounds are checked only when the pointer is dereferenced. Subtracting two pointers yields their element distance and requires both to point into the same allocation. -## 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` by -`Ptr::from_string_literal`. `Ptr` also carries the memory functions C -strings rely on: `memcpy` (which copies backwards when the ranges overlap), -`memset`, `memcmp`, and `to_rust_string` for crossing into Rust APIs. - -## Virtual classes - -A pointer to a virtual class cannot be a `Ptr`. The class is translated as a -Rust trait, and trait objects are unsized, which Rust marks with `dyn`. The -runtime provides a dedicated `PtrDyn` type for these pointers, kept -separate so the generic `Ptr` pays no cost for dynamic dispatch. `to_strong` -upgrades a `Ptr` into a `Value`, and `as_pointer_dyn` turns a -`Value` into a `PtrDyn`; a virtual call upgrades the pointer and -dispatches through the trait. - -## void pointers - -`void *` is translated as `AnyPtr`, a type-erased `Ptr`. `to_any` erases the -element type; `reinterpret_cast` recovers it, returning the original pointer -when the types match and a byte-level view otherwise. The `malloc` family -allocates and frees through `AnyPtr`. - -## Global variables - -Global variables are mapped to thread-local storage, because a `Value` 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. +## Integer casts + +Casts between pointers and integers are translated as `to_int` and `from_int`: + +```c +uintptr_t n = (uintptr_t)p; +int *q = (int *)n; +``` + +```rust +let n: Value = Rc::new(RefCell::new((*p.borrow()).to_int())); +let q: Value> = Rc::new(RefCell::new(>::from_int(*n.borrow()))); +``` + +Both currently panic when executed. Giving them well-defined semantics is work +in progress. diff --git a/docs/src/runtime/reinterpret.md b/docs/src/runtime/reinterpret.md index 83871a96..2cb183bd 100644 --- a/docs/src/runtime/reinterpret.md +++ b/docs/src/runtime/reinterpret.md @@ -19,6 +19,43 @@ pub trait ByteRepr: 'static { } ``` +A C struct is translated as a Rust struct whose fields are `Value`s, and the +code generator emits the `ByteRepr` implementation next to it: + +```c +struct header { + int tag; + int size; +}; +``` + +```rust +pub struct header { + pub tag: Value, + pub size: Value, +} + +impl ByteRepr for header { + fn byte_size() -> usize { + 8 + } + fn to_bytes(&self, buf: &mut [u8]) { + (*self.tag.borrow()).to_bytes(&mut buf[0..4]); + (*self.size.borrow()).to_bytes(&mut buf[4..8]); + } + fn from_bytes(buf: &[u8]) -> Self { + Self { + tag: Rc::new(RefCell::new(::from_bytes(&buf[0..4]))), + size: Rc::new(RefCell::new(::from_bytes(&buf[4..8]))), + } + } +} +``` + +`byte_size` is `sizeof(struct header)`. `to_bytes` writes each field at its C +offset into an 8-byte buffer, so the buffer holds the struct exactly as it would +sit in C memory. `from_bytes` reads such a buffer back into a fresh struct. + The primitive types serialize to their native-endian bytes, matching what C sees on the host. The [libc shims](./libc-shims.md#the-sockaddr-family) implement the trait by hand with the byte layout of their C structs. Types with no meaningful @@ -37,10 +74,14 @@ writes through a view are visible everywhere else: ```rust let p: Ptr = Ptr::alloc(0x0807060504030201); +// A view over p's allocation at byte offset 0, stepping by 1 byte. let bytes: Ptr = p.reinterpret_cast::(); +// Read: p.to_bytes() gives the 8 bytes, u8::from_bytes parses byte 0. assert_eq!(bytes.read(), 0x01); +// Write: p.to_bytes(), replace byte 7 with 0xAA, u64::from_bytes back into p. bytes.offset(7).write(0xAA); +// The write went into the original allocation. assert_eq!(p.read(), 0xAA07060504030201); ``` @@ -52,9 +93,39 @@ Deleting through a reinterpreted pointer frees the original allocation. That is how `free` works on a buffer that has been cast around: the pointer is reinterpreted to bytes and the original allocation is deleted. +## Known limitations + +Reading a struct through a reinterpreted pointer builds a fresh struct with +`from_bytes`, so its fields are new `Value`s that exist only as long as that +temporary struct. Three C patterns break because of this; all are set to be +fixed in the near future. + +1. Taking the address of a field of a reinterpreted struct yields a pointer into + the temporary, which dangles as soon as the temporary is dropped. +2. Writing to a field of a reinterpreted struct, translated as + `p.upgrade().deref().field.borrow_mut()`, mutates the temporary returned by + `p.upgrade().deref()` and never writes the bytes back to the original + allocation, so the write is lost. +3. [Union accessors](../codegen/unions.md) return pointers to the union's + storage; on a reinterpreted union that storage is the temporary, so the + returned pointer dangles. + ## AnyPtr casts `AnyPtr::reinterpret_cast` first tries to recover the pointer as it was erased: casting a `void *` back to the type it came from returns the original `Ptr`, with no byte view involved. Only a cast to a different type goes through the -byte representation. +byte representation: + +```rust +let p: Ptr = Ptr::alloc(0x0807060504030201); +let any: AnyPtr = p.to_any(); + +// Same type as erased: the original Ptr comes back. +let back: Ptr = any.reinterpret_cast::(); +assert!(back == p); + +// Different type: a byte view over p's allocation, as with Ptr::reinterpret_cast. +let bytes: Ptr = any.reinterpret_cast::(); +assert_eq!(bytes.read(), 0x01); +``` diff --git a/docs/src/runtime/void.md b/docs/src/runtime/void.md new file mode 100644 index 00000000..fa1eecc7 --- /dev/null +++ b/docs/src/runtime/void.md @@ -0,0 +1,44 @@ +# void Pointers + +`void *` is translated as `AnyPtr`, a type-erased `Ptr`. `to_any` erases the +element type and `reinterpret_cast` recovers it: + +```c +char data[] = "hi"; +void *vp = data; +char *cp = vp; +``` + +```rust +let data: Value> = Rc::new(RefCell::new(Box::from(*b"hi\0"))); +let vp: Value = Rc::new(RefCell::new((data.as_pointer() as Ptr).to_any())); +let cp: Value> = Rc::new(RefCell::new((*vp.borrow()).reinterpret_cast::())); +``` + +`reinterpret_cast` returns the original pointer when the requested type matches +the erased one, and a [byte-level view](./reinterpret.md) otherwise. + +The `malloc` family allocates and frees through `AnyPtr`, so the returned +pointer is cast to the requested type and cast back to free it: + +```c +int *p = malloc(sizeof(int)); +*p = 42; +free(p); +``` + +```rust +// malloc_refcount(n) is Ptr::alloc_array(vec![0u8; n].into_boxed_slice()).to_any() +let p: Value> = Rc::new(RefCell::new( + malloc_refcount(::std::mem::size_of::()).reinterpret_cast::(), +)); +(*p.borrow()).write(42); +free_refcount((*p.borrow()).to_any()); +``` + +`AnyPtr` also carries `memcpy`, `memset`, and `memcmp`, forwarding to the +`Ptr` versions from [C Strings](./cstr.md) over the byte view of its +pointee. + +Casts between `AnyPtr` and integers use the same `to_int` and `from_int` as +[`Ptr`](./rc.md#integer-casts). From ca5ddad05adea7a4e367d60d20f82eb50d9f9786 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Sun, 16 Aug 2026 17:47:49 +0100 Subject: [PATCH 19/30] Add increment and decrement section --- docs/src/SUMMARY.md | 1 + docs/src/runtime/inc-dec.md | 48 ++++++++++++++++++++++++++++++++++++ docs/src/runtime/overview.md | 7 +++--- 3 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 docs/src/runtime/inc-dec.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 802a0081..61ff6496 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -28,6 +28,7 @@ - [void Pointers](./runtime/void.md) - [Virtual Classes](./runtime/ptr-dyn.md) - [Type Reinterpretation](./runtime/reinterpret.md) +- [Increment and Decrement](./runtime/inc-dec.md) - [I/O and Formatting](./runtime/io.md) - [libc Shims](./runtime/libc-shims.md) - [Compat Helpers](./runtime/compat.md) diff --git a/docs/src/runtime/inc-dec.md b/docs/src/runtime/inc-dec.md new file mode 100644 index 00000000..816f4928 --- /dev/null +++ b/docs/src/runtime/inc-dec.md @@ -0,0 +1,48 @@ +# Increment and Decrement + +C's `++` and `--` are expressions: `x++` yields the old value and `++x` the new +one, and both can appear inside a larger expression. Rust only has the `x += 1` +statement, so the `inc` and `dec` modules define one trait per operator form: + +```rust +pub trait PostfixInc { fn postfix_inc(&mut self) -> Self; } +pub trait PrefixInc { fn prefix_inc(&mut self) -> Self; } +pub trait PostfixDec { fn postfix_dec(&mut self) -> Self; } +pub trait PrefixDec { fn prefix_dec(&mut self) -> Self; } +``` + +Each method updates the value in place and returns what the C expression +evaluates to: the postfix forms return a copy of the old value, the prefix forms +the new one. + +```c +int x = 0; +while (x++ < 100 && x != 50) { + ++x; +} +``` + +```rust +let x: Value = Rc::new(RefCell::new(0)); +while (*x.borrow_mut()).postfix_inc() < 100 && *x.borrow() != 50 { + (*x.borrow_mut()).prefix_inc(); +} +``` + +The traits are implemented for the integer types with wrapping arithmetic, so +overflow behaves as C's unsigned wraparound and never panics, and for `f32` and +`f64`. [`Ptr`](./rc.md#arithmetic) implements them by moving its offset one +element, and the map iterators by stepping to the neighbouring key. For each +translated enum the code generator emits `impl_enum_inc_dec!`, a macro exported +by `inc` that implements the four traits by converting through `i32`. + +The unsafe model uses the same traits for integers and floats. For raw pointers +the same method names come from separate `Unsafe*` traits (`UnsafePrefixInc` and +so on), whose methods are `unsafe fn` and step the pointer with `offset(1)`, so +the generated code reads the same in both models: + +```rust +let mut q: *mut i32 = p; +q.prefix_inc(); +q.postfix_dec(); +``` diff --git a/docs/src/runtime/overview.md b/docs/src/runtime/overview.md index 23b7cd39..9503cf12 100644 --- a/docs/src/runtime/overview.md +++ b/docs/src/runtime/overview.md @@ -34,12 +34,13 @@ The refcounted pointer model, the core of the refcount output: - [`reinterpret`](./reinterpret.md): the `ByteRepr` trait and allocation views that let a refcounted allocation be reinterpreted at the byte level, as C pointer casts do. -- `alloc`: `malloc`, `free`, `realloc`, and `calloc` over refcounted byte - arrays. +- [`alloc`](./rc.md#the-heap): `malloc`, `free`, `realloc`, and `calloc` over + refcounted byte arrays. Language-feature emulation, used by both models: -- `inc` and `dec`: traits implementing the four `++`/`--` operator forms. +- [`inc` and `dec`](./inc-dec.md): traits implementing the four `++`/`--` + operator forms. - `iterators`: iteration for C++ containers that need stable iterators, with an implementation for both refcount and unsafe. - `fn_ptr`: `FnPtr`, function pointers with C-style address identity. From 8591e7193dae6bf2a9eb27b8e9cfc2c7dad3b5fc Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Sun, 16 Aug 2026 18:00:40 +0100 Subject: [PATCH 20/30] Add iterators section --- docs/src/SUMMARY.md | 1 + docs/src/runtime/inc-dec.md | 7 ++-- docs/src/runtime/iterators.md | 71 +++++++++++++++++++++++++++++++++++ docs/src/runtime/overview.md | 4 +- 4 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 docs/src/runtime/iterators.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 61ff6496..9619ba19 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -29,6 +29,7 @@ - [Virtual Classes](./runtime/ptr-dyn.md) - [Type Reinterpretation](./runtime/reinterpret.md) - [Increment and Decrement](./runtime/inc-dec.md) +- [Iterators](./runtime/iterators.md) - [I/O and Formatting](./runtime/io.md) - [libc Shims](./runtime/libc-shims.md) - [Compat Helpers](./runtime/compat.md) diff --git a/docs/src/runtime/inc-dec.md b/docs/src/runtime/inc-dec.md index 816f4928..75133de9 100644 --- a/docs/src/runtime/inc-dec.md +++ b/docs/src/runtime/inc-dec.md @@ -32,9 +32,10 @@ while (*x.borrow_mut()).postfix_inc() < 100 && *x.borrow() != 50 { The traits are implemented for the integer types with wrapping arithmetic, so overflow behaves as C's unsigned wraparound and never panics, and for `f32` and `f64`. [`Ptr`](./rc.md#arithmetic) implements them by moving its offset one -element, and the map iterators by stepping to the neighbouring key. For each -translated enum the code generator emits `impl_enum_inc_dec!`, a macro exported -by `inc` that implements the four traits by converting through `i32`. +element, and the [map iterators](./iterators.md#stable-iterators) by stepping to +the neighbouring key. For each translated enum the code generator emits +`impl_enum_inc_dec!`, a macro exported by `inc` that implements the four traits +by converting through `i32`. The unsafe model uses the same traits for integers and floats. For raw pointers the same method names come from separate `Unsafe*` traits (`UnsafePrefixInc` and diff --git a/docs/src/runtime/iterators.md b/docs/src/runtime/iterators.md new file mode 100644 index 00000000..5bfb25b3 --- /dev/null +++ b/docs/src/runtime/iterators.md @@ -0,0 +1,71 @@ +# Iterators + +A C++ iterator is a pointer-like object: it is dereferenced, compared against +`end()`, and moved with `++` and `--`, and it stays usable across the statements +of a loop body. Rust iterators are consumed by a `for` loop and cannot be +compared or stepped backwards, so the runtime represents C++ iterators with +values of its own. + +## Random access iterators + +For `std::vector`, `std::string`, and arrays the iterator is a +[`Ptr`](./rc.md) into the container's buffer: `begin()` is `as_pointer()`, +`end()` is `to_end()`, and comparison and arithmetic are the pointer's own. +`Ptr` also implements `Iterator`, yielding a pointer to each element, so a +range-based `for` becomes a Rust `for` over the pointer: + +```cpp +std::vector v; +for (auto x : v) + printf("%d\n", x); +``` + +```rust +let v: Value> = Rc::new(RefCell::new(Vec::new())); +for x in v.as_pointer() as Ptr { + println!("{}", x.read()); +} +``` + +Two variants serve special cases. `StringIterator`, returned by +`to_string_iterator`, stops before the trailing zero byte, so iterating a +`std::string` visits its characters only. `PtrValueIter` yields copies of the +elements instead of pointers to them; rule bodies use it to feed a range of C +memory to Rust iterator adaptors: + +```rust +// std::accumulate(first, last, init) +let count = (last - first) as usize; +PtrValueIter::new(&first, count).fold(init, |acc, x| acc + x) +``` + +## Stable iterators + +`std::map` is translated as a `BTreeMap>`, which has no +addressable elements to point into. The runtime defines `MapIter` for it: a pair +of a handle to the map and the current key, with `None` standing for `end()`. +Because it stores a key rather than a position, it survives insertions and +removals elsewhere in the map, as C++ guarantees. `begin`, `end`, and `find_key` +construct one; `inc` and `dec` move to the neighbouring key; `erase` removes the +current entry and returns the iterator to the next; the `++`/`--` traits and +`Iterator` are implemented on top of these: + +```cpp +std::map m; +for (const auto &i : m) + sum += i.second; +``` + +```rust +let m: Value>> = Rc::new(RefCell::new(BTreeMap::new())); +for i in RefcountMapIter::begin(m.as_pointer()) { + (*sum.borrow_mut()) += (*i.second().borrow()); +} +``` + +`first()` and `second()` come from the `MapIterator` trait and take the place of +`it->first` and `it->second`. `MapIter` is generic over how the map is reached, +which is what gives it an implementation for both models: +`RefcountMapIter` holds a `Ptr>>` and returns +`Value` and `Value`; `UnsafeMapIterator` holds a +`*const BTreeMap>` and returns `*const K` and `*mut V`. diff --git a/docs/src/runtime/overview.md b/docs/src/runtime/overview.md index 9503cf12..0fafbc44 100644 --- a/docs/src/runtime/overview.md +++ b/docs/src/runtime/overview.md @@ -41,8 +41,8 @@ Language-feature emulation, used by both models: - [`inc` and `dec`](./inc-dec.md): traits implementing the four `++`/`--` operator forms. -- `iterators`: iteration for C++ containers that need stable iterators, with an - implementation for both refcount and unsafe. +- [`iterators`](./iterators.md): iteration for C++ containers that need stable + iterators, with an implementation for both refcount and unsafe. - `fn_ptr`: `FnPtr`, function pointers with C-style address identity. - `va_args`: `VaArg` and `VaList`, the representation of variadic calls. - The `goto`, `goto_block`, and `switch` proc macros, re-exported from From e9ffa712c392664cd6f53039e8a5d09e6c64ac16 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Sun, 16 Aug 2026 18:53:11 +0100 Subject: [PATCH 21/30] Add function pointer section --- docs/src/SUMMARY.md | 1 + docs/src/runtime/fn-ptr.md | 84 ++++++++++++++++++++++++++++++++++++ docs/src/runtime/overview.md | 3 +- 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 docs/src/runtime/fn-ptr.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 9619ba19..2076e2c3 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -30,6 +30,7 @@ - [Type Reinterpretation](./runtime/reinterpret.md) - [Increment and Decrement](./runtime/inc-dec.md) - [Iterators](./runtime/iterators.md) +- [Function Pointers](./runtime/fn-ptr.md) - [I/O and Formatting](./runtime/io.md) - [libc Shims](./runtime/libc-shims.md) - [Compat Helpers](./runtime/compat.md) diff --git a/docs/src/runtime/fn-ptr.md b/docs/src/runtime/fn-ptr.md new file mode 100644 index 00000000..6425a96d --- /dev/null +++ b/docs/src/runtime/fn-ptr.md @@ -0,0 +1,84 @@ +# Function Pointers + +A C function pointer can be null, compared for equality, cast to another +function pointer type and back, and stored in a `void *`. A Rust `fn` value can +be called and compared, but it is never null and its type is fixed, so the +refcount model translates function pointers as `FnPtr`, where `T` is the Rust +`fn` type of the target: + +```rust +pub struct FnPtr { /* the function as first stored, and its current cast */ } + +impl FnPtr { + pub fn null() -> Self; + pub fn new(f: T) -> Self; + pub fn is_null(&self) -> bool; + pub fn cast(&self, adapter: Option) -> FnPtr; + pub fn to_any(&self) -> AnyPtr; +} +``` + +`FnPtr` dereferences to the function, so a call through it is `(*fp)(args)`. +Calling a null pointer panics with `ub:`. + +```cpp +typedef int (*int_fn)(int); +int double_it(int x) { return x * 2; } + +int_fn fn = double_it; +int r = fn(5); +``` + +```rust +let fn_: Value i32>> = + Rc::new(RefCell::new(FnPtr:: i32>::new(double_it_0))); +let r: Value = Rc::new(RefCell::new((*(*fn_.borrow()))(5))); +``` + +## Casts + +C code casts function pointers to a different type and calls through the new +type. When the two types are not compatible this is undefined behavior, but the +argument types involved usually have the same representation, so implementations +accept the call and programs rely on it. Below, `add_offset` takes an `int *`, +but is called through a pointer that takes a `void *`: + +```c +typedef int (*generic_int_fn)(void *, int); +int add_offset(int *base, int offset) { return *base + offset; } + +generic_int_fn gfn = (generic_int_fn)add_offset; +int result = gfn(&val, 42); +``` + +In Rust `fn(Ptr, i32) -> i32` and `fn(AnyPtr, i32) -> i32` are unrelated +types, so the code generator emits an adapter: a function of the target type +that converts the arguments and calls the original. `cast` stores it, and calls +through the cast pointer go through the adapter: + +```rust +let gfn: Value i32>> = Rc::new(RefCell::new( + FnPtr::, i32) -> i32>::new(add_offset_4).cast:: i32>(Some( + (|a0: AnyPtr, a1: i32| -> i32 { add_offset_4(a0.reinterpret_cast::(), a1) }) + as fn(AnyPtr, i32) -> i32, + )), +)); +let result: Value = Rc::new(RefCell::new((*(*gfn.borrow()))(val.as_pointer().to_any(), 42))); +``` + +The code generator can build an adapter when the arguments and return type of +the two function types have the same representation. Otherwise it passes `None`, +and calling through the cast pointer panics with `ub:`. + +Equality compares the address of the function the pointer was created with. + +Casting a function pointer to `void *` is `to_any`, and `AnyPtr::cast_fn::` +recovers it. `reinterpret_cast` on an `AnyPtr` holding a function currently +panics, as do [integer casts](./rc.md#integer-casts) on a `Ptr`; both are set to +be fixed in the near future. + +## The unsafe model + +The unsafe model uses `Option` directly, with `None` as the null +pointer, and casts between function pointer types with `std::mem::transmute`; it +does not use `FnPtr`. diff --git a/docs/src/runtime/overview.md b/docs/src/runtime/overview.md index 0fafbc44..0f26023f 100644 --- a/docs/src/runtime/overview.md +++ b/docs/src/runtime/overview.md @@ -43,7 +43,8 @@ Language-feature emulation, used by both models: operator forms. - [`iterators`](./iterators.md): iteration for C++ containers that need stable iterators, with an implementation for both refcount and unsafe. -- `fn_ptr`: `FnPtr`, function pointers with C-style address identity. +- [`fn_ptr`](./fn-ptr.md): `FnPtr`, function pointers with C-style address + identity. - `va_args`: `VaArg` and `VaList`, the representation of variadic calls. - The `goto`, `goto_block`, and `switch` proc macros, re-exported from `libcc2rs-macros`, which rewrite unstructured control flow into state From 9d8bf185589b3f5e3fcb2519070915f433dc8d6e Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Sun, 16 Aug 2026 19:00:21 +0100 Subject: [PATCH 22/30] Add variadic args section --- docs/src/SUMMARY.md | 1 + docs/src/runtime/io.md | 4 +-- docs/src/runtime/overview.md | 3 +- docs/src/runtime/va-args.md | 63 ++++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 docs/src/runtime/va-args.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 2076e2c3..c6ee080e 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -31,6 +31,7 @@ - [Increment and Decrement](./runtime/inc-dec.md) - [Iterators](./runtime/iterators.md) - [Function Pointers](./runtime/fn-ptr.md) +- [Variadic Functions](./runtime/va-args.md) - [I/O and Formatting](./runtime/io.md) - [libc Shims](./runtime/libc-shims.md) - [Compat Helpers](./runtime/compat.md) diff --git a/docs/src/runtime/io.md b/docs/src/runtime/io.md index 0708850f..40b05f65 100644 --- a/docs/src/runtime/io.md +++ b/docs/src/runtime/io.md @@ -45,8 +45,8 @@ value. For those cases, and for functions that format into a buffer such as `snprintf`, the refcount model falls back to `format_c`; the unsafe model calls libc directly. -`format_c` evaluates a C format string against a slice of variadic arguments and -returns the formatted `String`: +`format_c` evaluates a C format string against a slice of +[variadic arguments](./va-args.md) and returns the formatted `String`: ```rust pub fn format_c(fmt: &str, va: &[VaArg]) -> String; diff --git a/docs/src/runtime/overview.md b/docs/src/runtime/overview.md index 0f26023f..c15c8591 100644 --- a/docs/src/runtime/overview.md +++ b/docs/src/runtime/overview.md @@ -45,7 +45,8 @@ Language-feature emulation, used by both models: iterators, with an implementation for both refcount and unsafe. - [`fn_ptr`](./fn-ptr.md): `FnPtr`, function pointers with C-style address identity. -- `va_args`: `VaArg` and `VaList`, the representation of variadic calls. +- [`va_args`](./va-args.md): `VaArg` and `VaList`, the representation of + variadic calls. - The `goto`, `goto_block`, and `switch` proc macros, re-exported from `libcc2rs-macros`, which rewrite unstructured control flow into state machines. diff --git a/docs/src/runtime/va-args.md b/docs/src/runtime/va-args.md new file mode 100644 index 00000000..9cd58d06 --- /dev/null +++ b/docs/src/runtime/va-args.md @@ -0,0 +1,63 @@ +# Variadic Functions + +Rust has no `...` parameters and no `va_list`. A variadic C function is +translated as a function whose last parameter is a slice of `VaArg`, an enum +with one variant per kind of value C's default argument promotions can produce: + +```rust +pub enum VaArg { + Int(i32), + UInt(u32), + Long(i64), + ULong(u64), + Double(f64), + RawPtr(*mut c_void), + Ptr(AnyPtr), +} +``` + +At a call site every extra argument is converted with `.into()`, which performs +the promotions (`char` and `short` to `int`, `float` to `double`) and erases +pointers to `AnyPtr` in the refcount model or `*mut c_void` in the unsafe model. +Inside the function, `va_list` is a `VaList`, a cursor over the slice: +`va_start` becomes `VaList::new(__args)`, `va_arg(ap, T)` becomes +`ap.arg::()`, `va_copy` is a plain copy of the cursor, and `va_end` is a +no-op: + +```c +int sum(int count, ...) { + va_list ap; + va_start(ap, count); + int total = 0; + for (int i = 0; i < count; i++) + total += va_arg(ap, int); + va_end(ap); + return total; +} + +sum(3, 10, 20, 30); +``` + +```rust +pub fn sum_0(count: i32, __args: &[VaArg]) -> i32 { + let ap: Value = Rc::new(RefCell::new(VaList::default())); + (*ap.borrow_mut()) = VaList::new(__args); + let total: Value = Rc::new(RefCell::new(0)); + // ... + (*total.borrow_mut()) += (*ap.borrow_mut()).arg::(); + // ... +} + +sum_0(3, &[10.into(), 20.into(), 30.into()]); +``` + +`arg::()` goes through the `VaArgGet` trait, implemented for the integer and +floating types, raw pointers, `Ptr`, `AnyPtr`, and `FnPtr`. Integer +variants convert freely among the integer types, as `va_arg` does with types of +the same rank; asking for a pointer where an integer was passed, or the reverse, +panics. + +Variadic libc functions such as `printf` and `fcntl` are handled by +[variadic rules](../rules/writing-rules.md#variadic-functions), whose bodies +receive the same `&[VaArg]` slice; `format_c` in the +[format module](./io.md#formatting) consumes one to evaluate a format string. From 41dcffd299c9a3a8f92d03483440b053df3c756b Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Sun, 16 Aug 2026 19:25:17 +0100 Subject: [PATCH 23/30] Add goto and switch macros --- docs/src/SUMMARY.md | 1 + docs/src/runtime/control-flow.md | 175 +++++++++++++++++++++++++++++++ docs/src/runtime/overview.md | 6 +- 3 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 docs/src/runtime/control-flow.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index c6ee080e..3d714784 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -32,6 +32,7 @@ - [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) diff --git a/docs/src/runtime/control-flow.md b/docs/src/runtime/control-flow.md new file mode 100644 index 00000000..3f1e3247 --- /dev/null +++ b/docs/src/runtime/control-flow.md @@ -0,0 +1,175 @@ +# 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 = Rc::new(RefCell::new(n)); + let count: Value = >::default(); + let acc: Value = >::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!`. `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 = >::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. diff --git a/docs/src/runtime/overview.md b/docs/src/runtime/overview.md index c15c8591..29978e84 100644 --- a/docs/src/runtime/overview.md +++ b/docs/src/runtime/overview.md @@ -47,9 +47,9 @@ Language-feature emulation, used by both models: identity. - [`va_args`](./va-args.md): `VaArg` and `VaList`, the representation of variadic calls. -- The `goto`, `goto_block`, and `switch` proc macros, re-exported from - `libcc2rs-macros`, which rewrite unstructured control flow into state - machines. +- The [`goto`, `goto_block`, and `switch`](./control-flow.md) proc macros, + re-exported from `libcc2rs-macros`, which rewrite unstructured control flow + into state machines. The OS and libc surface: From 0e2ee1b9747de0e6d523480c12d577e141a8f8ed Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Sun, 16 Aug 2026 20:22:29 +0100 Subject: [PATCH 24/30] Delete unused impl for Cursor --- libcc2rs/src/inc.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/libcc2rs/src/inc.rs b/libcc2rs/src/inc.rs index c83c68f1..52eeb531 100644 --- a/libcc2rs/src/inc.rs +++ b/libcc2rs/src/inc.rs @@ -1,21 +1,10 @@ // Copyright (c) 2022-present INESC-ID. // Distributed under the MIT license that can be found in the LICENSE file. -use std::io::Cursor; - pub trait PostfixInc { fn postfix_inc(&mut self) -> Self; } -impl PostfixInc for Cursor<*mut T> { - #[inline] - fn postfix_inc(&mut self) -> Self { - let clone = self.clone(); - self.set_position(self.position().wrapping_add(1)); - clone - } -} - macro_rules! postfix_nowrap_inc_impl { ($($type:ty),*) => { $(impl PostfixInc for $type { From fb23d47335e143b899f1da2dfc6d972da982dd29 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Sun, 16 Aug 2026 21:15:41 +0100 Subject: [PATCH 25/30] Add missing info --- docs/src/runtime/control-flow.md | 4 +- docs/src/runtime/cstr.md | 13 +++-- docs/src/runtime/fn-ptr.md | 6 +++ docs/src/runtime/io.md | 22 ++++++-- docs/src/runtime/iterators.md | 4 +- docs/src/runtime/libc-shims.md | 6 +++ docs/src/runtime/overview.md | 2 +- docs/src/runtime/ptr-dyn.md | 43 ++++++++++++++-- docs/src/runtime/rc.md | 88 ++++++++++++++++++++++++++++++-- docs/src/runtime/va-args.md | 4 +- docs/src/runtime/void.md | 7 +++ 11 files changed, 175 insertions(+), 24 deletions(-) diff --git a/docs/src/runtime/control-flow.md b/docs/src/runtime/control-flow.md index 3f1e3247..7e125cbc 100644 --- a/docs/src/runtime/control-flow.md +++ b/docs/src/runtime/control-flow.md @@ -128,7 +128,9 @@ switch!(match (*x.borrow()) { `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!`. `goto` and `switch` mix freely: a `switch!` can be +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. diff --git a/docs/src/runtime/cstr.md b/docs/src/runtime/cstr.md index 0fa199a3..ca5642e7 100644 --- a/docs/src/runtime/cstr.md +++ b/docs/src/runtime/cstr.md @@ -9,8 +9,11 @@ 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. -`CStringIterator` walks the bytes of a `Ptr` up to the null terminator, and -`Display` for `Ptr` prints them, so a C string can be formatted directly. -`with_slice` and `with_slice_mut` 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`. +There are two ways to hand C bytes to Rust code. `CStringIterator`, returned +by `to_c_string_iterator`, walks the bytes of a `Ptr` 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` 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`. diff --git a/docs/src/runtime/fn-ptr.md b/docs/src/runtime/fn-ptr.md index 6425a96d..e83a0b93 100644 --- a/docs/src/runtime/fn-ptr.md +++ b/docs/src/runtime/fn-ptr.md @@ -21,6 +21,12 @@ impl FnPtr { `FnPtr` dereferences to the function, so a call through it is `(*fp)(args)`. Calling a null pointer panics with `ub:`. +`FnPtr` stores its function type-erased and identifies it by address through +the `FnAddr` trait. Rust has no way to write an impl for every `fn` arity at +once, so `FnAddr` is implemented by a macro for `fn` types of zero to sixteen +parameters. A function with more parameters cannot be wrapped in an `FnPtr`, +and taking its address fails to compile with a missing `FnAddr` bound. + ```cpp typedef int (*int_fn)(int); int double_it(int x) { return x * 2; } diff --git a/docs/src/runtime/io.md b/docs/src/runtime/io.md index 40b05f65..a0142a19 100644 --- a/docs/src/runtime/io.md +++ b/docs/src/runtime/io.md @@ -11,7 +11,8 @@ flags that `feof` and `ferror` report long after the read that set them. as a `Ptr`, a [libc shim](./libc-shims.md) that holds the file descriptor together with these two flags. The standard streams are thread-local `CFile` values over descriptors 0, 1, and 2, returned by `c_stdin`, `c_stdout`, and -`c_stderr`. +`c_stderr`. `CFile` does no buffering at present: every read or write on it is +a system call on the descriptor. In the unsafe model streams stay raw: `stdin_unsafe`, `stdout_unsafe`, and `stderr_unsafe` return the process's `*mut libc::FILE` handles, whose symbol @@ -31,9 +32,13 @@ through the `CFile`; the unsafe variant forwards to `libc::fread`. ## C++ Streams In the refcount model `cin`, `cout`, and `cerr` are translated as -`Ptr` values over duplicates of the standard descriptors. In the -unsafe model `cin_unsafe`, `cout_unsafe`, and `cerr_unsafe` return raw pointers -to thread-local `std::fs::File` values. C++ streams do not map fully onto +`Ptr` values over duplicates of the standard descriptors, +returned by the `cin`, `cout`, and `cerr` functions. `Ptr` implements +`write_fmt` and `write_all` whenever `T: Write`, forwarding to the pointee +through `with_mut`, so `cout << x` becomes `write!(cout(), "{}", x)` and a raw +byte range is written with `cout().write_all(..)`. In the unsafe model +`cin_unsafe`, `cout_unsafe`, and `cerr_unsafe` return raw pointers to +thread-local `std::fs::File` values. C++ streams do not map fully onto `std::fs::File`, so this translation may change in the future. ## Formatting @@ -55,7 +60,14 @@ pub fn format_c(fmt: &str, va: &[VaArg]) -> String; Parsing and rendering come from the `sprintf` crate. The integer, character, string, and floating-point conversions are supported, and `%s` reads the argument through the refcounted pointer as a Rust string. A malformed format -string or an argument of the wrong kind is a panic. +string or an argument of the wrong kind is a panic. Three things are not +supported yet: + +1. `%p` renders through the pointer's [integer cast](./rc.md#integer-casts), + which currently panics. +2. `%n` is not handled. +3. A `*` width or precision (`%*d`, `%.*s`) is parsed but its integer argument + is not consumed, so the remaining arguments are misaligned. ## File descriptors diff --git a/docs/src/runtime/iterators.md b/docs/src/runtime/iterators.md index 5bfb25b3..39936b42 100644 --- a/docs/src/runtime/iterators.md +++ b/docs/src/runtime/iterators.md @@ -48,7 +48,9 @@ Because it stores a key rather than a position, it survives insertions and removals elsewhere in the map, as C++ guarantees. `begin`, `end`, and `find_key` construct one; `inc` and `dec` move to the neighbouring key; `erase` removes the current entry and returns the iterator to the next; the `++`/`--` traits and -`Iterator` are implemented on top of these: +`Iterator` are implemented on top of these. Two iterators compare equal when +they hold the same key, whichever map they came from, so `it != m.end()` +compares `Some(key)` against `None`: ```cpp std::map m; diff --git a/docs/src/runtime/libc-shims.md b/docs/src/runtime/libc-shims.md index d17b6307..38521d94 100644 --- a/docs/src/runtime/libc-shims.md +++ b/docs/src/runtime/libc-shims.md @@ -50,6 +50,12 @@ stdio stream logic (see [I/O and Formatting](./io.md)), and the `time` shims convert through the `jiff` crate. `CFdSet` and the `sockaddr` family depart further from their C counterparts. +Each shim module also gives the raw `libc` struct it mirrors an empty +`ByteRepr` impl (`impl ByteRepr for ::libc::stat {}`), whose methods panic. +These exist so that the generated `ByteRepr` implementation of a translated +struct with a libc struct member still compiles; reinterpreting such a struct +is not supported at present. + ## CFdSet nix has its own `FdSet`, but it is stricter than the C one: it ties the set to diff --git a/docs/src/runtime/overview.md b/docs/src/runtime/overview.md index 29978e84..14051cd3 100644 --- a/docs/src/runtime/overview.md +++ b/docs/src/runtime/overview.md @@ -66,7 +66,7 @@ The OS and libc surface: ## Dependencies -The crate has four dependencies: +The crate has five dependencies: - `libcc2rs-macros` provides the control-flow proc macros. - `libc` and `nix` provide the raw and safe OS interfaces the shims wrap. diff --git a/docs/src/runtime/ptr-dyn.md b/docs/src/runtime/ptr-dyn.md index d266fc6b..68edc5d5 100644 --- a/docs/src/runtime/ptr-dyn.md +++ b/docs/src/runtime/ptr-dyn.md @@ -3,7 +3,42 @@ A pointer to a virtual class cannot be a `Ptr`. The class is translated as a Rust trait, and trait objects are unsized, which Rust marks with `dyn`. The runtime provides a dedicated `PtrDyn` type for these pointers, kept -separate so the generic `Ptr` pays no cost for dynamic dispatch. `to_strong` -upgrades a `Ptr` into a `Value`, and `as_pointer_dyn` turns a -`Value` into a `PtrDyn`; a virtual call upgrades the pointer and -dispatches through the trait. +separate so the generic `Ptr` pays no cost for dynamic dispatch. + +A `PtrDyn` is created at the point where C++ converts a derived pointer to a +base pointer. `to_strong` upgrades the `Ptr` into its `Value`, +Rust's unsized coercion turns that into a `Value`, and +`as_pointer_dyn` takes the weak reference back out: + +```cpp +struct Base { virtual int f() const = 0; }; +struct Derived : Base { int f() const override { return 1; } }; + +Derived d; +Base *b = &d; +int r = b->f(); +``` + +```rust +let d: Value = Rc::new(RefCell::new(::default())); +let b: Value> = Rc::new(RefCell::new( + ((d.as_pointer()).to_strong() as Value).as_pointer_dyn(), +)); +let r: Value = Rc::new(RefCell::new(({ (*(*b.borrow()).upgrade().deref()).f() }))); +``` + +A virtual call goes through `upgrade`, which returns a `StrongPtrDyn` +holding the strong reference; its `deref` and `deref_mut` borrow the object and +the call dispatches through the trait's vtable. + +> [!WARNING] +> `StrongPtrDyn` is set to be removed for the same reasons as +> [`StrongPtr`](./rc.md#strong-pointers): it holds a strong reference that can +> outlive the object's C++ lifetime, and even as a temporary it spans the whole +> virtual call, so a method that deletes its own object panics on `delete`. + +`PtrDyn` is far smaller than `Ptr`: it is either null or a weak reference to a +single object. It has no arithmetic, no comparison, no array kinds, and no byte +view. Because `to_strong` is only defined for single-value pointers, a base +pointer into an array of polymorphic objects (a `Derived arr[N]` walked through +a `Base *`) cannot be formed. diff --git a/docs/src/runtime/rc.md b/docs/src/runtime/rc.md index 71721793..f238035a 100644 --- a/docs/src/runtime/rc.md +++ b/docs/src/runtime/rc.md @@ -75,8 +75,11 @@ comparison does. ## The heap `new` and `new[]` are translated as `Ptr::alloc` and `Ptr::alloc_array`, and -`malloc`, `calloc`, and `realloc` allocate through `Ptr::alloc_array` as well. -The allocation's `Rc` is deliberately leaked so the object outlives the +`malloc`, `calloc`, `realloc`, and `strdup` allocate through `Ptr::alloc_array` +as well; the `alloc` module defines them as named functions +(`malloc_refcount`, `free_refcount`, `realloc_refcount`, `calloc_refcount`, +`strdup_refcount`, and their `_unsafe` twins for the unsafe model). The +allocation's `Rc` is deliberately leaked so the object outlives the statement that created it. The leak is legitimate: a Rust program that leaks memory is still well typed. `delete` and `delete_array` recover the leaked reference and drop it: @@ -97,6 +100,27 @@ let d: Value> = Rc::new(RefCell::new(Ptr::alloc(0))); allocation: freeing twice, freeing through an offset pointer, or freeing a stack value panics with `ub:`. +A heap allocation can also change hands instead of being freed. `to_owned_opt` +recovers the leaked reference the same way `delete` does, but returns it to the +caller as an owning `Option>` (or `Option>>` for an +array), with `None` for the null pointer; from then on the allocation lives +exactly as long as that binding. It panics for stack, `Vec`, and reinterpreted +pointers, which have no leaked reference to recover. This is how +`std::unique_ptr` is translated: the smart pointer is an `Option>`, +its constructor and `reset` adopt a raw pointer with `to_owned_opt`, and +`as_pointer`, which is also implemented for `Option>` and yields null +for `None`, stands in for `get()`: + +```cpp +std::unique_ptr u(new int(1)); +int *raw = u.get(); +``` + +```rust +let u: Value>> = Rc::new(RefCell::new(Ptr::alloc(1).to_owned_opt())); +let raw: Value> = Rc::new(RefCell::new((*u.borrow()).as_pointer())); +``` + ## Dereferences A dereference becomes a short-lived borrow. `read` and `write` copy a value out @@ -132,8 +156,8 @@ In every case the `RefCell` is borrowed only for the duration of the access, which is what lets freely aliasing C++ pointers coexist with the borrow checker: no borrow outlives the expression that created it. When an expression needs an actual Rust reference, the pointer is upgraded to a -[`StrongPtr`](../codegen/pointers.md), which holds the allocation alive and -hands out a `Ref`. +[`StrongPtr`](#strong-pointers), which holds the allocation alive and hands out +a `Ref`. These borrows are the model's mutability checks, moved from compile time to run time. Rust's rule still holds, any number of readers or one writer, but it is @@ -143,6 +167,54 @@ traps. The code generator is responsible for not emitting such expressions: it stores intermediate results in temporaries, so the reading borrow ends before the writing borrow starts. +## Strong pointers + +`upgrade` turns a `Ptr` into a `StrongPtr`, the same pointer holding a +strong `Rc` to its allocation instead of a weak one: + +```rust +pub enum StrongPtr { + StackSingle(Rc>), + Vec { rc: Rc>>, offset: usize }, + StackArray { rc: Rc>>, offset: usize }, + Reinterpreted { alloc: Rc, byte_offset: usize, cell: RefCell> }, +} +``` + +Its one operation is `deref`, which returns a `Ref<'_, T>` to the pointee. The +`Ref` borrows the `StrongPtr`, so the borrow of the `RefCell` lasts as long as +the strong pointer does: in `p.upgrade().deref().field`, the temporary +`StrongPtr` lives until the end of the enclosing statement, and so does the +borrow. That is what makes a member access through a pointer expressible +without a closure. There is no `deref_mut`; writes go through `with_mut`. + +For the `Reinterpreted` variant there is no value to reference, only bytes in +another allocation. `deref` reads those bytes into the local `cell` and hands +out a `Ref` to that copy, refreshing it on every call. The copy is what +[Type Reinterpretation](./reinterpret.md#known-limitations) is about: writes +into the copy never reach the original allocation. + +> [!WARNING] +> `StrongPtr` is set to be removed. Holding a strong reference, even briefly, +> undermines the model in two ways: +> +> 1. Nothing prevents a `StrongPtr` from outliving its statement. One that is +> stored, returned, or bound to a local keeps the object alive past the point +> where C++ destroys it, so its destructor runs late and dangling accesses go +> unnoticed; a heap object held this way makes the later `delete` panic. The +> generator only ever emits it as a temporary, but hand-edited code or a rule +> can break that. +> 2. Even as a temporary, it lives for the whole statement. In +> `(*p.upgrade().deref()).method()` the strong reference is alive during the +> call, so a method that runs `delete this`, or otherwise deletes the object +> it was called on, hits `delete`'s reference-count check and panics with a +> spurious `ub: invalid delete`. + +`to_strong` skips the `StrongPtr` and returns the `Value` itself. It is only +defined for the single-value kinds and is what the code generator uses when it +needs the owning cell of a pointee, for example to coerce a `Value` to +a `Value` (see [Virtual Classes](./ptr-dyn.md)). + ## Arithmetic The offset lives in the pointer, so arithmetic never touches the allocation. @@ -151,6 +223,12 @@ end of the allocation, exactly as C++ allows; bounds are checked only when the pointer is dereferenced. Subtracting two pointers yields their element distance and requires both to point into the same allocation. +The pointer also knows the extent of its allocation: `len` is the number of +elements in it, whatever the pointer's offset, and `get_offset` is the +pointer's element index within it. The container rules build `end()` and +`back()` pointers from these (`to_end`, `to_last`) and turn a `[first, last)` +range into a count or an absolute index the same way. + ## Integer casts Casts between pointers and integers are translated as `to_int` and `from_int`: @@ -166,4 +244,4 @@ let q: Value> = Rc::new(RefCell::new(>::from_int(*n.borrow())) ``` Both currently panic when executed. Giving them well-defined semantics is work -in progress. +in progress ([#225](https://github.com/Cpp2Rust/cpp2rust/pull/225)). diff --git a/docs/src/runtime/va-args.md b/docs/src/runtime/va-args.md index 9cd58d06..7c5d13bb 100644 --- a/docs/src/runtime/va-args.md +++ b/docs/src/runtime/va-args.md @@ -54,8 +54,8 @@ sum_0(3, &[10.into(), 20.into(), 30.into()]); `arg::()` goes through the `VaArgGet` trait, implemented for the integer and floating types, raw pointers, `Ptr`, `AnyPtr`, and `FnPtr`. Integer variants convert freely among the integer types, as `va_arg` does with types of -the same rank; asking for a pointer where an integer was passed, or the reverse, -panics. +the same rank; asking for a pointer where an integer was passed, or the +reverse, panics, as does reading past the last argument. Variadic libc functions such as `printf` and `fcntl` are handled by [variadic rules](../rules/writing-rules.md#variadic-functions), whose bodies diff --git a/docs/src/runtime/void.md b/docs/src/runtime/void.md index fa1eecc7..03162695 100644 --- a/docs/src/runtime/void.md +++ b/docs/src/runtime/void.md @@ -40,5 +40,12 @@ free_refcount((*p.borrow()).to_any()); `Ptr` versions from [C Strings](./cstr.md) over the byte view of its pointee. +> [!WARNING] +> Two `AnyPtr` values are equal only when they were erased from the same +> pointer type and compare equal as that type. A `void *` obtained from a +> `Ptr` and one obtained from a `Ptr` into the same allocation compare +> unequal, where C would consider them the same address. This is set to be +> fixed by comparing through the byte view instead. + Casts between `AnyPtr` and integers use the same `to_int` and `from_int` as [`Ptr`](./rc.md#integer-casts). From f9cff9e0e4beeffe7be1c880e71971e8fb1d6296 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Sun, 16 Aug 2026 21:20:19 +0100 Subject: [PATCH 26/30] Run prettier --- docs/src/runtime/control-flow.md | 6 +++--- docs/src/runtime/cstr.md | 9 ++++----- docs/src/runtime/fn-ptr.md | 10 +++++----- docs/src/runtime/io.md | 4 ++-- docs/src/runtime/libc-shims.md | 10 +++++----- docs/src/runtime/ptr-dyn.md | 3 +-- docs/src/runtime/rc.md | 30 ++++++++++++++---------------- docs/src/runtime/va-args.md | 4 ++-- docs/src/runtime/void.md | 9 ++++----- 9 files changed, 40 insertions(+), 45 deletions(-) diff --git a/docs/src/runtime/control-flow.md b/docs/src/runtime/control-flow.md index 7e125cbc..29a34e8c 100644 --- a/docs/src/runtime/control-flow.md +++ b/docs/src/runtime/control-flow.md @@ -130,9 +130,9 @@ 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 +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 diff --git a/docs/src/runtime/cstr.md b/docs/src/runtime/cstr.md index ca5642e7..fcc195b8 100644 --- a/docs/src/runtime/cstr.md +++ b/docs/src/runtime/cstr.md @@ -9,11 +9,10 @@ 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` up to the null +There are two ways to hand C bytes to Rust code. `CStringIterator`, returned by +`to_c_string_iterator`, walks the bytes of a `Ptr` 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` 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`. +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`. diff --git a/docs/src/runtime/fn-ptr.md b/docs/src/runtime/fn-ptr.md index e83a0b93..0c6e7c78 100644 --- a/docs/src/runtime/fn-ptr.md +++ b/docs/src/runtime/fn-ptr.md @@ -21,11 +21,11 @@ impl FnPtr { `FnPtr` dereferences to the function, so a call through it is `(*fp)(args)`. Calling a null pointer panics with `ub:`. -`FnPtr` stores its function type-erased and identifies it by address through -the `FnAddr` trait. Rust has no way to write an impl for every `fn` arity at -once, so `FnAddr` is implemented by a macro for `fn` types of zero to sixteen -parameters. A function with more parameters cannot be wrapped in an `FnPtr`, -and taking its address fails to compile with a missing `FnAddr` bound. +`FnPtr` stores its function type-erased and identifies it by address through the +`FnAddr` trait. Rust has no way to write an impl for every `fn` arity at once, +so `FnAddr` is implemented by a macro for `fn` types of zero to sixteen +parameters. A function with more parameters cannot be wrapped in an `FnPtr`, and +taking its address fails to compile with a missing `FnAddr` bound. ```cpp typedef int (*int_fn)(int); diff --git a/docs/src/runtime/io.md b/docs/src/runtime/io.md index a0142a19..c89eebfd 100644 --- a/docs/src/runtime/io.md +++ b/docs/src/runtime/io.md @@ -11,8 +11,8 @@ flags that `feof` and `ferror` report long after the read that set them. as a `Ptr`, a [libc shim](./libc-shims.md) that holds the file descriptor together with these two flags. The standard streams are thread-local `CFile` values over descriptors 0, 1, and 2, returned by `c_stdin`, `c_stdout`, and -`c_stderr`. `CFile` does no buffering at present: every read or write on it is -a system call on the descriptor. +`c_stderr`. `CFile` does no buffering at present: every read or write on it is a +system call on the descriptor. In the unsafe model streams stay raw: `stdin_unsafe`, `stdout_unsafe`, and `stderr_unsafe` return the process's `*mut libc::FILE` handles, whose symbol diff --git a/docs/src/runtime/libc-shims.md b/docs/src/runtime/libc-shims.md index 38521d94..8a0727e3 100644 --- a/docs/src/runtime/libc-shims.md +++ b/docs/src/runtime/libc-shims.md @@ -50,11 +50,11 @@ stdio stream logic (see [I/O and Formatting](./io.md)), and the `time` shims convert through the `jiff` crate. `CFdSet` and the `sockaddr` family depart further from their C counterparts. -Each shim module also gives the raw `libc` struct it mirrors an empty -`ByteRepr` impl (`impl ByteRepr for ::libc::stat {}`), whose methods panic. -These exist so that the generated `ByteRepr` implementation of a translated -struct with a libc struct member still compiles; reinterpreting such a struct -is not supported at present. +Each shim module also gives the raw `libc` struct it mirrors an empty `ByteRepr` +impl (`impl ByteRepr for ::libc::stat {}`), whose methods panic. These exist so +that the generated `ByteRepr` implementation of a translated struct with a libc +struct member still compiles; reinterpreting such a struct is not supported at +present. ## CFdSet diff --git a/docs/src/runtime/ptr-dyn.md b/docs/src/runtime/ptr-dyn.md index 68edc5d5..254e127f 100644 --- a/docs/src/runtime/ptr-dyn.md +++ b/docs/src/runtime/ptr-dyn.md @@ -31,8 +31,7 @@ A virtual call goes through `upgrade`, which returns a `StrongPtrDyn` holding the strong reference; its `deref` and `deref_mut` borrow the object and the call dispatches through the trait's vtable. -> [!WARNING] -> `StrongPtrDyn` is set to be removed for the same reasons as +> [!WARNING] `StrongPtrDyn` is set to be removed for the same reasons as > [`StrongPtr`](./rc.md#strong-pointers): it holds a strong reference that can > outlive the object's C++ lifetime, and even as a temporary it spans the whole > virtual call, so a method that deletes its own object panics on `delete`. diff --git a/docs/src/runtime/rc.md b/docs/src/runtime/rc.md index f238035a..60b1844d 100644 --- a/docs/src/runtime/rc.md +++ b/docs/src/runtime/rc.md @@ -76,13 +76,12 @@ comparison does. `new` and `new[]` are translated as `Ptr::alloc` and `Ptr::alloc_array`, and `malloc`, `calloc`, `realloc`, and `strdup` allocate through `Ptr::alloc_array` -as well; the `alloc` module defines them as named functions -(`malloc_refcount`, `free_refcount`, `realloc_refcount`, `calloc_refcount`, -`strdup_refcount`, and their `_unsafe` twins for the unsafe model). The -allocation's `Rc` is deliberately leaked so the object outlives the -statement that created it. The leak is legitimate: a Rust program that leaks -memory is still well typed. `delete` and `delete_array` recover the leaked -reference and drop it: +as well; the `alloc` module defines them as named functions (`malloc_refcount`, +`free_refcount`, `realloc_refcount`, `calloc_refcount`, `strdup_refcount`, and +their `_unsafe` twins for the unsafe model). The allocation's `Rc` is +deliberately leaked so the object outlives the statement that created it. The +leak is legitimate: a Rust program that leaks memory is still well typed. +`delete` and `delete_array` recover the leaked reference and drop it: ```c int *d = new int(0); @@ -185,8 +184,8 @@ Its one operation is `deref`, which returns a `Ref<'_, T>` to the pointee. The `Ref` borrows the `StrongPtr`, so the borrow of the `RefCell` lasts as long as the strong pointer does: in `p.upgrade().deref().field`, the temporary `StrongPtr` lives until the end of the enclosing statement, and so does the -borrow. That is what makes a member access through a pointer expressible -without a closure. There is no `deref_mut`; writes go through `with_mut`. +borrow. That is what makes a member access through a pointer expressible without +a closure. There is no `deref_mut`; writes go through `with_mut`. For the `Reinterpreted` variant there is no value to reference, only bytes in another allocation. `deref` reads those bytes into the local `cell` and hands @@ -194,9 +193,8 @@ out a `Ref` to that copy, refreshing it on every call. The copy is what [Type Reinterpretation](./reinterpret.md#known-limitations) is about: writes into the copy never reach the original allocation. -> [!WARNING] -> `StrongPtr` is set to be removed. Holding a strong reference, even briefly, -> undermines the model in two ways: +> [!WARNING] `StrongPtr` is set to be removed. Holding a strong reference, even +> briefly, undermines the model in two ways: > > 1. Nothing prevents a `StrongPtr` from outliving its statement. One that is > stored, returned, or bound to a local keeps the object alive past the point @@ -224,10 +222,10 @@ pointer is dereferenced. Subtracting two pointers yields their element distance and requires both to point into the same allocation. The pointer also knows the extent of its allocation: `len` is the number of -elements in it, whatever the pointer's offset, and `get_offset` is the -pointer's element index within it. The container rules build `end()` and -`back()` pointers from these (`to_end`, `to_last`) and turn a `[first, last)` -range into a count or an absolute index the same way. +elements in it, whatever the pointer's offset, and `get_offset` is the pointer's +element index within it. The container rules build `end()` and `back()` pointers +from these (`to_end`, `to_last`) and turn a `[first, last)` range into a count +or an absolute index the same way. ## Integer casts diff --git a/docs/src/runtime/va-args.md b/docs/src/runtime/va-args.md index 7c5d13bb..e8ad6991 100644 --- a/docs/src/runtime/va-args.md +++ b/docs/src/runtime/va-args.md @@ -54,8 +54,8 @@ sum_0(3, &[10.into(), 20.into(), 30.into()]); `arg::()` goes through the `VaArgGet` trait, implemented for the integer and floating types, raw pointers, `Ptr`, `AnyPtr`, and `FnPtr`. Integer variants convert freely among the integer types, as `va_arg` does with types of -the same rank; asking for a pointer where an integer was passed, or the -reverse, panics, as does reading past the last argument. +the same rank; asking for a pointer where an integer was passed, or the reverse, +panics, as does reading past the last argument. Variadic libc functions such as `printf` and `fcntl` are handled by [variadic rules](../rules/writing-rules.md#variadic-functions), whose bodies diff --git a/docs/src/runtime/void.md b/docs/src/runtime/void.md index 03162695..cd7a5790 100644 --- a/docs/src/runtime/void.md +++ b/docs/src/runtime/void.md @@ -40,12 +40,11 @@ free_refcount((*p.borrow()).to_any()); `Ptr` versions from [C Strings](./cstr.md) over the byte view of its pointee. -> [!WARNING] -> Two `AnyPtr` values are equal only when they were erased from the same -> pointer type and compare equal as that type. A `void *` obtained from a +> [!WARNING] Two `AnyPtr` values are equal only when they were erased from the +> same pointer type and compare equal as that type. A `void *` obtained from a > `Ptr` and one obtained from a `Ptr` into the same allocation compare -> unequal, where C would consider them the same address. This is set to be -> fixed by comparing through the byte view instead. +> unequal, where C would consider them the same address. This is set to be fixed +> by comparing through the byte view instead. Casts between `AnyPtr` and integers use the same `to_int` and `from_int` as [`Ptr`](./rc.md#integer-casts). From 10ddf7857fd4f6ad4dd8deb734ea2115564091d2 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Sun, 16 Aug 2026 21:38:09 +0100 Subject: [PATCH 27/30] Move CStringIterator in iterator.rs --- docs/src/runtime/overview.md | 7 ++++--- libcc2rs/src/cstr.rs | 19 +------------------ libcc2rs/src/iterators.rs | 18 ++++++++++++++++++ libcc2rs/src/lib.rs | 1 - 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/docs/src/runtime/overview.md b/docs/src/runtime/overview.md index 14051cd3..871058c8 100644 --- a/docs/src/runtime/overview.md +++ b/docs/src/runtime/overview.md @@ -27,8 +27,8 @@ The refcounted pointer model, the core of the refcount output: - [`rc`](./rc.md): `Value` and `Ptr`, the refcounted stand-ins for C values and pointers. -- [`cstr`](./cstr.md): string literals, the `string.h` memory functions, and - iteration over `Ptr` byte strings. +- [`cstr`](./cstr.md): string literals and the `string.h` memory functions + over `Ptr` byte strings. - [`void`](./void.md): `AnyPtr`, the type-erased pointer for `void *`. - [`ptr_dyn`](./ptr-dyn.md): `PtrDyn`, pointers to virtual classes. - [`reinterpret`](./reinterpret.md): the `ByteRepr` trait and allocation views @@ -42,7 +42,8 @@ Language-feature emulation, used by both models: - [`inc` and `dec`](./inc-dec.md): traits implementing the four `++`/`--` operator forms. - [`iterators`](./iterators.md): iteration for C++ containers that need stable - iterators, with an implementation for both refcount and unsafe. + iterators, with an implementation for both refcount and unsafe, and over C + strings up to the null terminator. - [`fn_ptr`](./fn-ptr.md): `FnPtr`, function pointers with C-style address identity. - [`va_args`](./va-args.md): `VaArg` and `VaList`, the representation of diff --git a/libcc2rs/src/cstr.rs b/libcc2rs/src/cstr.rs index dc7f21ce..7ee7a418 100644 --- a/libcc2rs/src/cstr.rs +++ b/libcc2rs/src/cstr.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use std::fmt; use std::rc::Rc; +use crate::CStringIterator; use crate::rc::{Ptr, PtrKind}; impl fmt::Display for Ptr { @@ -201,21 +202,3 @@ impl Ptr { String::from_utf8_lossy(&bytes).into_owned() } } - -pub struct CStringIterator { - ptr: Ptr, -} - -impl Iterator for CStringIterator { - type Item = u8; - fn next(&mut self) -> Option { - // read until the null terminator - match self.ptr.read() { - 0 => None, - ch => { - self.ptr += 1; - Some(ch) - } - } - } -} diff --git a/libcc2rs/src/iterators.rs b/libcc2rs/src/iterators.rs index 28d4077a..e0e2975b 100644 --- a/libcc2rs/src/iterators.rs +++ b/libcc2rs/src/iterators.rs @@ -226,6 +226,24 @@ impl> PostfixDec for MapIter, +} + +impl Iterator for CStringIterator { + type Item = u8; + fn next(&mut self) -> Option { + // read until the null terminator + match self.ptr.read() { + 0 => None, + ch => { + self.ptr += 1; + Some(ch) + } + } + } +} + impl Ptr { pub fn to_string_iterator(&self) -> StringIterator { StringIterator { ptr: self.clone() } diff --git a/libcc2rs/src/lib.rs b/libcc2rs/src/lib.rs index 9e0566cb..aeaf527c 100644 --- a/libcc2rs/src/lib.rs +++ b/libcc2rs/src/lib.rs @@ -8,7 +8,6 @@ mod rc; pub use rc::*; mod cstr; -pub use cstr::*; mod void; pub use void::*; From 68a5633eed849e4976be40a0f758f92450306d9b Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Sun, 16 Aug 2026 21:41:55 +0100 Subject: [PATCH 28/30] Run prettier --- docs/src/runtime/overview.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/runtime/overview.md b/docs/src/runtime/overview.md index 871058c8..336b8f4a 100644 --- a/docs/src/runtime/overview.md +++ b/docs/src/runtime/overview.md @@ -27,8 +27,8 @@ The refcounted pointer model, the core of the refcount output: - [`rc`](./rc.md): `Value` and `Ptr`, the refcounted stand-ins for C values and pointers. -- [`cstr`](./cstr.md): string literals and the `string.h` memory functions - over `Ptr` byte strings. +- [`cstr`](./cstr.md): string literals and the `string.h` memory functions over + `Ptr` byte strings. - [`void`](./void.md): `AnyPtr`, the type-erased pointer for `void *`. - [`ptr_dyn`](./ptr-dyn.md): `PtrDyn`, pointers to virtual classes. - [`reinterpret`](./reinterpret.md): the `ByteRepr` trait and allocation views From 85460741557dafb560e587b93b01dcdb1f0281ec Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Mon, 17 Aug 2026 09:04:05 +0100 Subject: [PATCH 29/30] Fix warning callouts --- docs/src/rules/preprocessors.md | 4 +++- docs/src/runtime/ptr-dyn.md | 4 +++- docs/src/runtime/rc.md | 6 ++++-- docs/src/runtime/void.md | 12 +++++++----- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/src/rules/preprocessors.md b/docs/src/rules/preprocessors.md index ffff711b..c90434b2 100644 --- a/docs/src/rules/preprocessors.md +++ b/docs/src/rules/preprocessors.md @@ -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 diff --git a/docs/src/runtime/ptr-dyn.md b/docs/src/runtime/ptr-dyn.md index 254e127f..c8d24e28 100644 --- a/docs/src/runtime/ptr-dyn.md +++ b/docs/src/runtime/ptr-dyn.md @@ -31,7 +31,9 @@ A virtual call goes through `upgrade`, which returns a `StrongPtrDyn` holding the strong reference; its `deref` and `deref_mut` borrow the object and the call dispatches through the trait's vtable. -> [!WARNING] `StrongPtrDyn` is set to be removed for the same reasons as +> [!WARNING] +> +> `StrongPtrDyn` is set to be removed for the same reasons as > [`StrongPtr`](./rc.md#strong-pointers): it holds a strong reference that can > outlive the object's C++ lifetime, and even as a temporary it spans the whole > virtual call, so a method that deletes its own object panics on `delete`. diff --git a/docs/src/runtime/rc.md b/docs/src/runtime/rc.md index 60b1844d..89adc4b4 100644 --- a/docs/src/runtime/rc.md +++ b/docs/src/runtime/rc.md @@ -193,8 +193,10 @@ out a `Ref` to that copy, refreshing it on every call. The copy is what [Type Reinterpretation](./reinterpret.md#known-limitations) is about: writes into the copy never reach the original allocation. -> [!WARNING] `StrongPtr` is set to be removed. Holding a strong reference, even -> briefly, undermines the model in two ways: +> [!WARNING] +> +> `StrongPtr` is set to be removed. Holding a strong reference, even briefly, +> undermines the model in two ways: > > 1. Nothing prevents a `StrongPtr` from outliving its statement. One that is > stored, returned, or bound to a local keeps the object alive past the point diff --git a/docs/src/runtime/void.md b/docs/src/runtime/void.md index cd7a5790..871c0b25 100644 --- a/docs/src/runtime/void.md +++ b/docs/src/runtime/void.md @@ -40,11 +40,13 @@ free_refcount((*p.borrow()).to_any()); `Ptr` versions from [C Strings](./cstr.md) over the byte view of its pointee. -> [!WARNING] Two `AnyPtr` values are equal only when they were erased from the -> same pointer type and compare equal as that type. A `void *` obtained from a -> `Ptr` and one obtained from a `Ptr` into the same allocation compare -> unequal, where C would consider them the same address. This is set to be fixed -> by comparing through the byte view instead. +> [!WARNING] +> +> Two `AnyPtr` values are equal only when they were erased from the same pointer +> type and compare equal as that type. A `void *` obtained from a `Ptr` and +> one obtained from a `Ptr` into the same allocation compare unequal, where +> C would consider them the same address. This is set to be fixed by comparing +> through the byte view instead. Casts between `AnyPtr` and integers use the same `to_int` and `from_int` as [`Ptr`](./rc.md#integer-casts). From df0bebff3a509b475f10aac4756f4cf5001fd8b4 Mon Sep 17 00:00:00 2001 From: Lucian Popescu Date: Mon, 17 Aug 2026 09:51:20 +0100 Subject: [PATCH 30/30] Final reviews --- docs/src/rules/compat.md | 9 ++++++--- docs/src/runtime/compat.md | 9 +++++---- docs/src/runtime/control-flow.md | 15 +++++++++++++-- docs/src/runtime/fn-ptr.md | 20 +++++++++----------- docs/src/runtime/io.md | 18 +++++++++++++++--- docs/src/runtime/iterators.md | 13 ++++++++++--- docs/src/runtime/libc-shims.md | 9 ++++++--- docs/src/runtime/ptr-dyn.md | 24 ++++++++++++++++++------ docs/src/runtime/rc.md | 28 ++++++++++++++++------------ docs/src/runtime/reinterpret.md | 3 ++- docs/src/runtime/void.md | 12 ++++++++---- 11 files changed, 108 insertions(+), 52 deletions(-) diff --git a/docs/src/rules/compat.md b/docs/src/rules/compat.md index 5c45492b..9a555565 100644 --- a/docs/src/rules/compat.md +++ b/docs/src/rules/compat.md @@ -93,9 +93,12 @@ fn f1() -> Ptr { 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` 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 diff --git a/docs/src/runtime/compat.md b/docs/src/runtime/compat.md index c9fb1627..b08b0109 100644 --- a/docs/src/runtime/compat.md +++ b/docs/src/runtime/compat.md @@ -23,10 +23,11 @@ pub fn cpp2rust_errno() -> Ptr; ``` Refcount code reaches the operating system through the libc shims and nix, so -libc's `errno` is never read by this model. Instead, each rule is responsible -for writing the error code of a failed call into the refcounted value. Nothing -enforces this on the rule side, but a rule that skips the write breaks programs -that check `errno`, so it is part of writing a correct rule. +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). diff --git a/docs/src/runtime/control-flow.md b/docs/src/runtime/control-flow.md index 29a34e8c..d0b0c87a 100644 --- a/docs/src/runtime/control-flow.md +++ b/docs/src/runtime/control-flow.md @@ -62,8 +62,19 @@ becomes: 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; } + 0 => { + /* entry body */ + state = 1; + continue 'sm; + } + 1 => { + /* again body, with goto!('again) as */ + { + state = 1; + continue 'sm; + } + break 'sm; + } _ => break 'sm, } } diff --git a/docs/src/runtime/fn-ptr.md b/docs/src/runtime/fn-ptr.md index 0c6e7c78..2aa36f50 100644 --- a/docs/src/runtime/fn-ptr.md +++ b/docs/src/runtime/fn-ptr.md @@ -64,12 +64,16 @@ through the cast pointer go through the adapter: ```rust let gfn: Value i32>> = Rc::new(RefCell::new( - FnPtr::, i32) -> i32>::new(add_offset_4).cast:: i32>(Some( - (|a0: AnyPtr, a1: i32| -> i32 { add_offset_4(a0.reinterpret_cast::(), a1) }) - as fn(AnyPtr, i32) -> i32, - )), + FnPtr::, i32) -> i32>::new(add_offset_4) + .cast:: i32>(Some( + (|a0: AnyPtr, a1: i32| -> i32 { + add_offset_4(a0.reinterpret_cast::(), a1) + }) as fn(AnyPtr, i32) -> i32, + )), +)); +let result: Value = Rc::new(RefCell::new( + (*(*gfn.borrow()))(val.as_pointer().to_any(), 42), )); -let result: Value = Rc::new(RefCell::new((*(*gfn.borrow()))(val.as_pointer().to_any(), 42))); ``` The code generator can build an adapter when the arguments and return type of @@ -82,9 +86,3 @@ Casting a function pointer to `void *` is `to_any`, and `AnyPtr::cast_fn::` recovers it. `reinterpret_cast` on an `AnyPtr` holding a function currently panics, as do [integer casts](./rc.md#integer-casts) on a `Ptr`; both are set to be fixed in the near future. - -## The unsafe model - -The unsafe model uses `Option` directly, with `None` as the null -pointer, and casts between function pointer types with `std::mem::transmute`; it -does not use `FnPtr`. diff --git a/docs/src/runtime/io.md b/docs/src/runtime/io.md index c89eebfd..58767f27 100644 --- a/docs/src/runtime/io.md +++ b/docs/src/runtime/io.md @@ -22,8 +22,18 @@ names differ per platform (`stdin` on Linux, `__stdinp` on macOS). programs take their address: ```rust -pub fn fread_refcount(a0: AnyPtr, a1: usize, a2: usize, a3: Ptr) -> usize; -pub unsafe fn fread_unsafe(a0: *mut c_void, a1: usize, a2: usize, a3: *mut libc::FILE) -> usize; +pub fn fread_refcount( + a0: AnyPtr, + a1: usize, + a2: usize, + a3: Ptr, +) -> usize; +pub unsafe fn fread_unsafe( + a0: *mut c_void, + a1: usize, + a2: usize, + a3: *mut libc::FILE, +) -> usize; ``` The refcount variant reinterprets the destination as a byte array and reads @@ -91,7 +101,9 @@ In the `fstat` rule, the descriptor argument goes through `with_fd`: ```rust fn f2(a0: i32, a1: Ptr) -> i32 { - match FdRegistry::with_fd(a0, |fd: BorrowedFd<'_>| nix::sys::stat::fstat(fd)) { + match FdRegistry::with_fd(a0, |fd: BorrowedFd<'_>| { + nix::sys::stat::fstat(fd) + }) { // ... } } diff --git a/docs/src/runtime/iterators.md b/docs/src/runtime/iterators.md index 39936b42..7e1861f1 100644 --- a/docs/src/runtime/iterators.md +++ b/docs/src/runtime/iterators.md @@ -49,8 +49,7 @@ removals elsewhere in the map, as C++ guarantees. `begin`, `end`, and `find_key` construct one; `inc` and `dec` move to the neighbouring key; `erase` removes the current entry and returns the iterator to the next; the `++`/`--` traits and `Iterator` are implemented on top of these. Two iterators compare equal when -they hold the same key, whichever map they came from, so `it != m.end()` -compares `Some(key)` against `None`: +they hold the same key, so `it != m.end()` compares `Some(key)` against `None`: ```cpp std::map m; @@ -59,12 +58,20 @@ for (const auto &i : m) ``` ```rust -let m: Value>> = Rc::new(RefCell::new(BTreeMap::new())); +let m: Value>> = + Rc::new(RefCell::new(BTreeMap::new())); for i in RefcountMapIter::begin(m.as_pointer()) { (*sum.borrow_mut()) += (*i.second().borrow()); } ``` +> [!WARNING] +> +> Equality only looks at the key: iterators into two different maps compare +> equal when they hold the same key. In C++ comparing them is undefined +> behaviour, so the translation should panic with `ub:` instead; this will be +> fixed by comparing the map handles as well. + `first()` and `second()` come from the `MapIterator` trait and take the place of `it->first` and `it->second`. `MapIter` is generic over how the map is reached, which is what gives it an implementation for both models: diff --git a/docs/src/runtime/libc-shims.md b/docs/src/runtime/libc-shims.md index 8a0727e3..3adaa939 100644 --- a/docs/src/runtime/libc-shims.md +++ b/docs/src/runtime/libc-shims.md @@ -47,8 +47,8 @@ success nix returns a raw `libc::stat`, so the result goes through Most shims are plain data plus conversions like `Stat`. `CFile` carries the stdio stream logic (see [I/O and Formatting](./io.md)), and the `time` shims -convert through the `jiff` crate. `CFdSet` and the `sockaddr` family depart -further from their C counterparts. +convert through the `jiff` crate. `CFdSet` and the `sockaddr` family need more +than a field-by-field mirror and are described in their own sections below. Each shim module also gives the raw `libc` struct it mirrors an empty `ByteRepr` impl (`impl ByteRepr for ::libc::stat {}`), whose methods panic. These exist so @@ -82,7 +82,10 @@ the first two bytes and reinterprets the pointer as the concrete type before handing nix a typed address: ```rust -pub fn decode(addr: &Ptr, _len: u32) -> Option> { +pub fn decode( + addr: &Ptr, + _len: u32, +) -> Option> { let family = addr.reinterpret_cast::().read(); if family == libc::AF_INET as u16 { let m = addr.reinterpret_cast::().read(); diff --git a/docs/src/runtime/ptr-dyn.md b/docs/src/runtime/ptr-dyn.md index c8d24e28..94a59b8d 100644 --- a/docs/src/runtime/ptr-dyn.md +++ b/docs/src/runtime/ptr-dyn.md @@ -1,14 +1,24 @@ # Virtual Classes -A pointer to a virtual class cannot be a `Ptr`. The class is translated as a -Rust trait, and trait objects are unsized, which Rust marks with `dyn`. The -runtime provides a dedicated `PtrDyn` type for these pointers, kept -separate so the generic `Ptr` pays no cost for dynamic dispatch. +A pointer to a virtual class cannot be a `Ptr`. `Ptr` requires `T: Sized`, +the implicit bound on every generic parameter, and a virtual class is translated +as a Rust trait, whose trait object `dyn T` is unsized and cannot satisfy that +bound. The runtime provides a dedicated `PtrDyn` type, declared with +`T: ?Sized`, for these pointers, kept separate so the generic `Ptr` pays no cost +for dynamic dispatch. A `PtrDyn` is created at the point where C++ converts a derived pointer to a base pointer. `to_strong` upgrades the `Ptr` into its `Value`, Rust's unsized coercion turns that into a `Value`, and -`as_pointer_dyn` takes the weak reference back out: +`as_pointer_dyn` takes the weak reference back out. + +> [!NOTE] +> +> This coercion is why `Value` is a type alias for `Rc>` rather than +> a struct of its own: `Rc` already implements it, and a new type could only opt +> in through the nightly-only `CoerceUnsized` trait. + +The conversion looks like this: ```cpp struct Base { virtual int f() const = 0; }; @@ -24,7 +34,9 @@ let d: Value = Rc::new(RefCell::new(::default())); let b: Value> = Rc::new(RefCell::new( ((d.as_pointer()).to_strong() as Value).as_pointer_dyn(), )); -let r: Value = Rc::new(RefCell::new(({ (*(*b.borrow()).upgrade().deref()).f() }))); +let r: Value = Rc::new(RefCell::new( + ({ (*(*b.borrow()).upgrade().deref()).f() }), +)); ``` A virtual call goes through `upgrade`, which returns a `StrongPtrDyn` diff --git a/docs/src/runtime/rc.md b/docs/src/runtime/rc.md index 89adc4b4..c0dcd52a 100644 --- a/docs/src/runtime/rc.md +++ b/docs/src/runtime/rc.md @@ -78,9 +78,8 @@ comparison does. `malloc`, `calloc`, `realloc`, and `strdup` allocate through `Ptr::alloc_array` as well; the `alloc` module defines them as named functions (`malloc_refcount`, `free_refcount`, `realloc_refcount`, `calloc_refcount`, `strdup_refcount`, and -their `_unsafe` twins for the unsafe model). The allocation's `Rc` is -deliberately leaked so the object outlives the statement that created it. The -leak is legitimate: a Rust program that leaks memory is still well typed. +their `_unsafe` twins for the unsafe model). The allocation's `Rc` is leaked +with `Rc::into_raw` so the object outlives the statement that created it, and `delete` and `delete_array` recover the leaked reference and drop it: ```c @@ -97,18 +96,17 @@ let d: Value> = Rc::new(RefCell::new(Ptr::alloc(0))); `delete` checks that the pointer still points at the start of a live heap allocation: freeing twice, freeing through an offset pointer, or freeing a stack -value panics with `ub:`. +or `Vec` pointer panics with `ub:`. A heap allocation can also change hands instead of being freed. `to_owned_opt` recovers the leaked reference the same way `delete` does, but returns it to the caller as an owning `Option>` (or `Option>>` for an array), with `None` for the null pointer; from then on the allocation lives exactly as long as that binding. It panics for stack, `Vec`, and reinterpreted -pointers, which have no leaked reference to recover. This is how -`std::unique_ptr` is translated: the smart pointer is an `Option>`, -its constructor and `reset` adopt a raw pointer with `to_owned_opt`, and -`as_pointer`, which is also implemented for `Option>` and yields null -for `None`, stands in for `get()`: +pointers. This is how `std::unique_ptr` is translated: the smart pointer is +an `Option>`, its constructor and `reset` adopt a raw pointer with +`to_owned_opt`, and `as_pointer`, which is also implemented for +`Option>` and yields null for `None`, stands in for `get()`: ```cpp std::unique_ptr u(new int(1)); @@ -116,7 +114,8 @@ int *raw = u.get(); ``` ```rust -let u: Value>> = Rc::new(RefCell::new(Ptr::alloc(1).to_owned_opt())); +let u: Value>> = + Rc::new(RefCell::new(Ptr::alloc(1).to_owned_opt())); let raw: Value> = Rc::new(RefCell::new((*u.borrow()).as_pointer())); ``` @@ -176,7 +175,11 @@ pub enum StrongPtr { StackSingle(Rc>), Vec { rc: Rc>>, offset: usize }, StackArray { rc: Rc>>, offset: usize }, - Reinterpreted { alloc: Rc, byte_offset: usize, cell: RefCell> }, + Reinterpreted { + alloc: Rc, + byte_offset: usize, + cell: RefCell>, + }, } ``` @@ -240,7 +243,8 @@ int *q = (int *)n; ```rust let n: Value = Rc::new(RefCell::new((*p.borrow()).to_int())); -let q: Value> = Rc::new(RefCell::new(>::from_int(*n.borrow()))); +let q: Value> = + Rc::new(RefCell::new(>::from_int(*n.borrow()))); ``` Both currently panic when executed. Giving them well-defined semantics is work diff --git a/docs/src/runtime/reinterpret.md b/docs/src/runtime/reinterpret.md index 2cb183bd..8e1b4800 100644 --- a/docs/src/runtime/reinterpret.md +++ b/docs/src/runtime/reinterpret.md @@ -125,7 +125,8 @@ let any: AnyPtr = p.to_any(); let back: Ptr = any.reinterpret_cast::(); assert!(back == p); -// Different type: a byte view over p's allocation, as with Ptr::reinterpret_cast. +// Different type: a byte view over p's allocation, as with +// Ptr::reinterpret_cast. let bytes: Ptr = any.reinterpret_cast::(); assert_eq!(bytes.read(), 0x01); ``` diff --git a/docs/src/runtime/void.md b/docs/src/runtime/void.md index 871c0b25..e116c820 100644 --- a/docs/src/runtime/void.md +++ b/docs/src/runtime/void.md @@ -11,12 +11,15 @@ char *cp = vp; ```rust let data: Value> = Rc::new(RefCell::new(Box::from(*b"hi\0"))); -let vp: Value = Rc::new(RefCell::new((data.as_pointer() as Ptr).to_any())); -let cp: Value> = Rc::new(RefCell::new((*vp.borrow()).reinterpret_cast::())); +let vp: Value = + Rc::new(RefCell::new((data.as_pointer() as Ptr).to_any())); +let cp: Value> = + Rc::new(RefCell::new((*vp.borrow()).reinterpret_cast::())); ``` `reinterpret_cast` returns the original pointer when the requested type matches -the erased one, and a [byte-level view](./reinterpret.md) otherwise. +the erased one, and a [byte-level view](./reinterpret.md) otherwise, because C +code commonly casts `A *` to `void *` and reads it back as `B *`. The `malloc` family allocates and frees through `AnyPtr`, so the returned pointer is cast to the requested type and cast back to free it: @@ -28,7 +31,8 @@ free(p); ``` ```rust -// malloc_refcount(n) is Ptr::alloc_array(vec![0u8; n].into_boxed_slice()).to_any() +// malloc_refcount(n) is +// Ptr::alloc_array(vec![0u8; n].into_boxed_slice()).to_any() let p: Value> = Rc::new(RefCell::new( malloc_refcount(::std::mem::size_of::()).reinterpret_cast::(), ));