CNA-BUG-182: Win32Dialogs file dialogs run modally inside Show* and invoke the callback before returning, contrary to the IPlatformDialogs timing contract
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.
On the native WIN32 platform the three file dialogs run modally on the calling thread and call the result callback before returning, while IPlatformDialogs and FileDialog promise an asynchronous callback after the call returns.
- Identifier
CNA-BUG-182- Category
- Bug
- Subsystem
- Platforms
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Severity
- Low (a triage suggestion, not a project priority)
- Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- Yes: see Current tests
- Affected contract
- CNA::Platform::IPlatformDialogs::ShowOpenFileDialog, ShowSaveFileDialog and ShowOpenFolderDialog on the WIN32 platform, and through them CNA::Devices::FileDialog::ShowOpenFile, ShowSaveFile and ShowOpenFolder
Expected behaviour
IPlatformSystemServices.hpp: file dialogs are asynchronous, and "The callback fires exactly once per call, on the thread that pumps events, and after the Show* call has already returned", because a synchronous signature could only be honoured by pumping the event loop from inside a call the game makes during its own frame. FileDialog.hpp: "Every dialog call is asynchronous: it returns immediately, and the result arrives later via onResult".
Actual behaviour at TARGET
The three Win32Dialogs methods in Win32SystemServices.cpp create the COM dialog, call dialog->Show(parent), which runs a modal message loop on the calling thread until the user closes the dialog, then call onResult(paths), or onResult({}) on cancel or COM failure, and only then return. The game's frame is blocked for as long as the dialog is open, and the callback runs inside the caller's Show* call, before any statement that follows it. The SDL3 service and the X11/Wayland portal services return first and deliver later, as specified; the portal tests assert exactly that.
Source locations
modules/platform/src/Win32/Win32SystemServices.cpp— Win32Dialogs::ShowOpenFileDialog, ShowSaveFileDialog and ShowOpenFolderDialog call onResult before returningmodules/platform/include/CNA/Platform/IPlatformSystemServices.hpp— IPlatformDialogs file-dialog timing contractmodules/devices-ext/include/CNA/Devices/FileDialog.hpp— FileDialog documented as asynchronousmodules/platform/tests/CNA/Platform/Win32SystemServicesTests.cpp— only the empty-callback refusal is testedmodules/platform/tests/CNA/Platform/X11DesktopPortalTests.cpp— AnOpenDialogCarriesItsOptionsAndItsAnswerArrivesFromPump asserts the answer never arrives inside the callmodules/platform/tests/CNA/Platform/WaylandDesktopPortalTests.cpp— TheGameKeepsRunningWhileADialogIsOpen
Evidence
Checked by reading at 009d40f5; not executed on Windows, where showing the dialog needs a human. docs/platform-win32.md lists IFileOpenDialog/IFileSaveDialog without noting the synchronous shape, and the devices-ext internals page states that a real platform's callback fires after the call returns. For comparison, SDL's own Windows dialog backend runs the dialog on a detached thread of its own (read in a sibling SDL checkout at exactly the revision TARGET pins, cbe3fbe9: SDL_windowsdialog.c starts the file and folder dialogs with SDL_CreateThread and detaches the thread).
Focused reproduction
// Illustrative; CNA_PLATFORM=WIN32 with CNA_DEVICES=ON; not run.
bool returned = false;
CNA::Devices::FileDialog::ShowOpenFile(
[&](const std::vector<std::string>& files) {
// On WIN32 this runs inside ShowOpenFile: 'returned' is still false here.
});
returned = true; // reached only after the user closed the dialog
Current tests
Win32SystemServicesTests.cpp tests only the empty-callback refusal. FileDialogTests.cpp uses the canned dialog service, which also calls back synchronously, a simplification CannedDialogs.hpp documents for tests.
Regression test
Run the dialog on a worker thread or post its result to the platform's event queue so onResult fires from PollEvents after Show* returns, with a Win32 test (and a seam to substitute the COM dialog) asserting that the callback has not run when Show* returns and runs on the next PollEvents.
Blast radius
Games built on the native WIN32 platform that open file dialogs: frames stop while the dialog is open, and callbacks that assume they run after Show* returned see the opposite order. The SDL3 platform on Windows and the Linux platforms are unaffected.
Workaround
Write file-dialog callbacks so they are correct whether they run inside or after the Show* call (prepare all state before calling), and expect the game loop to pause on WIN32.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Internals
- Device extensions: dialogs
- Maintainer workflow
- Thread and callback map
- Tests and validation
- Win32 platform internals: tests
- Known issues
- Bug index