Skip to content

[Pigeon] add Sendable conformance and @Sendable closure annotations - #12902

Open
LongCatIsLooong wants to merge 6 commits into
flutter:mainfrom
LongCatIsLooong:pigeon-swift-sendable
Open

LongCatIsLooong wants to merge 6 commits into
flutter:mainfrom
LongCatIsLooong:pigeon-swift-sendable

Conversation

@LongCatIsLooong

@LongCatIsLooong LongCatIsLooong commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Fixes flutter/flutter#140439, but this PR alone doesn't allow people to turn on fully strict concurrency (or Swift 6 language mode).

Extracted from #12885 (adding isolation annotations is trickier, because the Swift compiler always treat isolation violations as errors regardless of the compiler flags specified. See the open questions in that PR), this PR adds Sendable in generated code based on the underlying platform channel threading contract. This does not affect FFI generated code.

This DOES NOT add Sendable conformances to pigeon-generated data classes:
for Swift structs, the compiler can infer the senability (a struct is sendable if all fields are sendable)

for non-final classes (pigeon generated class are not final), they shouldn't be marked as Sendable since subtypes can introduce non-thread-safe APIs.

Client code can use extensions to add @unchecked Sendable to pigeon generated data classes if so desired, since client code will be in the same Swift target as the pigeon generated data classes. Also @uncheck is required anyway because the compiler can't prove the type is sendable.

Breaking Change?

This change assumes the plugin has not enabled complete strict concurrency checking for the Swift target. When strict concurrency check is not set to complete, sendability diagnostics are warnings (or ignored) in Swift 5 language mode, so the opt-out can be done via compiler flags.

Adding sendability annotations can still be breaking if the client has already enabled fully strict concurrency checking for the swift target. But it is unlikely: with the pigeon generated swift code being in the same target as the client code, it is unlikely for the client target to enable full strict concurrency checking, unless the client code followed the (incorrect) threading contract currently generated by pigeon.

Versioning?

Tentatively bumped the version to 29.1.0 but if this is deemed a breaking change I can bump this to 30.0.0.

Pre-Review 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.

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. 2

swiftFunction: method.swiftFunction,
documentationComments: method.documentationComments,
serialBackgroundQueue: method.taskQueueType == TaskQueueType.serialBackgroundThread
serialBackgroundQueue: method.taskQueueType == .serialBackgroundThread

@LongCatIsLooong LongCatIsLooong Sep 17, 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.

This is for generating the host API message handler implementation in pigeon.

returnType: method.returnType,
isAsynchronous: method.isAsynchronous,
isAsynchronousCallback: true,
isCompletionClosureSendable: true,

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.

This is inside

  // Writes the delegate method that handles making a call from for a host
  // method.
  void _writeProxyApiHostMethodDelegateMethods(

///
/// Accessing and inserting to an InstanceManager is thread safe.
final class ${swiftInstanceManagerClassName(options)} {
final class ${swiftInstanceManagerClassName(options)}: @unchecked 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.

@unchecked because it uses locking primitives that swift concurrency doesn't understand (so the compiler can't prove the correctness).
Sendable because the public interface is thread safe and the class is final.

isAsynchronous: method.isAsynchronous,
isAsynchronousCallback: method.isAsynchronousCallback,
swiftFunction: method.swiftFunction,
isCompletionClosureSendable: !generatorOptions.useFfi,

@LongCatIsLooong LongCatIsLooong Sep 17, 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.

This is for generating method requirement signatures in the host API protocol. The completion closure is always sendable (unless using ffi) because FlutterReply is thread-safe.

_ everything: AllTypes, completion: @escaping @Sendable (Result<AllTypes, Error>) -> Void)
func echoNullable(
_ aString: String?, completion: @escaping @Sendable (Result<String?, Error>) -> Void)
func throwError(completion: @escaping @Sendable (Result<Any?, Error>) -> Void)

@LongCatIsLooong LongCatIsLooong Sep 17, 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.

It's possible that we can make this

  func throwError(completion: sending @escaping (Result<Any?, Error>) -> Void)

so the caller can send non-sendable results via completion block,
but sending is only available in Swift 6.0+.

Additionally, these methods are called by pigeon generated code so we can implement workarounds in pigeon generated code if needed (so the ugliness lives in pigeon generated code).

@LongCatIsLooong LongCatIsLooong Sep 17, 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.

Also using sending can break implementors (callers of completion), if they call completion.success on a value stored as an instance variable (so it violates the sending contract).

Ok looks like this is incorrect. sending doesn't imply the parameters are also sending. So the only caveat is sending requires Swift 6.0+

@LongCatIsLooong LongCatIsLooong changed the title [Pigeon] add Sendable conformance and @Sendable annotations [Pigeon] add Sendable conformance and @Sendable closure annotations Sep 17, 2026
@@ -1,3 +1,9 @@
## 29.1.0

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.

Technically this still can break existing clients, but it's unlikely so I only bumped minor version.

@LongCatIsLooong
LongCatIsLooong marked this pull request as ready for review September 17, 2026 19:52

@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 updates Pigeon to version 29.1.0, marking generated Swift completion closures for host APIs and ProxyApi host method delegates as @Sendable, and marking the generated InstanceManager as @unchecked Sendable. Feedback points out that using @Sendable closures with non-Sendable types like FlutterStandardTypedData or Any may trigger compiler warnings under Swift 6 strict concurrency, and suggests adding retroactive conformances or documenting these limitations.

Comment on lines +4038 to +4040
final sendablePrefix = isCompletionClosureSendable ? '@Sendable ' : '';
final completion =
'completion: @escaping $sendablePrefix(Result<$returnTypeString, $errorTypeName>) -> Void';

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

While adding @Sendable to completion closures is a key step toward Swift concurrency support, using @Sendable closures with non-Sendable types like Any (used for Object), [Any?] (used for List), or FlutterStandardTypedData will trigger compiler warnings or errors under Swift 6 / strict concurrency.

To make this fully compatible with strict concurrency:

  1. FlutterStandardTypedData: Consider generating a retroactive conformance in the helper section:
    extension FlutterStandardTypedData: @unchecked Sendable {}
  2. Any / Collections: Since Any is not Sendable, closures using Result<Any, Error> or Result<[Any?], Error> are technically not fully sendable-safe. We should consider if these types can be migrated to Sendable (i.e., Any & Sendable) in the future, or document this limitation.

@LongCatIsLooong LongCatIsLooong Sep 17, 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.

see: #12902 (comment),

With this change, client code doesn't have to deal with non-sendable types, as all relevant call sites are only in generated code. There will be warnings in pigeon generated code but unless the client turns on full strict concurrency for the target, it does not block compilation.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[pigeon] Support @Sendable in Swift code

1 participant