diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index ea243a85..361d065e 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -37,3 +37,7 @@ jobs: - name: Check format run: git diff --exit-code + + - name: Check markdown formatting + run: npx prettier@3.6.2 --check "src/**/*.md" + working-directory: docs diff --git a/docs/.prettierrc b/docs/.prettierrc new file mode 100644 index 00000000..cfae7e2e --- /dev/null +++ b/docs/.prettierrc @@ -0,0 +1,4 @@ +{ + "proseWrap": "always", + "printWidth": 80 +} diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 101b03d7..e11a8166 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -2,15 +2,27 @@ # The Project -* [Introduction](./project/introduction.md) -* [Building](./project/building.md) -* [Usage](./project/usage.md) -* [Test Suite](./project/test-suite.md) +- [Introduction](./project/introduction.md) +- [Building](./project/building.md) +- [Usage](./project/usage.md) +- [Test Suite](./project/test-suite.md) # Translation Rules -* [Overview](./rules/overview.md) +- [Overview](./rules/overview.md) +- [Rule Format](./rules/format.md) +- [Writing Rules](./rules/writing-rules.md) +- [Compat Shims](./rules/compat.md) +- [Conventions](./rules/conventions.md) +- [The Rule Preprocessors](./rules/preprocessors.md) +- [The Rules IR](./rules/ir.md) +- [Loading and Matching](./rules/loading.md) +- [The Matching Engine](./rules/matching.md) +- [Rule Rewriting](./rules/rewriting.md) # Code Generation -* [Overview](./codegen/overview.md) +- [Overview](./codegen/overview.md) +- [Pointers and References](./codegen/pointers.md) +- [Temporary Materialization](./codegen/temporaries.md) +- [Translation Plugins](./codegen/plugins.md) 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`). 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..4b2b5520 --- /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/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 new file mode 100644 index 00000000..5c45492b --- /dev/null +++ b/docs/src/rules/compat.md @@ -0,0 +1,145 @@ +# Compat Shims + +Rule matching needs a resolvable callee. The preprocessor keys every expression +rule on the function, method, constructor, constant, or global that the +pattern's `return` expression resolves to, and the only macros it can record are +those that +[expand to an integer literal](./writing-rules.md#enum-values-constants-and-macros), +which match by macro name. Any other macro is invisible to the rule system: by +the time clang has built the AST, the macro is gone and only its expansion +remains. + +That is a problem for a small set of libc APIs that are specified as macros over +platform internals: + +- `errno` is an object-like macro; glibc expands it to `(*__errno_location())`, + macOS to `(*__error())`. +- `assert` expands to a conditional that stringifies the condition and calls a + platform-specific failure handler with file and line arguments. +- `FD_SET`, `FD_CLR`, `FD_ISSET`, and `FD_ZERO` expand to bit manipulation on + the `fd_set` representation, through helpers that differ per platform. +- `ntohl`, `ntohs`, `htonl`, and `htons` expand to byte-swap builtins or to + nothing at all, depending on endianness. + +There is no stable, platform-independent callee here to key a rule on. The +_compat headers_ in `cpp2rust/compat/` fix this by rewriting each such macro +into a call to a synthetic, well-known function before matching happens. + +## How the shims work + +`cpp2rust/compat` is injected as a system include directory ahead of the +platform headers in every clang invocation the project makes: both when +`cpp2rust` parses the input program and when `cpp-rule-preprocessor` compiles +rule sources. The shared flag list lives in `cpp2rust/compat/platform_flags.h` +(`getPlatformClangBeginFlags`), and the directory path is baked in at build time +via the `COMPAT_INCLUDE_DIR` definition. + +A shim header sits at the same relative path as the real header it shadows +(`errno.h`, `sys/select.h`, `arpa/inet.h`, ...), so an ordinary +`#include ` 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/conventions.md b/docs/src/rules/conventions.md new file mode 100644 index 00000000..476ed08a --- /dev/null +++ b/docs/src/rules/conventions.md @@ -0,0 +1,68 @@ +# Conventions + +Most of these conventions are enforced by the preprocessors, and violating them +fails the build; the notes below call out the ones that are not checked. + +## Naming + +| Element | C++ side | Rust side | +| --------------------- | --------------------------------------------- | --------------------------------------------------- | +| Expression rule | `f1`, `f2`, ... | same name | +| Type rule | `t1`, `t2`, ... via `using`/`typedef` | `fn tN() -> RustType` with no arguments | +| Parameters | free-form (`o`, `it`, `key`, `dst`, `n`, ...) | must be `a0`, `a1`, ... consecutive from 0 | +| Generics | `T1`, `T2`, ... (type and non-type params) | `T1`, `T2`, ... consecutive from 1 | +| Variadic pack | `typename... Args` | trailing `va: &[VaArg]` | +| Locals in Rust bodies | | double-underscore prefix: `__v`, `__fd`, `__e`, ... | + +Notes: + +- Rule numbering is per module, and gaps are currently allowed (e.g. `rules/map` + has no `f4`), though this might change in the future. Names must be unique + across `src.c` and `src.cpp` combined. +- On the C++ side parameter names are free, but the _order_ defines the + placeholder indices: the first parameter is `a0` on the Rust side, the second + is `a1`, and so on. The receiver of a method rule is always the first + parameter, hence `a0`. +- Generic parameters are matched positionally between the two sides, so `T1` in + the Rust target means "whatever bound to `T1` in the C++ pattern". +- Locals introduced inside Rust rule bodies use a `__` prefix. This is not + checked by the build, but it is needed: rule bodies are spliced inline into + the generated code, so an unprefixed local could collide with a variable name + from the translated program. + +## Function qualifiers + +- In `tgt_unsafe.rs`, expression rules are `unsafe fn`; type rules (`tN`) are + plain `fn`. +- In `tgt_refcount.rs`, all rules are safe `fn`. The refcount model produces + fully safe Rust, so a refcount rule body must not need `unsafe`. + +The build does not check the qualifiers themselves; only rustc's usual rules +apply when the `rules` crate compiles. In particular, nothing stops an `unsafe` +block inside a refcount rule body from being spliced into the output, so keeping +refcount rules safe is what upholds the model's safety guarantee. + +## C++ pattern shape + +- An `fN` body must be exactly one `return` statement. The preprocessor rejects + anything else. +- `return` statements are not allowed inside Rust rule bodies; write the result + as a tail expression instead. +- Exercise exactly one construct per rule. If an API has several overloads, + write one rule per overload (including separate rules for `const T &` versus + `T &&` parameters). + +## Argument accesses + +Every use of an `aN` parameter in a rule body is classified as a read, write, or +move by the [rule preprocessor](./preprocessors.md#rule-preprocessor). Passing +an argument by value counts as a read, not a move; the only way to record a move +is `std::mem::take(&mut aN)`. + +## Type checking + +All `tgt_*.rs` files are compiled as part of the `rules` crate, so a rule body +that does not type-check against `libcc2rs`, `libc`, `nix`, etc. breaks the +build. If a rule needs a new crate dependency, add it to `rules/Cargo.toml` and +to the hardcoded crate list in `rule-preprocessor/src/semantic.rs` (see +[The Rule Preprocessors](./preprocessors.md#rule-preprocessor)). diff --git a/docs/src/rules/format.md b/docs/src/rules/format.md new file mode 100644 index 00000000..199c4481 --- /dev/null +++ b/docs/src/rules/format.md @@ -0,0 +1,152 @@ +# 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 { + Vec::new() +} +``` + +## 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..60201d34 --- /dev/null +++ b/docs/src/rules/ir.md @@ -0,0 +1,131 @@ +# 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; 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. + +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`, ...). + +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. + +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: + +```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. + +## 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 + [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` + 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. + +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 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 new file mode 100644 index 00000000..8865a8af --- /dev/null +++ b/docs/src/rules/loading.md @@ -0,0 +1,99 @@ +# Loading and Matching + +## 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 +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; + `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 + 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 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 + `_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][ volatile][ &|&&]`. +- 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. + +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. diff --git a/docs/src/rules/matching.md b/docs/src/rules/matching.md new file mode 100644 index 00000000..faee2222 --- /dev/null +++ b/docs/src/rules/matching.md @@ -0,0 +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`. + +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 dbdacce6..6e3d008e 100644 --- a/docs/src/rules/overview.md +++ b/docs/src/rules/overview.md @@ -1,8 +1,45 @@ # 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_refcount.rs` and -`tgt_unsafe.rs`). +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`). -This part of the book explains how rules work and how to write new ones. +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 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 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, + 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 + 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 + 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 new file mode 100644 index 00000000..ffff711b --- /dev/null +++ b/docs/src/rules/preprocessors.md @@ -0,0 +1,123 @@ +# The Rule Preprocessors + +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 + `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, 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: + +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][ &|&&]`, + 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: + +```bash +CARGO_TARGET_DIR= cargo +nightly run --release \ + --manifest-path rule-preprocessor/Cargo.toml -- /rules [rules-dir] +``` + +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. 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 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` becomes +`ir_unsafe.json`) and the module directory is the direct parent of the +`tgt_*.rs` file. diff --git a/docs/src/rules/rewriting.md b/docs/src/rules/rewriting.md new file mode 100644 index 00000000..7ce8efda --- /dev/null +++ b/docs/src/rules/rewriting.md @@ -0,0 +1,98 @@ +# 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 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. +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 an expression of reference type + (which includes an operator call returning a reference). + +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);`. + +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. 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 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. diff --git a/docs/src/rules/writing-rules.md b/docs/src/rules/writing-rules.md new file mode 100644 index 00000000..aa49121c --- /dev/null +++ b/docs/src/rules/writing-rules.md @@ -0,0 +1,338 @@ +# 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. + +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 +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`. + +## 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: + +```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++()`. 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 + +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. 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. + +## 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 { ... } +``` + +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: + +```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. + +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 + +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.