Skip to content

Add swift strict concurrency attributes to iOS API - part 1#184703

Open
LongCatIsLooong wants to merge 16 commits into
flutter:masterfrom
LongCatIsLooong:strict-concurrency-1
Open

Add swift strict concurrency attributes to iOS API - part 1#184703
LongCatIsLooong wants to merge 16 commits into
flutter:masterfrom
LongCatIsLooong:strict-concurrency-1

Conversation

@LongCatIsLooong

@LongCatIsLooong LongCatIsLooong commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

The platform messaging API and external texture API (FlutterChannels.h, FlutterBinaryMessenger.h, FlutterTexture.h) will be migrated in a separate PR.

I ran my audit script locally to verify all protocols and classes have annotations (except for some UIKit subclasses that are obviously going to be @MainActor-isolated and everything from FlutterTexture.h, FlutterBinaryMessenger.h, FlutterChannels.h).

Towards #160969
Fixes: #181033,
Should also fix. #182319 but it will take a while to reach stable.

Update: the platform messaging API is a little tricky to migrate (#184737). For the setMessageHandler API, it is easier to work around the dynamic actor isolation crash if it's left unmigrated.

Pre-launch Checklist

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

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.

@github-actions github-actions Bot added platform-ios iOS applications specifically engine flutter/engine related. See also e: labels. a: desktop Running on desktop team-ios Owned by iOS platform team team-macos Owned by the macOS platform team labels Apr 7, 2026
@LongCatIsLooong LongCatIsLooong added the CICD Run CI/CD label Apr 7, 2026
@flutter-dashboard

This comment was marked as resolved.

@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 7, 2026
@LongCatIsLooong LongCatIsLooong added the CICD Run CI/CD label Apr 7, 2026
@LongCatIsLooong LongCatIsLooong marked this pull request as ready for review April 7, 2026 21:12
@LongCatIsLooong LongCatIsLooong requested a review from a team as a code owner April 7, 2026 21:12
@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.

@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 introduces Swift strict concurrency annotations across the Darwin platform headers, including NS_SWIFT_SENDABLE, NS_SWIFT_NONSENDABLE, NS_SWIFT_UI_ACTOR, and NS_SWIFT_NONISOLATED. These changes aim to improve thread safety and Swift interoperability. Additionally, a new test file and build configuration were added to verify these concurrency constraints. Feedback indicates that the ios_swift_strict_concurrency_test source set in the build file is redundant as the source file is already included in another target. There is also a suggestion to remove an unused import in the new Swift test file to ensure compatibility with older toolchains.

Comment thread engine/src/flutter/shell/platform/darwin/ios/BUILD.gn
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 7, 2026
@LongCatIsLooong LongCatIsLooong added the CICD Run CI/CD label Apr 7, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 7, 2026
@LongCatIsLooong LongCatIsLooong marked this pull request as draft April 8, 2026 00:17
@LongCatIsLooong LongCatIsLooong added the CICD Run CI/CD label Apr 8, 2026
@LongCatIsLooong LongCatIsLooong marked this pull request as ready for review April 8, 2026 05:23

@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 introduces Swift concurrency annotations, such as NS_SWIFT_SENDABLE, NS_SWIFT_UI_ACTOR, and NS_SWIFT_NONISOLATED, to various Objective-C headers in the Darwin platform to support strict concurrency checking. It also includes a new Swift test target and a test file, SwiftStrictConcurrencyTest.swift, to verify that the annotated classes and protocols conform to the expected concurrency requirements. The review feedback suggests improving the readability of the annotations in FlutterPlugin.h by ensuring consistent spacing between the API macros and the new Swift attributes.

API_DEPRECATED(
"See -[UIApplicationDelegate application:didRegisterUserNotificationSettings:] deprecation",
ios(8.0, 10.0));
ios(8.0, 10.0))NS_SWIFT_UI_ACTOR;

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.

medium

For better readability and consistency with other annotations in this file, there should be a space between the closing parenthesis of the API_DEPRECATED macro and the NS_SWIFT_UI_ACTOR attribute.

        ios(8.0, 10.0)) NS_SWIFT_UI_ACTOR;

