Host Handle Policy
Defending against dangling pointers and memory leaks
Exposing raw C++ pointers directly into a dynamic script environment is a common source of Use-After-Free (UAF) bugs and game crashes.
Zephyr uses an internal 4-tier generational handle system to safely pass game engine objects (entities, components, etc.) into scripts. When a lifecycle boundary is crossed (e.g. a scene change), all handles in the affected tier and below are invalidated atomically.
Handle Tiers
🔴 Frame
Shortest lifespan. Invalidated when the current call-stack frame returns. (C++ API: create_frame_handle)
🟠 Tick
Invalidated at the end of each engine tick (advance_tick()). (C++ API: create_tick_handle)
🟡 Persistent
Invalidated on scene/level transitions (advance_scene()). (C++ API: create_persistent_handle)
🟢 Stable
Kept until explicitly released (invalidate_host_handle()). The only tier that can be serialized (saved).
Runtime Control
C++ engine developers must call these APIs at lifecycle boundaries for handle policy to work correctly:
// Scene / level change: invalidates Persistent and below
rt.advance_scene();
// Start of each render frame: invalidates Tick and below
rt.advance_tick();VM behavior on invalid handles
If a script tries to access an invalidated handle, a trap fires immediately in Debug builds (for diagnostics). In Release builds, the runtime follows the configured Handle Recovery Policy — typically throwing a handle-exception runtime error.
Handle Serialization (Save / Load)
When serializing game state (Save Envelope v2), the VM performs a full object-tree traversal:
std::string json = rt.serialize_value(my_script_root_value);Only Stable-tier handles are permitted in the serialized output. If Persistent, Tick, or Frame handles are encountered during traversal, serialize_value() raises an exception to prevent data corruption from short-lived references.
Smart Handle Wrapper (ZephyrHandle<T>)
To prevent a script value from being accidentally expired by a Frame or Tick collection cycle while still held on the C++ side, use the RAII-based ZephyrHandle<T> smart wrapper:
// Activate a smart handle around a host object pointer.
// The pointer is pinned (safe from reclamation) for the lifetime of the wrapper.
auto h = ZephyrHandle<Player>(vm, vm.make_host_object(player_ptr));
// Safely access the inner pointer through the wrapper.
if (h) {
h.get()->damage(10);
}The wrapper calls vm.pin_value() on construction and vm.unpin_value() on destruction, giving the GC and handle invalidation system an absolute hold-off signal.