Skip to content

[patch] Run window teardown when the run loop throws - #426

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/teardown-on-loop-exception-jysu12
Sep 17, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
claude/teardown-on-loop-exception-jysu12

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #406

What was wrong

An exception from a consumer's OnRender/OnUpdate callback propagates straight out of window.Run(). Two gaps meant that path skipped teardown:

  1. RunWindowLoop had no try/finally. TeardownWindow() sat on the line after window!.Run(), so a throw skipped it entirely — the window was left undisposed and ImGuiApp.window still pointed at the orphaned one, which also made a later Start() throw "Application is already running."
  2. TeardownWindow was two steps short of the Closing handler. It ran CleanupController/CleanupInputContext/CleanupOpenGL but not CleanupPinnedFontData() or FontHelper.CleanupCustomFonts(). Closing doesn't fire on a faulted loop, so the pinned font GCHandles and the Marshal.AllocHGlobal buffers leaked — on the embedded path too, which already had its finally but converged on the shorter sequence.

This matters for hosts that catch the exception and keep running (embedding hosts, plugin hosts, harnesses that restart sessions): each faulted session leaked a GL context, controller, input context and pinned native memory.

What changed

ImGui.App/ImGuiApp.cs:

  • Extracted the five-call cleanup sequence into CleanupWindowResources(), per the issue's own suggestion, and pointed all three paths at it — the Closing handler, and TeardownWindow() for the blocking and embedded loops. Three call sites converging on one implementation is what stops a path drifting short again.
  • TeardownWindow() now calls it, then disposes and clears window as before.
  • RunWindowLoop() does its teardown in a finally.

Every call in the sequence is idempotent, which a normal close relies on: it runs the Closing handler and then the teardown.

No change was needed in ImGuiApp.Embedded.cs — EmbeddedSession.RunLoop already had the finally; it picks up the font cleanups through the shared method.

Tests

Two tests added to tests/ImGui.App.Tests/WindowTeardownTests.cs:

  • RunWindowLoopTearsDownWhenTheLoopThrows — a mock window whose Run(Action) throws. Asserts the window and input context are disposed, the static fields are cleared (so Start() is legal again), and that the consumer's exception still reaches the caller unchanged rather than being swallowed by the teardown.
  • TeardownWindowReleasesPinnedFontMemory — seeds a pinned GCHandle and an AllocHGlobal buffer and asserts teardown drains both lists. This covers the embedded path as well, since it ends in the same method.

Verified both fail on the pre-fix source and pass with it (source change stashed, rebuilt, re-run): 2 failed / 436 passed before, 438 passed after.

Regression slice, all green on Linux:

Project Result
ImGui.App.Tests 438 passed
ImGui.App.Testing.Tests 82 passed
ImGui.Widgets.Tests 283 passed
ImGui.Popups.Tests 23 passed, 1 skipped (pre-existing)
ImGui.Color.Tests 20 passed

dotnet build ImGui.sln succeeds with 0 warnings.

🤖 Generated with Claude Code

https://claude.ai/code/session_011rsegt5ct1Cg7M8ffSGgUi


Generated by Claude Code

An exception from a consumer's OnRender or OnUpdate callback propagates
straight out of window.Run(). RunWindowLoop called TeardownWindow on the
line after Run(), so on that path it never ran: the window was left
undisposed and the static field still pointed at it, which also made a
later Start() throw "Application is already running."

TeardownWindow was also missing the two font cleanups that the Closing
handler did. Closing does not fire on a faulted loop, so the pinned font
GCHandles and the AllocHGlobal buffers leaked on both the blocking and
the embedded paths - the embedded session already had its finally, but
it converged on a teardown that was short two steps.

Factor the five cleanups into CleanupWindowResources and have all three
paths - the Closing handler, and TeardownWindow for the blocking and
embedded loops - go through it, then put RunWindowLoop's teardown in a
finally. Every call in the sequence is idempotent, which a normal close
relies on: it runs the handler and then the teardown.

Fixes #406

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rsegt5ct1Cg7M8ffSGgUi
@sonarqubecloud

Copy link
Copy Markdown

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.

Exceptions from the render/update loop skip all teardown, leaking the GL context, controller, input context, and pinned font memory

1 participant