Add swift strict concurrency attributes to iOS API - part 1#184703
Add swift strict concurrency attributes to iOS API - part 1#184703LongCatIsLooong wants to merge 16 commits into
Conversation
This comment was marked as resolved.
This comment was marked as resolved.
|
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. |
There was a problem hiding this comment.
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.
8ce4e38 to
8186314
Compare
There was a problem hiding this comment.
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; |
| API_DEPRECATED( | ||
| "See -[UIApplicationDelegate application:didReceiveLocalNotification:] deprecation", | ||
| ios(4.0, 10.0)); | ||
| 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; |
There was a problem hiding this comment.
(et format did this I think).
|
|
||
| #import <Foundation/Foundation.h> | ||
|
|
||
| NS_SWIFT_NONSENDABLE |
There was a problem hiding this comment.
Can you explain why this one is non-sendable?
There was a problem hiding this comment.
I believe this PR is still pending design doc review.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Why is this one non-sendable? It doesn't have any properties and only has a static method?
There was a problem hiding this comment.
Same comment as above.
There was a problem hiding this comment.
Changed to Sendable, same as FlutterHourFormat.
| * | ||
| * This provides the engine bridge to the listener. | ||
| */ | ||
| NS_SWIFT_NONSENDABLE |
There was a problem hiding this comment.
Why not NS_SWIFT_UI_ACTOR?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 = ...
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I don't see implicit engine stuff in the link. am i missing anything.
There was a problem hiding this comment.
Oh you are referring to FlutterPlugin - why is that plugin Sendable? I think we should remove that if it's incorrect annotation.
There was a problem hiding this comment.
Looking closer, it looks like the complaint isn't around FlutterPlugin, but pigeon?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
Why not NS_SWIFT_UI_ACTOR?
There was a problem hiding this comment.
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_ACTORif the protocol is not strictly tied to the@MainActor.
section in the design doc.
| @end | ||
|
|
||
| FLUTTER_DARWIN_EXPORT | ||
| NS_SWIFT_NONSENDABLE |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Hmmm, this isn't static, should it be NS_SWIFT_NONISOLATED?
There was a problem hiding this comment.
The implementation doesn't use any instance variables so can be made static:
hellohuanlin
left a comment
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Can you add comment of why these attributes are used (like what you did in FlutterMessageCodec).
There was a problem hiding this comment.
added to writer/reader/ReaderWriter.
| * binary encoding or extensions thereof. | ||
| */ | ||
| FLUTTER_DARWIN_EXPORT | ||
| NS_SWIFT_SENDABLE |
There was a problem hiding this comment.
Why is FlutterStandardReader and FlutterStandardWriter non-sendable, but FlutterStandardReaderWriter sendable?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
It can be subclassed. And the engine implementation assumes it's thread-safe.
|
|
||
| #import <Foundation/Foundation.h> | ||
|
|
||
| NS_SWIFT_NONSENDABLE |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Same comment as above.
| * | ||
| * This provides the engine bridge to the listener. | ||
| */ | ||
| NS_SWIFT_NONSENDABLE |
There was a problem hiding this comment.
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.
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).
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
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
I changed the |
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
setMessageHandlerAPI, 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-assistbot 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.