[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.

20 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

First, congrats on the acceptance of 0436. It is really neat!

I have a question about ivars declared in ObjC @interface. For example:

@interface ViewController : UIViewController {
    NSInteger _b;
}

@end

Accessing _a in ObjC code causes this linking error:

Undefined symbols for architecture arm64:
"OBJC_IVAR$_ViewController._a", referenced from:
-[ViewController(AAA) foo] in ViewController+AAA.o

I try to implement the ivar in Swift implementation but couldn't find a way to do that. For example:

@objc @implementation extension ViewController {
    @objc var _a: NSInteger = 0
}

It causes this error:

Property '_a' does not match any property declared in the headers for 'ViewController'; did you use the property's Swift name?

If I add final as Xcode suggests, I go back to the linking error.

@objc @implementation extension ViewController {
    @objc final var _a: NSInteger = 0
}

There is no way to directly define or access an ivar from Swift, even with @objc @implementation. Clang Importer simply ignores them.

When using @objc @implementation, a non-ObjC final property serves the same purpose as an ivar—it adds a "stored property" to the class without adding ObjC accessor methods—but final properties can't be accessed from Objective-C, whether via ivars or otherwise.

There's an argument to be made that @objc @implementation should give you a warning or error when you use it on a class with ivars, since the class it generates won't actually have those ivars. However, in practice, explicitly-declared ivars are very rarely used, especially from outside the implementation, so it's not clear whether this really causes any problems. (The current behavior falls out of the fact that Clang Importer completely ignores ivars in the header—there's literally no trace of them in the Swift declarations.)