Memory · Lesson 15.3

Raw memory

Source
Allocate a block with Alloc, clear it with Zero and give it back with Free, checking for null first.
You'll need: Pointer, Defer, For

Every value so far had its size fixed when the program was compiled: a uint, an array of four int64s, a struct. But often you only learn how much memory you need while the program runs — how many lines are in a file, how many players joined. That memory has to be asked for, and this lesson shows the rawest way to do it.

Nothing here is checked for you. That is the point: once you have seen every rule you must keep by hand, the tools in the rest of this part will make sense.

Three functions

The Memory package gives you three functions, and between them they cover a block's whole life:

FunctionDoes
Alloc(bytes)finds a fresh block and returns its address, or null if there is none
Zero(p, n)fills n bytes at p with zeros
Free(p)gives the block back

Ask in bytes

Alloc counts in bytes, not in elements, so the request is the element count times the size of one element:

let count: uint = 8;
let size = count * sizeof(uint);

sizeof(uint) is 8 on a 64-bit machine, so eight elements need 64 bytes. Layout later in this part looks at sizes properly.

Say what lives there

The block arrives as *var opaque: "writable memory, contents unknown". An opaque pointee has no type, so you cannot read or write through it yet. A cast says what will live there:

var values = Alloc(size) as *var uint;

From then on the pointer indexes like an array: values[i] is the i-th uint after the address.

Check, then register the release

Allocation can fail, and the only sign is a null. Check before the first use, and register the Free straight after the check:

if values == null {
    PrintLine("out of memory");
    return 1;
}

defer Free(values);

The defer makes sure that every later path out of Main — the normal return 0, or any early return you add later — frees the block exactly once.

flowchart LR
    alloc["Alloc(size)"] --> q{"null?"}
    q -- "yes" --> oom["report it,<br/>return 1"]
    q -- "no" --> d["defer Free(values)"]
    d --> z["Zero(values, size)"]
    z --> use["values[i] = i * i"]
    use --> free["Free runs as<br/>Main returns"]

Fresh memory is not empty

A new block holds whatever bytes were last left there. Zero makes its contents definite before anything reads them:

Zero(values, size);

After that, the program fills the block with squares and adds them up — ordinary indexing, on memory that did not exist when the program was compiled.

After Free

When the deferred Free has run, values still holds the old address, but the memory is no longer yours. Reading it, writing it or freeing it a second time are all bugs, and nothing reports them: the program may seem to work, crash later somewhere unrelated, or quietly corrupt other data.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// Every value so far had its size fixed when the program was compiled. When the amount of memory
// is only known while the program runs, it has to be asked for, and this is the rawest way:
//
//     Alloc(bytes)  hands back the address of a fresh block, or `null` if there is none to give
//     Zero(p, n)    fills the block with zero bytes
//     Free(p)       gives the block back
//
// The block arrives as `*var opaque`: "writable memory, contents unknown". A cast says what will
// live there, and from then on the pointer indexes like an array: `values[i]` is the i-th element
// after the address.
//
// Nothing here is checked for you. A `null` must be caught before use, the block must be freed
// exactly once, and it must not be touched afterwards. The rest of this part is about tools that
// make those rules harder to break.
import Io::{ Print, PrintLine };
import Memory::{ Alloc, Free, Zero };

func Main() -> int {
    // A count decided at run time. `sizeof(uint)` is the size of one element in bytes.
    let count: uint = 8;
    let size = count * sizeof(uint);
    PrintLine("asking for {} elements, {} bytes", count, size);

    var values = Alloc(size) as *var uint;

    // Allocation can fail, and the only sign is `null`. Check before the first use.
    if values == null {
        PrintLine("out of memory");
        return 1;
    }

    // Registered right after the check, so every later path out of `Main` frees the block once.
    defer Free(values);

    // Fresh memory holds whatever was there before. `Zero` makes its contents definite.
    Zero(values, size);
    Print("after Zero:");
    for i in 0..count {
        Print(" {}", values[i]);
    }
    PrintLine();

    for i in 0..count {
        values[i] = i * i;
    }
    Print("squares:   ");
    var total: uint = 0;
    for i in 0..count {
        Print(" {}", values[i]);
        total += values[i];
    }
    PrintLine();
    PrintLine("total:      {}", total);

    // After the deferred `Free` runs, `values` still holds the old address, but the memory is no
    // longer yours. Reading it, or freeing it again, is a bug that nothing reports.
    return 0;
}

Besides Io, its Rux.toml lists Memory under [Dependencies].

Run it

cd Examples/Memory/RawMemory
rux run
asking for 8 elements, 64 bytes
after Zero: 0 0 0 0 0 0 0 0
squares:    0 1 4 9 16 25 36 49
total:      140

Common mistakes

Using the block before the cast.
Without the cast, values is a *var opaque, and values[0] = 1 fails with error: cannot assign 'int' to 'opaque'. Cast once, straight after Alloc, to the type the block will hold.
Casting to a read-only pointer.
Alloc(size) as *uint gives a pointer you cannot write through: values[0] = 1 then fails with error: cannot modify data through read-only pointer '*uint'. A block you mean to fill needs *var.
Asking for elements instead of bytes.
Alloc(count) asks for 8 bytes, not for 8 uints. It compiles and may even appear to work, while every write past the first element lands in memory that belongs to something else. Always multiply by sizeof.
Skipping the null check, or freeing twice.
None of these are compile errors: forgetting to check for null, forgetting to Free, freeing the same block twice, or using it after Free. Keep the pattern from this lesson — check, then defer Free — and the next lessons give you tools that make these mistakes harder to write.

Try it yourself

  1. Change count to 20 and predict the byte count on the first line before you run.
  2. Store int32 values instead of uints. What must change besides the cast?
  3. Add an early return 2; inside the loop that adds up total, taken once total passes 100. The defer still frees the block — convince yourself why.

Learn more

  • Alloc, Zero and Free in the API reference
  • Defer — how deferred statements run on every path out
  • Pointer slice — turning this block into an ordinary slice