|
| 1 | +# Continuations for interfacing async tasks with synchronous code |
| 2 | + |
| 3 | +* Proposal: [SE-ABCD](ABCD-continuation.md) |
| 4 | +* Authors: [John McCall](https://github.com/rjmccall), [Joe Groff](https://github.com/jckarter), [Doug Gregor](https://github.com/DougGregor), [Konrad Malawski](https://github.com/ktoso) |
| 5 | +* Review Manager: TBD |
| 6 | +* Status: **Awaiting review** |
| 7 | + |
| 8 | +## Introduction |
| 9 | + |
| 10 | +Asynchronous Swift code needs to be able to work with existing synchronous |
| 11 | +code that uses techniques such as completion callbacks and delegate methods to |
| 12 | +respond to events. Asynchronous tasks can suspend themselves on |
| 13 | +**continuations** which synchronous code can then capture and invoke to |
| 14 | +resume the task in response to an event |
| 15 | + |
| 16 | +Swift-evolution thread: |
| 17 | + |
| 18 | +- [Structured concurrency](https://forums.swift.org/t/concurrency-structured-concurrency/41622) |
| 19 | + |
| 20 | +## Motivation |
| 21 | + |
| 22 | +Swift APIs often provide asynchronous code execution by way of a callback. This |
| 23 | +may occur either because the code itself was written prior to the introduction |
| 24 | +of async/await, or (more interestingly in the long term) because it ties in |
| 25 | +with some other system that is primarily event-driven. In such cases, one may |
| 26 | +want to provide an async interface to clients while using callbacks internally. |
| 27 | +In these cases, the calling async task needs to be able to suspend itself, |
| 28 | +while providing a mechanism for the event-driven synchronous system to resume |
| 29 | +it in response to an event. |
| 30 | + |
| 31 | +## Proposed solution |
| 32 | + |
| 33 | +The library will provide APIs to get a **continuation** for the current |
| 34 | +asynchronous task. Getting the task's continuation suspends the task, and |
| 35 | +produces a value that synchronous code can then use a handle to resume the |
| 36 | +task. Given a completion callback based API like: |
| 37 | + |
| 38 | +``` |
| 39 | +func beginOperation(completion: (OperationResult) -> Void) |
| 40 | +``` |
| 41 | + |
| 42 | +we can turn it into an `async` interface by suspending the task and using its |
| 43 | +continuation to resume it when the callback is invoked, turning the argument |
| 44 | +passed into the callback into the normal return value of the async function: |
| 45 | + |
| 46 | +``` |
| 47 | +func operation() async -> OperationResult { |
| 48 | + // Suspend the current task, and pass its continuation into a closure |
| 49 | + // that executes immediately |
| 50 | + return await withUnsafeContinuation { continuation in |
| 51 | + // Invoke the synchronous callback-based API... |
| 52 | + beginOperation(completion: { result in |
| 53 | + // ...and resume the continuation when the callback is invoked |
| 54 | + continuation.resume(returning: result) |
| 55 | + }) |
| 56 | + } |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +## Detailed design |
| 61 | + |
| 62 | +### Raw unsafe continuations |
| 63 | + |
| 64 | +The library provides two functions, `withUnsafeContinuation` and |
| 65 | +`withUnsafeThrowingContinuation`, that allow one to call into a callback-based |
| 66 | +API from inside async code. Each function takes an *operation* closure, |
| 67 | +which is expected to call into the callback-based API. The closure |
| 68 | +receives a continuation instance that must be resumed by the callback, |
| 69 | +either to provide the result value or (in the throwing variant) the thrown |
| 70 | +error that becomes the result of the `withUnsafeContinuation` call when the |
| 71 | +async task resumes: |
| 72 | + |
| 73 | + |
| 74 | +``` |
| 75 | +struct UnsafeContinuation<T> { |
| 76 | + func resume(returning: T) |
| 77 | +} |
| 78 | +
|
| 79 | +func withUnsafeContinuation<T>( |
| 80 | + _ operation: (UnsafeContinuation<T>) -> () |
| 81 | +) async -> T |
| 82 | +
|
| 83 | +struct UnsafeThrowingContinuation<T> { |
| 84 | + func resume(returning: T) |
| 85 | + func resume(throwing: Error) |
| 86 | + func resume<E: Error>(with result: Result<T, E>) |
| 87 | +} |
| 88 | +
|
| 89 | +func withUnsafeThrowingContinuation<T>( |
| 90 | + _ operation: (UnsafeThrowingContinuation<T>) -> () |
| 91 | +) async throws -> T |
| 92 | +``` |
| 93 | + |
| 94 | +The operation must follow one of the following invariants: |
| 95 | + |
| 96 | +- Either the resume function must only be called *exactly-once* on each |
| 97 | + execution path the operation may take (including any error handling paths), |
| 98 | + or else |
| 99 | +- the resume function must be called exactly at the end of the operation |
| 100 | + function's execution. |
| 101 | + |
| 102 | +`Unsafe*Continuation` is an unsafe interface, so it is undefined behavior if |
| 103 | +these invariants are not followed by the operation. This allows |
| 104 | +continuations to be a low-overhead way of interfacing with synchronous code. |
| 105 | +Wrappers can provide checking for these invariants, and the library will provide |
| 106 | +one such wrapper, discussed below. |
| 107 | + |
| 108 | +Using the `Unsafe*Continuation` API, one may for example wrap such |
| 109 | +(purposefully convoluted for the sake of demonstrating the flexibility of |
| 110 | +the continuation API) function: |
| 111 | + |
| 112 | +``` |
| 113 | +func buyVegetables( |
| 114 | + shoppingList: [String], |
| 115 | + // a) if all veggies were in store, this is invoked *exactly-once* |
| 116 | + onGotAllVegetables: ([Vegetable]) -> (), |
| 117 | +
|
| 118 | + // b) if not all veggies were in store, invoked one by one *one or more times* |
| 119 | + onGotVegetable: (Vegetable) -> (), |
| 120 | + // b) if at least one onGotVegetable was called *exactly-once* |
| 121 | + // this is invoked once no more veggies will be emitted |
| 122 | + onNoMoreVegetables: () -> (), |
| 123 | + |
| 124 | + // c) if no veggies _at all_ were available, this is invoked *exactly once* |
| 125 | + onNoVegetablesInStore: (Error) -> () |
| 126 | +) |
| 127 | +// returns 1 or more vegetables or throws an error |
| 128 | +func buyVegetables(shoppingList: [String]) async throws -> [Vegetable] { |
| 129 | + try await withUnsafeThrowingContinuation { continuation in |
| 130 | + var veggies: [Vegetable] = [] |
| 131 | +
|
| 132 | + buyVegetables( |
| 133 | + shoppingList: shoppingList, |
| 134 | + onGotAllVegetables: { veggies in continuation.resume(returning: veggies) }, |
| 135 | + onGotVegetable: { v in veggies.append(v) }, |
| 136 | + onNoMoreVegetables: { continuation.resume(returning: veggies) }, |
| 137 | + onNoVegetablesInStore: { error in continuation.resume(throwing: error) }, |
| 138 | + ) |
| 139 | + } |
| 140 | +} |
| 141 | +
|
| 142 | +let veggies = try await buyVegetables(shoppingList: ["onion", "bell pepper"]) |
| 143 | +``` |
| 144 | + |
| 145 | +Thanks to weaving the right continuation resume calls into the complex |
| 146 | +callbacks of the `buyVegetables` function, we were able to offer a much nicer |
| 147 | +overload of this function, allowing async code to interact with this function in |
| 148 | +a more natural straight-line way. |
| 149 | + |
| 150 | +### Checked continuations |
| 151 | + |
| 152 | +`Unsafe*Continuation` provides a lightweight mechanism for interfacing |
| 153 | +sync and async code, but it is easy to misuse, and misuse can corrupt the |
| 154 | +process state in dangerous ways. In order to provide additional safety and |
| 155 | +guidance when developing interfaces between sync and async code, the |
| 156 | +library will also provide a wrapper which checks for invalid use of the |
| 157 | +continuation: |
| 158 | + |
| 159 | +``` |
| 160 | +struct CheckedContinuation<T> { |
| 161 | + func resume(returning: T) |
| 162 | +} |
| 163 | +
|
| 164 | +func withCheckedContinuation<T>( |
| 165 | + _ operation: (CheckedContinuation<T>) -> () |
| 166 | +) async -> T |
| 167 | +
|
| 168 | +struct CheckedThrowingContinuation<T> { |
| 169 | + func resume(returning: T) |
| 170 | + func resume(throwing: Error) |
| 171 | + func resume<E: Error>(with result: Result<T, E>) |
| 172 | +} |
| 173 | +
|
| 174 | +func withCheckedThrowingContinuation<T>( |
| 175 | + _ operation: (CheckedThrowingContinuation<T>) -> () |
| 176 | +) async throws -> T |
| 177 | +``` |
| 178 | + |
| 179 | +The API is intentionally identical to the `Unsafe` variants, so that code |
| 180 | +can switch easily between the checked and unchecked variants. For instance, |
| 181 | +the `buyVegetables` example above can opt into checking merely by turning |
| 182 | +its call of `withUnsafeThrowingContinuation` into one of `withCheckedThrowingContinuation`: |
| 183 | + |
| 184 | +``` |
| 185 | +// returns 1 or more vegetables or throws an error |
| 186 | +func buyVegetables(shoppingList: [String]) async throws -> [Vegetable] { |
| 187 | + try await withCheckedThrowingContinuation { continuation in |
| 188 | + var veggies: [Vegetable] = [] |
| 189 | +
|
| 190 | + buyVegetables( |
| 191 | + shoppingList: shoppingList, |
| 192 | + onGotAllVegetables: { veggies in continuation.resume(returning: veggies) }, |
| 193 | + onGotVegetable: { v in veggies.append(v) }, |
| 194 | + onNoMoreVegetables: { continuation.resume(returning: veggies) }, |
| 195 | + onNoVegetablesInStore: { error in continuation.resume(throwing: error) }, |
| 196 | + ) |
| 197 | + } |
| 198 | +} |
| 199 | +``` |
| 200 | + |
| 201 | +Instead of leading to undefined behavior, `CheckedContinuation` will instead |
| 202 | +ignore attempts to resume the continuation multiple times, logging a warning |
| 203 | +message. `CheckedContinuation` will also log a warning if the continuation |
| 204 | +is discarded without ever resuming the task, which leaves the task stuck in its |
| 205 | +suspended state, leaking any resources it holds. |
| 206 | + |
| 207 | +## Alternatives considered |
| 208 | + |
| 209 | +### Name `CheckedContinuation` just `Continuation` |
| 210 | + |
| 211 | +We could position `CheckedContinuation` as the "default" API for doing |
| 212 | +sync/async interfacing by leaving the `Checked` word out of the name. This |
| 213 | +would certainly be in line with the general philosophy of Swift that safe |
| 214 | +interfaces are preferred, and unsafe ones used selectively where performance |
| 215 | +is an overriding concern. There are a couple of reasons to hesitate at doing |
| 216 | +this here, though: |
| 217 | + |
| 218 | +- Although the consequences of misusing `CheckedContinuation` are not as |
| 219 | + severe as `UnsafeContinuation`, it still only does a best effort at checking |
| 220 | + for some common misuse patterns, and it does not render the consequences of |
| 221 | + continuation misuse entirely moot: dropping a continuation without resuming |
| 222 | + it will still leak the un-resumed task, and attempting to resume a |
| 223 | + continuation multiple times will still cause the information passed through |
| 224 | + the continuation to be lost. It is still a serious programming error if |
| 225 | + a `with*Continuation` operation misuses the continuation; |
| 226 | + `CheckedContinuation` only helps make the error more apparent. |
| 227 | +- Naming a type `Continuation` now might take the "good" name away if, |
| 228 | + after we have move-only types at some point in the future, we want to |
| 229 | + introduce a continuation type that statically enforces the exactly-once |
| 230 | + property. |
| 231 | + |
| 232 | + |
| 233 | +### Don't expose `UnsafeContinuation` |
0 commit comments