[Pitch 3] Objective-C implementations in Swift

Time for most likely the final pitch on this feature:

Introduction

We propose an alternative to @objc classes where Objective-C header @interface declarations are implemented by Swift extensions. The resulting classes will be implemented in Swift, but will be indistinguishable from Objective-C classes, fully supporting Objective-C subclassing and runtime trickery.

<snip>

Proposed solution

We propose adding a new attribute, @implementation, which allows a Swift extension to replace an Objective-C @implementation block. You write headers as normal for an Objective-C class, but instead of writing an @implementation in an Objective-C file, you write an @implementation extension in a Swift file. You can even port an existing class’s implementation to Swift one category at a time without breaking backwards compatibility.

Implementation experience has revealed some new constraints on Swift-only initializers, and the ObjC metadata situation is becoming clearer (although it's still not 100% final yet). I also now have a much better picture of how future directions ought to work—basically, by extending the capabilities of generated-header-based interop and then making sure those new extensions support @implementation as well.

The implementation still has a number of known bugs, but once I take care of the resilient metadata problem, I think it'll be good enough to be reviewable.

19 Likes

Follow-up on recent posts in the second pitch thread:

One of the limitations of @implementation is that it has the same "view" of the imported class as any other Swift code would, so it's not possible to import the code with lightweight generics in other code but without it for @implementation. (Except by playing games with different header content, I suppose, though clang modules intentionally don't make that easy to do.)

Ultimately, I do think @implementation could support lightweight generics, although it might require changing the way all Swift code imports them. It's just an especially complex problem that's only necessary for a very small number of classes, so I chose to subset it out.

I think the compiler will always be able to infer this correctly. When @implementation is applied to an extension, it can infer it from the type being extended; if @implementation is eventually applied to other declarations, it can infer it from other attributes applied to the declaration (@objc, @_cdecl, @_expose(Cxx)).

1 Like

In the proposal, I see:

The compiler currently has experimental support for @implementation @_cdecl for global functions; it's behind a separate experimental feature flag because it's not part of this proposal.

So I just want to make sure we don't accidentally force ourselves into a corner if we want to promote @implementation @_cdecl (with whatever spelling we give it.) :slight_smile:

1 Like

Right. So the thought is that something like:

@implementation public func foo() { ... }

Is always going to be illegal (or perhaps it would create an implementation of a Swift function declared elsewhere with @extern?), whereas any of these hypothetical syntaxes:

@implementation @objc(NSFoo) public func foo() { ... }
@implementation @cdecl(foo) public func foo() { ... }
@implementation @expose(Cxx) public func foo() { ... }

Would make it clear which language we're trying to provide an implementation for. (And this isn't a concern for @implementation extension because we can infer it from the type we're extending.)

2 Likes

Is this something that it would still be useful for users to understand at a glance without needing to track down the original type definition? Also, we're still attaching the category name to the @implementation attribute, is that something that would potentially make more sense to live on @objc, e.g @implementation @objc(category: Animation) extension MYFlippableViewController?

1 Like

Understood! I see how having @cdecl() separate would be useful (i.e. it would allow specifying the exported name with C "mangling" rather than Swift mangling.)

1 Like

That's actually a super-interesting suggestion. @objc extension implicitly makes members @objc by default, which is good, but that includes final members, which is bad. @objc(SomeName) extension is currently allowed(!) but it ignores the custom Swift name. Perhaps we could make final members implicitly @nonobjc when in an @objc @implementation extension and pull the category name from the @objc attribute.

