ktm v0.28.3

Memory

Where values live, how to allocate memory that outlives a function, how to give it back, and the habits that keep a program free of leaks. ktm has no garbage collector: memory you allocate is yours until you free it.

Three places a value lives

Allocating and freeing

The mem module allocates, resizes and frees.

FunctionDoes
mem.alloc(size)Allocates size bytes, not zeroed
mem.allocZero(size)Allocates size bytes, all zero
mem.realloc(p, size)Resizes; mem.realloc(null, n) is mem.alloc(n)
mem.free(p)Gives the memory back; mem.free(null) does nothing
mem.size(p)The size that was asked for (0 for null)
mem.copy(dst, src, n)Copies n bytes; returns dst
mem.set(dst, byte, n)Fills n bytes with byte; returns dst
import mem;

void main() {
    i64 n = 4;
    i64 *nums = mem.allocZero(n * sizeof(i64));
    nums[2] = 7;
    printf("%d %d, %d bytes\n", nums[0], nums[2], mem.size(nums));
    mem.free(nums);
}
0 7, 32 bytes

The result of mem.alloc is a void *, which converts to any pointer type without a cast.

An array of structs

import mem;

struct Player { u8 *name; i64 score; };

void main() {
    Player *team = mem.allocZero(3 * sizeof(Player));
    team[0].name = "ada";
    team[0].score = 10;
    team[2].name = "linus";
    for (i64 i = 0; i < 3; i++) {
        printf("%s: %d\n", team[i].name ? team[i].name : "(empty)", team[i].score);
    }
    mem.free(team);
}
ada: 10
(empty): 0
linus: 0

A value that outlives its function

A function that makes something on the heap and returns the pointer hands its ownership to the caller, who frees it later.

import mem;

struct Counter { i64 value; };

Counter *counterNew(i64 start) {
    Counter *c = mem.alloc(sizeof(Counter));
    c.value = start;
    return c;
}

void main() {
    Counter *c = counterNew(5);
    c.value++;
    printf("%d\n", c.value);
    mem.free(c);
}
6

Growing

mem.realloc gives a block a new size and keeps its contents (as much as fits). It may move the block, so always use the pointer it returns.

import mem;

void main() {
    i64 cap = 2, len = 0;
    i64 *items = mem.alloc(cap * sizeof(i64));
    for (i64 i = 1; i <= 5; i++) {
        if (len == cap) {
            cap *= 2;
            items = mem.realloc(items, cap * sizeof(i64));
        }
        items[len] = i * i;
        len++;
    }
    for (i64 i = 0; i < len; i++) {
        printf("%d ", items[i]);
    }
    printf("(capacity %d)\n", cap);
    mem.free(items);
}
1 4 9 16 25 (capacity 8)

You rarely need to write this: vec.Vec is a growable array that does it for you.

import vec;

void main() {
    vec.Vec<i64> items = vec.new<i64>();
    for (i64 i = 1; i <= 5; i++) {
        items.push(i * i);
    }
    for (auto x : items) {
        printf("%d ", x);
    }
    printf("(%d items)\n", items.len);
    items.free();
}
1 4 9 16 25 (5 items)

Copying and filling

import mem;

void main() {
    u8 a[8], b[8];
    mem.set(a, 'x', 7);
    a[7] = 0;
    mem.copy(b, "abcdefg", 8);    // 7 letters and the NUL
    printf("%s %s\n", a, b);

    i64 src[3] = {1, 2, 3}, dst[3];
    mem.copy(dst, src, sizeof(src));
    printf("%d %d %d\n", dst[0], dst[1], dst[2]);
}
xxxxxxx abcdefg
1 2 3

mem.copy copies front to back. For a source and a destination that overlap, use libc.memmove.

Who frees what

The rule that keeps programs leak-free: every allocation has one owner, who frees it once. The standard library says who owns what in each function's description; the usual cases are:

Freeing at the end of a block

defer runs a statement when the block ends, however it ends: the end of the block, a return, a break, or an error. Writing the free right after the allocation means it can't be forgotten.

import mem;

i64 sumFirst(i64 n) {
    i64 *nums = mem.alloc(n * sizeof(i64));
    defer mem.free(nums);
    for (i64 i = 0; i < n; i++) {
        nums[i] = i;
    }
    if (n > 3) {
        return -1;      // the defer still frees nums
    }
    i64 s = 0;
    for (i64 i = 0; i < n; i++) {
        s += nums[i];
    }
    return s;
}

void main() {
    printf("%d %d\n", sumFirst(3), sumFirst(10));
}
3 -1

scoped

For a value with a free() method, scoped in front of its declaration means "free it at the end of the block". It is the shortest way to own something.

import str, vec;

void main() {
    scoped String name = str.from("ada");
    scoped vec.Vec<i64> nums = vec.new<i64>();
    nums.push(1);
    nums.push(2);
    name.append(" lovelace");
    printf("%s, %d numbers\n", name, nums.len);
}
ada lovelace, 2 numbers

Your own types can be scoped by having a free() method:

import mem;

struct Grid { i64 w; i64 h; u8 *cells; };

Grid gridNew(i64 w, i64 h) {
    Grid g = {w, h, mem.allocZero(w * h)};
    return g;
}

void Grid.free() {
    printf("freeing %dx%d\n", self.w, self.h);
    mem.free(self.cells);
}

void Grid.set(i64 x, i64 y) {
    self.cells[y * self.w + x] = 1;
}

void main() {
    scoped Grid g = gridNew(4, 3);
    g.set(1, 2);
    printf("%d %d\n", g.cells[2 * 4 + 1], g.cells[0]);
}
1 0
freeing 4x3

defer and scoped covers both in depth.

Finding memory bugs

Run a program with -jit -Memsafe and ktm watches every allocation. Freeing a block twice, or freeing something that was never allocated, is reported (and skipped, so the heap stays healthy) as it happens; memory still allocated at exit is reported then, each with the line that allocated it.

import mem;

void main() {
    u8 *a = mem.alloc(16);
    u8 *b = mem.alloc(32);
    b[0] = 1;
    mem.free(a);
    printf("done\n");
}
done

That program leaks b, which -Memsafe reports when it ends:

$ ktm -jit -Memsafe leaky.ktm
done
memsafe: 1 allocation(s) (32 bytes) never freed
  0x55d1c2a4f2e8        32 bytes  at leaky.ktm:5 in main

Safety and debugging shows the other checks.

Where it differs from C