[Windows] GdiplusStartup prevents AppLifecycleListener.onExitRequested from firing
- Dominant language
- Dart
- Stars
- 179k
- Forks
- 31.1k
- PR merge metrics
- PR metrics pending
Description
### Steps to reproduce
1. Create a Windows app with `flutter create --empty --platforms=windows gdiplus_exit_repro`.
2. Add `ffi: ^2.1.4` to `dependencies`.
3. Replace `lib/main.dart` with the code sample below.
4. Run `flutter run -d windows`.
5. Before clicking **Initialize GDI+**, close the window using the title-bar X. The `onExitRequested` callback runs and cancels the close.
6. Click **Initialize GDI+**, then close the window using the title-bar X again.
This is a standalone reproduction and does not use pdfrx or PDFium.
### Expected results
`AppLifecycleListener.onExitRequested` should be invoked when the user closes the last visible Flutter application window, regardless of invisible helper windows created by native libraries. Returning `AppExitResponse.cancel` should keep the app open.
### Actual results
After `GdiplusStartup` runs with its default background-thread behavior, closing the Flutter window bypasses `AppLifecycleListener.onExitRequested` and terminates the app.
GDI+ creates an invisible, unowned top-level window with the class name `GDI+ Hook Window Class`. Flutter's Windows lifecycle implementation appears to count every unowned top-level window belonging to the process in `WindowsLifecycleManager::IsLastWindowOfProcess()`. It therefore sees the Flutter window plus the GDI+ helper window and decides that the Flutter window is not the last process window, so the cancelable exit request is not dispatched.
Relevant engine code:
https://github.com/flutter/engine/blob/dcb2cce79439796768e67474f710221f23d797ab/shell/platform/windows/windows_lifecycle_manager.cc
A similar symptom involving an extra native/helper window was reported in #168335, although that issue was closed without a general fix.
The original downstream report is https://github.com/espresso3389/pdfrx/issues/682. pdfrx uses the standard PDFium binaries from bblanchon/pdfium-binaries; the minimal reproduction below shows that PDFium is not required to trigger the Flutter lifecycle problem.
### Code sample
Code sample
```dart
import 'dart:ffi';
import 'dart:io';
import 'dart:ui' show AppExitResponse;
import 'package:ffi/ffi.dart';
import 'package:flutter/material.dart';
final class GdiplusStartupInput extends Struct {
@Uint32()
external int gdiplusVersion;
external Pointer debugEventCallback;
@Int32()
external int suppressBackgroundThread;
@Int32()
external int suppressExternalCodecs;
}
final class GdiplusStartupOutput extends Struct {
external Pointer notificationHook;
external Pointer notificationUnhook;
}
typedef GdiplusStartupNative = Int32 Function(
Pointer token,
Pointer input,
Pointer output,
);
typedef GdiplusStartupDart = int Function(
Pointer token,
Pointer input,
Pointer output,
);
final Pointer _gdiplusToken = calloc();
final Pointer _gdiplusInput = calloc();
void initializeGdiplus() {
if (!Platform.isWindows) {
throw UnsupportedError('This reproduction only applies to Windows.');
}
_gdiplusInput.ref
..gdiplusVersion = 1
..debugEventCallback = nullptr
..suppressBackgroundThread = 0
..suppressExternalCodecs = 0;
final startup = DynamicLibrary.open('gdiplus.dll')
.lookupFunction(
'GdiplusStartup',
);
final status = startup(_gdiplusToken, _gdiplusInput, nullptr);
if (status != 0) throw StateError('GdiplusStartup failed: $status');
}
void main() => runApp(const ReproApp());
class ReproApp extends StatefulWidget {
const ReproApp({super.key});
@override
State createState() => _ReproAppState();
}
class _ReproAppState extends State {
late final AppLifecycleListener _lifecycleListener;
var _gdiplusInitialized = false;
var _exitRequestCount = 0;
@override
void initState() {
super.initState();
_lifecycleListener = AppLifecycleListener(
onExitRequested: () async {
debugPrint('AppLifecycleListener.onExitRequested FIRED');
setState(() => _exitRequestCount++);
return AppExitResponse.cancel;
},
);
}
@override
void dispose() {
_lifecycleListener.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Flutter GDI+ exit reproduction')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('GDI+ initialized: $_gdiplusInitialized'),
Text('onExitRequested calls: $_exitRequestCount'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _gdiplusInitialized
? null
: () {
initializeGdiplus();
setState(() => _gdiplusInitialized = true);
},
child: const Text('Initialize GDI+'),
),
],
),
),
),
);
}
}
```
### Screenshots or Video
_No response_
### Logs
Logs
```console
> flutter analyze
Analyzing flutter_gdiplus_exit...
No issues found!
> flutter build windows
Building Windows application...
Build succeeded.
Before GDI+ initialization, clicking X prints:
AppLifecycleListener.onExitRequested FIRED
After GDI+ initialization, clicking X prints nothing and the process exits.
```
### Flutter Doctor output
Doctor output
```console
[√] Flutter (Channel stable, 3.47.2, on Microsoft Windows [Version 10.0.26340.9233], locale ja-JP)
• Flutter version 3.47.2 on channel stable at D:\flutter
• Upstream repository https://github.com/flutter/flutter
• Framework revision d3b14c8769 (2026-08-26 16:07:51 -0700)
• Engine revision a804b26164
• Dart version 3.13.2
• DevTools version 2.60.0
[√] Windows Version (Windows 11 or higher, 26H2, 2009)
[√] Android toolchain - develop for Android devices (Android SDK version 36.1.0)
• Android SDK at C:\Users\kawasaki\AppData\Local\Android\sdk
• Java version OpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108)
• All Android licenses accepted.
[√] Chrome - develop for the web
• Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe
[√] Visual Studio - develop Windows apps (Visual Studio Professional 2026 18.8.0)
• Visual Studio at C:\Program Files\Microsoft Visual Studio\18\Professional
• Windows 10 SDK version 10.0.26100.0
[√] Connected device (3 available)
• Windows (desktop) • windows • windows-x64 • Microsoft Windows [Version 10.0.26340.9233]
• Chrome (web) • chrome • web-javascript • Google Chrome 152.0.7977.65
• Edge (web) • edge • web-javascript • Microsoft Edge 153.0.4234.19
[√] Network resources
• All expected network resources are available.
• No issues found!
```
Contributor guide
Research direction
Start with shell/platform/windows/windows_lifecycle_manager.cc, especially WindowsLifecycleManager::IsLastWindowOfProcess(), and run the standalone GDI+ reproduction described in the issue. Compare window counting before and after GdiplusStartup. Done means closing the last visible Flutter window invokes AppLifecycleListener.onExitRequested and AppExitResponse.cancel keeps the app open despite GDI+'s invisible helper window.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart
- Domain
- desktop, operating-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 65/100