8.18.1.1. Application lifecycle

A long-running dasImgui program needs two compatible owners:

  • daslang-live calls the exported init / update / shutdown functions and collects between update calls.

  • Standalone daslang calls main(), which owns the same lifecycle and must also provide a safe garbage-collection boundary.

The canonical application shape below supports windowed, live-reload, and headless execution from one source file. It follows the same memory rule as utils/dasllama-server/main.das: application state that survives a frame is global, and collection happens only after one update has finished and before the next frame creates collectable locals.

Source: modules/dasImgui/examples/tutorial/application_lifecycle.das.

 1options gen2
 2options _comment_hygiene = true
 3options gc
 4options persistent_heap
 5
 6require imgui/imgui_harness
 7
 8// =============================================================================
 9// TUTORIAL: application_lifecycle - one source file for standalone and live.
10//
11// The lifecycle exports are owned by daslang-live when hosted. main() owns the
12// same lifecycle in standalone mode. After update() returns, main calls the
13// harness GC helper: frame locals are dead, and the helper is a no-op when the
14// same source is hosted by daslang-live.
15//
16// STANDALONE: daslang.exe modules/dasImgui/examples/tutorial/application_lifecycle.das
17// LIVE:       daslang-live modules/dasImgui/examples/tutorial/application_lifecycle.das
18// HEADLESS:   daslang.exe modules/dasImgui/examples/tutorial/application_lifecycle.das -- --headless --headless-frames=600
19// =============================================================================
20
21var private g_frames = 0
22
23[export]
24def init() {
25    harness_init("dasImgui application lifecycle", 760, 440)
26}
27
28[export]
29def update() {
30    if (!harness_begin_frame()) return
31    harness_new_frame()
32
33    SetNextWindowPos(ImVec2(40.0f, 40.0f), ImGuiCond.Always)
34    SetNextWindowSize(ImVec2(680.0f, 340.0f), ImGuiCond.Always)
35    window(APPLICATION_WIN, (text = "Application lifecycle", closable = false,
36                             flags = ImGuiWindowFlags.None)) {
37        text("init -> update* -> shutdown")
38        separator()
39        text("frame {g_frames}")
40        text("The same file runs under daslang and daslang-live.")
41        text("Standalone GC runs only between completed frames.")
42        spacing(APP_SPACING)
43        if (button(QUIT_BUTTON, (text = "Quit"))) {
44            request_exit()
45        }
46    }
47
48    harness_end_frame()
49    g_frames++
50}
51
52[export]
53def shutdown() {
54    harness_shutdown()
55}
56
57[export]
58def main() {
59    init()
60    while (!exit_requested()) {
61        update()
62        // Safe boundary: update returned, so no frame-local collectable value
63        // remains live on the JIT stack. The live host collects for us.
64        harness_maybe_collect_gc()
65    }
66    shutdown()
67}

8.18.1.1.1. Lifecycle ownership

init()

Creates long-lived resources. It is called once by standalone main and by the live host on load/reload. Harness setup is reload-safe.

update()

Processes exactly one frame. A false result from harness_begin_frame() skips rendering while the host is paused, reloading, closing, or a headless frame cap has been reached.

shutdown()

Releases resources in reverse ownership order. The harness distinguishes a live reload from final process exit and preserves the live ImGui context when appropriate.

main()

Is used only by standalone daslang. It calls init once, repeatedly calls update, and always finishes with shutdown.

8.18.1.1.2. The two heap options

Long-running applications that allocate transient strings or containers use:

options persistent_heap
options gc

persistent_heap makes the context heap collectable. gc enables collection in the context. Both are needed before calling heap_collect and both are entry-program policies, so the tutorial declares them explicitly.

They do not make ordinary JIT stack locals traceable. A collection while a live string, array, table, JSON value, or other collectable object exists only in a JIT local can reclaim that object and crash. This is why the lifecycle shape, rather than merely the two options, is the safety mechanism.

8.18.1.1.3. The safe collection point

GC is the safety net, not the per-frame ownership mechanism. Long-running daslang applications should release uniquely-owned transient arrays, strings, tables, JSON trees, and replaced caches with delete as soon as their ownership ends. Stable derived data should be cached and rebuilt only when its inputs change. With that discipline, the heap reuses freed storage and a real collection is needed only occasionally.

Standalone programs do not receive the live host’s between-update collection pass. Their main loop must call harness_maybe_collect_gc() immediately after update() returns. The helper delegates to the existing glfw_live fragmentation heuristic: calling the check every loop does not mean collecting every loop. It only collects when the heap has enough unused space to make compaction worthwhile. It is a no-op under daslang-live, because the host already owns collection there.

This placement matters. Do not call heap_collect from the middle of widget rendering or while a request/frame-local collectable value must remain alive. Keep durable state in module globals, keep standalone main free of collectable locals that span the collection call, and collect only after update returns completely.

For a non-harness service, use the explicit server form:

[export]
def main() {
    init()
    while (!exit_requested()) {
        update()
        maybe_collect_gc() // update returned; request locals are dead
    }
    shutdown()
}

For non-harness services the equivalent call is their own maybe_collect_gc. utils/dasllama-server/main.das is the production reference. Its maybe_collect_gc skips host-owned live mode, rate-limits collections, uses heap-fragmentation ratios, and can honor a forced diagnostic collection. Simple GLFW applications can use live/glfw_live’s existing maybe_collect_gc helper at the same post-update boundary.

8.18.1.1.4. Delete-first example

def update() {
    var response <- build_transient_response()
    send(response)
    delete response // deterministic ownership end; no GC required here
}

For a cached UI layout, keep the cache global. When document text, available width, font roles, style, or zoom changes, delete the old cached layout and replace it once. An idle frame should render the retained layout without creating a new graph for GC to discover later.

8.18.1.1.5. Run modes

# Standalone window.
daslang.exe modules/dasImgui/examples/tutorial/application_lifecycle.das

# Live-reload host owns init/update/shutdown.
daslang-live modules/dasImgui/examples/tutorial/application_lifecycle.das

# CPU-only smoke run with a deterministic exit.
daslang.exe modules/dasImgui/examples/tutorial/application_lifecycle.das -- --headless --headless-frames=600

See also

  • Live reload for reload state and lifecycle hooks.

  • Harness + headless mode for the harness backend split.

  • skills/imgui_application.md for the implementation checklist used when creating or reviewing an application.