Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Boxing

In the refcount model a variable is boxed: its type T is wrapped in Value<T>, an alias for Rc<RefCell<T>> (see Reference Counting). Without the box, taking the address of a variable would need a Rust reference, and arbitrary C++ aliasing cannot be expressed with references.

Not every type position is boxed. ConverterRefCount keeps a stack of conversion kinds, conversion_kind_, and the construct that owns the type pushes one before printing it:

  • FullRefCount: pushed by variable declarations; Convert(QualType) wraps the result in Value<...>.
  • Pointee: pushed by field declarations; the bare type is printed. Fields that are arrays, or whose type maps to a Vec or a Box (std::vector, std::string, std::array), push FullRefCount instead (see below).
  • Unboxed: pushed by parameter lists, return types, and record names; the bare type is printed.
  • Ptr: pushed by a pointer type for its pointee; also printed bare.

The result by position:

PositionintItemint[3]
local variable, globalValue<i32>Value<Item>Value<Box<[i32]>>
function parameter, return typei32Itemdecays to Ptr<i32>
struct fieldi32ItemValue<Box<[i32]>>
pointee of Ptr<T>, element of a containeri32ItemBox<[i32]>

Parameters arrive unboxed and are re-boxed by the function preamble; return values are unboxed:

int add(int a, Item item) { return a + item.id; }
#![allow(unused)]
fn main() {
pub fn add_0(a: i32, item: Item) -> i32 {
    let a: Value<i32> = Rc::new(RefCell::new(a));
    let item: Value<Item> = Rc::new(RefCell::new(item));
    return *a.borrow() + (*item.borrow()).id;
}
}

C++ passes arguments to functions by copy, so signatures stay unboxed; boxing the copy on entry then lets the body treat parameters exactly like local variables. The preamble skips reference parameters, which are a Ptr<T> and never boxed.

Nested containers, library ones and arrays alike, box each level except the innermost, so that every inner container can be borrowed and mutated on its own, and a pointer can be taken to it. The boxing is written into the type rules themselves: std::vector<std::vector<int>> maps to Vec<Value<Vec<i32>>>, and the carray rules map int a[2][2] to Box<[Value<Box<[i32]>>]>, both before the outer Value<...> of the declaration is added.

Struct fields are stored inline, so that a whole struct is a single allocation, and a pointer to a field records the struct’s allocation and the field’s byte offset (see Pointers). Arrays and vectors are the exception: an array field is a Value<Box<[T]>> of its own, and a vector field a Value<Vec<T>>. A pointer to an element, or to a field of an element, then has the array or the vector as its allocation instead of the struct, and pointer arithmetic moves between elements as for any other array:

struct Holder { std::vector<Point> points; int n; };
h.points[0].y = 5;
#![allow(unused)]
fn main() {
pub struct Holder {
    #[offset(0)]
    pub points: Value<Vec<Point>>,
    #[offset(24)]
    pub n: i32,
}
(*(*h.borrow()).points.borrow_mut())[(0_usize) as usize].y = 5;
}

An array or vector field is accessed like a local one, through its own borrow() or borrow_mut(), and the struct is only borrowed immutably to reach it. As Value is shared on clone(), structs with such fields implement Clone by copying the arrays and vectors, instead of deriving it.