(Would it be better for users? Um, maybe? I suppose that's a question of taste.)

1 Like

This is great, and once it lands I think I would start strongly recommending to our dev teams that they start using this anywhere that they're using @objc classes today, as long as it's a compatible use case. Having the header be written manually instead of an output of a Swift compile action would remove Swift compilation from the critical path of other Objective-C compilation actions, eliminating a big bottleneck in some builds.

The proposal says "[...] you would arrange for Swift to import it through an umbrella or bridging header." Is a bridging header specifically required here, or is it sufficient to just make sure that the header is made available to ClangImporter in a module with the same name as the Swift code being compiled? I'm thinking specifically of explicit module builds, which bridging headers don't quite align with. For example, if I compile Foo.h into a Foo.pcm and then pass that to a Swift compile containing these @implementation extensions, also using module name Foo, then will either -import-underlying-module or an explicit @_exported import Foo in the source files containing the extension work to let it find the module defining the Objective-C interface?

Similarly (this is getting into the weeds of implementation rather than design a bit), but if the implementation doesn't already do so, can we make sure that symbol references in indexstore like Foo in @implementation extension Foo point back to the original header correctly? Hopefully the interaction between Swift and Clang's indexer logic already does this without too much additional work, but I wanted to make sure it wasn't overlooked.

The latter, although without the same name requirement. For instance, you can implement something from a private module with an internal or @_implementationOnly import.

1 Like

The proposal defines an upcoming feature flag ObjCImplementation.

The Source Compatibility section notes:

This change is additive and doesn't affect existing code. Replacing an Objective-C @implementation declaration with a Swift @implementation extension is invisible to the library's Objective-C and Swift clients, so it should not be source-breaking unless the implementations have observable behavior differences.

Assuming this proposal is implemented in the Swift 6.0 release, what is the intended behavior of this flag?

Will the feature be always enabled in Swift 6 language mode and not require the flag?

Will the feature only be enabled in Swift 5 language mode if the flag is used?

If the feature is additive with no source compatibility issues, why wouldn't the feature enabled in both Swift 5 and Swift 6 language mode without a flag? Is an upcoming feature flag needed?

Ah, that’s a mistake. If approved, this feature would be enabled in all Swift language version modes, so the language feature flag won’t be an upcoming feature flag. We regret the error.

3 Likes

Thank you for clarifying. I've added a comment to the proposal to remove the Upcoming Feature Flag.

My gut feeling is that 'category name' is so Objective-C specific that it's a bit odd to have it attached to the @implementation attribute, especially as an unlabeled argument. I also have minor qualms that it 'burns' arbitrary strings in the argument position there before we have an idea of what exactly @implementation might look like for other languages—if we end up wanting to pass other information to the @implementation attribute it seems we'd be forced to label that argument, at least for Objective-C.

6 Likes

I'm leaning in the direction of @objc(CategoryName), but if we go that way, I think I'd want @objc(CategoryName) to also work on ordinary extensions. (It's actually allowed by the parser, but the category name is ignored; PrintAsClang and IRGen generate arbitrary category names instead, and in fact they generate different arbitrary category names.) I don't want to commit to that change without trying to implement it.

3 Likes

I've pushed a few revisions to the proposal:

  • Removed the upcoming feature flag (thanks, @James_Dempsey)

  • Expanded acknowledgements

  • Documented back deployment behavior (tl;dr: classes with certain kinds of types in their stored properties won't back deploy, but most classes will)

I'm still planning to try out @Jumhyn's suggestion of using @objc(CategoryName) @implementation; I think that will either become the design or it'll be discussed in Alternatives Considered.

7 Likes

I'll just add that if @objc(CategoryName) @implementation turns out to be unworkable for some reason then I'd probably lean towards a labeled argument on @implementation, e.g. @implementation(category: CategoryName) or even @implementation(objcCategory: CategoryName).

2 Likes

@implementation(objc) and @implementation(objc, category: CategoryName)? :bike::house:

6 Likes

I spent some time implementing @objc(CategoryName) @implementation yesterday and it seems to work really nicely, so I've updated the proposal to use it. (No PR for the implementation yet because I need apple/swift#73128 to land first.) It looks like this:

@objc @implementation extension MYFlippableViewController {
    ...
}

@objc(Animation) @implementation extension MYFlippableViewController {
    ...
}
12 Likes