Skip to content

Memory Ownership

Andrew Lind edited this page Jul 13, 2026 · 3 revisions

A whole-program escape/ownership analysis decides what each class frees, erring toward a leak (safe) over a double-free:

  • destructors free what a class newed (and the typed pointer handed to a base's void* field);
  • short-lived heap locals are freed at scope close, and before every early return;
  • owned pointer fields are NULL-initialized, freed before reassignment, and freed in the destructor;
  • owned containers are walked and freed element-by-element.

Borrowed dependencies, value containers, objects owned by a receiver, and fields handed back out of the object are left alone. A field reference is recognized whether written this.field or bare field (Haxe lets you omit this.), so ownership does not hinge on the qualifier.

The ownership overrides

For an injected pointer the class stores but did not new — where own-vs-borrow is not statically decidable — mark the field @owned to have the destructor free it (a scalar with delete, a container element-by-element); the local-scope counterpart @delete var x = … frees a marked local at scope close. Unmarked injected pointers stay borrowed.

These overrides are obeyed but advisory-checked — the analysis warns when a tag looks unsound (e.g. an @owned field that is also handed out); auto-inferring an injected pointer's ownership would need interprocedural call-site analysis and is a future improvement.

A new passed to a constructor parameter the class owns is emitted inline — the constructed object frees it — rather than hoisted into a scope-owned local that would double-free it.

@sink — ownership transfer

@sink marks a value as handed off — the current scope no longer frees it. It can sit on a consuming parameter, on a call argument, or on a local declaration; all three say the same thing (ownership leaves here) and share the same conservative trust model.

A method/function parameter can be marked @sinkfunction setKey(key, @sink val:JValue) — a consuming parameter that takes ownership across the call (distinct from @owned, which says a field frees its member when the object dies; @sink says ownership transfers in at this call): a new handed to a @sink position is emitted inline, and an owned local handed there has its scope-close free dropped, so the value is freed once (by the receiver) rather than dangled by the caller. This is the explicit answer to a retaining method (store(x) that keeps x), which an intraprocedural analysis cannot otherwise see; @sink on a by-value parameter (where there is nothing to consume) is flagged as a no-op.

@sink may also be written at the call site, on the argument itself — new Quad(@sink new Vertex(x, y)) or foo(@sink v). (Haxe permits metadata on any expression; Hatchet parses it, and any expression-position metadata other than @sink is inert, as it is under hxcpp.) A call-site @sink transfers ownership of that argument to the callee exactly as the parameter form does — a @sink new X(...) is emitted inline and a @sink local has its scope-close free dropped:

var quad = new Quad(
  @sink new Vertex(x, y),
  @sink new Vertex(x + cell, y));
// → Quad* quad = new Quad(new Vertex(x, y), new Vertex(x + cell, y));
//   delete quad;   // the vertices are transferred; the caller frees only `quad`

A local declaration can be marked @sink var x = new X(...), which suppresses the scope-close free of x entirely — the exact inverse of @delete var (which adds one). Use it when ownership is handed off with no single call to attach the marker to (x is stored through a native API, registered in a global, or otherwise leaves the scope in a way the analysis cannot follow):

@sink var s:Vertex = new Vertex(engine, x, y);
// → Vertex* s = new Vertex(engine, x, y);   // and NO `delete s;` at scope close

The marker is in every case an assertion that ownership leaves the current scope, so it applies even when the destination is opaque to the analysis (an @:native or otherwise unresolved class) — it overrides the inferred ownership. If nothing actually takes ownership the result is a leak, never a double-free. @sink on a value local or argument (a struct/array/map/primitive — nothing to hand off) is flagged as a no-op.

A new pushed into a container that escapes the scope — a class-owned field container, or a local container that is returned/stored — likewise comes to rest there and is not freed locally.

Exception unwind

On exception unwind (a throw inside a try), the scope-close frees do not run, so owned objects created in the try before the throw leak — a deliberate extension of the conservative bias (never a double-free/use-after-free); free them in the catch if it matters. (Exceptions must be enabled on the target — VC6 /GX; g++ enables them by default.)


The @owned / @sink / @delete tags are summarized alongside the rest in Metadata. Property setters interact with ownership — see Members & Access.

Clone this wiki locally