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<T>,
the translation of a C++ variable, and Ptr<T>, 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<T>, an alias for
Rc<RefCell<T>>. Taking the address of a variable becomes a call to
as_pointer, which produces a Ptr<T>:
int b = 2;
int *b_ptr = &b;
*b_ptr = 3;
#![allow(unused)]
fn main() {
let b: Value<i32> = Rc::new(RefCell::new(2));
let b_ptr: Value<Ptr<i32>> = 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<T> 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<T> knows what it points into:
Null: the null pointer, and the default value.StackSingleandHeapSingle: a single value.StackArrayandHeapArray: a fixed-size array.StackVecandHeapVec: a growable buffer;std::vectorcontents and string literals live in one.Field: a field of a struct (see Pointers to fields).Reinterpreted: a byte-level view produced by a cast (see Type Reinterpretation).
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.
Pointers to fields
Fields are stored inline in their struct, so a struct and all of its fields
share one allocation and one RefCell. A pointer to a field is to a struct what
a pointer to an element is to an array: it records a weak reference to the
allocation that holds the struct, its root, together with the byte offset of the
field in it. field_ptr! creates one:
struct point { int x; int y; };
struct point p;
int *y = &p.y;
#![allow(unused)]
fn main() {
let p: Value<point> = Rc::new(RefCell::new(<point>::default()));
let y: Value<Ptr<i32>> = Rc::new(RefCell::new(field_ptr!(p, y)));
}
field_ptr!(p, y) works on a Value or a Ptr to a struct. The offsets are
those of the C layout of the struct, which the code generator gets from Clang
and writes on the fields as #[offset(N)] attributes (any constant expression
works, e.g., offset_of! in the libc shims);
#[derive(Record)] turns them into the implementation of the Record trait:
#![allow(unused)]
fn main() {
#[derive(Clone, Record, Default)]
pub struct point {
#[offset(0)]
pub x: i32,
#[offset(4)]
pub y: i32,
}
}
Creating a field pointer allocates nothing. The byte offset of the field is kept in the pointer’s offset, which for a field pointer counts bytes, as for a reinterpreted pointer. Pointers to fields of fields, and to fields of array elements, add up the offsets, and keep the allocation of the outermost struct or array as their root.
An access through a field pointer borrows the root, and finds the field from its
offset and its type with Record::locate, which the derive generates as a
comparison of the offset against those of the fields, recursing into nested
structs. A field is identified by its type too, as a struct and its first field
share an offset. An offset that doesn’t find a field of the pointer’s type
panics with ub: invalid field pointer.
Arrays and vectors are not stored inline: an array field is a Value<Box<[T]>>
of its own, and a std::vector field a Value<Vec<T>>. Pointers to their
elements are ordinary array pointers, and pointers to the fields of those have
the array or vector as their root. A field pointer hence always points to a
single object, which is why it needs no element index.
array_field_ptr!(p, arr) is a pointer to element 0 of the array field arr,
and is how the elements of an array field are accessed. It is an ordinary array
pointer into the array’s Value, except when p is a reinterpreted pointer:
the array then lives in the bytes of the original allocation, so the result is a
reinterpreted pointer to those bytes, at the offset of the field. Writes to the
elements hence reach the original allocation, and the pointer can go past the
end of the struct, as C code does with a trailing char name[1] in an
over-allocated struct.
Reading a field through a pointer borrows the struct only for the duration of a
closure, e.g., p.with(|s| s.y), so that the borrow ends before the rest of the
statement runs. field!(p, y) is the place of the field, which is read and
written through p, projected to the field by a closure, without looking the
field up: field!(p, y).write(3). It holds a reference to p, and takes no
more room than a pointer. field_ptr! is only used where an actual pointer to
the field is needed.
A field of a reinterpreted struct is itself a reinterpreted pointer, to the bytes of the field. Conversely, reinterpreting a field pointer views the bytes of the field alone, as if it were its own allocation.
Since all fields share the borrow of their struct, an expression must not write to a field while another field of the same struct is borrowed. The code generator scopes the borrows of field reads so that they end before any write in the same statement.
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:
int *d = new int(0);
*d = 5;
delete d;
#![allow(unused)]
fn main() {
let d: Value<Ptr<i32>> = 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<Value<T>> (or Option<Value<Box<[T]>>> 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<T> is translated: the smart pointer is
an Option<Value<T>>, its constructor and reset adopt a raw pointer with
to_owned_opt, and as_pointer, which is also implemented for
Option<Value<T>> and yields null for None, stands in for get():
std::unique_ptr<int> u(new int(1));
int *raw = u.get();
#![allow(unused)]
fn main() {
let u: Value<Option<Value<i32>>> =
Rc::new(RefCell::new(Ptr::alloc(1).to_owned_opt()));
let raw: Value<Ptr<i32>> = 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:
*d = 5;
int v = *d;
#![allow(unused)]
fn main() {
(*d.borrow()).write(5);
let v: Value<i32> = 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):
#![allow(unused)]
fn main() {
v.with_mut(|v| v.push(20));
}
Applied rule bodies are the main producer of these calls (see
Rule Rewriting). write itself is a thin wrapper: it
is defined as with_mut(|v| *v = value).
with_slice and with_slice_mut are the same idea over a range of elements:
they expose len bytes starting at the pointer 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.
In every case the RefCell is borrowed only for the duration of the access,
which is what lets freely aliasing C++ pointers coexist with the borrow checker:
no borrow outlives the expression that created it. When an expression needs an
actual Rust reference, the pointer is upgraded to a
StrongPtr, which holds the allocation alive and hands out
a Ref.
These borrows are the model’s mutability checks, moved from compile time to run
time. Rust’s rule still holds, any number of readers or one writer, but it is
enforced when the access happens: an expression that writes a variable while
also reading it through an alias, such as *x.borrow_mut() = *x.borrow() + 1,
traps. The code generator is responsible for not emitting such expressions: it
stores intermediate results in temporaries, so the reading borrow ends before
the writing borrow starts.
Strong pointers
upgrade turns a Ptr<T> into a StrongPtr<T>, the same pointer holding a
strong Rc to its allocation instead of a weak one:
#![allow(unused)]
fn main() {
pub enum StrongPtr<T> {
StackSingle(Rc<RefCell<T>>),
Vec { rc: Rc<RefCell<Vec<T>>>, offset: usize },
StackArray { rc: Rc<RefCell<Box<[T]>>>, offset: usize },
Field { root: Rc<dyn Root>, offset: usize },
Reinterpreted { alloc: OriginalAlloc, byte_offset: usize, cell: RefCell<Option<T>> },
}
}
deref 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. The code generator
prefers the with and with_mut closures, and upgrades only where the result
of an access must borrow the pointee beyond a closure. There is no deref_mut;
writes go through write and with_mut, including those to a field of a struct
reached through a pointer: field!(p, x).write(v).
For the Reinterpreted variant there is no value to reference, only bytes in
another allocation. deref reads those bytes into a local cell and hands out a
Ref to that copy, refreshing it on every call. with_mut writes the bytes
through to the original allocation before it returns, so that a write is visible
through every other pointer at once.
Warning
StrongPtris set to be removed. Holding a strong reference, even briefly, undermines the model in two ways:
- Nothing prevents a
StrongPtrfrom 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 laterdeletepanic. The generator only ever emits it as a temporary, but hand-edited code or a rule can break that.- 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 runsdelete this, or otherwise deletes the object it was called on, hitsdelete’s reference-count check and panics with a spuriousub: invalid delete.
Where the code generator needs a different view of the same allocation, it does
not upgrade at all. decay turns a pointer to a whole Vec<T> or Box<[T]>
into a pointer to its first element by re-tagging the existing weak reference,
and Ptr::to_dyn (see Virtual Classes) does the same for the
upcast to a trait object.
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:
uintptr_t n = (uintptr_t)p;
int *q = (int *)n;
#![allow(unused)]
fn main() {
let n: Value<usize> = Rc::new(RefCell::new((*p.borrow()).to_int()));
let q: Value<Ptr<i32>> =
Rc::new(RefCell::new(<Ptr<i32>>::from_int(*n.borrow())));
}
Both currently panic when executed. Giving them well-defined semantics is work in progress (#225).