From 506878ea2472f207b4c8a303c63d96163032fa14 Mon Sep 17 00:00:00 2001 From: Becca Royal-Gordon Date: Fri, 28 Mar 2025 19:01:02 -0700 Subject: [PATCH 1/3] Add `@abi` proposal --- proposals/NNNN-abi-attr.md | 946 +++++++++++++++++++++++++++++++++++++ 1 file changed, 946 insertions(+) create mode 100644 proposals/NNNN-abi-attr.md diff --git a/proposals/NNNN-abi-attr.md b/proposals/NNNN-abi-attr.md new file mode 100644 index 0000000000..a72094ccca --- /dev/null +++ b/proposals/NNNN-abi-attr.md @@ -0,0 +1,946 @@ +# Controlling the ABI of a function, initializer, property, or subscript + +* Proposal: [SE-NNNN](NNNN-abi-attr.md) +* Authors: [Becca Royal-Gordon](https://github.com/beccadax) +* Review Manager: TBD +* Status: **Awaiting review** +* Implementation: behind experimental feature `ABIAttribute` (refinements in [swiftlang/swift#79466](https://github.com/swiftlang/swift/pull/80383)) +* Review: ([pitch](https://forums.swift.org/t/pitch-controlling-the-abi-of-a-declaration/75123)) + +## Introduction + +We propose introducing the `@abi` attribute, which provides an alternate +version of the declaration used for name mangling. This feature would allow +developers of ABI-stable libraries to make minor changes, such as changing +the sendability of a parameter or renaming a declaration (so long as source +compatibility is preserved in a backwards-deployable way), without requiring +deep knowledge of compiler implementation details. + +## Motivation + +Maintainers of ABI-stable libraries sometimes need to update or correct +existing declarations for various reasons: + +1. To adopt new language features, like changing `@Sendable` to `sending`, + in an existing declaration. + +2. To replace an existing declaration with a source-compatible but ABI-breaking + equivalent, like replacing a `rethrows` method with one using typed + `throws`. + +3. To correct a mistake, like removing an unnecessary `@escaping` attribute or + adding a `Sendable` generic constraint. + +4. To rename an API whose name is felt to be catastrophically confusing. + +Many revisions will cause fundamental changes in how an API will be used at the +machine code level that clients must account for; for instance, changing `` +to `` requires callers to generate code that will pass the witness +table for `T`'s `Hashable` conformance. However, some features are designed to +have little or no impact on the code generated by the caller. For example, +these two declarations: + +```swift +// `T` must be `Sendable` +func fn(_: T) {} + +// `T` parameter must be `sending` +func fn(_: borrowing sending T) {} // note: 'borrowing sending' is currently banned, + // pending a decision on whether it should have the + // meaning we want it to have here +``` + +Have identical parameter signatures at the IR level, with one pointer to the +argument and another pointer to `T`'s value witness table, and use the same +result type and calling convention too: + +```text +define hidden swiftcc void @"$s4main2fnyyxs8SendableRzlF"(ptr noalias %0, ptr %T) + ^~~~~~~~~~~~ ^~~~~~~~~~~~~~~~~~~~~~~~ +define hidden swiftcc void @"$s4main2fnyyxlF"(ptr noalias %0, ptr %T) + ^~~~~~~~~~~~ ^~~~~~~~~~~~~~~~~~~~~~~~ +``` + +Other details, such as the parameter ownership conventions, also line up to +make this work; suffice it to say, the function generated when you use +`borrowing sending` is perfectly capable of handling the arguments passed by +callers that think the parameter is `Sendable`. The only differences between +them are the compile-time checks applied by the compiler and the part of their +mangled names that indicates the feature being used: + +```text +define hidden swiftcc void @"$s4main2fnyyxs8SendableRzlF"(ptr noalias %0, ptr %T) + ^~~~~~~~~~~~~ 'T: Sendable' +define hidden swiftcc void @"$s4main2fnyyxlF"(ptr noalias %0, ptr %T) + ^ 'T' ('sending' is not indicated by the mangled name) +``` + +Thus, if there was a way to tell the compiler to continue using the mangled +name for `fn(_: T)`, a library designer could actually change the +declaration to be treated like `fn(_: borrowing sending T)` when compiling +with the new version of the library *without* breaking ABI compatibility. + +This is part of how the `@preconcurrency` attribute works. `@preconcurrency` +has two effects: It instructs the type checker to permit Swift 5 code to use +the declaration in ways that would violate the rules of certain concurrency +annotations, and it causes those annotations to be omitted from the +declaration's mangled name. That makes it perfect for retrofitting sendability +checking onto APIs that were created before Swift Concurrency was introduced. +However, it is designed specifically for that exact task, which makes it +inflexible: It cannot be used to suppress some concurrency features but not +others (for instance, to amend a mistake in one parameter without affecting +other parts of the declaration), and it cannot be applied to adopt +non-concurrency features which have the same property of being ABI-compatible +except for a different mangled name. + +For everything else, there's the compiler-internal `@_silgen_name` attribute. +`@_silgen_name` is an internal hack that overrides name mangling at specific +points in the compiler, replacing the mangled name with an arbitrary string. +If you know the original mangled name, therefore, you can use this attribute +to keep that name stable even if the declaration has evolved enough that it +would normally use a different name. That makes `@_silgen_name` enormously +flexible—it can be used to handle an arbitrary set of changes, and the +standard library uses it extensively for this purpose. For example, when the +standard library introduced a new `Collection.map(_:)` that used typed +`throws` in [SE-0413][], it continued to support clients expecting the old +`rethrows`-based `map` by using a `@_silgen_name` hack and +`@usableFromInline internal`: + +```swift +extension Collection { + // New `map(_:)` using typed `throws`: + @inlinable + @backDeployed(...) // slight lie, but that's irrelevant here + public func map( + _ transform: (Element) throws(E) -> T + ) throws(E) -> [T] { + // ...actual implementation of `map` omitted... + } + + // Wrapper with the same ABI as the old `map(_:)` which used `rethrows`: + @_silgen_name("$sSlsE3mapySayqd__Gqd__7ElementQzKXEKlF") + // ^-- func map<$T>(_: (Self.Element) throws -> $T) rethrows -> Swift.Array<$T> + // in Swift.Collection extension from module Swift + @usableFromInline + func __rethrows_map( + _ transform: (Element) throws -> T + ) throws -> [T] { // 'throws' and 'rethrows' have the same ABI + try map(transform) // calls through to the new `map(_:)` + } +} +``` + +This creates a declaration which is written in Swift source code as +`__rethrows_map(_:)`, but which has the mangled name of a function named +`map(_:)`. When a module is compiled against this new version of the standard +library, calls to `map(_:)` will use the new method directly; if a module is +compiled against an older standard library, though, it will end up calling the +`__rethrows_map(_:)` compatibility wrapper instead. + +Although it is a powerful tool, `@_silgen_name` has its own set of serious +drawbacks: + +* It has absolutely no compile-time safety checking. +* It works only with functions and is incompatible with certain function + features like opaque return types and `@backDeployed`.[1] +* It requires deep knowledge of the name mangling and calling convention to use + correctly. + +In practice, you basically need to be a Swift compiler or runtime engineer to +use it correctly. For this reason `@_silgen_name` has never been proposed to +Swift Evolution or recommended for general use. + +Library maintainers need a tool that is much more flexible than +`@preconcurrency` but also much safer and more ergonomic than `@_silgen_name`. + +> *[1] This is because the name mangling has facilities to create multiple +> symbols that are all related to the same declaration, but `@_silgen_name` +> only provides an override for the name of the main symbol. Any declaration +> that requires more than one symbol—such as a type declaration, a function +> with an opaque return type, or a function with a back-deployment +> thunk—would have no way to generate a mangled name for these additional +> symbols.* + + [SE-0413]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0413-typed-throws.md#effect-on-abi-stability + +## Proposed solution + +We propose a new attribute, called `@abi`, which specifies an alternate +declaration that provides its ABI for name mangling purposes. This alternate +declaration is enclosed within the argument parentheses; it has no body or +initializer expression but is otherwise a syntactically complete declaration. + +For example, the `@_silgen_name`-using `__rethrows_map(_:)` method shown in the +Motivation section could be written much more clearly by using `@abi`: + +```swift +extension Collection { + // Wrapper with the same ABI as the old `map(_:)` which used `rethrows`: + @abi( + func map( + _ transform: (Element) throws -> T + ) rethrows -> [T] + ) + @usableFromInline + func __rethrows_map( + _ transform: (Element) throws -> T + ) throws -> [T] { // 'throws' and 'rethrows' have the same ABI + try map(transform) // calls through to the new `map(_:)` + } +} +``` + +Notice how the `@abi` attribute basically contains the original version of the +declaration. When Swift is performing name mangling, this declaration is what +it will use; for all other functions, it will use the outer `__rethrows_map` +declaration. In particular, the `map(_:)` call in the body doesn't get resolved +to the `map(_:)` function in the `@abi` attribute; it looks for other +implementations and eventually finds the new typed-throws `map(_:)`. + +What's more, the ABI declaration can be checked against the original one to +make sure they're compatible. For example, at the ABI level `throws` and +`rethrows` are interchangeable, but a non-`throws`/`rethrows` method handles +its return values differently from them. If the maintainer accidentally dropped +the `throws` effect while implementing this function, the compiler would +complain about the mismatch: + +```swift +extension Collection { + @abi( + func map( + _ transform: (Element) throws -> T + ) rethrows -> [T] // error: 'rethrows' doesn't match API + ) + @usableFromInline + func __rethrows_map( + _ transform: (Element) throws -> T + ) -> [T] { // Whoops, should be 'throws' or 'rethrows'! + try map(transform) + } +} +``` + +This checking also makes sure that the details specified in the `@abi` +attribute are actually relevant. For example, the `@abi` attribute +automatically inherits the access control, availability, and `@objc`-ness of +the API it's attached to, so these are omitted from the `@abi` attribute. +Default arguments, too, are left out because they're irrelevant to ABI. The +compiler will diagnose this unnecessary information and suggest removing it. + +All sorts of precision changes are possible. Here's another use for a +`@_silgen_name` hack in the standard library: The maintainers discovered a data +race safety bug in an API that had already shipped and needed to add an +`@Sendable` attribute to prevent it, but `@preconcurrency` alone would have +also suppressed the `@Sendable` attribute on the parameter that had been +correctly annotated. `@abi` makes it easy to fix this sort of problem: + +```swift +public struct AsyncStream { + // ...other declarations omitted... + + @abi( + init( + unfolding produce: @escaping /* not @Sendable */ () async -> Element?, + onCancel: (@Sendable () -> Void)? = nil + ) + ) + @preconcurrency + public init( + unfolding produce: @escaping @Sendable () async -> Element?, + onCancel: (@Sendable () -> Void)? = nil + ) { + // Implementation omitted + } +} +``` + +Because `@preconcurrency` is applied to the outer declaration, but not to the +one inside the `@abi` attribute, its typechecking effects will be applied +(improving source compatibility for code written before the second `@Sendable` +was added) but its name mangling effects will not (keeping the mangled name +stable to preserve ABI compatibility). + +This feature goes beyond what `@_silgen_name` could do, however. For example, +it can be applied to `var` and `let` declarations: + +```swift +@abi(var oldName: Int) +public var newName: Int +``` + +The mangled name of an accessor includes the mangled name of the variable or +subscript it belongs to; thanks to `@abi`, the accessors for this variable will +have `oldName` mangled into their names. + +### Supported changes (and unsupported uses of them) + +This feature can be used to override the mangling of a declaration's: + +* Name, argument labels, and (for unary operator functions) fixity (`prefix` + vs. `postfix`) + +* Preconcurrency status, actor isolation (where this does not affect calling + convention), and execution environment + +* Generic constraints to marker protocols (`BitwiseCopyable`, `Copyable`, + `Escapable`, `Sendable`) + +* Certain aspects of parameter and `self` behavior (variadic (vs. `Array`); + `@autoclosure`; `sending`; ownership specifiers as long as the behavior is + compatible) + +* Certain aspects of argument, result, and thrown types (marker protocols in + existentials; tuple element labels; `@escaping`, `@Sendable`, and `sending` + results on closures) + +Note that some of these changes relate to safety properties of your code, such +as data race safety and escapability. When you use `@abi` to maintain ABI +compatibility with older versions of your library while tightening safety +constraints for new clients, you must take special care to remember that +clients compiled without those changes may violate the new constraints. In +practice, this means that you should probably only use `@abi` to make +retroactive changes to safety constraints when you know that violating the +constraint was *always* unsafe and it simply wasn't enforced until now. + +For instance, the `AsyncStream.init(unfolding:onCancel:)` example above adds +`@Sendable` to a closure parameter that previously didn't have the attribute. +This is appropriate because the closure was *always* run concurrently; code +that passed a non-`@Sendable` closure was already buggy, so this change merely +made the bug easier to detect. It would have been inappropriate if the closure +was originally run synchronously and was changed to run concurrently, because +code that previously worked fine would now have new data races. + +(`@abi` can still be used to help implement behavior changes, but the pattern +is different: you make the original version `@usableFromInline internal` and +change its API name to something you won't use by accident, applying `@abi` to +keep its mangled name the same as always. Then you create a new declaration +with the old API name and the behavior changes, using `@backDeployed` to ensure +that new binaries can interoperate with old versions of your library. +`__rethrows_map(_:)` is a good example of this pattern.) + +In short: Much like when `@inlinable` is used, **it is the developer's +responsibility to ensure that the current behavior of the declaration is +compatible with clients built against older versions of it. The compiler +doesn't understand the history of your codebase and cannot detect some +mistakes.** + +## Detailed design + +### Grammar + +An `@abi` attribute's argument list must have exactly one argument, which in +this proposal must be one of the following productions: + +* *function-declaration* +* *initializer-declaration* +* *constant-declaration* +* *variable-declaration* +* *subscript-declaration* + +This argument must *not* include any of the following sub-productions: + +* *code-block* +* *getter-setter-block* +* *getter-setter-keyword-block* +* *willSet-didSet-block* +* *initializer* (initial value expression) + +To that end, we amend the following productions in the Swift grammar to make +code blocks optional: + +```text + initializer-declaration → initializer-head generic-parameter-clause? + parameter-clause async? throws-clause? +- generic-where-clause? initializer-body ++ generic-where-clause? initializer-body? + + initializer-declaration → initializer-head generic-parameter-clause? + parameter-clause async? 'rethrows' +- generic-where-clause? initializer-body ++ generic-where-clause? initializer-body? + + subscript-declaration → subscript-head subscript-result generic-where-clause? +- code-block ++ code-block? +``` + +We don't need to worry about ambiguity in terminating these productions because +the block-less forms always occur in `@abi` attributes; its closing parenthesis serves as a terminator for the declaration. + +> **Note**: If future development of the `@abi` attribute requires additional +> information to be added to it, this can be done by adding new productions at +> the beginning of the argument list, terminated by a comma or colon to +> distinguish them from declaration modifiers: +> +> ```swift +> @abi(unchecked, func liveDangerously(_: AnyObject)) // Future direction +> func liveDangerously(_ object: AnyObject?) { ... } +> ``` + +### Terminology and basic concepts + +Syntactically, an `@abi` attribute involves two declarations. The *ABI-only +declaration* is the one in the attribute's argument list; the *API-only +declaration* is the one the attribute is attached to. + +```swift +@abi(func abiOnlyDeclaration()) +func apiOnlyDeclaration() {} +``` + +A declaration which does not involve an `@abi` attribute at all—that is, which +is neither API-only nor ABI-only—is called a *normal declaration*. + +There are two *ABI roles*: + +* An *API-providing declaration* determines the behavior of the declaration in + source code: what name developers write to address it, what constraints and + behaviors are applied at use sites, how it is implemented (its body, + accessors, or members), etc. + +* An *ABI-providing declaration* determines how the declaration affects mangled + symbol names--both its own name and any names derived from it. + +Every declaration has at least one of these roles. Every declaration also has a +*counterpart* which fulfills the roles it does not. When the compiler wants to +compute some aspect of a declaration pertaining to a role that declaration does +not have, it automatically substitutes the declaration's counterpart. + +Roles and counterparts work as follows: + +| Declaration is… | ABI-providing | API-providing | Counterpart | +| --------------- | ------------- | ------------- | ------------------------------------------- | +| Normal | ✅ | ✅ | Is its own counterpart | +| ABI-only | ✅ | | Declaration `@abi` attribute is attached to | +| API-only | | ✅ | Declaration in `@abi` attribute | + +### Declaration checking + +When you use the `@abi` attribute, Swift validates various aspects of the +ABI-providing declaration in light of its ABI counterpart. An *aspect* is any +way in which the external appearance of a declaration might vary. Attributes +and modifiers are aspects, but so are the declaration's name, its result or +value types, its generic signature (if it has one), its parameter list (if it +has one), its effects (if it has any), and so on. + +#### Aspects with no ABI impact must be omitted + +Many aspects of a declaration only matter for an API-providing declaration; +they're irrelevant on a declaration that's ABI-only. These include: + +* Default arguments for parameters + +* Attributes which only affect compile-time checking or behavior, such as + `@unsafe`, `@discardableResult`, or result builder attributes + +* Certain attributes and modifiers which have ABI effects, but where the + compiler has been designed to inherit the ABI-providing declaration's + behavior from its API counterpart where needed: + + * `@objc` (and its ilk) and `dynamic`, including inference behaviors + * Access control modifiers and `@usableFromInline` + * `@inlinable` and other attributes controlling inlining + * `@available` and `@backDeployed` + * `override` + +These aspects are generally *forbidden* on an ABI-providing declaration. If +they are present, the compiler will diagnose an error and suggest they be +removed. + +In practice, this means that an `@abi` attribute is often significantly shorter +than the declaration it's attached to because it doesn't need to specify as +much information: + +```swift +@abi( + // Same signature as below, except `T` is not `Sendable`. + static + func assumeIsolated( + _ operation: @MainActor () throws -> T, + file: StaticString, + line: UInt + ) rethrows -> T +) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) // not needed in @abi +@usableFromInline // not needed in @abi +internal // not needed in @abi +static +func assumeIsolated( + _ operation: @MainActor () throws -> T, + file: StaticString = #fileID, // default argument not needed in @abi + line: UInt = #line // default argument not needed in @abi +) rethrows -> T { + ... +} +``` + +The intended workflow is that a developer can paste the entire original +declaration into an `@abi` attribute and the compiler will then tell them which +parts of it they should remove. + +#### Call compatibility + +For aspects which *do* have ABI impact, the compiler enforces that the +ABI-providing declaration is *call-compatible* with its API-providing +counterpart. Broadly, "call compatibility" means that, other than the mangled +names, the machine code generated to call the ABI-providing declaration would +be equally able to call its API counterpart. For instance: + +* Declarations are of the same fundamental kind (a `func` for a `func`, a `var` + for a `var`, etc.), so they expose the same basic capabilities and entry + points. + +* Effects match closely enough that the caller and callee will agree on which + stack should be used and which implicit parameters will be passed. + +* Inputs and outputs are passed similarly enough to ensure type and memory + safety, including compatible memory management behavior, and including the + implicit inputs and outputs used for generic parameters, `self`, and + throwing. + +However, call compatibility does *not* require aspects of the declaration +that only change the mangled name and/or compile-time checking to match: + +* Names, argument labels, or other name-like traits of a declaration (such + as the fixity modifiers for operator functions) may vary. + +* Aspects which affect only syntax (and possibly mangling) may vary. For + instance, a regular closure may be used instead of an `@autoclosure`; tuple + types with different element labels may be used; an array parameter may be + used instead of a variadic parameter; ordinary optionals may be used instead + of implicitly-unwrapped optionals; `throws` and `rethrows` may be used + interchangeably. + +* Concurrency safety and lifetime restrictions which don't affect the ability + to call the declaration may vary. For instance, a non-escaping closure may be + used instead of an `@escaping` closure; `sending` modifiers, `Sendable` + constraints, or neither may be used interchangeably so long as memory + management isn't affected; isolation may vary so long as extra data does not + need to be passed; `~Copyable` and `~Escapable` constraints may vary. + +#### Impact on redeclaration checking + +A declaration must have a unique signature in each of its roles. That is, an +API-only declaration is checked against API-providing declarations; an +ABI-only declaration is checked against ABI-providing declarations; a normal +declaration is checked twice, first against API-providing declarations and then +against ABI-providing declarations. + +In general, name lookup will return declarations with the API-providing role +and will ignore declarations with the ABI-providing role. Even when you're +writing an ABI-only declaration, you should use the API names of other +declarations, not the ABI names. + +#### Declaring multiple variables + +When `var` or `let` is used, the ABI-providing declaration must bind the same +number of patterns, each of which has the same number of variables, as its API +counterpart. That is, the first of these is valid, while the others are not: + +```swift +// OK: +@abi(var x, y: Int) +var a, b: Int + +// Mismatched: +@abi(var x, y, z: Int) +var a, b: Int + +// Also mismatched: +@abi(var x, y: Int) +var a: Int, (b1, b2): (Int, Int) + +// Mismatched even though the total adds up: +@abi(var x, y, z: Int) +var a: Int, (b1, b2): (Int, Int) +``` + +An ABI-providing declaration does *not* infer missing types from its API +counterpart. In practice, this means that an ABI-providing declaration +may need to explicitly declare types that its API counterpart infers from an +initial value expression. + +An ABI-providing `var` or `let` does not have a list of accessors or specify +anything about them; in a sense, it can be thought of as inferring its +accessors from its API counterpart. + +### Limitations on feature scope + +#### Supported declaration kinds + +In this proposal, `@abi` may be applied only to `func`, `init`, `var`, `let`, +and `subscript` declarations. Other declarations are less straightforward to +support in various ways; see the future directions section for details. + +#### Language features with auxiliary declarations + +`@abi` can neither contain, nor be applied alongside, `lazy` or a property +wrapper. These features implicitly create auxiliary declarations, and it isn't +clear how those should interact with `@abi`. + +#### Limited support for macros + +Neither attached nor freestanding macros can be used inside an `@abi` +attribute. None of the attached macro roles would be useful since ABI-providing +declarations do not have bodies, members, accessors, or extensions; the +freestanding macro roles, on the other hand, expand to complete declarations, +while some of the future directions involve supporting special stub syntax +which would be incompatible here. + +`@abi` can still be applied *alongside* an attached macro or *to* a +freestanding macro, although in practice many macros will need to handle `@abi` +attributes specially. + +### Non-normative: Precise rules as currently implemented + +To help evaluate how these principles will work in practice, we've listed the +current implementation's rules below. **However, we do not guarantee that the +rules listed here will exactly match the final behavior of the feature.** +Basically, we don't want to put every bug fix through an amendment or every +tiny, straightforward expansion of capabilities through a proposal. + +#### Must be omitted (no ABI impact or inheritance in place) + +* Default arguments on parameters +* Result builder attributes on parameters or declarations +* `@available` +* `@inlinable`, `@inline`, `@backDeployed`, `@usableFromInline`, + `@_alwaysEmitIntoClient`, `@_transparent` +* Objective-C opt-in attributes (`@objc`, `@IBAction`, `@IBDesignable`, + `@IBInspectable`, @IBOutlet`, `@IBSegueAction`, `@GKInspectable`, + `@NSManaged`, `@nonobjc`) +* `optional` modifier in `@objc` protocols +* `@NSCopying` +* `@_expose` and `@_cdecl` +* `@LLDBDebuggerFunction` +* `dynamic` modifier and `@_dynamicReplacement` +* `@specialize` on functions and initializers +* `override` modifier +* Access control (`open`, `public`, `package`, `internal`, `fileprivate`, + `private`) +* Setter access control (`open(set)`, `public(set)`, `package(set)`, + `internal(set)`, `fileprivate(set)`, `private(set)`) +* `@_spi` and `@_spi_available` +* Reference ownership (`weak`, `unowned`, `unowned(unsafe)`) +* `@warn_unqualified_access` +* `@discardableResult` +* `@implementation` on functions +* `@differentiable`, `@derivative`, `@transpose` +* `@noDerivative` on declarations other than parameters +* `@exclusivity` +* `@safe` and `@unsafe` +* `@abi` +* Unsupported features (`lazy`, property wrapper attributes, attached macro + attributes) + +#### Must be specified and must match + +* Declaration kind (`func`, `var`, etc.) +* `convenience` and `required` modifiers on initializers +* `distributed` modifier +* Result type of functions +* Failability of initializers +* Value type of subscripts and variables +* Number of parameters on functions, initializers, and subscripts +* Parameter types +* `inout` on parameters and `mutating`/`nonmutating` modifiers on members +* `@noDerivative` on parameters +* `@_addressable` on parameters and `@_addressableSelf` on members +* `@lifetime` attributes (NOTE: this probably ought to be "vary with + constraints", but an interaction with `@_addressableForDependencies` needs + to be worked out) +* `async` effect +* Aspects of types which are not listed elsewhere + +#### Allowed to vary, but with constraints + +* `throws` effect and thrown type (`rethrows` is equivalent to `throws`) +* Generic signature of functions, initializers, and subscripts (marker + protocols may vary) +* Variadic parameter types (`T...` and `Array` are treated as equivalent) +* Parameter ownership specifiers and `self` ownership modifiers (ones with + equivalent memory management behavior may be substituted for one another) +* `sending` on parameter and result types (so long as its ownership behavior + is preserved) +* `static`, `class`, and `final` modifiers (`class final` is equivalent to + `static`) +* Actor isolation (other than `@isolated(any)`, which is incompatible with + the others) + +#### Allowed to vary arbitrarily + +* Base names of functions and variables +* Argument labels and parameter names +* `prefix` and `postfix` modifiers on operator functions +* Whether optionals are implicitly unwrapped +* Element labels in tuple types +* `@autoclosure` on parameters +* `@escaping` on closures +* `@Sendable` on closures +* `isolated` on parameters +* `_const` on parameters and variables +* Generic parameter names +* Use of type sugar (e.g. `Optional` and `T?` are equivalent) +* Use of generic types that have a same-type constraint (e.g. in the + presence of `T == Int`, `T` and `Int` are equivalent) +* Use of marker protocols in existential types +* `@Sendable` on functions and initializers +* `@preconcurrency` +* `@execution` +* Aspects of declarations which are not listed elsewhere + +## Source compatibility + +This feature is additive and affects only the ABI. However, many of the changes +that can be effected using it can be source-breaking unless done with care. For +example: + +* When renaming a declaration, make sure there's a declaration with the + original name that can be called in the same situations, and consider using + `@backDeployed` to ensure recompiled clients don't have to raise their + minimum deployment target. + +* When a type changes, make sure that it will either become more broad, or that + you are willing to accept any breakage that results. For example, switching + from a `Sendable` constraint to a `(borrowing) sending` parameter strictly + increases the set of valid callers, so that's probably always okay; switching + from no constraint to a `Sendable` constraint, on the other hand, will break + some callers, but might be acceptable if the missing `Sendable` constraint + created an opportunity for data races. + +## ABI compatibility + +This feature is intended to give libraries additional options to evolve APIs +without breaking the corresponding ABIs. + +We are currently evaluating adoption in `stdlib/public`. So far, it looks like +we can replace all uses of `@_silgen_name` to specify mangled Swift names with +uses of `@abi`, and in some cases remove hacks; this will be roughly 75 +declarations. + +Note that this feature does not subsume the use of `@_silgen_name` with an +arbitrary, C-style symbol name to either declare a function implemented in +C++ using the Swift calling convention, or to generate a symbol that's easy to +access from C-family code or a compiler intrinsic. About 200 of the uses of +`@_silgen_name` in `stdlib/public` are of this type; we expect these to remain +as-is. + +## Implications on adoption + +This feature is intended to help ease the adoption of other new features by +allowing a declaration's ABI to be "pinned" to its original form even as it +continues to evolve. Note that there is only ever a need to specify the +*original* form of the declaration, not any revisions that may have occurred +between then and the current form; there is therefore never a reason you would +need to specify more than one `@abi` attribute, nor to tie an `@abi` attribute +to a specific platform version. + +In module interfaces, the `@abi` attribute is partially suppressible. +Specifically, for `func`s that do not use `@backDeployed` and do not have +opaque result types, the compiler emits a module interface that falls back to +using an equivalent `@_silgen_name` attribute. For other declarations, however, +the compiler falls back to an `@available(*, unavailable)` attribute instead, +with a message indicating that the developer will need a newer compiler to use +the declaration. + +## Future directions + +### Unchecked mode + +There may be situations where a skilled engineer knows that a specific use of +`@abi` is compatible, but the compiler does not know how to prove that. While +that can often be considered a compiler bug—the checker *should* be able to +tell that the code is safe—it may be useful, either as a workaround or to +handle extreme edge cases, to be able to turn off `@abi`'s compatibility +checking: + +``` +@abi(unchecked, func liveDangerously(_: AnyObject)) +func liveDangerously(_ object: AnyObject?) { ... } +``` + +### Support for types and extensions + +It ought to be possible to use `@abi` with types: + +```swift +@abi(struct Buffer: ~Copyable) +public struct FrameBuffer: ~Copyable { ... } + +@available(*, unavailable, renamed: "FrameBuffer") +@abi(typealias FrameBuffer) // keeps `typealias Buffer` from colliding with `struct Buffer` +typealias Buffer = FrameBuffer +``` + +Here, the library maintainer discovered after shipping that the name `Buffer` +is too vague—clients didn't understand what it meant, and some of them even +had another type named `Buffer`. When they rebuild with the new version of the +library, they will get an error with a fix-it to change `Buffer` to +`FrameBuffer`, but it will still use the name `Buffer` at the ABI level so +that existing binaries don't break. This will apply not only to the type +itself, but also to its members and even to functions with a `FrameBuffer` in +their overload signature. + +Type renaming may create challenges for module interface source stability, +since a module interfaces could refer to a type by an older or newer name than +its current one. It might be possible to address this by making module +interfaces always refer to types by their ABI name. (This should be +non-breaking as long as it's introduced at the same time as `@abi` for types.) + +This could also be used to affect the inference of properties of other +declarations. Consider this example: + +```swift +@abi(protocol Component) +@preconcurrency @MainActor // Added after shipping +public protocol Component { ... } + +extension Component { + public func onEvent(_ handler: @Sendable @escaping () -> Void) -> some Component { ... } +} +``` + +The library maintainer decided after the fact that `Component`s should be +isolated to the main actor, but that broke some Swift 5 code, so they added +`@preconcurrency` for its typechecking effects. However, `@preconcurrency` then +got applied to `onEvent(_:)`, suppressing its `@Sendable` attribute, which +changed its ABI. Using `@abi` on `Component` should override the ABI effects of +`@preconcurrency` not just for `Component` itself, but for every declaration +nested inside it. + +To support this kind of inference, the compiler may need to add an inferred +`@abi` attribute when a declaration with both roles depends on one which +provides only one role. For instance, if a type conforms to a protocol whose +`@abi` attribute specifies different actor isolation or different marker +protocols, Swift may need to add an inferred `@abi` attribute so the type's ABI +will be compatible with the protocol's ABI while its API will be compatible +with the protocol's ABI. + +### Support for enum cases + +It would probably be possible to allow `@abi` to be attached to a `case` +declaration, allowing it to backwards-compatibly rename or otherwise control +the ABI of enum cases. + +### Support for auxiliary declarations + +It might be possible to allow `@abi` to be used with `lazy` and property +wrappers either by coming up with rules to derive an `@abi` attribute for those +declarations, or by creating a syntax that can specify them: + +```swift +@abi(nonisolated var currentValue: Int) +@abi(for: projection, nonisolated var $currentValue: Binding) +@abi(for: storage, nonisolated var _currentValue: Binding) +@MainActor @Binding var currentValue: Int +``` + +### Support for accessors + +It might be possible to allow `@abi` to be attached to individual accessors. + +### Support for context changes + +It might be possible to allow an ABI-providing declaration to belong to a +different context than its counterpart—for instance, turning a global variable +into a static property, or moving a method to a single-property `@frozen` +wrapper struct. + +A particularly interesting one might be allowing an extension member to +be mangled as a member of the main type declaration, or vice versa, since users +may not be aware of the ABI impact of moving a declaration from one to the +other. + +### Equivalent type attribute + +Many uses of `@abi` only change one or two types in a complicated declaration. +It might be possible to provide an `@abi` *type* attribute that can be applied +on the spot as a shorthand: + +```swift +public func runConcurrently( + _ body: @escaping @abi(() -> Void) @Sendable () -> Void +) { ... } + +// Equivalent to: +@abi(func runConcurrently(_: @escaping () -> Void)) +public func runConcurrently( + _ body: @escaping @Sendable () -> Void +) { ... } +``` + +### Support for moving declarations to a different module + +It might be possible to roll some of the functionality of the compiler-internal +`@_originallyDefinedIn` attribute into this attribute. + +## Alternatives considered + +### Many narrow features + +The original motivation for this proposal involved fairly narrow cases where +`@preconcurrency` was too blunt an instrument, such as suppressing the ABI +impact of one sendability annotation while leaving others intact. One could +imagine designing individual type or decl attributes for each specific change +one might wish to make, but this would require both a lot of effort from +compiler engineers to design a specific tool for each problem, and a lot of +effort from library maintainers to figure out which tool to apply to a given +task and how to use it. + +### An argument that describes the differences + +Rather than creating many totally separate features, one could imagine an +`@abi` declaration attribute with an argument list which somehow described the +differences between the API and ABI. However, we see use cases for changing +virtually every aspect of an API—its name; adding or removing declaration +attributes, modifiers, and effects; adding or removing inherited protocols and +generic constraints; changing parameter, result, and error types; changing type +attributes and modifiers; even changing individual sub-types within generic +types, function types, and protocol compositions at any position in the +declaration—and a mini-language to address and edit all of these different +aspects of a declaration seems difficult to design and tedious to learn. +By contrast, re-specifying the entire declaration in the argument reuses the +developer's existing knowledge of how to read and write declarations and gives +them an easy way to adopt it (just copy the existing declaration into an `@abi` +attribute before you start editing in new features). + +### Syntax where the two declarations are peers + +We could design this differently such that the API-only and ABI-only +declarations are peers in the same context: + +```swift +// This declaration provides the ABI... +@abi(for: __rethrows_map(_:)) @usableFromInline func map( + _ transform: (Element) throws -> T +) rethrows -> [T] + +// ...for this declaration +@usableFromInline func __rethrows_map( + _ transform: (Element) throws -> T +) throws -> [T] { + try map(transform) +} +``` + +In theory, this design could simplify parsing, since the `@abi` attribute's +argument might just be an ordinary expression. However, it introduces several +complications: + +1. Merely giving the name of a declaration may not be specific enough in the + presence of overloads, or even when the API and ABI have the same name but + slight type differences. We might normally tell developers to work around + this by using more specific names, but that's not really an appropriate + answer for a tool which is designed to allow fine control of API and ABI + naming. +2. A lot of compiler logic would have to be modified to filter out ABI-only or + API-only declarations when it walked through lists of top-level decls or + members. The current design, where the ABI-only declarations are tucked away + in attributes, keeps them from being accessed by accident. +3. If the future direction for `@abi` on type declarations is taken, the + productions for full type declarations will not be suitable, as they require + member blocks. + +## Acknowledgments + +Thanks to Holly Borla for recognizing the need for an `@abi` attribute. From e761386878a4e2be5b2428a5b95b77822575107d Mon Sep 17 00:00:00 2001 From: Becca Royal-Gordon Date: Tue, 1 Apr 2025 12:17:25 -0700 Subject: [PATCH 2/3] Take corrections from Ben Rimmington Co-authored-by: Ben Rimmington --- proposals/NNNN-abi-attr.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/proposals/NNNN-abi-attr.md b/proposals/NNNN-abi-attr.md index a72094ccca..010aa9a068 100644 --- a/proposals/NNNN-abi-attr.md +++ b/proposals/NNNN-abi-attr.md @@ -4,7 +4,7 @@ * Authors: [Becca Royal-Gordon](https://github.com/beccadax) * Review Manager: TBD * Status: **Awaiting review** -* Implementation: behind experimental feature `ABIAttribute` (refinements in [swiftlang/swift#79466](https://github.com/swiftlang/swift/pull/80383)) +* Implementation: behind experimental feature `ABIAttribute` (refinements in [swiftlang/swift#80383](https://github.com/swiftlang/swift/pull/80383)) * Review: ([pitch](https://forums.swift.org/t/pitch-controlling-the-abi-of-a-declaration/75123)) ## Introduction @@ -348,7 +348,7 @@ This argument must *not* include any of the following sub-productions: To that end, we amend the following productions in the Swift grammar to make code blocks optional: -```text +```diff initializer-declaration → initializer-head generic-parameter-clause? parameter-clause async? throws-clause? - generic-where-clause? initializer-body @@ -417,7 +417,7 @@ Roles and counterparts work as follows: ### Declaration checking When you use the `@abi` attribute, Swift validates various aspects of the -ABI-providing declaration in light of its ABI counterpart. An *aspect* is any +ABI-providing declaration in light of its API counterpart. An *aspect* is any way in which the external appearance of a declaration might vary. Attributes and modifiers are aspects, but so are the declaration's name, its result or value types, its generic signature (if it has one), its parameter list (if it @@ -607,7 +607,7 @@ tiny, straightforward expansion of capabilities through a proposal. * `@inlinable`, `@inline`, `@backDeployed`, `@usableFromInline`, `@_alwaysEmitIntoClient`, `@_transparent` * Objective-C opt-in attributes (`@objc`, `@IBAction`, `@IBDesignable`, - `@IBInspectable`, @IBOutlet`, `@IBSegueAction`, `@GKInspectable`, + `@IBInspectable`, `@IBOutlet`, `@IBSegueAction`, `@GKInspectable`, `@NSManaged`, `@nonobjc`) * `optional` modifier in `@objc` protocols * `@NSCopying` @@ -754,7 +754,7 @@ tell that the code is safe—it may be useful, either as a workaround or to handle extreme edge cases, to be able to turn off `@abi`'s compatibility checking: -``` +```swift @abi(unchecked, func liveDangerously(_: AnyObject)) func liveDangerously(_ object: AnyObject?) { ... } ``` From 496ec86f40b616fdb1957e4865007f22571a0467 Mon Sep 17 00:00:00 2001 From: Holly Borla Date: Fri, 11 Apr 2025 08:51:08 -0700 Subject: [PATCH 3/3] Assign SE-0476 and put into active review. --- proposals/{NNNN-abi-attr.md => 0476-abi-attr.md} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename proposals/{NNNN-abi-attr.md => 0476-abi-attr.md} (99%) diff --git a/proposals/NNNN-abi-attr.md b/proposals/0476-abi-attr.md similarity index 99% rename from proposals/NNNN-abi-attr.md rename to proposals/0476-abi-attr.md index 010aa9a068..a4e409cd26 100644 --- a/proposals/NNNN-abi-attr.md +++ b/proposals/0476-abi-attr.md @@ -1,9 +1,9 @@ # Controlling the ABI of a function, initializer, property, or subscript -* Proposal: [SE-NNNN](NNNN-abi-attr.md) +* Proposal: [SE-0476](0476-abi-attr.md) * Authors: [Becca Royal-Gordon](https://github.com/beccadax) -* Review Manager: TBD -* Status: **Awaiting review** +* Review Manager: [Holly Borla](https://github.com/hborla) +* Status: **Active Review (April 11 - April 25, 2025)** * Implementation: behind experimental feature `ABIAttribute` (refinements in [swiftlang/swift#80383](https://github.com/swiftlang/swift/pull/80383)) * Review: ([pitch](https://forums.swift.org/t/pitch-controlling-the-abi-of-a-declaration/75123))