+1, the attribute soup is really confusing in Swift. I would be happiest if @inline(...) could consume @inlinable and have sensible enough default behaviors that we wouldn’t need @backDeployed or @_{}EmitIntoClient.
@export(interface, implementation) is essentially equivalent to @inlinable. There's a table covering the relationship with @inline. Is something specifically unclear here that I can answer?
The entire propose's motivation section is describing why we need the equivalent of @_{}EmitIntoClient. Do you disagree with what is presented there?
Doug
There's a table covering the relationship with
@inline. Is something specifically unclear here that I can answer?
Yes please, what happens if I say @inline(always) but I don’t say @export(...) at all? Does that mean no symbol in the library? Similarly @inline(never) without export(…) would mean there’s always a symbol in the library, even in Embedded Swift? Could I expect the compiler to automatically apply @inline(never) for me in EmbeddedSwift if my function used something that I internal imported or do I need to be careful about that still?
The entire propose's motivation section is describing why we need the equivalent of @_{}EmitIntoClient. Do you disagree with what is presented there?
I think I understand that part about “explicitly state the intent to emit symbols”, but I’m a little confused about the “provide the function definition to clients” part because isn’t that what @inline(…) does?
The @inline attribute has no effect on symbol availability whatsoever. It is an instruction about whether the inliner should, in fact, do inlining if it has access to the body. That's why we're moving to terminology that does not mention "inline" at all.
I think it would be nice to infer @export(implementation) if the body of a function depends on internal imports. I don't think it's this proposal.
SE-0496 has @inline(always) infer @inlinable (described here). I can have this pitch spell out that the presence of @export disables that inference.
Doug
Maybe this is just my ObjC background showing, but would it make sense for @inline(always/never) to have those behaviors? It seems really weird to me that I would have an inline function that also produced a callable symbol in my library. (i.e. NS_INLINE NSRect NSMakeRect(CGFloat x, CGFloat y, CGFloat w, CGFloat h) {…} can’t be a callable function in the Foundation library, at least unless you write a separate function body without the NS_INLINE somewhere else.)
Would it make sense to say @export(module, library) instead of @export(implementation, interface)? I find the latter confusing because I’m trained to think @interface = .h which is roughly equivalent to a module in Swift. But in this case interface means the function’s interface that goes in the library and its implementation that controls what goes in the module. Does @emitInto(module, library) have a bit less overload with a re-exporting module import or an exported symbol in a library?
This proposal is distinct from the @inline(always) proposal because sometimes you want to tell the optimizer to inline (or not), and sometimes you want to say whether the definition should be emitted into the owning module or into clients.
If you want to map from the C languages, it's something like:
@export(interface)is like putting a declaration in the header and the implementation in the.cfile@export(implementation)is like putting astaticdefinition in the header@export(interface, implementation)is like.... putting aninline externdefinition in the header and also having a non-inline definition in the.cfile.
The @inline(always) from SE-0496 is like the GNU __attribute__((always_inline)), i.e., it controls optimizer behavior.
We tend to use "module" and "library" somewhat synonymously in Swift, so I don't see how this would be better.
Doug
While I agree that "module" and "library" are often conflated in practice, I do wonder if something like "module" and "binary" might do a better job of tying these attributes to concrete build artifacts than the more abstract "interface" and "implementation" .
This proposal is distinct from the
@inline(always)proposal because sometimes you want to tell the optimizer to inline (or not), and sometimes you want to say whether the definition should be emitted into the owning module or into clients.
I should’ve said “should they have these behaviors by default”. I get that there will be someone out there who needs ultimate control, and these are two distinct proposals. But the table in your proposal doesn’t say what happens if @export is omitted, and it doesn’t say how @export interacts with @inlinable (when it’s not implied by @inline) either.
I’m sorry if I’m being thick here, but I still don’t really understand all the combinations either. Like how is @inline(never) @export(implementation)useful? I’m trying to map such a thing into my old ObjC world and it doesn’t really make sense. @export(implementation) feels like it’s already covered by @inline(always)/@inlinable, unless some of the oddball combinations in the table are more than esoteric? I’m not trying to argue with you, feel free to say “this is for a very small audience and hardly any developers will actually need to use these things”, I just wanted to understand the proposal a bit better.
This is effectively the following in a .h:
__attribute__((__noinline__))
int something_here(int x, int y) {
return x + y;
}
You’re exposing the implementation in the header file, but telling the compiler it can’t inline this call into clients (of course, this particular example is simple enough that the compiler constant fold things away). This is somewhat useful (albeit not common) for us in the stdlib because 1. @export(implementation) allows us to not commit to an ABI symbol in our dylib (clients get their own copy) and 2. @inline(never) forces a function call to this copy in client binaries (perhaps the function is cold).
Interesting, I haven’t seen __attribute__((__noinline__)) used in header files before. Kinda weird but I think I get it a little better, thanks!
I really like the current state of the proposal with the usage of @export. In my opinion, it describes the concepts that are currently spelled with @inlinable and the @_emit attributes a lot better.
@exportthat includes theimplementationargument inherits all of the restrictions as@inlinablethat are outlined in SE-0193, for example, the definition itself can only reference public entities or those that are themselves@usableFromInline.
It would be great if we can address the @usableFromInline in this matrix as well. Could this map to @export(implementation) as well? I think it could be confusing for users to use @export(implementation) and immediately be required to add @usableFromInline to internal methods.
I know this is most likely outside the scope of this proposal but I would love if we just made @export work with private and fileprivate access levels. Since, we (Swift Server ecosystem) are heavy users of the current @inlinable/@usableFromInline attributes we had to largely give up on private/fileprivate access levels.
That's because the answer is both "it's complicated" and "it might change," so it would be wrong to nail it down. The answers are different from Embedded vs. non-Embedded Swift, and are subject to the optimizations that are applied, which I thought was covered in the section describing these optimizations. We may very well change the defaults here.
The proposal contains this text:
The existing
@inlinablefor public symbols is subsumed by@export(interface, implementation), meaning that there is a callable symbol, but the definition is also available for specialization/inlining/etc.
Are you looking for something along the lines of "@inlinable cannot be used with @export"? I'll add that.
@usableFromInline is more akin to @export(interface), because it makes the function part of the binary interface even though it's not part of the public interface. In the same way that @export(interface, implementation) subsumes @inlinable, @export(interface) could subsume @usableFromInline. That's worth calling out in the proposal, thank you!
I just tripped over this as well, and it is annoying. I think that the main issue here is that making @export(interface) work with private/fileprivate access levels means that we need to tie down their mangling in ways that we haven't wanted to before. I do think this would need to be a separate proposal if only because we need to spell out what the evolution path looks like for a private thing that is @export(interface). It might include silly restrictions like "you can't rename the file containing the @export(interface) private declaration."
Doug
I don’t want to go too deep into this here to avoid derailing the pitch but I’m curious about this. Is this mangling only relevant for code compiled with library evolution? Secondly, how do the current CMO modes handle private/fileprivate symbols? Are they still exporting their implementations and ignore the mangling problem or are they not exporting those symbols at all?
More generally, are you proposing to deprecate the existing attributes while introducing new ones with this pitch? If so, it would be good to add a section on how packages that use the existing attributes and have to support multiple Swift releases are expected to migrate.
If I understand it correct, this proposal is adding two things.
- Introducing a way to force implementations to be written to the module without also making it available as a callable symbol which
@inlinabledoes. (So roughly equivalent to writing out the implementation in a C header):@export(implementation). If unset, the implementation might still be written to the module, e.g.@usableFromInline internal funcwill probably write the implementation into to the module. I think this attribute would be implied by@backDeployed,@inlinable, and@usableFromInline? - Introducing a way to force something callable to be written to the library (roughly equivalent to
externor__attribute__((visibility("default")))in C?):@export(interface). If unset, something callable might still be written to the module, e.g.public funcwill probably cause a linker symbol to be emitted to a dylib.
IMO these two things are different enough that they deserve more specifically named attributes. Maybe @alwaysEmitImplementationIntoModule or @implementationInModule, and @alwaysExportToLibrary?
For my use case with hiding a function implementation in Embedded Swift, do I need a @neverEmitImplementationIntoModule, or is @alwaysExportToLibrary enough?
For my use case with hiding a function implementation in Embedded Swift, do I need a
@neverEmitImplementationIntoModule, or is@alwaysExportToLibraryenough?
Or I guess as the proposal is written, @export(interface) is effectively @neverEmitImplementationIntoModule @alwaysExportToLibrary. That’s pretty subtle, I took that as “force exporting the interface, leave the default behavior for the implementation” until I read the proposal for the 5th time. I still think things would be clearer with distinct attributes that are more descriptive than @export(interface, implementation)
Two observations:
-
I still think the "module" vs. "library" distinction is too subtle. Owen's idea of using "module" and "binary" is a little better, but to me "module" doesn't mean "clients get the implementation and should do something with it".
-
We're getting pretty close to where we started, with
@_alwaysEmitIntoClientand@_neverEmitIntoClient;)Doug
The mangling is only critical for code compiled with library evolution. CMO effectively requires that all of the modules involved in CMO be built together. It can make sense with library evolution when the CMO is across modules build together; this is what "package CMO" is about, at the risk of diving into yet more compilation-model details. All that said, we shouldn't have the language behavior differ when library evolution is enabled (or not).
I am not proposing to deprecate, no. I pushed an update to try to clarify, but @inlinable and @usableFromInlineare statements of intent that don't force where symbols show up (or don't). The@export` attribute I'm proposing (however it's spelled) forces the issue.
Doug
Maybe it’s a C vs Swift thing. I understand pretty well the idea of “write the implementation in the header file where clients can see it”. To me the equivalent Swift idea is “write the implementation in the module where clients can see it”, just it’s in a .swiftinterface instead of a .h. That’s why I keep wanting to use the word “module” in the attribute name.
Maybe experienced Swift developers don’t have that C mental model, and it makes better sense to just say “expose the implementation to clients”. If that’s the case then can we use a different word than “export”? That one makes me think about linker symbols, or maybe module re-exporting, and adding a third meaning might just be too much to handle. How about @exposeImplementationToClients(always|never)?
I still find “interface” confusing too. I have too much association with the header file/module being the “interface” and I wouldn’t guess that “not exporting the interface” means “removing the callable from the library”, and not something like hiding it from the module (i.e. making it internal). What if we used the term “callable” instead: @exportCallable(always|never)?
I like two separate attributes because even if they have a relationship where @exposeImplementationToClients(never) might imply @exportCallable(always), I find it unintuitive that @export(implementation) would imply interface=false instead of interface=default behavior. I guess we could fix that with Jed’s @export(implementation: true, interface: false) and you have to specify both, but then is default an allowed value?
(Is @inlinable the equivalent of @exposeImplementationToClients(always) @exportCallable(always)?)
For me personally, @export(interface) and @export(implementation) are clear and immediately understandable, in a way that none of the other options discussed have been.
Every alternative I’ve seen, has emphatically not been clear to me. They have all been confusing or ambiguous or just plain not obvious in what they do. I have had to puzzle and ponder and think “Wait, what is this trying to say?”
In contrast, @export(interface) and @export(implementation) have been so clear and obvious to me that I have never once had any question about what they do.
I support using clearer names and avoiding "inlin" in any of them. @export reads fine to me.
If I'm understanding correctly, then:
@export(interface)and we deprecate@usableFromInline@export(interface, implementation)and we deprecate@inlinable@export(implementation)and we deprecate@_alwaysEmitIntoClient.