Chapter 10

Structs and Inline Data

A Goose struct is a named, packed sequence of fields. That part is familiar. What is new is that a struct can hold variable-size data, such as strings and lists, inline, with no pointers to anywhere else. This chapter covers structs from the basics up to records that are just runs of bytes.

#Declaring and building structs10.1

struct Goose { name: u8[..12], weight: f32 = 4.0, eggs: i32, tagged: bool }

fn main() {
    let a = Goose { name: "Ada", eggs: 3, tagged: true };   // named fields
    let b = Goose { "Bea", 3.5, 1, false };                  // positional
    let c = Goose { name: "Cleo", .. };                      // the rest default
    print(a);
    print(b);
    print(c);
}
Goose { "Ada", 4.0, 3, true }
Goose { "Bea", 3.5, 1, false }
Goose { "Cleo", 4.0, 0, false }

There are two ways to write a struct literal. The named form gives fields by name, in any order, and may leave out any field that has a declared default, like weight here. The positional form lists the fields in declaration order, and may leave out defaulted fields only at the end. The two forms do not mix.

Ending a literal with .. lets you leave out any field. A field without a declared default then takes its type's zero value: 0, false, an empty string or array, or null. Without the .., leaving out a field that has no default is an error:

struct Goose { name: u8[..12], eggs: i32 }

fn main() {
    let g = Goose { name: "Ada" };
}
error: missing initializer for field eggs of Goose (it has no default)

A field's default is an expression evaluated each time a literal leaves the field out. It can call functions and read globals, so a default can hand out serial numbers, for example. default<T>() builds a T with every field at its default, which is useful in generic code.

A global var declared with only a type starts out with every field at its default, so var settings: Config; at the top of a file needs no initializer.

#Structs are nominal10.1.1

Two structs with the same fields are different types:

struct Hen { eggs: i64 }
struct Goose { eggs: i64 }

fn main() {
    let h = Hen { 3 };
    let g: Goose = h;
}
error: expected a value of type Goose, got Hen

This is the way to make a type that is "an i64, but not just any i64": wrap it in a one-field struct.

#Packed layout10.2

Goose lays out a struct's fields in declaration order with no padding. Unaligned access is assumed to be cheap, as it is on every current desktop and phone CPU. A struct holding a u8, an i32 and another u8 takes 6 bytes, where C would use 12:

struct Tiny { a: u8, b: i32, c: u8 } C (aligned) a b b b b c 12 bytes Goose (packed) a b b b b c 6 bytes grey cells are padding: C inserts it, Goose does not
C pads fields to their alignment. Goose packs them back to back.
struct Tiny { a: u8, b: i32, c: u8 }

fn main() {
    var two: Tiny[>..] = [Tiny { 1, 2, 3 }, Tiny { 4, 5, 6 }];
    print(bytes_of(two).len, " bytes for two");
}
12 bytes for two

bytes_of(a) gives a read-only view of an array's elements as bytes, which is handy for seeing exactly how much space data takes. It comes back in Chapter 21.

When a struct must match a layout defined elsewhere, such as a C struct or a file format, write the padding yourself. pad n inserts n unused bytes, and a bare pad aligns the next field to its own size:

struct Record { tag: u8, pad, size: u32 }

fn main() {
    var rs: Record[>..] = [Record { 1, 512 }];
    print(rs[0], " in ", bytes_of(rs).len, " bytes");
}
Record { 1, 512 } in 8 bytes

#Field qualifiers10.3

A field declared with let cannot be assigned after the struct is built, and a field declared const cannot have its contents written either:

struct Tag { id: u32, let issued: u32 }

fn main() {
    var t = Tag { 7, 2026 };
    t.id = 8;            // fine
    t.issued = 2027;
}
error: cannot assign to let issued (§4.4)

#Variable-size fields10.4

So far every field has been fixed-size. A struct can also hold variable-size values: a finished string u8[], a frozen array T[], or a varint. They sit inline, in declaration order, which makes the whole struct variable-size.

varint deserves an introduction. It is an integer type for storage only. A varint field takes one byte for small values and more for large ones, up to ten bytes for the largest. Reading one gives an i64. Combined with u8[varint] and T[varint], whose lengths are varints too, it lets you describe records that take exactly as many bytes as their contents need:

struct Item  { sku: u8[varint], qty: varint, cents: varint }
struct Order { id: varint, customer: u8[varint], items: Item[varint] }

