Skip to content

druntime: defer rt_term to atexit on Emscripten#5129

Open
ramen-bully wants to merge 1 commit into
ldc-developers:masterfrom
revate:rt_term-emscripten-upstream
Open

druntime: defer rt_term to atexit on Emscripten#5129
ramen-bully wants to merge 1 commit into
ldc-developers:masterfrom
revate:rt_term-emscripten-upstream

Conversation

@ramen-bully
Copy link
Copy Markdown

DISCLAMER: i used AI for finding and writing this, i don't know anything about compilers or emscripten..
sorry if this is completely wrong, i tested with some sokol projects and it fixed my project.

Emscripten programs that register an async event loop (sokol_app, emscripten_set_main_loop, emscripten_request_animation_frame_loop, etc.) return from main while user code is still scheduled to run via JS callbacks. The unmodified _d_run_main2 epilogue runs rt_term immediately, which tears down the GC (gc_termGcx.Dtor → unmap every pool). Subsequent malloc()s from those callbacks reuse the addresses and clobber any live D objects allocated before main returned.

Register rt_term via atexit on Emscripten instead of calling it inline. This handles all three cases correctly:

  • EXIT_RUNTIME=0 async programs (browser tabs): atexit never fires, the page lives until the tab closes, the GC stays alive while rAF callbacks run.
  • EXIT_RUNTIME=1 sync programs: emscripten runs atexit handlers when main returns, rt_term fires at the right time, module destructors and gc_term run normally.
  • EXIT_RUNTIME=1 programs that call exit() after cancelling their event loop: same as above — atexit fires on exit().

The deferral preserves D-side teardown semantics for programs that genuinely want them, while preventing the heap-clobber for programs whose "main returned" is only a JS-scheduling artifact.

Gated on version (Emscripten); other targets unchanged.

Emscripten programs that register an async event loop (sokol_app,
emscripten_set_main_loop, emscripten_request_animation_frame_loop,
etc.) return from main while user code is still scheduled to run via
JS callbacks. The unmodified `_d_run_main2` epilogue runs `rt_term`
immediately, which tears down the GC (`gc_term` → `Gcx.Dtor` → unmap
every pool). Subsequent malloc()s from those callbacks reuse the
addresses and clobber any live D objects allocated before main
returned.

Register `rt_term` via `atexit` on Emscripten instead of calling it
inline. This handles all three cases correctly:

  - `EXIT_RUNTIME=0` async programs (browser tabs): atexit never
    fires, the page lives until the tab closes, the GC stays alive
    while rAF callbacks run.
  - `EXIT_RUNTIME=1` sync programs: emscripten runs atexit handlers
    when main returns, rt_term fires at the right time, module
    destructors and `gc_term` run normally.
  - `EXIT_RUNTIME=1` programs that call `exit()` after cancelling
    their event loop: same as above — atexit fires on `exit()`.

The deferral preserves D-side teardown semantics for programs that
genuinely want them, while preventing the heap-clobber for programs
whose "main returned" is only a JS-scheduling artifact.

Gated on `version (Emscripten)`; other targets unchanged.
{
rt_term();
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest putting this as nested static function directly above the single usage below.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants