SE-0497: Controlling function definition visibility in clients

Hello Swift community,

The review of "Controlling function definition visibility in clients" begins now and runs through October 27, 2025. The proposal is available here:

swift-evolution/proposals/0497-definition-visibility.md at main · swiftlang/swift-evolution · GitHub

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 the review manager by email or Swift Forums DM. When contacting the review manager directly, please include "SE-0497" in the subject line.

Trying it out

If you'd like to try this proposal out, you can use existing compiler-internal attributes with the same behavior:

SE-0497 spelling Internal spelling Requirements
@export(implementation) @_alwaysEmitIntoClient Swift 5.0 compiler or later
@export(interface) @_neverEmitIntoClient main snapshot toolchain

You can download a toolchain supporting @_neverEmitIntoClient here or by using swiftly install main-snapshot; any main development snapshot dated August 21, 2025 or later should do.

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

swift-evolution/process.md at main · swiftlang/swift-evolution · GitHub

Happy reviewing,

—Becca Royal-Gordon
SE-0497 Review Manager

12 Likes

I think overriding “inlineability” (which I have always understood as more of an implementation detail) with “export visibility” (something the user already has a grasp of how to wield) is a fantastic change in the short and long term.

Is the information from this macro going to be visible to e.g. the swift package manager? The intro section does a great job of explaining the invisible implementation details that result from the lack of the more fine-grained dependency info this proposal establishes. I think most package managers wouldn't be able to make use of that info, but I know spack regularly incorporates declared visibility information (e.g. static or shared, whether to enable a specific -D argument) declared by the packages it builds, and incorporates that into its ASP logic program to resolve against.

I suspect that adding a package manager into the mix would complicate this a lot. But I did just want to note that more granular dependency info is useful for much more than just build performance!

2 Likes

Quick review management note: I’ve pushed the ending date of this review back a week to correct a scheduling mistake. It will now end on October 27, 2025.

2 Likes

I did a quick peruse of the proposal. One thing I noticed is the overlap between this and @inlinable and @usableFromInline. Using export in the way specified in the proposal is to my mind a lot more straight forward to understand.

Has there been any discussion about deprecating inlinable/usableFromInlineable in favor of these attributes at some point in the future (of course with an appropriate migrator tool to automate the conversion).

I’ve been reading through this and thinking about how we would use these attributes in Foundation and similar ABI stable libraries (with evolution enabled). My initial reaction was that the table had a wide variety of possibilities that made it challenging to understand what combination of attributes I should be picking for a particular desired behavior, and I have a few questions based on this:

@export(interface, implementation)

Do you have an example of when someone would want to use this attribute? In particular, it looks like it is very similar to @inlinable today, which in ABI stable frameworks like Foundation we’ve found extremely problematic. @inlinable leaves the decision of using a client-emitted copy vs. an ABI entry point up to the compiler when building clients, which makes answering “where is the ABI boundary in this code” ambiguous and hard to determine. In general, we’ve almost always regretted using this instead of @_alwaysEmitIntoClient or no attribute/@usableFromInline. It sounds like this is being introduced to support Embedded Swift where @inlinable is even more intent-based rather than explicit. If there aren’t good use cases for framework authors to use this attribute (unless I’m missing use cases), would it be possible to avoid perpetuating the problems with @inlinable further by preventing usage of @export(interface, implementation)when library evolution is enabled? That alone for non-Embedded developers would greatly simplify the decision tree and (IMO) guide library authors in a better direction (I’m trying to think through what this table would look like for library evolution authors with the requirements that brings and finding that the table has a lot of overlap/ambiguity that would be great to reduce).

@inlinable and @usableFromInline

I saw in the pitch thread that you suggested not deprecating these attributes because they express intent over explicit instructions. I think it would be a good step to revisit this decision. IIUC, in non-Embedded Swift @inlinable is equivalent to @export(interface, implementation) and @usableFromInline is equivalent to @export(interface) on non-public declarations. Is this understanding correct? If so, we could reduce a lot of the confusion around which attributes to use by deprecating these (perhaps only in non-Embedded mode) in favor of the new attributes. I can see why a straightforward deprecation for Embedded swift may not make sense if the behaviors are different, but for non-Embedded more (or just library evolution mode) should these be deprecated to provide clarity? In general, having multiple attributes that mean the same thing can be confusing, but especially when it concerns where an ABI boundary is for a library that confusing nature can lead to shipping bugs that are hard to recover from.