API_DEPRECATED(
"See -[UIApplicationDelegate application:didReceiveLocalNotification:] deprecation",
ios(4.0, 10.0));
ios(4.0, 10.0))NS_SWIFT_UI_ACTOR;

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.

medium

For better readability and consistency with other annotations in this file, there should be a space between the closing parenthesis of the API_DEPRECATED macro and the NS_SWIFT_UI_ACTOR attribute.

        ios(4.0, 10.0)) NS_SWIFT_UI_ACTOR;

performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
completionHandler:(void (^)(BOOL succeeded))completionHandler
API_AVAILABLE(ios(9.0));
API_AVAILABLE(ios(9.0))NS_SWIFT_UI_ACTOR;

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.

medium

For better readability and consistency with other annotations in this file, there should be a space between the closing parenthesis of the API_AVAILABLE macro and the NS_SWIFT_UI_ACTOR attribute.

    API_AVAILABLE(ios(9.0)) NS_SWIFT_UI_ACTOR;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

(et format did this I think).


#import <Foundation/Foundation.h>

NS_SWIFT_NONSENDABLE

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.

Can you explain why this one is non-sendable?

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.

I believe this PR is still pending design doc review.

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.

Looks like this is only static helpers, so it is confusing to explicitly marking it as either sendable or non-sendable, since Sendability is irrelevant here (we are not creating any instance of it).

A potential solution can be to create a custom no-op annotation (something like FLUTTER_SWIFT_CONCURRENCY_AUDITED_STATIC_UTIL), this would help us distinguish between unaudited annotations vs audited but no-op annotations.

This seems to be a missing case not discussed in the design doc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A custom macro works but I'm a bit concerned about exposing it, since if you want to use it in public APIs you will have to expose the macro as part of the public API, but the annotation is internal and for dev-purpose-only so it shouldn't pollute the public name space.

Technically this type is still inhabited (it still has init) so I'll change it to Sendable.

Also added a "uninhabited types such as helper classes that only have static methods." to classes that should be made NS_SWIFT_SENDABLE, but the decision is quite arbitrary, I just went with the one that makes the flow easier to write.

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.

As discussed offline, this should be annotated with non-isolated. The concept of "sendability" is only relevant for an instance, and marking static utils as sendable or non-sendable is confusing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I still prefer not introducing a special case just for static helpers (also since NS_SWIFT_NONISOLATED is usually only used on types when one needs to opt out of @MainActor-isolation, for instance the superclass is @MainActor-isolated).

