diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index e11a8166..3d714784 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -20,9 +20,28 @@ - [The Matching Engine](./rules/matching.md) - [Rule Rewriting](./rules/rewriting.md) +# The Runtime Library + +- [Overview](./runtime/overview.md) +- [Reference Counting](./runtime/rc.md) +- [C Strings](./runtime/cstr.md) +- [void Pointers](./runtime/void.md) +- [Virtual Classes](./runtime/ptr-dyn.md) +- [Type Reinterpretation](./runtime/reinterpret.md) +- [Increment and Decrement](./runtime/inc-dec.md) +- [Iterators](./runtime/iterators.md) +- [Function Pointers](./runtime/fn-ptr.md) +- [Variadic Functions](./runtime/va-args.md) +- [Control Flow Macros](./runtime/control-flow.md) +- [I/O and Formatting](./runtime/io.md) +- [libc Shims](./runtime/libc-shims.md) +- [Compat Helpers](./runtime/compat.md) + # Code Generation - [Overview](./codegen/overview.md) - [Pointers and References](./codegen/pointers.md) +- [Unions](./codegen/unions.md) +- [Global Variables](./codegen/globals.md) - [Temporary Materialization](./codegen/temporaries.md) - [Translation Plugins](./codegen/plugins.md) 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/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/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/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/compat.md b/docs/src/runtime/compat.md new file mode 100644 index 00000000..b08b0109 --- /dev/null +++ b/docs/src/runtime/compat.md @@ -0,0 +1,33 @@ +# Compat Helpers + +Some C interfaces are macros or platform-specific symbols rather than plain +functions. On the source side, `cpp2rust` rewrites them into ordinary calls (see +[Compat Shims](../rules/compat.md)); the `compat` module is the runtime side of +that rewrite. + +`errno` expands to a platform-specific function call (`__errno_location` on +Linux, `__error` on macOS). + +In the unsafe model, `cpp2rust_errno_unsafe` binds both platform symbols under +one name and returns the real libc `errno` location: + +```rust +pub unsafe fn cpp2rust_errno_unsafe() -> *mut i32; +``` + +In the refcount model, `errno` is a thread-local refcounted `i32` that the +runtime maintains itself: + +```rust +pub fn cpp2rust_errno() -> Ptr; +``` + +Refcount code reaches the operating system through the libc shims and nix, so +libc's `errno` is never read by this model. Keeping the cell current is a +discipline of the rules: every rule that translates a call that can fail must +write the error code into `cpp2rust_errno()` on the failure path (see +[Compat Shims](../rules/compat.md)); nothing enforces this, and a rule that +skips the write breaks programs that check `errno`. + +`malloc_usable_size` is bound under one name for both platforms (the symbol is +`malloc_size` on macOS). diff --git a/docs/src/runtime/control-flow.md b/docs/src/runtime/control-flow.md new file mode 100644 index 00000000..d0b0c87a --- /dev/null +++ b/docs/src/runtime/control-flow.md @@ -0,0 +1,188 @@ +# Control Flow Macros + +Rust has no `goto`, and a `match` arm never falls into the next one. The +`libcc2rs-macros` crate provides two procedural macros, re-exported by +`libcc2rs`, that express these C constructs as a state machine. Both models use +them. + +## goto_block + +`goto_block!` takes a sequence of labeled blocks. Execution starts in the first +block and falls through from each block into the next; `goto!('label)` jumps to +the block with that label, forwards or backwards: + +```c +int retry(int n) { + int count = 0; + int acc = 0; +again: + count += 1; + acc += n; + if (count < 3) + goto again; + return acc; +} +``` + +```rust +pub fn retry_0(n: i32) -> i32 { + let n: Value = 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!`. A `continue` in a case is not captured by the +`switch!`: as in C, it continues the loop enclosing the `switch`, and is a +compile error when there is none. `goto` and `switch` mix freely: a `switch!` +can be nested in a `goto_block!`, a `goto!` inside a case can target a label of +the enclosing block, and a label attached to a `case` is supported. Statements +between the `switch` and its first `case` are not supported yet. + +## Hoisted declarations + +In C a variable declared in one case is visible in the cases after it, because +they all belong to the same block. Each `switch!` arm is a separate Rust block, +so the code generator hoists such declarations above the macro and leaves an +assignment in the case: + +```c +switch (x) { +case 1: + r = 1; + int y; + y = 10; + r += y; +case 2: + y = 20; + r = y; + break; +} +``` + +```rust +let y: Value = >::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/cstr.md b/docs/src/runtime/cstr.md new file mode 100644 index 00000000..fcc195b8 --- /dev/null +++ b/docs/src/runtime/cstr.md @@ -0,0 +1,18 @@ +# C Strings + +C and C++ strings are byte strings: programs manipulate individual bytes and the +contents need not be valid UTF-8, so strings are translated as `u8` buffers +rather than Rust `String` values. A string literal becomes a per-thread interned +buffer with a trailing zero byte, handed out as a `Ptr` 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. + +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 new file mode 100644 index 00000000..2aa36f50 --- /dev/null +++ b/docs/src/runtime/fn-ptr.md @@ -0,0 +1,88 @@ +# 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:`. + +`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; } + +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. diff --git a/docs/src/runtime/inc-dec.md b/docs/src/runtime/inc-dec.md new file mode 100644 index 00000000..75133de9 --- /dev/null +++ b/docs/src/runtime/inc-dec.md @@ -0,0 +1,49 @@ +# 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](./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 +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/io.md b/docs/src/runtime/io.md new file mode 100644 index 00000000..58767f27 --- /dev/null +++ b/docs/src/runtime/io.md @@ -0,0 +1,130 @@ +# 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`. `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 +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, +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 + +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](./va-args.md) 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. 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 + +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/iterators.md b/docs/src/runtime/iterators.md new file mode 100644 index 00000000..7e1861f1 --- /dev/null +++ b/docs/src/runtime/iterators.md @@ -0,0 +1,80 @@ +# 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. Two iterators compare equal when +they hold the same key, so `it != m.end()` compares `Some(key)` against `None`: + +```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()); +} +``` + +> [!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: +`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/libc-shims.md b/docs/src/runtime/libc-shims.md new file mode 100644 index 00000000..3adaa939 --- /dev/null +++ b/docs/src/runtime/libc-shims.md @@ -0,0 +1,111 @@ +# 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 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 +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 +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..336b8f4a --- /dev/null +++ b/docs/src/runtime/overview.md @@ -0,0 +1,75 @@ +# Overview + +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::*; +``` + +## Module map + +The modules fall into three groups. + +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. +- [`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. +- [`alloc`](./rc.md#the-heap): `malloc`, `free`, `realloc`, and `calloc` over + refcounted byte arrays. + +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, 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 + variadic calls. +- 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: + +- [`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 + +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. +- `jiff` backs the time shims. +- `sprintf` backs `printf`-style formatting. diff --git a/docs/src/runtime/ptr-dyn.md b/docs/src/runtime/ptr-dyn.md new file mode 100644 index 00000000..94a59b8d --- /dev/null +++ b/docs/src/runtime/ptr-dyn.md @@ -0,0 +1,57 @@ +# Virtual Classes + +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. + +> [!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; }; +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 new file mode 100644 index 00000000..c0dcd52a --- /dev/null +++ b/docs/src/runtime/rc.md @@ -0,0 +1,251 @@ +# 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`, 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 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 +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 +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. 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 +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`](#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 +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. + +## 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. +`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. + +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`: + +```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 ([#225](https://github.com/Cpp2Rust/cpp2rust/pull/225)). diff --git a/docs/src/runtime/reinterpret.md b/docs/src/runtime/reinterpret.md new file mode 100644 index 00000000..8e1b4800 --- /dev/null +++ b/docs/src/runtime/reinterpret.md @@ -0,0 +1,132 @@ +# 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; +} +``` + +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 +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); +// 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); +``` + +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. + +## 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: + +```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/va-args.md b/docs/src/runtime/va-args.md new file mode 100644 index 00000000..e8ad6991 --- /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, 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 +receive the same `&[VaArg]` slice; `format_c` in the +[format module](./io.md#formatting) consumes one to evaluate a format string. diff --git a/docs/src/runtime/void.md b/docs/src/runtime/void.md new file mode 100644 index 00000000..e116c820 --- /dev/null +++ b/docs/src/runtime/void.md @@ -0,0 +1,56 @@ +# 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, 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: + +```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. + +> [!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). 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..7ee7a418 --- /dev/null +++ b/libcc2rs/src/cstr.rs @@ -0,0 +1,204 @@ +// 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::CStringIterator; +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() + } +} 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/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 { diff --git a/libcc2rs/src/iterators.rs b/libcc2rs/src/iterators.rs index 623a1110..e0e2975b 100644 --- a/libcc2rs/src/iterators.rs +++ b/libcc2rs/src/iterators.rs @@ -225,3 +225,45 @@ 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() } + } +} + +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..aeaf527c 100644 --- a/libcc2rs/src/lib.rs +++ b/libcc2rs/src/lib.rs @@ -7,6 +7,14 @@ pub use reinterpret::ByteRepr; mod rc; pub use rc::*; +mod 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()); + } +}