Allocators & Ownership

Allocators & Ownership

Allocators and Ownership

Heap ownership crosses the boundary in both directions, using the mechanism C++ already has for exactly this purpose.

Class-specific operator new and operator delete

Every exported Kairo class carries its own allocation operators, bound to Kairo's global allocator:

class MyKairoClass {
public:
    static void *operator new(size_t);
    static void  operator delete(void *);
    static void *operator new[](size_t);
    static void  operator delete[](void *);
    ~MyKairoClass();
};

So from C++, the ordinary spelling is the correct one:

MyKairoClass *p = new MyKairoClass("hi");
delete p;   // runs Kairo's destructor, then returns memory to Kairo's global allocator

This works because Kairo emits the class, and therefore emits the destructor. ~MyKairoClass performs Kairo's destruction semantics; C++'s delete merely sequences destructor-then-deallocate, which is the same pair of operations Kairo spells as delete obj followed by @free, which together are @destroy.

Pointer parameters and return values stay raw *T. There is no wrapper type, no ABI change, and no cost at the call boundary.

Global operator new overrides do not affect Kairo objects

A class-specific operator new takes precedence over a global replacement. A C++ translation unit that overrides global new/delete (for a pool, an instrumented heap, a leak tracker) therefore does not touch Kairo allocations. The isolation is structural, not a restriction.

The operators bind to the global allocator, always

Kairo's scoped allocator mechanism works by swapping the allocator the process is currently using. The emitted operator new and operator delete deliberately bypass that indirection and address the global allocator directly.

This matters. Consider a C++ delete that happens while a Kairo scoped allocator is active:

@mem::set_scoped_allocator(ArenaAllocator)
fn work() {
    call_into_cxx()      // C++ runs `delete p` on a globally-allocated Kairo object
}

If operator delete followed the ambient allocator, that object would be handed to the arena's deallocator instead of the global one. Binding to the global allocator unconditionally makes the pairing correct regardless of what is active at the call site.

The invariant that makes this sound: objects reachable from C++ were allocated by the global allocator. A pointer allocated through a scoped allocator that escapes the allocator's scope is a hard error under Tether, so a scoped-allocated object cannot reach a C++ delete in the first place.

What is still an error

The operators fix new/delete. They do not make every deallocation valid:

  • free() on a pointer from @create: wrong deallocator, no destructor.
  • delete on a pointer from @alloc: @alloc returns untyped storage with no constructed object.
  • delete on an object allocated by a C++ operator new override in a TU that predates the Kairo declaration.

These are what the ownership annotations below are for.

CxxNewAllocator

For code that wants Kairo's own allocations to follow C++'s allocation path, so that a global operator new override does apply to them, core provides CxxNewAllocator:

@mem::set_allocator(core::CxxNewAllocator)

This is the unusual case, not the default. The intentional friction rule applies: the annotation must appear at the top of every file in the affected dependency graph.

Ownership annotations in generated headers

Declarations emitted for the C++ side carry Clang's ownership attributes, tagged with the Kairo allocator identity:

void *kairo_alloc(size_t) __attribute__((ownership_returns(kairo_global)));
void  kairo_free(void *)  __attribute__((ownership_takes(kairo_global, 1)));

Clang's static analyzer (MallocChecker) uses these to flag mismatched allocation and deallocation across the boundary. Because kcc controls the compiler invocation, this checking is available as a driver flag rather than requiring a separate scan-build step.

Note

The allocator identity string is part of the interop contract and is pinned in the ABI specification. Both sides must agree on it.

Kairo's own semantic analysis enforces the same rule independently and produces a Kairo-quality diagnostic. The Clang attributes exist so that C++ consumers benefit too.

Lifetime annotations

Emitted declarations carry [[clang::lifetimebound]] and the [[gsl::Owner]] / [[gsl::Pointer]] pair where the Kairo side can prove the relationship. Unlike the ownership attributes, these are diagnosed during ordinary compilation via -Wdangling, so a lifetime relationship proven on the Kairo side becomes a real compile-time warning on the C++ side.

See also