You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Windows] Application::Quit posts WM_QUIT before destroying windows #57
On Windows, Application::Quit() ends in PostQuitMessage() (src/platform/windows/application_windows.cpp) without destroying the application's windows first.
window_manager had the same pattern in destroy(), and there it made apps take several seconds to exit, or stop responding, since Flutter 3.24. The window and the Flutter engine were torn down after the message loop had already stopped.
Expected: destroy the windows (so the Flutter view controller shuts down its engine while messages are still pumped), then post WM_QUIT. Needs checking in a Flutter app, including with a close handler that intercepts the system close button.
Investigated on Windows 11 with Flutter 3.47.5 (stable), using flutter_window_example (the FlutterViewController runner) and flutter_multiple_window_example (the headless-engine runner), with Application.instance.quit(7) fired from a timer.
What happens today: a crash during teardown, not a hang
The multi-second hang from window_manager#478 does not reproduce on 3.47.5: the process is gone about 100 ms after quit(). But it does not end cleanly. Under cdb, once the loop has ended:
window_example!wWinMain
→ FlutterWindow::~FlutterWindow // member flutter_controller_ is destroyed
→ FlutterDesktopViewControllerDestroy
→ NtUserDestroyWindow // the engine destroys the Flutter view (child HWND)
→ WM_PARENTNOTIFY to the still-alive top-level window
→ (core's window subclass procs, forwarding)
→ Win32Window::WndProc → FlutterWindow::MessageHandler
→ FlutterViewController::HandleTopLevelWindowProc // on the half-destroyed controller
→ flutter_windows.dll: access violation reading [rcx+10h], rcx = 0
Quit() stops the loop with the top-level window still alive. The runner then destroys the controller from ~FlutterWindow rather than from OnDestroy, and MSVC's unique_ptr does not clear the pointer before it deletes. So the parent's message handler calls into the controller while it is being destroyed. The engine's normal shutdown is skipped: an end-of-wWinMain trace never prints, and Dart gets no lifecycle callbacks. Without a debugger the process still exits with code 0 and leaves no WER event. My guess is that x64 Windows swallows the exception because it is raised inside a window-proc callback; I did not pin down what finally ends the process. The multi-window example shows the same missing end-of-wWinMain trace, but I did not run it under the debugger.
When the host owns the loop, Quit() now emits ApplicationExitingEvent and then posts a step to the main thread that:
destroys the thread's top-level windows. It uses DestroyWindow, not WM_CLOSE, so a close handler that intercepts the close button cannot veto the quit. Message-only windows (dispatcher, menu and shortcut hosts) are not enumerated.
posts WM_QUIT(exit_code) last, after the PostQuitMessage(0) the runner issues on WM_DESTROY. The last one wins, so the exit code survives.
The step is posted rather than run inline. With merged platform and UI threads, the call comes from inside a Dart FFI call, and shutting the engine down under its own running isolate is unsafe; I did not test the inline variant. Run()'s own loop is unchanged.
Results
Time from quit() until the process exited, 3 runs each:
before
after
flutter_window_example
93–107 ms (via the crash above)
192–233 ms, clean
flutter_multiple_window_example
63–69 ms
177–179 ms, clean
The extra time is the engine shutting down properly. After the fix, in the single-window example: the controller reset in OnDestroy takes ~63 ms, wWinMain returns at ~78 ms, and CRT exit plus DLL, Dart VM and driver-thread teardown take the rest. There is no access violation, and the process ends through the CRT's exit. The multi-window example now also delivers AppLifecycleState.inactive and hidden to Dart.
A C++ probe linked against core pumps the loop like a host. It creates a core window plus a runner-like window that vetoes WM_CLOSE and posts WM_QUIT(0) on destroy:
Before: the windows survive the quit.
After: all checks pass:
both windows are alive during ApplicationExitingEvent, and destroyed afterwards (the vetoing one exactly once);
the host sees WM_QUIT with Quit()'s code (7/9/11), not 0;
a second quit in the same process and a quit from a worker thread both work;
the dispatcher still works afterwards.
The exit code cannot be checked end to end in the Flutter examples, because the stock runner's wWinMain always returns EXIT_SUCCESS.
Activity
lijy91 commentedon Sep 27, 2026
Investigated on Windows 11 with Flutter 3.47.5 (stable), using
flutter_window_example(theFlutterViewControllerrunner) andflutter_multiple_window_example(the headless-engine runner), withApplication.instance.quit(7)fired from a timer.What happens today: a crash during teardown, not a hang
The multi-second hang from window_manager#478 does not reproduce on 3.47.5: the process is gone about 100 ms after
quit(). But it does not end cleanly. Under cdb, once the loop has ended:Quit()stops the loop with the top-level window still alive. The runner then destroys the controller from~FlutterWindowrather than fromOnDestroy, and MSVC'sunique_ptrdoes not clear the pointer before it deletes. So the parent's message handler calls into the controller while it is being destroyed. The engine's normal shutdown is skipped: an end-of-wWinMaintrace never prints, and Dart gets no lifecycle callbacks. Without a debugger the process still exits with code 0 and leaves no WER event. My guess is that x64 Windows swallows the exception because it is raised inside a window-proc callback; I did not pin down what finally ends the process. The multi-window example shows the same missing end-of-wWinMaintrace, but I did not run it under the debugger.Fix (core cd7cd6d, not pushed yet)
When the host owns the loop,
Quit()now emitsApplicationExitingEventand then posts a step to the main thread that:DestroyWindow, notWM_CLOSE, so a close handler that intercepts the close button cannot veto the quit. Message-only windows (dispatcher, menu and shortcut hosts) are not enumerated.WM_QUIT(exit_code)last, after thePostQuitMessage(0)the runner issues onWM_DESTROY. The last one wins, so the exit code survives.The step is posted rather than run inline. With merged platform and UI threads, the call comes from inside a Dart FFI call, and shutting the engine down under its own running isolate is unsafe; I did not test the inline variant.
Run()'s own loop is unchanged.Results
Time from
quit()until the process exited, 3 runs each:flutter_window_exampleflutter_multiple_window_exampleThe extra time is the engine shutting down properly. After the fix, in the single-window example: the controller reset in
OnDestroytakes ~63 ms,wWinMainreturns at ~78 ms, and CRT exit plus DLL, Dart VM and driver-thread teardown take the rest. There is no access violation, and the process ends through the CRT'sexit. The multi-window example now also deliversAppLifecycleState.inactiveandhiddento Dart.A C++ probe linked against core pumps the loop like a host. It creates a core window plus a runner-like window that vetoes
WM_CLOSEand postsWM_QUIT(0)on destroy:ApplicationExitingEvent, and destroyed afterwards (the vetoing one exactly once);WM_QUITwithQuit()'s code (7/9/11), not 0;The exit code cannot be checked end to end in the Flutter examples, because the stock runner's
wWinMainalways returnsEXIT_SUCCESS.