Skip to content

[engine][macOS/iOS] Make Flutter's Public Headers Swift 6 Friendly #160969

@bc-lee

Description

@bc-lee

Use case

With the release of Swift 6, the Swift community is adopting Swift Strict Concurrency and the Swift 6 Language Mode. However, based on my testing, Flutter's public headers are not fully compatible with Swift 6.

For instance, the following code generates warnings when used in Swift Strict Concurrency (-swift-version 5 -enable-upcoming-feature StrictConcurrency) and errors in Swift 6 Language Mode (-swift-version 6):

@MainActor
class StringChannel {
    static let shared = StringChannel()

    static func setUp(
        binaryMessenger: FlutterBinaryMessenger,
        codec: NSObjectProtocol & FlutterMessageCodec
    ) {
        let channel = FlutterBasicMessageChannel(
            name: "com.example.TestApp/StringChannel",
            binaryMessenger: binaryMessenger,
            codec: codec
        )

        channel.setMessageHandler { (message, reply) in
            print("native: StringChannel received message: \(message ?? "nil")")
            DispatchQueue.global().asyncAfter(deadline: .now() + 1) { @Sendable in
                let message = "StringChannel reply message"
                print("native: StringChannel sending reply: \(message)")
                reply(message)  // <-- Warning: Capture of 'reply' with non-sendable type 'FlutterReply' in a `@Sendable` closure
                
                Task { @MainActor in
                    let message = "StringChannel message from background"
                    print("native: StringChannel sending message: \(message)")
                    // There are two versions of sendMessage in FlutterChannel.h. One does not 
                    // have a reply callback, while the other does. It seems that if a Swift 
                    // function is async or in an isolated context, Swift always chooses the 
                    // version with a reply callback and converts it to an async function. 
                    // That's why we need to await the result of sendMessage.
                    let reply = await channel.sendMessage(message)  
                    // <-- Warning: Capture of 'channel' with non-sendable type 'FlutterBasicMessageChannel' in a `@Sendable` closure
                    // <-- Warning: Non-sendable type 'Any?' returned by implicitly asynchronous call cannot cross actor boundary; this is an error in Swift 6 Language Mode
                    print("native: StringChannel received reply: \(reply ?? "nil")")
                }
            }
        }
    }
}

Currently, the workaround is to use the @preconcurrency attribute when importing the Flutter module in Swift. However, this is neither ideal nor sustainable, as the Swift compiler bypasses all concurrency checks for the Flutter module.

Proposal

To address this issue, I propose adding proper Swift concurrency annotations to Flutter's public headers. For example:

#if __has_attribute(swift_attr)
#define FLUTTER_SWIFT_SENDABLE __attribute__((swift_attr("@Sendable")))
#define FLUTTER_SWIFT_MAIN_ACTOR __attribute__((swift_attr("@MainActor")))
#else
#define FLUTTER_SWIFT_SENDABLE
#define FLUTTER_SWIFT_MAIN_ACTOR
#endif

typedef void (^FLUTTER_SWIFT_SENDABLE FlutterReply)(FLUTTER_SWIFT_SENDABLE id _Nullable reply);

FLUTTER_DARWIN_EXPORT
FLUTTER_SWIFT_MAIN_ACTOR
@interface FlutterBasicMessageChannel : NSObject
...
@end

This approach would:

  1. Make FlutterReply sendable, enabling its use in Swift concurrency contexts without warnings.
  2. Isolate FlutterBasicMessageChannel to the main actor, similar to @UiThread annotations used in Android (flutter/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java).

The swift_attr attribute is supported by Clang and ignored in non-Swift contexts, ensuring no runtime behavior changes.

Apple provides a Swift 6 Concurrency Migration Guide, which includes guidelines for Objective-C APIs. These resources offer a solid foundation for making Flutter's public headers Swift 6 compatible.

To ensure compatibility and prevent regressions:

  1. Add Swift Test Code: Include test cases in the Flutter repository that use Flutter's public headers to verify concurrency compliance.
  2. Enable GN Build System for Swift: The existing GN build system supports Swift. While some helper scripts (available in the Chromium repository) might be needed, I have successfully set this up locally and found it straightforward.
  3. CI Validation: Enable warnings as errors for Swift, ensuring compatibility with Swift 6 in CI pipelines.

(Based on feedback, I can create a detailed design document outlining the implementation in greater depth.)

Metadata

Metadata

Labels

P2Important issues not at the top of the work listc: proposalA detailed proposal for a change to Flutterengineflutter/engine related. See also e: labels.platform-iosiOS applications specificallyplatform-macosBuilding on or for macOS specificallyteam-iosOwned by iOS platform teamtriaged-iosTriaged by iOS platform team

Type

No type
No fields configured for issues without a type.

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions