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

Hello, Swift community!

The review of SE-0476: Controlling the ABI of a function, initializer, property, or subscript begins now and runs through April 25th, 2025.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager by email or DM. When contacting the review manager directly, please put "SE-0476" in the subject line.

Trying it out

If you'd like to try this proposal out, you can download a toolchain supporting it for Linux, Windows, or macOS using a recent main development snapshot from Swift.org - Download Swift and enabling the ABIAttribute experimental feature. Note that there are several bug fixes under development.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at:

https://github.com/swiftlang/swift-evolution/blob/main/process.md

Thank you,

Holly Borla
Review Manager

18 Likes

+1, very excited for this proposal! It helps address real problems that I definitely agree are worth addressing, and with an elegant solution.

  • Does this proposal fit well with the feel and direction of Swift?

In offline discussions it's come up a few times that it feels a bit strange, syntactically, to write a full declaration within parentheses. Are there any prior precedents for that?

We're generally more used to writing declarations within braces — were trailing braces considered as an alternative syntax, and/or would that be compatible with how attribute syntax works today? For example:

@abi { var oldName: Int }
public var newName: Int

I adapted some other examples from the proposal to sketch that out a bit more:

Examples using trailing braces instead of parentheses
@abi { func abiOnlyDeclaration() }
func apiOnlyDeclaration() {
  ...
}
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(_:)`
    }
}
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
    }
}

The example below specifically stood out to me, because the placement of the comma almost makes it look (at a glance) like two comma-separated arguments, instead of a single, contained declaration:

@abi(var x, y: Int)
var a, b: Int

// versus

@abi { var x, y: Int }
var a, b: Int

I'm curious if there's any technical or legibility concern with that, especially when considering some of the future directions that might add additional parameters to the attribute:

@abi(unchecked, var x, y: Int)
var a, b: Int

// versus

@abi(unchecked) { var x, y: Int }
var a, b: Int

A related observation that didn't occur to me at first, but I noticed while collecting these examples: since @abi() attributes don't contain bodies/implementations of declarations, braces never or rarely appear inside them. That also seems like a legibility benefit in favor of using trailing braces for @abi instead, helping differentiate the attribute's syntax from the contained declaration's syntax.

15 Likes

Great points! I also find the parentheses style a bit visually ambiguous, especially in the @abi(var x, y: Int) case. Braces feel more natural for wrapping declarations and help with clarity. Curious to hear if that tradeoff was explored!

It would be a significantly larger deviation from the current attribute grammar, but having a declaration there is already somewhat tricky for the parser—it’s tricky for the parser to recover from a malformed @abi attribute.

What bugs me more about it is that curly braces normally allow 0…infinity declarations, where here you will have to have exactly one declaration. (I also just sort of think it’s ugly.That’s very subjective, though.)

True. If we think this is a serious problem, we could limit @abi to single-variable pattern bindings; honestly, it would simplify the implementation a bit.

4 Likes

This is most excellent! I've committed a horrendous number of @_silgen_name APIs because how we evolve the Concurrency library to always adopt the latest and greatest way to spell some property of a function, add some parameter etc etc. So I'm very much the target audience here :slight_smile:

I like how we explain this as ABI and API providing declarations, it's quite clean and the checking between the decls is great. Gives us much more safety than the (no safety) silgen names.

I like the way this is spelled. The strings we use with silgen names are unmaintainable and very difficult to work with. It is also not something I'd like to show to a non-Swift-language developer if they'd ever have to keep ABI, it is just too low level for normal day to day development.

I'm also very interested in this shape of @abi() for method versioning which is something I'd want to look into some day but is unrelated to this proposal. I do think this sets fantastic groundwork for any such "here's the base version of the function" thanks to just being able to spell out the signature in the attribute.


A bit on the implementation side, but I'd like to understand how to evolve this attribute as we add new language features. Generally adding attributed or keywords we decide if they influence ABI or not etc. Do I understand the PR right that it is enough to do the usual introduction of new attrs in DeclAttr.def for the @abi() to understand its impact, and e.g. ban it from the ABI providing bit. Or is there some new places we'll have to update for @abi() to understand that?

Overall very excited for this and didn't notice anything worrying AFAICS, thanks for the great work on this!

2 Likes

Being able to correct missteps or fix things that have shipped is huge! There are plenty of areas in which we have introduced features post-facto that would be best to apply retroactively to functions (like typed throws) - this opening that window to make it easier to apply is a wonderful step forward and will have a ton of impact not just for the things that need updating but ALSO it will aide us in evolving things from here on out.

I don't think the audience size of this should be under-estimated; adding this will change the bugs like "this function is missing typed rethrows" from un-approachable to something that might very well be approachable for a newcomer to standard library and concurrency library contributions.

3 Likes

Would it ever make sense to have two @abi properties on a single declaration? Or for a symbol in one namespace to be ABI-aliased to a symbol in another namespace?

I share some of the concerns from the pitch thread that this syntax is difficult to read. If it is reasonable for a single symbol to have multiple @abi declarations, perhaps it would make sense to borrow the design of Objective-C’s @compatibility_alias:

extension Collection {
    // Wrapper with the same ABI as the old `map(_:)` which used `rethrows`:
    @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(_:)`
    }

    // Re-export the wrapper under the old ABI name.
    public compatibilityalias func map<T>(_ transform: (Element) throws -> T) rethrows -> [T] = __rethrows_map
}

