Repository navigation
[Windows] Application::Quit posts WM_QUIT before destroying windows #57
Description
Activity
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: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 = 0Quit()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:- destroys the thread's top-level windows. It uses
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. - posts
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:before after flutter_window_example93–107 ms (via the crash above) 192–233 ms, clean flutter_multiple_window_example63–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
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:- 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_QUITwithQuit()'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.
- both windows are alive during
The exit code cannot be checked end to end in the Flutter examples, because the stock runner's
wWinMainalways returnsEXIT_SUCCESS.- destroys the thread's top-level windows. It uses
On Windows,
Application::Quit()ends inPostQuitMessage()(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.Background: