Contents

SE-0476: Controlling the ABI of a function, initializer, property, or subscript

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.

  1. To replace an existing declaration with a source-compatible but ABI-breaking

equivalent, like replacing a rethrows method with one using typed throws.

  1. To correct a mistake, like removing an unnecessary @escaping attribute or

adding a Sendable generic constraint.

  1. 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:

// `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:

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:

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:

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-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:

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:

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:

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:

@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:

 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:

@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.

@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:

@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:

// 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 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 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 = 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:

@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: 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:

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:

  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.

  1. 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.

  1. 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.