[Pitch] Objective-C implementations in Swift

As promised, I've prepared a pitch for a new, somewhat niche Objective-C interop feature I've been working on:

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 ObjC subclassing and runtime trickery.

That is, with @objcImplementation, you hand-write header files just as you would for an Objective-C class, but instead of implementing the methods and properties in Objective-C, you implement them in Swift. A draft proposal with more details is available as a gist.

A prototype of this feature has been merged into main with the underscored name @_objcImplementation, but that implementation is not yet complete. A lot of checks for invalid code have not been implemented, some of the rules may need to be refined to make initializers work better, important features like protocol conformances have not been tested, and you may have problems with ambiguities between member implementations (which are supposed to be invisible) and the ObjC declarations they're implementing. Bottom line is, don't assume that the current implementation reflects the final experience of this feature.

Besides the usual suggestions to refine the design, I'm especially interested to hear about whether you may have use cases for @objcImplementation and what those use cases are like. One of my concerns with this feature is that it may be difficult to teach developers how it is different from traditional Objective-C interop and when to use each one. I want to make sure we can give them clear guidance.

37 Likes

This is an awesome milestone for ObjC interop! The requirement that an @objcImplementation extension implement every method declared in the @interface makes sense as a way to ensure the developer didn't forget anything, but I could see intermediate states also being useful; if developers are rewriting existing ObjC classes in Swift, they could want to do so incrementally, keeping some methods implemented in ObjC while moving others over to Swift. And during testing and development, it may be useful to build and run with some members missing in order to test the ones that have been implemented. Maybe the @objcImplementation attribute could accept another argument, like partial: true, to allow the implementation to be incomplete?

10 Likes

The current design does already allow incremental porting as long as the increments are entire Objective-C categories, and moving a method or computed property to a different category doesn't affect its ABI, so you can move declarations from the Foo category to the Foo_Swift category without breaking clients.

A more granular incremental porting feature would be a little trickier because clang would generate some parts of the Objective-C metadata and Swift would generate other parts. Maybe the right set of limitations could make it happen, though, especially if we hand responsibility for ivars entirely to clang in that situation.

4 Likes

This is great to see and definitely exciting. I wonder if it could be applied to alternate objc implementations like gnustep one day :stuck_out_tongue:

I love a lot of the elements of this design, and love where it’s going.

Objective-C programmers expect API headers to serve as a second source of documentation on the APIs, but generated headers are disorganized, unreadable messes because Swift cannot mechanically produce the formatting that a human engineer would add to a handwritten header.

I feel this is probably my biggest frustration when transitioning to SwiftUI, and should be something we consider addressing for both Obj-C and pure Swift?

2 Likes

Swift interfaces contain the same documentation, so I'm not sure what you want here. In fact, motivation #2 in the proposal doesn't seem super strong, as there's nothing about the generated header that requires its current poor layout. Given the compiler controls that output, couldn't it simply provide better output? Include the inline docs, sort the output, have proper spacing, and it would look fine.

The generated header has a lot of necessary boilerplate.

The draft proposal doesn't mention the existing @objcMembers attribute, which was introduced by SE-0160. Is that attribute still relevant?

Swift has a really different philosophy than Objective-C about how libraries are written, packaged, and documented.

  • In Swift, public and internal declarations are mixed in the same source files; in Objective-C, the public interface is described in separate files.
  • In Swift, APIs are described to clients in automatically-generated files and tools are available to render convenient textual views for debugging and exploration; in Objective-C, APIs are described to clients in hand-maintained files and debugging and exploration is not aided by the same sort of rendering tools.
  • In Swift, documentation is written in a specific doc comment format that is part of the language, and then processed into well-organized documents; in Objective-C, documentation practices are less standardized and comments in the handwritten header files often include information that's hard to find elsewhere.

I don't think there's anything wrong with Swift's general approach (although of course the tools can always be improved). The only problem is that the @objc generated header tries to force Objective-C programmers to live by Swift's rules just because the authors of a library chose to implement it in Swift.

@objcMembers simply says that the members of a class should be @objc by default. It's pretty orthogonal to this proposal.

2 Likes
  1. Will this require the Objc header to be made public in a module? This is currently the case for mixed-langauge modules in order for Swift to import Objc, and is a big limitation that should be lifted.

  2. Is there any support for free functions? We should be able to incrementally migrate static Objc/C methods as well to Swift using this feature, as these are often used for binary size savings.

I was going to ask why this isn’t just a special kind of @objc class since the only difference at run time is not having Swift metadata attached; the Swift compiler is still the one responsible for generating the class and metaclass symbols despite the source using extension. But I think the answer of “header control” settles it.

1 Like

Would there be any way to use this with a header that declares methods but a Swift implementation that resolves them dynamically (using resolveInstanceMethod / class_addMethod)?