flutter / flutter/flutter

[Windows] GdiplusStartup prevents AppLifecycleListener.onExitRequested from firing

Open
#192,304 1 comment 0 reactions 0 assignees View on GitHub
engine has reproducible steps P2 platform-windows team-windows triaged-windows
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.