Skip to content

iOS: Ensure engine is run on the main thread - #191626

Merged
cbracken merged 2 commits into
flutter:masterfrom
cbracken:engine-thread-checks
Aug 25, 2026
Merged

cbracken merged 2 commits into
flutter:masterfrom
cbracken:engine-thread-checks

Conversation

@cbracken

Copy link
Copy Markdown
Member

Previously, running a FlutterEngine from a background queue silently gave us an engine whose platform thread was not the main thread. The first symptom an app sees is an assertion deep inside DisplayLinkManager.shared, which is accurate but confusing and looks like a bug rather than an intentional precondition.

MakeThreadHost adopts the calling thread as the engine's platform/UI thread. That thread is required to be the main thread for several reasons:

  • it reads UIKit state and runs every plugin and platform channel callback
  • nothing else pumps its run loop

We now assert this via FML_CHECK in:

  • createShell:libraryURI:initialRoute:: covers the run* methods and FlutterViewController's implicit engine
  • spawnWithEntrypoint:libraryURI:initialRoute:entrypointArgs:: covers the second and later engines in a FlutterEngineGroup.

A spawned engine inherits its spawner's task runners rather than adopting the calling thread, so the check in createShell constrains every engine in a group.

This also adds an FML_DCHECK during engine construction since it reads UIApplication lifecycle state and registers UIKit observers.

This also annotates the Swift-visible entry points NS_SWIFT_UI_ACTOR so that Swift callers get a compile-time error rather than a runtime abort. FlutterViewController doesn't need an annotation since, like all view controllers, it already inherits main actor isolation from UIViewController.

Issue: #190909
Related: #190725
Related: #165904

No test changes, since these are assertions and unlike GoogleTest, XCUITest and Swift Testing tests have no death-test. This doesn't add any additional runtime aborts since DisplayLinkManager.shared already does this; it just pushes them up to where they're actually actionable by the user, and provides better error messages.

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

Previously, running a `FlutterEngine` from a background queue silently gave
us an engine whose platform thread was not the main thread. The first
symptom an app sees is an assertion deep inside `DisplayLinkManager.shared`,
which is accurate but confusing and looks like a bug rather than an
intentional precondition.

`MakeThreadHost` adopts the calling thread as the engine's platform/UI
thread. That thread is required to be the main thread for several
reasons:
* it reads UIKit state and runs every plugin and platform channel
  callback
* nothing else pumps its run loop

We now assert this via `FML_CHECK` in:
* `createShell:libraryURI:initialRoute:`: covers the `run*` methods and
  `FlutterViewController`'s implicit engine
* `spawnWithEntrypoint:libraryURI:initialRoute:entrypointArgs:`: covers
  the second and later engines in a `FlutterEngineGroup`.

A spawned engine inherits its spawner's task runners rather than
adopting the calling thread, so the check in `createShell` constrains
every engine in a group.

This also adds an `FML_DCHECK` during engine construction since it reads
`UIApplication` lifecycle state and registers UIKit observers.

This also annotates the Swift-visible entry points `NS_SWIFT_UI_ACTOR`
so that Swift callers get a compile-time error rather than a runtime
abort. `FlutterViewController` doesn't need an annotation since, like
all view controllers, it already inherits main actor isolation from
`UIViewController`.

Issue: flutter#190909
Related: flutter#190725
Related: flutter#165904

No test changes, since these are assertions and unlike GoogleTest,
XCUITest and Swift Testing tests have no death-test. This doesn't add
any additional runtime aborts since `DisplayLinkManager.shared` already
does this; it just pushes them up to where they're actually actionable
by the user, and provides better error messages.
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Aug 25, 2026
@cbracken
cbracken requested a review from a team as a code owner August 25, 2026 00:11
@flutter-dashboard

Copy link
Copy Markdown

It looks like this pull request may not have tests. Please make sure to add tests or get an explicit test exemption before merging.

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. If you believe this PR qualifies for a test exemption, contact "@test-exemption-reviewer" in the #hackers channel in Discord (don't just cc them here, they won't see it!). The test exemption team is a small volunteer group, so all reviewers should feel empowered to ask for tests, without delegating that responsibility entirely to the test exemption group.

@github-actions github-actions Bot added platform-ios iOS applications specifically engine flutter/engine related. See also e: labels. team-ios Owned by iOS platform team labels Aug 25, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enforces that FlutterEngine instances, groups, and headless runners are created and run on the main thread by adding documentation, NS_SWIFT_UI_ACTOR annotations, @MainActor isolation, and runtime main-thread checks (FML_DCHECK and FML_CHECK). The feedback suggests simplifying the runtime checks in FlutterEngine.mm by using the more direct and idiomatic class method [NSThread isMainThread] instead of querying the current thread instance.

Comment thread engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm Outdated
Comment thread engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm Outdated
Comment thread engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm Outdated
@stuartmorgan-g

Copy link
Copy Markdown
Contributor

test-exempt: annotation change that is compiler-enforced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD engine flutter/engine related. See also e: labels. platform-ios iOS applications specifically team-ios Owned by iOS platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants