|
| 1 | +# Function Back Deployment |
| 2 | + |
| 3 | +* Proposal: [SE-NNNN](NNNN-back-deploy.md) |
| 4 | +* Author: [Allan Shortlidge](https://github.com/tshortli) |
| 5 | +* Implementation: [apple/swift#41271](https://github.com/apple/swift/pull/41271), [apple/swift#41348](https://github.com/apple/swift/pull/41348), [apple/swift#41416](https://github.com/apple/swift/pull/41416), [apple/swift#41612](https://github.com/apple/swift/pull/41612) as the underscored attribute `@_backDeploy` |
| 6 | +* Review Manager: TBD |
| 7 | +* Status: **Awaiting implementation** |
| 8 | + |
| 9 | +## Introduction |
| 10 | + |
| 11 | +Resilient Swift libraries, such as the ones present in the SDKs for Apple's platforms, are distributed as dynamic libraries. Authors of these libraries use `@available` annotations to indicate the operating system version that a declaration was introduced in. For example, suppose this were the interface of ToastKit, a library that is part of the toasterOS SDK: |
| 12 | + |
| 13 | +```swift |
| 14 | +@available(toasterOS 1.0, *) |
| 15 | +public struct BreadSlice { ... } |
| 16 | + |
| 17 | +@available(toasterOS 1.0, *) |
| 18 | +public struct Toast { ... } |
| 19 | + |
| 20 | +@available(toasterOS 1.0, *) |
| 21 | +public struct Toaster { |
| 22 | + public func makeToast(_ slice: BreadSlice) -> Toast |
| 23 | +} |
| 24 | +``` |
| 25 | + |
| 26 | +In response to developer feedback, the ToastKit authors enhance `Toaster` in toasterOS 2.0 with the capability to make toast in batches: |
| 27 | + |
| 28 | +```swift |
| 29 | +extension Toaster { |
| 30 | + @available(toasterOS 2.0, *) |
| 31 | + public func makeBatchOfToast(_ slices: [BreadSlice]) -> [Toast] { |
| 32 | + var toast: [Toast] = [] |
| 33 | + for slice in slices { |
| 34 | + toast.append(makeToast(slice)) |
| 35 | + } |
| 36 | + return toast |
| 37 | + } |
| 38 | +} |
| 39 | +``` |
| 40 | + |
| 41 | +Unfortunately, developers who wish to both distribute an app compatible with toasterOS 1.0 and also adopt `makeBatchOfToast(_:)` must call the API conditionally to account for its potential unavailability: |
| 42 | + |
| 43 | +```swift |
| 44 | +let slices: [BreadSlice] = ... |
| 45 | +if #available(toasterOS 2.0, *) { |
| 46 | + let toast = toaster.makeBatchOfToast(slices) |
| 47 | + // ... |
| 48 | +} else { |
| 49 | + // ... do something else, like reimplement makeBatchOfToast(_:) |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +Considering that the implementation of `makeBatchOfToast(_:)` is self contained and could run unmodified on toasterOS 1.0, it would be ideal if the ToastKit authors had the option to back deploy this new API to older OSes and allow clients to adopt it unconditionally. |
| 54 | + |
| 55 | +The `@_alwaysEmitIntoClient` attribute is an unofficial Swift language feature that can be used to solve this problem. The bodies of functions with this attribute are emitted into the library's `.swiftinterface` (similarly to `@inlinable` functions) and the compiler makes a local copy of the annotated function in the client module. References to these functions _always_ resolve to a copy in the same module so the function is effectively not a part of the library's ABI. |
| 56 | + |
| 57 | +While `@_alwaysEmitIntoClient` can be used to back deploy APIs, there are some drawbacks to using it. Since a copy of the function is always emitted, there is code size overhead for every client even if the client's deployment target is new enough that the library API would always be available at runtime. Additionally, if the implementation of the API were to change in order to improve performance, fix a bug, or close a security hole then the client would need to be recompiled against a new SDK before users benefit from those changes. An attribute designed specifically to support back deployment should avoid these drawbacks by ensuring that: |
| 58 | + |
| 59 | +1. The API implemention from the original library is preferred at runtime when it is available. |
| 60 | +2. Fallback copies of the API implementation are absent from clients binaries when they would never be used. |
| 61 | + |
| 62 | +Swift-evolution thread: TBD |
| 63 | + |
| 64 | +## Proposed solution |
| 65 | + |
| 66 | +Add a `@backDeploy(before: ...)` attribute to Swift that can be used to indicate that a copy of the function should be emitted into the client to be used at runtime when executing on an OS prior to a specific version. The attribute can be adopted by ToastKit's authors like this: |
| 67 | + |
| 68 | +```swift |
| 69 | +extension Toaster { |
| 70 | + @available(toasterOS 1.0, *) |
| 71 | + @backDeploy(before: toasterOS 2.0) |
| 72 | + public func makeBatchOfToast(_ breadSlices: [BreadSlice]) -> [Toast] { ... } |
| 73 | +} |
| 74 | +``` |
| 75 | + |
| 76 | +The API is now available on toasterOS 1.0 and later so clients may now reference `makeBatchOfToast(_:)` unconditionally. The compiler detects applications of `makeBatchOfToast(_:)` and generates code to automatically handle the potentially runtime unavailability of the API. |
| 77 | + |
| 78 | +## Detailed design |
| 79 | + |
| 80 | +The `@backDeploy` attribute may apply to functions, methods, and subscripts. Properties may also have the attribute as long as the they do not have storage. The attribute takes a comma separated list of one or more platform versions, so declarations that are available on more than one platform can be back deployed to multiple platforms with a single attribute. The following are examples of legal uses of the attribute: |
| 81 | + |
| 82 | +```swift |
| 83 | +extension Temperature { |
| 84 | + @available(toasterOS 1.0, ovenOS 1.0, *) |
| 85 | + @backDeploy(before: toasterOS 2.0, ovenOS 2.0) |
| 86 | + public var degreesFahrenheit: Double { |
| 87 | + return (degreesCelcius * 9 / 5) + 32 |
| 88 | + } |
| 89 | +} |
| 90 | + |
| 91 | +extension Toaster { |
| 92 | + /// Returns whether the slot at the given index can fit a bagel. |
| 93 | + @available(toasterOS 1.0, *) |
| 94 | + @backDeploy(before: toasterOS 2.0) |
| 95 | + public subscript(fitsBagelsAt index: Int) -> Bool { |
| 96 | + get { return index < 2 } |
| 97 | + } |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +### Behavior of back deployed APIs |
| 102 | + |
| 103 | +When the compiler encounters a call to a back deployed function, it generates and calls a thunk instead that forwards the arguments to either the library copy of the function or a fallback copy of the function. For instance, suppose the client's code looks like this: |
| 104 | + |
| 105 | +```swift |
| 106 | +let toast = toaster.makeBatchOfToast(slices) |
| 107 | +``` |
| 108 | + |
| 109 | +The transformation done by the compiler would effectively result in this: |
| 110 | + |
| 111 | +```swift |
| 112 | +let toast = toaster.makeBatchOfToast_thunk(slices) |
| 113 | + |
| 114 | +// Compiler generated |
| 115 | +extension Toaster { |
| 116 | + func makeBatchOfToast_thunk(_ breadSlices: [BreadSlice]) -> [Toast] { |
| 117 | + if #available(toasterOS 2.0, *) { |
| 118 | + return makeBatchOfToast(breadSlices) // call the original |
| 119 | + } else { |
| 120 | + return makeBatchOfToast_fallback(breadSlices) // call local copy |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + func makeBatchOfToast_fallback(_ breadSlices: [BreadSlice]) -> [Toast] { |
| 125 | + // ... copy of function body from ToastKit |
| 126 | + } |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +When the deployment target of the client app is at least toasterOS 2.0, the optimizer can eliminate the branch in `makeBatchOfToast_thunk(_:)` and make `makeBatchOfToast_fallback(_:)` an unused function. |
| 131 | + |
| 132 | +### Restrictions on declarations that may be back deployed |
| 133 | + |
| 134 | +There are rules that limit which declarations may have a `@backDeploy` attribute: |
| 135 | + |
| 136 | +* The declaration must be `public` or `@usableFromInline` since it only makes sense to offer back deployment for declarations that would be used by other modules. |
| 137 | +* Only functions that can be invoked with static dispatch are eligible to back deploy, so back deployed instance and class methods must be `final`. The `@objc` attribute also implies dynamic dispatch and therfore is incompatible with `@backDeploy`. |
| 138 | +* Explicit availability must be specified with `@available` on the same declaration for each of the platforms that the declaration is back deployed on. |
| 139 | +* The declaration should be available earlier than the platform versions specified in `@backDeploy` (otherwise the fallback functions would never be called). |
| 140 | +* The `@_alwaysEmitIntoClient` and `@_transparent` attributes are incompatible with `@backDeploy` because they require that the function body to always be emitted into the client, defeating the purpose of `@backDeploy`. Declarations with `@inlinable` are also restricted from using `@backDeploy` since inlining behavior is dictated by the optimizer and use of the library function when it is available could be inconsistent as a result. |
| 141 | + |
| 142 | +### Requirements for the bodies of back deployed functions |
| 143 | + |
| 144 | +The restrictions on the bodies of back deployed functions are the same as `@inlinable` functions. The body may only reference declarations that are accessible to the client, such as `public` and `@usableFromInline` declarations. Similarly, those referenced declarations must also be at least as available the back deployed function, or `if #available` must be used to handle potential unavailability. Type checking in `@backDeploy` function bodies must ignore the library's deployment target since the body will be copied into clients with unknown deployment targets. |
| 145 | + |
| 146 | +## Source compatibility |
| 147 | + |
| 148 | +The introduction of this attribute to the language is an additive change and therefore doesn't affect existing Swift code. |
| 149 | + |
| 150 | +## Effect on ABI stability |
| 151 | + |
| 152 | +The `@backDeploy` attribute has no effect on the ABI of Swift libraries. A Swift function with and without a `@backDeploy` attribute has the same ABI; the attribute simply controls whether the compiler automatically generates additional logic in the client module. The thunk and fallback functions that are emitted into the client do have a special mangling to disambiguate them from the original function in the library, but these symbols are never referenced accross separately compiled modules. |
| 153 | + |
| 154 | +## Effect on API resilience |
| 155 | + |
| 156 | +By itself, adding a `@backDeploy` attribute to a declaration does not affect source compatibility for clients of a library, and neither does removing the attribute. However, adding a `@backDeploy` attribute would typically be done simultaneously with expanding the availability of the declaration. Expansion of the availability of an API is source compatible for clients, but reversing that expansion would not be. |
| 157 | + |
| 158 | +## Alternatives considered |
| 159 | + |
| 160 | +### Extend @available |
| 161 | + |
| 162 | +Another possible design for this feature would be to augment the existing `@available` attribute with the ability to control back deployment: |
| 163 | + |
| 164 | +```swift |
| 165 | +extension Toaster { |
| 166 | + @available(toasterOS, introduced: 1.0, backDeployBefore: 2.0) |
| 167 | + public func makeBatchOfToast(_ breadSlices: [BreadSlice]) -> [Toast] |
| 168 | +} |
| 169 | +``` |
| 170 | + |
| 171 | +This design has the advantage of grouping the introduction and back deployment versions together in a single attribute. The `@available` attribute already has quite a few responsibilities, though, and this design does not call as much attention to the fact that the declaration has important new behaviors, like exposing the function body to clients. It would also be more awkward to emit clear compiler diagnostics when there are issues related to back deployment since an individual component of an attribute would need to be identified in the messages, instead of an entire attribute. |
| 172 | + |
| 173 | +## Future directions |
| 174 | + |
| 175 | +### Back deployment for other kinds of declarations |
| 176 | + |
| 177 | +It would also be useful to be able to back deploy other types of declarations, like entire enums or structs. Exploring the feasability of such a feature is out of scope for this proposal, but it does seem like the attribute should be designed to allow it to be used for this purpose. |
| 178 | + |
| 179 | +## Acknowledgments |
| 180 | + |
| 181 | +Thank you to Alexis Laferriere, Ben Cohen, and Xi Ge for their help designing the feature and to Slava Pestov for his assistance with SILGen. |
0 commit comments