SE-0476: Controlling the ABI of a function, initializer, property, or subscript
* Proposal: [SE-0476](0476-abi-attr.md) * Authors: [Becca Royal-Gordon](https://github.com/beccadax) * Review Manager: [Holly Borla](https://github.com/hborla) * Status: **Implemented (Swift 6.2)** * Review: ([pitch](https://forums.swift.org/t/pitch-controlling-the-abi-of-a-declaration/75123)) ([review](https://forums.swift.org/t/se-0476-controlling-the-abi-of-a-function-initializer-property-or-subscript/79233)) ([acceptance](https://forums.swift.org/t/accepted-with-modifications-se-0476-controlling-the-abi-of-a-function-initializer-property-or-subscript/79644))
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 `<T>`
to `<T: Hashable>` 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: Sendable>(_: T) {}
// `T` parameter must be `sending`
func fn<T>(_: 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: Sendable>(_: T)`, a library designer could actually change the
declaration to be treated like `fn<T>(_: 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<T, E>(
_ 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<T>(
_ 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`.<sup>[1]</sup>
* 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-stabilityProposed 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<T>(
_ transform: (Element) throws -> T
) rethrows -> [T]
)
@usableFromInline
func __rethrows_map<T>(
_ 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<T>(
_ transform: (Element) throws -> T
) rethrows -> [T] // error: 'rethrows' doesn't match API
)
@usableFromInline
func __rethrows_map<T>(
_ 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<Element> {
// ...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:
```diff
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 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
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<T>(
_ 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<T: Sendable>(
_ 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<T>` 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<T>` 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 elsewhereSource 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 funcs 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:
@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 = FrameBufferHere, 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:
@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 Components 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:
@abi(nonisolated var currentValue: Int)
@abi(for: projection, nonisolated var $currentValue: Binding<Int>)
@abi(for: storage, nonisolated var _currentValue: Binding<Int>)
@MainActor @Binding var currentValue: IntSupport 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:
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:
// This declaration provides the ABI...
@abi(for: __rethrows_map(_:)) @usableFromInline func map<T>(
_ transform: (Element) throws -> T
) rethrows -> [T]
// ...for this declaration
@usableFromInline func __rethrows_map<T>(
_ 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:
- 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.
- 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.
- If the future direction for
@abion 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.