As part of the @always(inline) discussion about controlling the optimizer's inlining, the idea of formalizing the underscored @_alwaysEmitIntoClient came up. Separately, the Embedded Swift Linkage model discussion discussed cases where treating everything as inlinable everywhere causes problems.
Here is a proposal to formalize the behavior of @_alwaysEmitIntoClient and also its opposite, @_neverEmitIntoClient. The full proposal text is here, but the specific proposal can be summed up as introducing two modifiers on the existing @inlinable attribute:
@inlinable(only): means that the function definition is inlinable and can only be used by inlining. Practically speaking, this means that a client has must emit its own definition of the function in order to use it, because the defining module does not emit a copy as a public, callable symbol. This spelling formalizes @_alwaysEmitIntoClient.
@inlinable(never): means that the function definition is never available to callers, even if the compiler options (aggressive cross-module optimization, Embedded Swift, etc.) would make it so by default. The defining module will emit a public, callable symbol that the client can use. At the time of this writing, the Swift main branch provides this behavior with the @_neverEmitIntoClient attribute.
I Googled the difference between the @inline(never) and the underscored @_neverEmitIntoClient that this would rename, and this seems like a good explanation of the distinction (and I’ve renamed the attribute to match the pitch for clarity’s sake):
Example scenario
Imagine you have a private helper function _expensiveCalculation() inside a library.
// A private helper function
@inline(never) // Prevents the local compiler from inlining it to keep binary size down
@inlinable(never) // Ensures the implementation is not copied into the client
internal func _expensiveCalculation() -> Int {
// A long, complex function body
return ...
}
// An @inlinable function that can be inlined into a client
@inlinable
public func publicApi() -> Int {
// Calls the internal helper function
return _expensiveCalculation()
}
In this scenario:
The @inlinable attribute on publicApi() allows its implementation to be inlined into a client's code.
However, the call to _expensiveCalculation() will not be inlined because of the @inlinable(never) attribute.
Instead, the client's binary will contain a call to the external _expensiveCalculation() symbol in the library.
The @inline(never) is redundant here for cross-module calls, but it could further prevent the compiler from inlining it even within the library itself.
I too think it would be a mistake to have the attributes @inlinable(...) and @inline(...) be spelled so similarly. The discussion in the thread on @inline(always) had mention of @_alwaysEmitIntoClient being somewhat orthogonal to @inline, but the pitched spellings here feel like a 5° difference than a 90° one—so close that they're almost parallel if you squint at them.
I'd rather we use a name that conveys the notion of "this will never have an ABI entry point" vs. "this will always and only have an ABI entry point". @Andrew_Trick 's suggestions above seem like a reasonable alternative.
I’ll counterpropose a syntax I’ve brought up a few places before for @inlinable(always): @abi(none). Since this attribute only affects the ABI, adding it to the @abi attribute keeps related concerns syntactically similar. It also naturally conveys to the reader that it can be safely ignored if you’re not trying to understand the ABI of a module. And it’s hopefully clear that it is incompatible with the @abi(func foo() syntax that defines a different ABI name for the symbol (since there is no actual ABI being produced).
The motivation for @inlinable(never)seems to be based around three use cases:
Allowing external linking. This seems better served by other language/ecosystem features, like SwiftPM/dynamic linking, public symbols, and @c @implementation. I’m don’t see how these existing features are insufficient to cover this use-case.
Stabilizing the public ABI across code changes. This feels better placed at the build system level (like we have today with library evolution). Manually identifying places in your code that need to be marked as non-inlinable externally seems like a recipe for extremely fragile builds. Maintaining ABI stability using library evolution mode is already quite challenging, and having to manually spell out the constraints would make it even more complex.
Build performance hacks. This use case feels very clearly like the responsibility of the build system to configure. For example, it could maintain a memory of which function implementation changes led to the longest incremental compiles and use that to configure the compilation process to temporarily stabilize those functions to speed up future compilation. This kind of thing would not need any sort of source-level feature to activate it and would therefore keep the source code focused on implementation rather than configuration.
Finally, the table at the bottom of the document does a good job of explaining the different combinations of @inline and @inlinable, but it does not explain when each of those combinations would be applicable. I think if we had a better idea of the value of each one, it could inform a different syntax that leaves fewer different behaviors for users of the language to understand while preserving the most useful options.
Those can work. The equivalent to today's @inlinable would then be @export(interface, implementation), I presume?
@Joe_Groff had separately pointed out that we already have a spelling for "exported out of the module": public. We could view this as being related to access control, e.g., public(interface) and public(implementation). We might just be trading one misleading association (inlining) with another (access control).
It's more than ABI, though, and people use @inlinable (one of the things subsumed here) as a performance optimization. Overloading @abi seems like another almost-equivalence that we might regret.
public doesn't necessarily mean that it's linkable externally, only that Swift code in another module can call it.
I would hope that improvements to the build system [*] here would mean that the vast majority of Swift developers never need to think about these attributes, because Swift just does the right thing: expose implementations for performance when it's safe, hide them for incremental build performance, whatever. Even an excellent system is likely to make the wrong choice for some users some of the time. Having language-level controls available for users to express what they need is important for those cases.
Doug
[*] It's not just one build system, it's every build system. I deal with SwiftPM, swift-build, CMake, and Makefile-based Swift projects on a daily basis.
This specific use case (the code must always be copied into the client and is never allowed to be referenced from an ABI symbol in the module that originally contained it) does seem to be about ABI, though. It’s independent of whether you allow clients to subsequently inline their copy of the function into their call sites. That’s why I think the @abi(none) annotation for that specific use case is warranted.
This is a good reason to choose a syntax further away from @inlinable, which is commonly needed as part of the balance of evolution flexibility and specialization/runtime performance. @inline and this new feature, on the other hand, seem to be more focused on overriding compiler heuristics about when inlining is appropriate. That feels to me like a much more advanced feature that should not be presented (in code completion or otherwise) as very similar to @inlinable.
As I think about this some more, there's another flavor of "export" that we don't entirely cover, which is link-time symbol visibility (Ă la __attribute__((visibility(...))) or dllexport). In Swift, it's inherently tied to access control: public and package not only mean "exported out of the module" but "exported out of the image by default" ("protected" visibility) whereas less-than-package implies "hidden" visibility.
In other words, the Swift compiler today sort of has a baked in assumption that each module is going to be its own dylib. But when building a large product that's broken into a number of small modules statically linked together, those may not be the boundaries I want—I would have public/package declarations that are public only because they need to be referenced cross-module, but I would want a smaller set of symbols exported to clients of my library. Today, I believe that's only possible by passing explicit exported symbol lists to the linker.
I'm bringing this up not because I want to bog down this specific pitch with covering that as well (it's not strictly related), but if we do consider a syntax like @export(...) or a public(...) suffix, I'd want us to try to design it such that it could be extended in the future to control link-time visibility as well.
This is an artifact of our current implementation, but I don't think we ever intended this to be the only way we map language access control to symbol visibility. It almost never makes sense to dynamically export and eagerly keep all public symbols unless you're building for a dylib, and in at least some situations, we don't—embedded comes to mind by necessity, but for Windows support as well, I believe there is a flag to tell the compiler what sort of build product is being built, so that the compiler knows whether it needs to dllexport public symbols or not. Having fine-grained export controls might be inevitable to deal with cases where manual control is necessary, but I think that visibility could be better controlled by giving the compiler better understanding of the build product from the build system.
One nice thing (maybe the only nice thing) about tying it to @inlinable is that we already allow @inlinable on not only formally public symbols but de-facto public symbols as well (such as @usableFromInline definitions), which seems like the right domain for this attribute.
Yes, that's right. I think both Andy's attribute formulation and Joe's suggestion can be extended reasonable with an optional argument to override the defaults we get based on access control, e.g., @exported(interface, visibility: hidden) or public(interface, visibility: hidden).
From an implementation standpoint, it's a whole lot easier to make the attribute version work, so long as we stick within the existing attribute grammar. Adding parameterization to access control specifiers will break more existing tools. I don't feel strongly about it, though.
The other nice thing about @inlinable is that it exists already. If we go add something like @export, we should probably consider deprecating @inlinable in the long term and suggesting the longer-but-more-descriptive @export(interface, implementation).
Broadly speaking, one thing I would like to see in the combined @exported and @inline discussions right now is a matrix of the possible combinations. In C and C++, there are combinations like __attribute__((noinline)) inline void foo() that mean something that is occasionally useful (especially due to the nature of C headers) but broadly inscrutable unless you know your attributes and linkages. Presenting the combinations that we’re coming up for Swift would be really good to evaluate whether we’re designing along the right axes and covering all the things that developers actually want to do. [EDIT: your proposal has it! I tuned it out because I looked at the markdown instead of the rendered file. Could you add a column for the case without any @inline attribute?]
Specifically to this proposal: I think there’s benefits to overloading public/package for this feature, while keeping both on their own working the same way that they do today. I found myself asking some questions that would all be avoided if this went on the access modifier:
What happens if I use @exported on a fully non-public member? (in a design where @exported is folded into public, this is a non-question)
Is there any way to/is it desirable to have a different “export visibility” on the getters and setters of properties? (with @exported you cannot choose, or at least it requires computed properties; if it went on the access modifier, you could feasibly have public(get, interface, implementation) public(set, interface) var foo)
This is a future direction only, but what does @exported(visibility: hidden) public mean? what does @exported(visibility: visible) internal mean? (if @exported was a part of public, this is again a non-question because you achieve visibility(hidden) by using the internal access modifier)
As a downside, this doesn’t play well with @usableFromInline internal (internal(interface), internal(implementation) don’t convey the right thing).
For the sake of clarity I think the proposal probably ought to address how @export interacts with access modifiers. For example, is this a combination that would be accepted by the compiler?
@exported(interface)
private func f() { /*...*/ }
For someone used to writing "desktop" Swift this combination probably looks non-sensical because private and @exported seem to contradict each other. If I understand the proposal correctly, though, it seems like this combination is in fact acceptable because it would be meaningful in Embedded Swift, where you might be trying to assert that f() is part of the closure of code emitted into object files to be linked, which is something that we lack control over today in Embedded Swift.
@inlinable in its current form only affects public and @usableFromInline declarations (though it appears that we allow it on all internal declarations). It would make sense to me for @export also to only apply to effectively-public declarations. It seems to me that exporting a symbol for a declaration subjects that declaration to many of the same considerations as being formally public, since in either case the declaration becomes accessible to code outside of the currently-compiled module's control.
I think this potentially contradicts the bigger picture aims of the proposal. We want to make it possible to delineate a closure of code in the module that is guaranteed to be emitted into object files when compiled in Embedded Swift. One of the reasons you’d want to do this is to be able to actually hide some dependencies in Embedded Swift, something that isn’t possible with compilation model we have today.
If you can only add @exported(interface) to public and internal declarations, then you can’t guarantee that the private declarations that are implementation details of those annotated declarations are also only emitted into object files. The private declarations are already effectively @exported(implementation) in Embedded Swift, violating the mental model one might have from desktop Swift that the implementation of a private decl can never escape the module it belongs to.
[file]private declarations are only accessible to other declarations in the same file, so I don't think being able to apply this attribute to them gives you any encapsulation benefit, since code from outside your module would have to do so via public or usableFromInline internal declarations.
Unless we require Embedded Swift code to annotate everything with @exported(implementation) in order to achieve the compilation model it has today, I don’t think that’s true:
@export(interface)
public func f() {
h() // This call is always encapsulated
}
// @export is unspecified. It is defacto 'interface' in desktop Swift, but in Embedded Swift it is effectively 'implementation'.
public func g() {
h() // This call is not encapsulated in Embedded Swift
}
private func h() {
// This implementation is effectively re-exported via g() when compiling for Embedded Swift
}
If the user wants to be able to ensure that the implementation of h() is encapsulated when compiling for Embedded Swift, how should that work?
@inlinable today precludes a function body from referring to private declarations. It seems reasonable to say that explicitly @export(implementation) declarations also can't refer to private methods. When you're in wmo-all-the-things-by-default build mode, I guess you'd need a new annotation on h(), but I'm not sure that's @export(interface); to me, that implies the compiler is going to emit a linkable symbol for the declaration, which for private declarations is only going to make encapsulation worse by making them effectively public. But then, if you're in wmo-all-the-things-by-default mode, it seems to me you've already forgone pretty much any separation of compilation units, unless you @export(interface) every single public entry point anyway, so I'm not sure how much benefit you'd get from trying to control encapsulation from the level of individual private declarations
I think embedded Swift is a red herring here; it makes as much sense to build leaf apps in the same sort of expose-everything mode in desktop Swift, and conversely, it should be possible to build an embedded Swift project as a dylib or static library (with restrictions on what the public interface can express) if you want it to be fully separately compiled.