fn main() {
    var book: Order[>..] = [];
    book.push(Order { id: 1001, customer: "ada",
                      items: [Item { sku: "CORN-1", qty: 2, cents: 350 },
                              Item { sku: "OATS", qty: 1, cents: 900 }] });
    book.push(Order { id: 1002, customer: "bea",
                      items: [Item { sku: "CORN-1", qty: 5, cents: 350 }] });
    for o in book {
        var total = 0;
        for it in o.items { total += it.qty * it.cents; }
        print(o.customer, " owes ", total, " cents");
    }
    print(bytes_of(book).len, " bytes for the whole order book");
}
ada owes 1600 cents
bea owes 1750 cents
42 bytes for the whole order book

Each order is its fields laid end to end, including its list of items, and the whole order book is one block. Here is a smaller order, byte by byte:

Order { id: 7, customer: "ada", items: [Item { "OATS", 2, 90 }] } 14 3 a d a 1 4 O A T S 4 180 1 id 7 customer: length 3, then the bytes 1 item sku qty 2 cents 90 14 bytes, no pointers. A varint field stores its value zig-zag encoded, so small negative numbers are as short as small positive ones.
A record with variable-size fields is a run of bytes: each length comes just before the data it describes.

In C, Odin or Rust, Order would be a struct holding a pointer to a customer string and a pointer to an array of items, each of which holds a pointer to its own SKU string. Building it would take several allocations, and the struct itself would be dozens of bytes before you counted any of the data.

The cost of this layout is that the fields after a variable-size one are found by walking, and an array of such records is sequential: you can iterate it but not index it, as you saw in Chapter 7. Variable-size fields are also frozen once built. A varint field, for example, cannot be assigned, because a larger value might need more bytes:

struct Order { id: varint, customer: u8[varint] }

fn main() {
    var book: Order[>..] = [Order { 1, "ada" }];
    for o in book { o.id = 2; }
}
error: varint fields are written only at construction (§3.6)

Fixed-size fields of a variable-size struct can still be assigned. Note that the loop above wrote for o in book, not for &o in book: elements that are not fixed-size are never copied, so for binds them by reference already, and the compiler warns that an & would be redundant. Use variable-size records for data you build once and then traverse: log entries, parsed documents, messages. Use fixed-size records, with u8[..k] for their strings, for data you index, sort and update.

#A growable tail10.5

A struct's last field may be a growable array. The struct then becomes resizable itself: it can be a local, a global or a by-value parameter, but not an array element, and it can have only one such field. The tail grows through the struct like any growable array:

struct Barn { name: u8[..8], log: u8[][>..] }

fn note(b: Barn&, line: u8[:]) { b.log.push(line); }

fn main() {
    var barn = Barn { name: "north", log: [] };
    note(barn, "fed the geese");
    note(barn, "collected 4 eggs");
    print(barn.name, ": ", barn.log);
}
north: ["fed the geese", "collected 4 eggs"]

#Generic structs10.6

A struct can take type parameters, which are filled in when it is used. They can usually be inferred from the field values:

struct Pair<T> { a: T, b: T }

fn main() {
    let weights = Pair<i64> { 4200, 3900 };
    let ratios = Pair { a: 0.5, b: 1.5 };
    print(weights, " ", ratios);
}
Pair<i64> { 4200, 3900 } Pair<f64> { 0.5, 1.5 }

#Arithmetic and equality10.7

== and != compare structs field by field. The arithmetic operators work field by field too, on two values of the same struct whose fields are all numbers of one kind, and * and / also accept a single number on one side. This is how the standard library's vector types work, without any operator overloading:

struct V2 { x: f32, y: f32 }

fn main() {
    let a = V2 { 1.0, 2.0 };
    let b = V2 { 0.5, 0.5 };
    let c = a + b;
    print(c, " ", c * 2.0, " ", -c, " ", c == V2 { 1.5, 2.5 });
}
V2 { 1.5, 2.5 } V2 { 3.0, 5.0 } V2 { -1.5, -2.5 } true

The same memberwise arithmetic works on fixed arrays: [1, 2] + [10, 20] is [11, 22]. There are no < or > on structs.

#Exercises10.8

  1. Declare struct Nest { owner: u8[..12], eggs: i32 = 0, warm: bool = true } and build three nests using the named, positional and .. forms.

  2. Using bytes_of, find out how many bytes these two structs take. Explain the difference.

goose check struct A { flag: bool, count: i64, small: u8 } struct B { count: i64, flag: bool, small: u8 }

  1. Change Order so that each order also records a paid: bool. Where in the struct should the field go, so that it can be read without walking past the variable-size fields? Can it be assigned after the order is built?