- Proposal: SE-0297
- Author: Doug Gregor
- Review Manager: Chris Lattner
- Status: Implemented (Swift 5.5)
- Decision Notes: Rationale
- Implementation: Partially available in recent
main
snapshots behind the flag-Xfrontend -enable-experimental-concurrency
- Introduction
- Motivation
- Proposed solution
- Detailed design
- Source compatibility
- Revision history
- Future Directions
Swift's concurrency feature involves asynchronous functions and actors. While Objective-C does not have corresponding language features, asynchronous APIs are common in Objective-C, expressed manually through the use of completion handlers. This proposal provides bridging between Swift's concurrency features (e.g., async
functions) and the convention-based expression of asynchronous functions in Objective-C. It is intended to allow the wealth of existing asynchronous Objective-C APIs to be immediately usable with Swift's concurrency model.
For example, consider the following Objective-C API in CloudKit:
- (void)fetchShareParticipantWithUserRecordID:(CKRecordID *)userRecordID
completionHandler:(void (^)(CKShareParticipant * _Nullable, NSError * _Nullable))completionHandler;
This API is asynchronous. It delivers its result (or an error) via completion handler. The API directly translates into Swift:
func fetchShareParticipant(
withUserRecordID userRecordID: CKRecord.ID,
completionHandler: @escaping (CKShare.Participant?, Error?) -> Void
)
Existing Swift code can call this API by passing a closure for the completion handler. This proposal provides an alternate Swift translation of the API into an async
function, e.g.,
func fetchShareParticipant(
withUserRecordID userRecordID: CKRecord.ID
) async throws -> CKShare.Participant
Swift callers can invoke fetchShareParticipant(withUserRecordID:)
within an await
expression:
guard let participant = try? await container.fetchShareParticipant(withUserRecordID: user) else {
return nil
}
Swift-evolution thread: [Concurrency] Interoperability with Objective-C
On Apple platforms, Swift's tight integration with Objective-C APIs is an important part of the developer experience. There are several core features:
- Objective-C classes, protocols, and methods can be used directly from Swift.
- Swift classes can subclass Objective-C classes.
- Swift classes can declare conformance to Objective-C protocols.
- Swift classes, protocols, and methods can be made available to Objective-C via the
@objc
attribute.
Asynchronous APIs abound in Objective-C code: the iOS 14.0 SDK includes nearly 1,000 methods that accept completion handlers. These include methods that one could call directly from Swift, methods that one would override in a Swift-defined subclass, and methods in protocols that one would conform to. Supporting these use cases in Swift's concurrency model greatly expands the reach of this new feature.
The proposed solution provides interoperability between Swift's concurrency constructs and Objective-C in various places. It has several inter-dependent pieces:
- Translate Objective-C completion-handler methods into
async
methods in Swift. - Allow
async
methods defined in Swift to be@objc
, in which case they are exported as completion-handler methods. - Provide Objective-C attributes to control over how completion-handler-based APIs are translated into
async
Swift functions.
The detailed design section describes the specific rules and heuristics being applied. However, the best way to evaluate the overall effectiveness of the translation is to see its effect over a large number of Objective-C APIs. This pull request demonstrates the effect that this proposal has on the Swift translations of Objective-C APIs across the Apple iOS, macOS, tvOS, and watchOS SDKs.
An Objective-C method is potentially an asynchronous completion-handler method if it meets the following requirements:
- The method has a completion handler parameter, which is an Objective-C block that will receive the "result" of the asynchronous computation. It must meet the following additional constraints:
- It has a
void
result type. - It is called exactly once along all execution paths through the implementation.
- If the method can deliver an error, one of the parameters of the block is of type
NSError *
that is not_Nonnull
. A non-nilNSError *
value typically indicates that an error occurred, although the Cswift_async
attribute can describe other conventions (discussed in the section on Objective-C attributes).
- It has a
- The method itself has a
void
result type, because all results are delivered by the completion handler block.
An Objective-C method that is potentially an asynchronous completion-handler method will be translated into an async
method when it is either annotated explicitly with an appropriate swift_async
attribute (described in the section on Objective-C attributes) or is implicitly inferred when the following heuristics successfully identify the completion handler parameter:
- If the method has a single parameter, and the suffix of the first selector piece is one of the following phrases:
WithCompletion
WithCompletionHandler
WithCompletionBlock
WithReplyTo
WithReply
the sole parameter is the completion handler parameter. The matching phrase will be removed from the base name of the function when it is imported.
- If the method has more than one parameter, the last parameter is the completion handler parameter if its selector piece or parameter name is
completion
,withCompletion
,completionHandler
,withCompletionHandler
,completionBlock
,withCompletionBlock
,replyTo
,withReplyTo
,reply
, orreplyTo
. - If the method has more than one parameter, and the last parameter ends with one of the suffixes from the first bullet, the last parameter is the completion handler. The text preceding the suffix is appended to the base name of the function.
When the completion handler parameter is inferred, the presence of an NSError *
parameter that is not _Nonnull
in the completion handler block type indicates that the translated method can deliver an error.
The translation of an asynchronous Objective-C completion-handler method into an async
Swift method follows the normal translation procedure, with the following alterations:
- The completion handler parameter is removed from the parameter list of the translated Swift method.
- If the method can deliver an error, it is
throws
in addition to beingasync
. - The parameter types of the completion handler block type are translated into the result type of the
async
method, subject to the following additional rules:- If the method can deliver an error, the
NSError *
parameter is ignored. - If the method can deliver an error and a given parameter has the
_Nullable_result
nullability qualifier (see the section on Objective-C attributes below), it will be imported as optional. Otherwise, it will be imported as non-optional. - If there are multiple parameter types, they will be combined into a tuple type.
- If the method can deliver an error, the
The following PassKit API demonstrates how the inference rule plays out:
- (void)signData:(NSData *)signData
withSecureElementPass:(PKSecureElementPass *)secureElementPass
completion:(void (^)(NSData *signedData, NSData *signature, NSError *error))completion;
Today, this is translated into the following completion-handler function in Swift:
@objc func sign(_ signData: Data,
using secureElementPass: PKSecureElementPass,
completion: @escaping (Data?, Data?, Error?) -> Void
)
This will be translated into the following async
function:
@objc func sign(
_ signData: Data,
using secureElementPass: PKSecureElementPass
) async throws -> (Data, Data)
When the compiler sees a call to such a method, it effectively uses withUnsafeContinuation
to form a continuation for the rest of the function, then wraps the given continuation in a closure. For example:
let (signedValue, signature) = try await passLibrary.sign(signData, using: pass)
becomes pseudo-code similar to
try withUnsafeContinuation { continuation in
passLibrary.sign(
signData, using: pass,
completionHandler: { (signedValue, signature, error) in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: (signedValue!, signature!))
}
}
)
}
Additional rules are applied when translating an Objective-C method name into a Swift name of an async
function:
- If the base name of the method starts with
get
, theget
is removed and the leading initialisms are lowercased. - If the base name of the method ends with
Asynchronously
, that word is removed.
If the completion-handler parameter of the Objective-C method is nullable and the translated async
method returns non-Void
, it will be marked with the @discardableResult
attribute. For example:
-(void)stopRecordingWithCompletionHandler:void(^ _Nullable)(RPPreviewViewController * _Nullable, NSError * _Nullable)handler;
will become:
@discardableResult func stopRecording() async throws -> RPPreviewViewController
Many Swift entities can be exposed to Objective-C via the @objc
attribute. With an async
Swift method, the compiler will add an appropriate completion-handler parameter to the Objective-C method it creates, using what is effectively the inverse of the transformation described in the previous section, such that the Objective-C method produced is an asynchronous Objective-C completion-handler method. For example, a method such as:
@objc func perform(operation: String) async -> Int { ... }
will translate into the following Objective-C method:
- (void)performWithOperation:(NSString * _Nonnull)operation
completionHandler:(void (^ _Nullable)(NSInteger))completionHandler;
The Objective-C method implementation synthesized by the compiler will create a detached task that calls the async
Swift method perform(operation:)
with the given string, then (if the completion handler argument is not nil
) forwards the result to the completion handler.
For an async throws
method, the completion handler is extended with an NSError *
parameter to indicate the error, any non-nullable pointer type parameters are made _Nullable
, and any nullable pointer type parameters are made _Nullable_result
. For example, given:
@objc func performDangerousTrick(operation: String) async throws -> String { ... }
the resulting Objective-C method will have the following signature:
- (void)performDangerousTrickWithOperation:(NSString * _Nonnull)operation
completionHandler:(void (^ _Nullable)(NSString * _Nullable, NSError * _Nullable))completionHandler;
Again, the synthesized Objective-C method implementation will create a detached task that calls the async throws
method performDangerousTrick(operation:)
. If the method returns normally, the String
result will be delivered to the completion handler in the first parameter and the second parameter (NSError *
) will be passed nil
. If the method throws, the first parameter will be passed nil
(which is why it has been made _Nullable
despite being non-optional in Swift) and the second parameter will receive the error. If there are non-pointer parameters, they will be passed zero-initialized memory in the non-error arguments to provide consistent behavior for callers. This can be demonstrated with Swift pseudo-code:
// Synthesized by the compiler
@objc func performDangerousTrick(
operation: String,
completionHandler: ((String?, Error?) -> Void)?
) {
runDetached {
do {
let value = try await performDangerousTrick(operation: operation)
completionHandler?(value, nil)
} catch {
completionHandler?(nil, error)
}
}
}
Actor classes can be @objc
and will be available in Objective-C as are other classes. Actor classes require that their superclass (if there is one) also be an actor class. However, this proposal loosens that requirement slightly to allow an actor class to have NSObject
as its superclass. This is conceptually safe because NSObject
has no state (and its layout is effectively fixed that way), and makes it possible both for actor classes to be @objc
and also implies conformance to NSObjectProtocol
, which is required when conforming to a number of Objective-C protocols and is otherwise unimplementable in Swift.
A member of an actor class can only be @objc
if it is either async
or is outside of the actor's isolation domain. Synchronous code that is within the actor's isolation domain can only be invoked on self
(in Swift). Objective-C does not have knowledge of actor isolation, so these members are not permitted to be exposed to Objective-C. For example:
actor class MyActor {
@objc func synchronous() { } // error: part of actor's isolation domain
@objc func asynchronous() async { } // okay: asynchronous
@objc @actorIndependent func independent() { } // okay: actor-independent
}
A Swift async
function will always suspend, return, or (if it throws) produce an error. For completion-handler APIs, it is important that the completion handler block be called exactly once on all paths, including when producing an error. Failure to do so will break the semantics of the caller, either by failing to continue or by executing the same code multiple times. While this is an existing problem, widespread use of async
with incorrectly-implemented completion-handler APIs might exacerbate the issue.
Fortunately, because the compiler itself is synthesizing the block that will be passed to completion-handler APIs, it can detect both problems by introducing an extra bit of state into the synthesized block to indicate that the block has been called. If the bit is already set when the block is called, then it has been called multiple times. If the bit is not set when the block is destroyed, it has not been called at all. While this does not fix the underlying problem, it can at least detect the issue consistently at run time.
The transformation of Objective-C completion-handler-based APIs to async Swift APIs could benefit from the introduction of additional annotations (in the form of attributes) to guide the process. For example:
_Nullable_result
. Like_Nullable
, indicates that a pointer can be null (ornil
)._Nullable_result
differs from_Nullable
only for parameters to completion handler blocks. When the completion handler block's parameters are translated into the result type of anasync
method, the corresponding result will be optional.__attribute__((swift_async(...)))
. An attribute to control the translation of an asynchronous completion-handler method to anasync
function. It has several operations within the parentheses:__attribute__((swift_async(none)))
. Disables the translation toasync
.__attribute__((swift_async(not_swift_private, C)))
. Specifies that the method should be translated into anasync
method, using the parameter at indexC
as the completion handler parameter. The first (non-self
) parameter has index 1.__attribute__((swift_async(swift_private, C)))
. Specifies that the method should be translated into anasync
method that is "Swift private" (only for use when wrapping), using the parameter at indexC
as the completion handler parameter. The first (non-self
) parameter has index 1.
__attribute__((swift_attr("swift attribute")))
. A general-purpose Objective-C attribute to allow one to provide Swift attributes directly. In the context of concurrency, this allows Objective-C APIs to be annotated with a global actor (e.g.,@MainActor
).__attribute__((swift_async_name("method(param1:param2:)")))
. Specifies the Swift name that should be used for theasync
translation of the API. The name should not include an argument label for the completion handler parameter.__attribute__((swift_async_error(...)))
. An attribute to control how passing anNSError *
into the completion handle maps into the method beingasync throws
. It has several possible parameters:__attribute__((swift_async_error(none)))
: Do not import asthrows
. TheNSError *
parameter will be considered a normal parameter.__attribute__((swift_async_error(zero_argument, N)))
: Import asthrows
. When the Nth parameter to the completion handler is passed the integral value zero (includingfalse
), the async method will throw the error. The Nth parameter is removed from the result type of the translatedasync
method. The first parameter is numbered1
.__attribute__((swift_async_error(nonzero_argument, N)))
: Import asthrows
. When the Nth parameter to the completion handler is passed a non-zero integral value (includingtrue
), the async method will throw the error. The Nth parameter is removed from the result type of the translatedasync
method. The first parameter is numbered1
.
Generally speaking, changes to the way in which Objective-C APIs are translated into Swift are source-breaking changes. To avoid breaking source compatibility, this proposal involves translating Objective-C asynchronous completion-handler methods as both their original completion-handler signatures and also with the new async
signature. This allows existing Swift code bases to gradually adopt the async
forms of API, rather than forcing (e.g.) an entire Swift module to adopt async
all at once.
Importing the same Objective-C API in two different ways causes some issues:
-
Overloading of synchronous and asynchronous APIs. Objective-C frameworks may have evolved to include both synchronous and asynchronous versions of the same API, e.g.,
- (NSString *)lookupName; - (void)lookupNameWithCompletionHandler:(void (^)(NSString *))completion;
which will be translated into three different Swift methods:
@objc func lookupName() -> String @objc func lookupName(withCompletionHandler: @escaping (String) -> Void) @objc func lookupName() async -> String
The first and third signatures are identical except for being synchronous and asynchronous, respectively. The async/await design doesn't allow such overloading to be written in the same Swift module, but it can happen when translating Objective-C APIs or when importing methods from different Swift modules. The async/await design accounts for such overloading by favoring synchronous functions in synchronous contexts and asynchronous functions in asynchronous contexts. This overloading should avoid breaking source compatibility.
-
Another issue is when an asynchronous completion-handler method is part of an Objective-C protocol. For example, the
NSURLSessionDataDelegate
protocol includes this protocol requirement:@optional - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler;
Existing Swift code might implement this requirement in a conforming type using its completion-handler signature
@objc func urlSession( _ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void ) { ... }
while Swift code designed to take advantage of the concurrency model would implement this requirement in a conforming type using its
async
signature@objc func urlSession( _ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse ) async -> URLSession.ResponseDisposition { ... }
Implementing both requirements would produce an error (due to two Swift methods having the same selector), but under the normal Swift rules implementing only one of the requirements will also produce an error (because the other requirement is unsatisfied). Swift’s checking of protocol conformances will be extended to handle the case where multiple (imported) requirements have the same Objective-C selector: in that case, only one of them will be required to be implemented.
-
Overriding methods that have been translated into both completion-handler and
async
versions have a similar problem to protocol requirements: a Swift subclass can either override the completion-handler version or theasync
version, but not both. Objective-C callers will always call to the subclass version of the method, but Swift callers to the "other" signature will not unless the subclass's method is marked with@objc dynamic
. Swift can infer that theasync
overrides of such methods are@objc dynamic
to avoid this problem (because suchasync
methods are new code). However, inferring@objc dynamic
on existing completion-handler overrides can change the behavior of programs and break subclasses of the subclasses, so at best the compiler can warn about this situation.
-
Post-review:
await try
becomestry await
based on result of SE-0296 review- Added inference of
@discardableResult
forasync
methods translated from completion-handler methods with an optional completion handler.
-
Changes in the second pitch:
- Removed mention of asynchronous handlers, which will be in a separate proposal.
- Introduced the
swift_async_error
Clang attribute to separate out "throwing" behavior from theswift_async
attribute. - Added support for "Swift private" to the
swift_async
attribute. - Tuned the naming heuristics based on feedback to add (e.g)
reply
,replyTo
,completionBlock
, and variants. - For the rare case where we match a parameter suffix, append the text prior to the suffix to the base name.
- Replaced the
-generateCGImagesAsynchronouslyForTimes:completionHandler:
example with one from PassKit. - Added a "Future Directions" section about
NSProgress
.
-
Original pitch (document and forum thread).
Some Objective-C completion-handler methods return an NSProgress to allow the caller to evaluate progress of the asynchronous operation. Such methods are not imported as async
in this proposal, because the method does not return void
. For example:
- (NSProgress *)doSomethingThatTakesALongTimeWithCompletionHandler:(void (^)(MyResult * _Nullable, NSError * _Nullable))completionHandler;
To support such methods would require some kind of integration between NSProgress
and Swift's tasks. For example, when calling such a method, the NSProgress
returned from such a call to be recorded in the task (say, in some kind of task-local storage). The other direction, where a Swift-defined method overrides a method, would need to extract an NSProgress
from the task to return. Such a design is out of scope for this proposal, but could be introduced at some later point.