extension Optional {
    // Perhaps a future Swift has decided to conform Optional to Collection.
    // Now we can re-export the wrapper implementation from Collection as the old Optional.map.
    public compatibilityalias func map<T>(_ transform: (Wrapped) throws -> T) rethrows -> [T] = Collection.__rethrows_map
}
1 Like

The current implementation is that there are flags you use in DeclAttr.def to define an attribute's @abi behavior:

  • ForbiddenInABIAttr: corresponds to the proposal's "Must be omitted" category
  • EquivalentInABIAttr: corresponds to "Must be specified and must match"
  • UnconstrainedInABIAttr: corresponds to "Allowed to vary arbitrarily" or, if extra logic is hand-implemented, "Allowed to vary, but with constraints"
  • UnreachableInABIAttr: attributes that can't be applied to any of the decl kinds that can have @abi on them (many of these are type-only and would have to be defined if we added support for @abi on types)

There's a static assert in the compiler source which ensures that every DeclAttr.def entry specifies exactly one of these flags. The upshot is, it's impossible to define a new attribute without thinking about its @abi behavior.

It's also worth noting that EquivalentInABIAttr uses the attribute's isEquivalent() method as its test; this is also something engineers are required to implement for non-simple attributes.

(For other kinds of additions or changes to the language, we unfortunately must rely on compiler engineers remembering to update the code.)


I don't think so. If we wanted to have a way to alias arbitrary declarations, I think the natural implementation would be to add an aliasing feature which used the mangling natural to the alias declaration and then allow @abi to be applied to that alias:

public enum TextAlignment {
    case left, center, right

    // Originally `auto`, now `justified`
    @abi(case auto)    // note: @abi doesn't support case yet; it's just a good use case for explicit aliasing
    case justified
    
    // Alias for source compatibility
    @available(macOS, deprecated: 15, renamed: "justified")
    @abi(case __auto_alias)
    alias case auto = justified
}

The (admittedly compiler-internal) @_originallyDefinedIn attribute can be used to move a declaration from one module to another without breaking the ABI of the old module; I'm not sure why else you would want to do this, and I don't think we could design that capability without a use case in mind.

Excellent, that was the bit I was actually worried about. Thanks for explaining the attributes, that makes sense.

There's going to be more complex attributes which will need more special case, but at least simple ones in there will be handled this way, that's nice, thank you.

Yeah. There are a few of those now, and I had a lot of success with simply making them UnconstrainedInABIAttr and then adding code to ABIDeclChecker that looked at the declaration holistically and validated the overall semantic.

1 Like

Yes, please! A thousand times yes! Refactoring public interfaces in libraries is going to be so much easier.

Thank you to everyone who participated in this review discussion! The proposal has been accepted with modifications:

Holly Borla
Review Manager

1 Like