1 Like

While I sympathize with wanting to simplify things, I think that deprecating @usableFromInline and @inlinable just in non-Embedded would potentially increase complexity and confusion rather than reducing it, especially for developers that need to write Embedded-agnostic libraries. The effect of @exported(interface) in Embedded does not seem appropriate as a drop-in replacement for every declaration in existing libraries that use @usableFromInline today, but such a deprecation would encourage over-using it and make it harder to switch back to @usableFromInline when being prescriptive about linkage creates unnecessary optimization barriers.

I think the analogies to @usableFromInline and @inlinable are mostly useful to illustrate the concept, but shouldn't be understood as suggesting that @export(interface) is actively recommended as a substitute for @usableFromInline in most code. In my mind, library developers should continue to prefer @usableFromInine for scenarios where augmenting access control is necessary and only reach for @export(interface) when they know that they need to guarantee that a symbol is emitted.

I can understand where you’re coming from for the Embedded-agnostic case so that’s a fair point that splitting the diagnostic on this mode may just cause more problems. My sentiment is actually probably more related to libraries with evolution enabled than just non-Embedded libraries, so I’m not sure if changing things based on library evolution would be any better.

To help me understand more, do you have examples of when it’d be preferred for a library developer to use @usableFromInline instead of @export(interface)? I think in general with library evolution enabled I’d never want to use @usableFromInline over @export(interface) since we care a lot about where our ABI boundary is and knowing when symbols are emitted is crucial. Since @usableFromInline can sometimes (always with LE, but IIUC not always in Embedded) emit a symbol, library developers need to treat it as if it always emits a symbol because changing the symbol is ABI breaking for clients that depend on it. For example even in Foundation, we can’t change @usableFromInline declarations even if no inlinable declarations call it because there may have been uses in old, now-removed inlined code that clients still have in their binaries today. In that regard, it seems equivalent (and therefore somewhat ambiguous when deciding what to use) with @usableFromInline. Are there situations that I’m not thinking of, or is the difference more apparent for embedded libraries only (or libraries without evolution enabled only)?

Let's take an example from a library you are familiar with, swift-foundation:

extension AttributedString.CharacterView: BidirectionalCollection {
    // ...
    @available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)
    @usableFromInline
    internal var _count: Int {
        _characters.count
    }
    // ...
}

The _count property here has been annotated with @usableFromInline because it is used from the body of the getter for public var count, which has been marked @_alwaysEmitIntoClient. When Foundation is built with library evolution, exposing an ABI symbol for _count is necessary to allow the implementation of count to be emitted into the client and @usableFromInline guarantees that this will be the case when library evolution is enabled.

Now suppose that in the future Foundation is updated to be compatible with Embedded Swift. Embedded Swift does not support library evolution today, though in the future it possibly could. Regardless, libraries will often be compiled without library evolution in Embedded Swift because users of Embedded Swift often want to build a completely standalone binary that links all dependencies statically. Now imagine you replaced this @usableFromInline attribute with @export(interface). If you did that, you would be asserting to the compiler that no matter how Foundation is being compiled, there must always be an exported symbol for _count. Why would that be desirable? When Foundation is compiled for Embedded Swift without library evolution, what purpose would it serve for _count to always have an exported symbol? I think it would simply inhibit cross module optimization for no concrete benefit.

With the scenario above in mind, I think this question may be backward; it's the concrete use cases for @export(interface) that are harder to understand and need more elaboration/justification, in my opinion.

I may be misreading what you're saying, but it sounds like you might be under the impression that @usableFromInline does not always guarantee that a symbol be emitted under library evolution. That isn't the case; under library evolution @usableFromInline and @export(interface) are functionally equivalent as I understand it. But they aren't equivalent outside of library evolution, and that's why @export(interface) is not a drop-in replacement, especially for code that needs to be compiled in a variety of configurations.

1 Like

Thanks for the concrete example. That definitely helps and think I understand the differences here better now. It does indeed seem like @usableFromInline is what we’d want then, and we’d reach for @export(interface) not just when we need to guarantee a symbol is emitted from the perspective of library evolution, but when we want it to be forced to be used even on non-evolution/embedded modes (which does seem more rare).

