Spec chapter 08
Memory Model and Unsafe Code
Value vs reference
| Kind | Semantics |
|---|---|
Primitives, Struct, tuples, fixed arrays T[N] | values: copied on assignment |
Class, Interface values, String, dynamic arrays T[], collections | references: shared; copy explicitly with .Clone() |
Automatic reference counting
- Class instances are refcounted. Cleanup is deterministic: no GC pauses mid-frame.
- There is no
Delete. An object is freed when its last strong reference goes away. Destroy()(if defined) runs at that moment. See 06.- An object whose last reference was one of another object’s fields goes once that object’s
fields are all released, then what it held goes, and so on, one object at a time, so letting
go of a chain of any length never runs out of stack. The
Destroy()s run in the same order as ever: an object’s, then its first field’s object’s and what that held, then its second’s. What aDestroy()itself lets go of goes at once. - Normal objects use non-atomic refcounts (fast). Only
Shared Classinstances use atomic refcounts (see 10-concurrency.md).
Cycles
- Break cycles with
Weak<T>references, or capture lists in closures (see 05). - Debug builds include a cycle detector that reports leaked cycles.
Class Node
Field children: Node[]
Field parent: Weak<Node> // doesn't keep the parent alive
End ClassA Weak<T> is assigned a T. Reading it gives a T?, which is Null once the object is gone,
so node.parent?.name works directly.
- The
Tis a Class, or a type parameter that a constraint says is one,Where T: Class(see 07). A Weak is assigned aT, a Class that extends it, aT?, orNull, which refers to nothing. That’s also its zero, so a field of one needs noNew. - What reading it gives holds a reference of its own, so the object stays while it’s used.
- Its object can go at any time, so a null check doesn’t narrow a Weak. Read it into a Local, and check that.
- A Struct that holds a Weak has no
==, which would compare the references. - When an object’s last strong reference goes, its
Destroy()runs and its fields are released at once. Its memory waits for the last Weak to it.
Leak reports
A program that ends with objects or Strings still alive prints how many of each, by Class:
leak: 2 Node objects were never released. Debug builds keep track of every object, so the
report also points out each cycle among them, field by field:
leak: a cycle of 2 objects: Node.next -> Node.next -> the first Node
leak: a cycle of 3 objects: Tree.children -> Tree[] element -> Tree.parent -> the first TreeObjects that only a cycle keeps alive are in the counts, not the cycles.
Engine resources
Entities, textures, and other engine-owned resources keep an explicit Free(), because the
scene owns them. Handles are generational, so using a freed resource raises a clear
error instead of crashing.
Pointers and unsafe code
Normal code never needs pointers. They exist for FFI and low-level work.
Unsafe
Local buf: Ptr<UInt8> = Mem.Alloc(1024)
Defer Mem.Free(buf)
buf[0] = 255
Local p = AddressOf(particle) // Ptr<Particle>
p.Value.life = 1.0
End Unsafe
Unsafe Function Blit(dst: Ptr<UInt8>, src: Ptr<UInt8>, n: Int)
...
End Function- Dereferencing, pointer arithmetic,
AddressOf, and raw memory (Mem.*) are only allowed insideUnsafeblocks orUnsafe Functions. Ptr<T>values can be stored and passed around in safe code; they just can’t be dereferenced there.- A
Ptr<T>points at a value C code has: a number,Bool, Enum,CString,Ptr, Callback, or a fixed array or Struct of these, which holds no references;Ptr<Void>points at anything, as C’svoid*, andVoidis a type nowhere else. A type parameter is the T when its constraints say it’sCData(see 12), as inFunction First<T: CData>(p: Ptr<T>): T. Nullis a Ptr’s zero, C’s NULL;p == Nullandp == qcompare addresses, in safe code too, andPtr<T>?isn’t a type you write, since a Ptr can be Null already, so??and?.don’t take a Ptr, which is compared with Null instead. A generic’sT?of a Ptr, like what aMap<Int, Ptr<Node>>’sGetgives, is a T? all the same, whose Null is “none”, apart from a Ptr it holds that’s Null. APtr<Void>converts to and from anyPtr<T>withoutAs. A Ptr isn’t Sendable, or shown in a string;p As UInt64is its address.- What a Ptr points at isn’t part of it, so a Struct can point at its own Struct, as a linked list’s nodes do.
- Only Unsafe code reads or sets
p.Value, what a Ptr points at, and its parts, likep.Value.life; indexes it,p[i], with any integer, which can be negative; steps it,p + nandp - n, by T’s size, or counts the T’s between two of one type,p - q, anInt64, when a T takes some bytes; converts it withAsto another Ptr, to and fromInt64orUInt64, or between aCStringand aPtr<UInt8>,Ptr<Int8>, orPtr<Void>; takesAddressOf; reads a CString’s text withToString(); calls a Callback (see 12); or calls an Unsafe Function or Method, likeMem’s. APtr<Void>has no Value, indexes, or steps. - A debug build panics where a Null Ptr’s Value is used or it’s indexed, “the Ptr is Null”, and
where a Null Callback is called, “the Callback is Null”.
ToString()on a Null CString panics in every build. AddressOf(x)takes a Local, ByRef parameter, Global, field, element of aT[]or fixed array, or a Struct’s Self, and gives aPtr<T>that’s good while what it points at is there: until the Local’s block ends, the object goes, or theT[]changes size. It can change what it points at, so it takes only what code could change there: not a loop variable, a ReadOnly Field or Static Field, or a part of a value the statement makes, likeMake().x; not Self in a lambda or aParallel For, which has a copy of its own; and in aParallel For, nothing from outside but an outside array’s element at the pass’s own index. A place through a Ptr, likep.Valueorp[i], is reached before what comes after it is evaluated, as one in aT[]is (see 03), and it can go ByRef, since what a Ptr points at doesn’t move, two of them in one call too, since C’s memory can be shared.SizeOf<T>()is how many bytes a T takes in memory, as C lays it out, anInt64known at compile time, for the same types a Ptr points at, whichever comes first in the code: a Struct or Enum declared after it is measured whole. A T of more bytes than an Int64 counts, or whose size depends on itself, like a Struct with a Field ofInt8[SizeOf<T>()], is an error, and a Ptr at one doesn’t step. A T with type parameters in it is measured for each instance, as it’s made, and isn’t known at compile time in the generic code. The target is the one programs are built for, which decides how bigCLong,CULong, andCWCharare.- An
Unsafe FunctionorUnsafe Method(orUnsafe Static Method) is one that only Unsafe code calls. It isn’t a function value, and it can’t be an Operator, Destroy, a Const Function, or an Interface’s Method, or implement one, or override a Method that isn’t Unsafe, since code calls those without saying so. AFor ... Inthat calls an UnsafeIterate()is in Unsafe code, and reflection’sFor Methodleaves Unsafe Methods out. An Unsafe Function’s defaults are its Unsafe code. A lambda made in Unsafe code is Unsafe code too, and safe code can call it. - A Const Function has no Unsafe code, since it runs in the compiler.
std.ffi’sMemhas Unsafe Static Methods over the C runtime:Alloc(bytes)gives that much zeroed memory, andAllocUninitialized(bytes)as much that isn’t zeroed;Resize(memory, bytes)gives what those or Resize gave, or new memory forNull, in that much memory, keeping what fits, with what it adds not zeroed;Free(memory)gives it back;AllocAligned(bytes, alignment)gives zeroed memory at a multiple ofalignment, a power of two, which onlyFreeAligned(memory)gives back;Copy(destination, source, bytes), which can overlap;Fill(destination, value, bytes); andCompare(a, b, bytes), which gives 0 for the same bytes, or -1 or 1 as the first byte that differs is lower ataorb. Sizes are Int64s, like SizeOf’s, a Count, andp - q, so one that went below 0 panics instead of wrapping around, an alignment that isn’t a power of two panics, and the Allocs and Resize panic when there isn’t that much memory.Memitself isn’t made.std.ffigives a CString more than its UnsafeToString():CString.From(text)makes a NUL-terminated copy of a String’s UTF-8 that C code can keep, in safe code;Length()counts its bytes before the first NUL, panicking onNull; andFree()gives back what From made, panicking for any other CString, or one given back already. The copy is the runtime’s, so leak reports count one that’s never freed.
Report an issue with this page or with FutureBlitz on GitHub.