The class does not have un-protected mutable states so it still makes sense it is marked @Sendable (it's just that nobody will try to "send" it).

* `FlutterHeadlessDartRunner`.
*/
FLUTTER_DARWIN_EXPORT
NS_SWIFT_NONSENDABLE

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.

Why is this one non-sendable? It doesn't have any properties and only has a static method?

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.

Same comment as above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed to Sendable, same as FlutterHourFormat.

*
* This provides the engine bridge to the listener.
*/
NS_SWIFT_NONSENDABLE

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.

Why not NS_SWIFT_UI_ACTOR?

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.

Things like FlutterImplicitEngineDelegate must be NS_SWIFT_UI_ACTOR. Looking at the code, the delegate method is called by UI code (e.g. FlutterViewController's awakeFromNib, which is main actor isolated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's a protocol so I only put NS_SWIFT_UI_ACTOR to the (only) method requirement. It's probably fine to mark the entire protocol NS_SWIFT_UI_ACTOR but only marking the method is the safer bet (so it does not force conforming classes to be @MainActor, less chance of breaking existing code).

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.

Offline sync: Why we have to use UIActor for things like engine implicit protocol? This is because implementor can access main isolated api:

func didInitializeImplicitEngine() {
// this is main isoalted, so you can access those APIs. 
let vc = engine.viewController
vc.view.color = ...
}

@LongCatIsLooong LongCatIsLooong Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There was existing code broken by putting @MainActor on FlutterPlugin. There was this class that conforms to FlutterPlugin and another protocol with a nonisolated method (let's say func method()). Making FlutterPlugin @MainActor broke the code because func method() was required by the other protocol to be nonisolated but conforming to the @MainActor-isolated FlutterPlugin made the default isolation @MainActor. I'm leaning towards changing the design doc to say that always use NS_SWIFT_NONSENDABLE when migrating existing API.

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.

I don't see implicit engine stuff in the link. am i missing anything.

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.

Oh you are referring to FlutterPlugin - why is that plugin Sendable? I think we should remove that if it's incorrect annotation.

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.

Looking closer, it looks like the complaint isn't around FlutterPlugin, but pigeon?

@LongCatIsLooong LongCatIsLooong Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The problem with the internal code is exactly https://github.com/swiftlang/swift-evolution/blob/main/proposals/0316-global-actors.md#global-actor-inference, the concrete type is inferred to have @MainActor isolation because we made FlutterPlugin @MainActor-isolated, but to conform the other protocol some of the methods must be nonisolated (right now since they do not have any isolation specifier, they are @MainActor-isolated by default since the concrete type now has @MainActor-isolation). The breakage can also be fixed by explicitly marking those methods nonisolated but that require changing source code.

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.

Is this the swift bug that you mentioned earlier? I remember you mentioned something related to whether they are inside same file?

Are there any drawback of marking the protocol as non-isolated but all its functions to be isolated (which is a weird practice not done by Apple)? If no drawback, I think we can go ahead with this, but we have to document this behavior nicely.

* Wraps a `UIView` for embedding in the Flutter hierarchy
*/
NS_SWIFT_NONSENDABLE
@protocol FlutterPlatformView <NSObject>

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.

Why not NS_SWIFT_UI_ACTOR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here and below, for protocols, in case the conforming type is actually not @MainActor isolated we should avoid NS_SWIFT_UI_ACTOR, to avoid breaking existing code. There's a

For migrating existing APIs, avoid NS_SWIFT_UI_ACTOR if the protocol is not strictly tied to the @MainActor.

section in the design doc.

@end

FLUTTER_DARWIN_EXPORT
NS_SWIFT_NONSENDABLE

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.

Why not NS_SWIFT_UI_ACTOR?

* @return the file name to be used for lookup in the main bundle.
*/
- (NSString*)lookupKeyForAsset:(NSString*)asset;
- (NSString*)lookupKeyForAsset:(NSString*)asset NS_SWIFT_NONISOLATED;

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.

Hmmm, this isn't static, should it be NS_SWIFT_NONISOLATED?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The implementation doesn't use any instance variables so can be made static:

- (NSString*)lookupKeyForAsset:(NSString*)asset {
return [FlutterDartProject lookupKeyForAsset:asset];
}

@hellohuanlin hellohuanlin 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.

I didn't review the whole PR since I'd expect to leave similar comments to other places. Adding comments on the reason behind annotation would make review easier (like what you did for FlutterMessageCodec.

Also about your design doc, this PR doesn't seem to be consistent with design doc. For example, the PR description still says

I ran my audit script locally to verify all protocols and classes have annotations (except for some UIKit subclasses that are obviously going to be @MainActor-isolated and everything from FlutterTexture.h, FlutterBinaryMessenger.h, FlutterChannels.h).

The design doc seems to agree to always annotating the types regardless. So I am not sure what the current state is.

There are also unresolved but related issues in the design doc, for example, whether a parameter or return value should be sending can affect the decision of whether a protocol should be annotated as Sendable. e.g. there could be cases where a compilation failure could be resolved by either marking param/return values as sending, or marking types as Sendable. Not saying there must be such cases in this project, but this hasn't been discussed).

Also the static utils use case i commented hasn't been discussed in the design doc.

The ideal result I was hoping is that, after reviewing the design doc, it would be straightforward to review this PR. This isn't the case so far.

/**
* A message encoding/decoding mechanism.
*
* The implementation must be thread-safe as the codec can be used on any thread.

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.

The implementation must be thread-safe as the codec can be used on any thread.

Used on any thread doesn't mean it must be thread safe. There's a difference between "used on any thread", vs "used across multiple thread". The former is actor isolation (or non-isolated in this case), and the latter is Sendability (or sendable in this case)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah good catch. Rephrased to "initialized and used on different threads".

* The encoding is extensible via subclasses overriding `writeValue`.
*/
FLUTTER_DARWIN_EXPORT
NS_SWIFT_NONSENDABLE

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.

Can you add comment of why these attributes are used (like what you did in FlutterMessageCodec).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added to writer/reader/ReaderWriter.

* binary encoding or extensions thereof.
*/
FLUTTER_DARWIN_EXPORT
NS_SWIFT_SENDABLE

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.

Why is FlutterStandardReader and FlutterStandardWriter non-sendable, but FlutterStandardReaderWriter sendable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

FlutterStandardReaderWriter is basically just a namespace for two factory methods that create readers / writers. It doesn't have mutable states like the real reader / write classes do.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It can be subclassed. And the engine implementation assumes it's thread-safe.


#import <Foundation/Foundation.h>

NS_SWIFT_NONSENDABLE

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.

Looks like this is only static helpers, so it is confusing to explicitly marking it as either sendable or non-sendable, since Sendability is irrelevant here (we are not creating any instance of it).

A potential solution can be to create a custom no-op annotation (something like FLUTTER_SWIFT_CONCURRENCY_AUDITED_STATIC_UTIL), this would help us distinguish between unaudited annotations vs audited but no-op annotations.

This seems to be a missing case not discussed in the design doc.

* `FlutterHeadlessDartRunner`.
*/
FLUTTER_DARWIN_EXPORT
NS_SWIFT_NONSENDABLE

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.

Same comment as above.

*
* This provides the engine bridge to the listener.
*/
NS_SWIFT_NONSENDABLE

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.

Things like FlutterImplicitEngineDelegate must be NS_SWIFT_UI_ACTOR. Looking at the code, the delegate method is called by UI code (e.g. FlutterViewController's awakeFromNib, which is main actor isolated.

@LongCatIsLooong

Copy link
Copy Markdown
Contributor Author

I didn't review the whole PR since I'd expect to leave similar comments to other places. Adding comments on the reason behind annotation would make review easier (like what you did for FlutterMessageCodec.

Added docs to codecs APIs since they're mostly used for channels and can be sent across threads (they are actually sent across threads in engine).

Also about your design doc, this PR doesn't seem to be consistent with design doc. For example, the PR description still says

I ran my audit script locally to verify all protocols and classes have annotations (except for some UIKit subclasses that are obviously going to be @MainActor-isolated and everything from FlutterTexture.h, FlutterBinaryMessenger.h, FlutterChannels.h).

The design doc seems to agree to always annotating the types regardless. So I am not sure what the current state is.

Those will be in a different PR (part 2) since most of them are slightly trickier to annotate because they actually have threading contracts. Arguably FlutterTexture.h can be in this PR too (since at this point I'm convinced we should avoid sending during migration).

There are also unresolved but related issues in the design doc, for example, whether a parameter or return value should be sending can affect the decision of whether a protocol should be annotated as Sendable. e.g. there could be cases where a compilation failure could be resolved by either marking param/return values as sending, or marking types as Sendable. Not saying there must be such cases in this project, but this hasn't been discussed).

The design doc does say "avoid sending for migration". Part 1 of this migration is really straightforward as you can see there's no API that can use sending. As for FlutterTexture (which will be in a separate PR) upon close inspection the sending keyword isn't a good fit for current contract either (instead the implementation must be thread-safe).

Also the static utils use case i commented hasn't been discussed in the design doc.

The ideal result I was hoping is that, after reviewing the design doc, it would be straightforward to review this PR. This isn't the case so far.

I changed the NS_SWIFT_NONSENDABLE on helper classes to NS_SWIFT_SENDABLE and updated the design doc. The decision to go with either feels really arbitrary though so I'm open to changing this back to NS_SWIFT_NONSENDABLE.

@github-actions github-actions Bot removed the CICD Run CI/CD label Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: desktop Running on desktop CICD Run CI/CD engine flutter/engine related. See also e: labels. platform-ios iOS applications specifically team-ios Owned by iOS platform team team-macos Owned by the macOS platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

App fails to start after UISceneDelegate migration when using Swift 6

4 participants