The AssemblyScript Runtime
The runtime provides the functionality necessary to dynamically allocate and deallocate memory of objects, arrays and buffers, as well as keep track of references that are no longer used.
Interface
-
__alloc(size:
usize
, id:u32
= 0):usize
Dynamically allocates a chunk of memory of at least the specified size and returns its address. Alignment is guaranteed to be 16 bytes to fit up to v128 values naturally. -
__realloc(ref:
usize
, size:usize
):usize
Dynamically changes the size of a chunk of memory, possibly moving it to a new address. -
__free(ref:
usize
):void
Frees a dynamically allocated chunk of memory by its address. -
__retain(ref:
usize
):usize
Retains a reference to an instance of a reference type. The instance doesn't become collected as long as there's at least one retained reference. Returns the retained reference. -
__release(ref:
usize
):void
Releases a reference to an instance of a reference type. The instance is considered for collection once all references to it have been released. -
__collect():
void
Forces a full garbage collection cycle. By default this means that reference cycles are resolved and possibly collected.
Internals
-
__retainRelease(newRef:
usize
, oldRef:usize
):usize
Retains a reference to an instance of a reference type while releasing the reference it replaces. Returns the retained reference. -
__visit(ref:
usize
, cookie:u32
):void
Concrete visitor implementation called during traversal. Cookie can be used to indicate one of multiple operations.
Built-ins
-
__typeinfo(id:
u32
):RTTIFlags
Obtains the runtime type information for objects with the specified runtime id. Runtime type information is a set of flags indicating whether a reference type is managed, an array or similar, and what the relevant alignments when creating an instance are etc. -
__visit_globals(cookie:
u32
):void
Calls__visit
on each global that is of a reference type. Not used anymore (originally provided to support tracing GCs) but still here for possible future use. -
__visit_members(ref:
usize
, cookie:u32
):void
Calls__visit
on each member of the instance pointed to byref
that is of a reference type.
Full/half
The full runtime is based on the TLSF memory manager and a pure reference counting garbage collector and provides all the functionality necessary. The half alias is essentially the same, but doesn't expose the runtime API so unused runtime code can be DCE'ed.
Stub/none
The stub runtime, though fully functional, provides minimal dynamic memory allocation but no deallocation. Useful for prototyping or very short-lived programs with hardly any memory footprint. The none alias is the same, but doesn't expose the runtime API so unused runtime code can be DCE'ed.
Integration notes
The underlying reference counting implementation works very similar to other implementations. When an object is stored in a local, global or field, its reference becomes retained (reference count is incremented by 1), respectively when it becomes deleted, it is released (reference count is decremented by 1). Once the reference count reaches zero, the object is considered for collection. Now, if an object is inherently acyclic (most objects are), it is free'd right away, while otherwise it is added to a cycle pool and considered for cycle collection on the next __collect
.
Differences to other implementations include:
- A new object from
__alloc
doesn't start with a reference count of 1, but 0. - When an object is returned from a function, it remains at RC + 1 and the caller is expected to release it.
- Getters, setters, operator overloads and constructors are function calls and behave like one.
- Functions with reference type arguments currently retain each such argument when called, and release it again when exited (unless it is returned of course).
Even though the rules are simple, working with the runtime internals within standard library code can be tricky and requires knowledge of where the compiler will insert runtime calls automatically. For instance, when changetype
ing a pointer to a reference type or vice-versa, the typechange is performed with no side effects. Means: No retains or releases are inserted and the user has to take care of the implications.
GOOD: In case of doubt, the following pattern is universal:
var ref = changetype<Ref>(__alloc(SIZE, idof<Ref>())); // retains the object in `ref`
...
return ref; // knows `ref` is already retained and simply returns it
BAD: One common pattern one shouldn't use is:
someFunc(changetype<Ref>(__alloc(SIZE, idof<Ref>()))); // might free the object before the call returns
You know the drill.
BONUS: Beware of runtime calls in conditional expressions like a ternary IF, logical AND or OR. Each arm can be in either of two states (either in-flight if immediately retained/returned or not if the expression or the target doesn't support it). Don't fight the compiler there.