I think, perhaps, then my main worry is really just about @export(interface, implementation)/@inlinable since that’s been so problematic for libraries with LE enabled and it’d be great to avoid propagating similar issues with new attributes. Are there any situations similar to the above that would make it problematic to discourage this use (via deprecation or otherwise) when library evolution is enabled? In other words, are there any situations where libraries with evolution enabled should use one of these over @usableFromInline or @export(interface)/@_alwaysEmitIntoClient or should we take this new attribute as an opportunity to discourage wide use beyond specific required scenarios?

Can you be more specific about what has been problematic?

I think @inlinable continues to have a clear use case even if this proposal is accepted. For developers of libraries with ABI stable distributions, @inlinable can be used to begin exposing the implementation of an existing ABI stable function after its original introduction. This permits improving performance without breaking ABI stability. For Embedded builds of the same library, though, there is no inherent need to mandate the existence of an exported symbol.

@export(interface, implementation) is not a suitable drop-in replacement for @inlinable for this reason. I expect that scenarios which call for the specific semantics of @export(interface, implementation) to be especially rare because in Embedded Swift this attribute would mandate the emission of a symbol while simultaneously not offering encapsulation benefit.

@Douglas_Gregor In Alternatives Considered, the proposal says:

The @inlinable attribute already exists and is equivalent to the proposed @export(interface, implementation)

I think that ought to be updated for the reasons I've explained above. They're only equivalent in the context of library evolution.

EDIT: After discussing with Doug just now, I'd actually like to recommend we remove @export(interface, implementation) as an accepted spelling because it's hard to imagine a use case for it. It seems like accepting this spelling mainly just attracts additional confusion over whether @export is actually intended to subsume @usableFromInline and @inlinable, which it is not despite the fact that in library evolution mode it offers equivalent functionality.

Thank you for finding that, I thought I'd removed all of the implications from the proposal text. I put up a pull request to update the wording.

I think I agree. We don't have a solid use case for this beyond "it's what @inlinable does but consistently across all compilation models", which isn't actually a great motivation.

The funny thing is, that when we take away @export(interface, implementation), I start to want to go back to my original proposal with @inlinable(only) and @inlinable(never).

Doug

1 Like

Ah thanks for calling this out. I was thinking purely about when introducing a brand new declaration and not about cases when evolving an existing declaration. In fact, I have used this technique for AttributedString to take a non-inlinable function and make it @inlinable later to allow inlining into the client without breaking ABI. So I agree that is a good use case, it’s mainly problematic when introducing a new declaration because in those cases we’d much rather declare it as @_alwaysEmitIntoClient or not inlinable at all to make the ABI boundary clearer. But, the distinction between those isn’t represented in the language and instead comes down to our guidance when introducing APIs.

If @export(interface, implementation) is prohibited for now (until a better use case is determined), I think that’d definitely simplify the decision tree a little bit. I think that means that overall the guidance for library evolution authors would boil down to:

  • If the function should be an ABI boundary / part of the ABI:
    • If public, add no annotation
    • If internal, add @usableFromInline
      • If the library is also built without evolution and the ABI entry point is required in that scenario as well, use @export(interface) instead
  • If the function should not be part of the ABI:
    • Use @export(implementation)
  • If the function is pre-existing ABI but we want to enable inlining into clients in a later version:
    • Add @inlinable

And as a separate decision, library authors can add @inline(always) or @inline(never) to the declaration to force or prevent inlining the body into callers that can see the implementation (depending on which of the attributes above is used).

Does that align with your expectations of what we’d recommend library authors to use?

2 Likes

Yes, I agree 100% with your decision tree description.

Yes, I agree that introducing new APIs as @inlinable doesn't make sense most of the time for libraries with evolution enabled. Why take on an additional ABI liability if you're already committed to exposing the implementation from the beginning?

1 Like

Hello Swift community,

Based on review feedback so far, the authors have revised the proposal to remove the combined @export(interface, implementation) form of the attribute and explicitly forbid the use of more than one @export attribute on a single declaration. Adding this functionality is now discussed as a future direction. You may read the current text of the proposal at its original location or look at the PR to see what has changed.

Since this revision simply removes a feature from the proposal and is not expected to be controversial, the review will still end on October 27, 2025 as previously scheduled.

Thanks,

—Becca Royal-Gordon
SE-0497 Review Manager

3 Likes