[Pitch 2] Objective-C implementations in Swift

Work on my previously pitched Objective-C interop feature is nearing completion, so I thought I would reopen discussion in preparation for an evolution proposal.

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.

When using this feature, the developer hand-writes Objective-C headers just as they normally would for an Objective-C class, but implements their declarations in Swift by using an extension marked with the new @implementation attribute.

This alternative is useful to replace an existing Objective-C class's implementation with a safer Swift version, or when vending functionality written in Swift to an audience of Objective-C developers. It's also helpful when build system limitations prevent the use of the generated Swift.h header. It is not a complete replacement for @objc, which is much more convenient for, say, creating a UIViewController subclass that your Objective-C code calls but doesn't subclass.

The most notable change from the previous draft is that we have changed the attribute from @objcImplementation to @implementation; this is because there has been significant interest in a similar feature for @_cdecl functions and even some for an equivalent C++ feature, so it seems wise to use a name that's agnostic to the language. Certain details of the proposal, such as the rules for initializers, have also been refined as a result of implementation experience.

The full proposal is available as a gist. The feature is also usable in trunk development snapshots, except that the attribute is named @_objcImplementation there; there are a few known bugs and limitations, but nothing that I believe should impact the proposal.

26 Likes

Will this include the ability to import into Swift arguments as force-unwrapped types?

For example:

NS_HEADER_AUDIT_BEGIN(nullability, sendability)
@interface Foo : NSObject
- (void)myGreatMethod:(id)argument;
@end
NS_HEADER_AUDIT_END(nullability, sendability)

In Swift, I need the ability to implement this as:

@implementation extension Foo {
    func myGreatMethod(_ argument: Any!) {
        guard let argument else { /* invalid, but possible via ObjC */ }
    }
}

In ObjC, nullability is just a suggestion, really. When we converted the implementation of some of Foundation's ObjC types into Swift we were forced to put intermediate ObjC subclasses with alternate method implementations of the main API to deal with this possibility.

2 Likes

Yes:

Member implementations must have an overload signature that closely matches the ObjC declaration’s. However, types that are non-optional in the ObjC declaration may be implicitly unwrapped optionals in the member implementation if this is ABI-compatible; this is because Objective-C does not prevent clients from passing nil or implementations from returning nil when nonnull is used, and member implementations may need to implement backwards compatibility logic for this situation.

6 Likes

When Swift generates metadata for an @implementation extension , it will generate metadata that matches what clang would have generated for a similar @implementation

Does this mean that swapping out an Objc implementation for the equivalent code written in Swift should have zero binary size impact?

I wouldn’t say “zero impact”—they’re different compilers, so they’ll generate different code—but I would expect it to be similar.

This is a pretty neat addition!

I don’t like to bikeshed, but I do fear that @implementation would be quite unclear to anyone without an Objective C background (e.g. including C++ devs, who would also be using this feature). Perhaps something like @implementationOnly could help? (But then again, that’s a bit ugly :smile:)

To that end, is extension the correct construct here in the first place? The act of defining implementations to an already-declared interface seems quite semantically distinct from “extending” a type. Perhaps should be its own declaration kind, with a new keyword?

1 Like

I'm worried that will get confused with the existing @_implementationOnly attribute, which is applied to import statements, not extension blocks.

I’m very willing to entertain bikeshedding on the attribute name, but there’s an underscored implementationOnly on import statements and sometimes on other declarations that does something very different from this feature; I wouldn’t want anyone to confuse them.

I don’t think this feature carries enough weight to justify a new declaration kind; the only ones we've ever added are associatedtype and actor, which are a lot more central to the type system than this would be. I think extension is appropriate because it is adding more members to the type, just members that have already been declared somewhere else.

8 Likes

Strongly +1 for this. Can't wait to see the formal proposal landing on Swift 5.10.

1 Like

If there's an interest in different name candidates, what about something like more verbose @implementsImportedType or @implementationOfImportedType, to make it more clear what implementation actually pertains to.

2 Likes

+1 on this, but I do want to highlight one snippet:

As a special exception to the usual rule, a non-category @implementation extension can declare stored properties. They can be (perhaps implicitly) @objc or they can also be final ; in the latter case they are only accessible from Swift. Note that @implementation does not have an equivalent to implicit @synthesize —you must declare a var explicitly for each @property in the header that you want to be backed by a stored property.

I have mixed feelings about this special casing, and there certainly has been interest here in the past in having extensions within the same declaring module also support declaring stored properties. Would it be worth re-exploring general availability of this instead of making @implementation extension the sole exception to the current rule?

Why would it need a new declaration kind ?

Is there some constraint that prevent to use @implementation class instead ?

Keywords like class, struct, func, etc. imply the declaration of something, whereas extensions are providing bits of implementation for something declared elsewhere—whether it's a type that you're adding methods to, or a protocol that you're providing default implementations for. I think it's really useful to maintain this distinction in the language and the mental model, so using @implementation extension feels like the right choice here.

While it is unfortunate that we have to carve out an exception for ivar storage that isn't supported elsewhere in the language, it's also the only way to provide that information in some cases (such as Objective-C classes, which treat ivars as part of the implementation block, not the interface). Nothing prevents someone from pitching a more general ability to declare stored properties in same-file extensions in the future, but I know that it might meet some resistance because there's a nice benefit to being able to look at the main declaration of a type and know that all of its storage is there—it's entire shape is defined in one place.

I wonder if it's worth trying to bring that analogy over to @implementation extensions as well? As the pitch is currently written, is it possible for stored properties to be defined in multiple non-category @implementation extensions? If so, do you think it would be worthwhile to limit them to a single extension, or perhaps have a dedicated @implementation(storage) extension? Or do you not think that would hold its weight?

4 Likes

You can only have one implementation extension per class-and-category (or lack-of-category) combination, so this is impossible by definition.

4 Likes

Then I think that's a perfect landing spot. The shape of the thing would be seen by looking at just the category-less @implementation extension, similar to looking at a main type declaration today, so it feels like less of a special case around stored properties in extensions as a general feature.

I don't think the proposal should require that we generalize stored properties non other kinds of extensions, nor does anything in this proposal shut the door on that in the future if we wanted to visit it.

3 Likes

Overall +1. Great work.

I'm curious about one point, in the proposal, you said you won't support objc lightweight generics, and I wonder is it possible that OC generics is supported in a limited way?

For example, user can still define a generic class in header files, but when implemented using Swift, Swift sees the header in a way that all generic signatures definitions are dropped, and all generic references are erased to Any.

Like this:

// OC header
@interface Config<T>: NSObject
- (void)setValue:(T)value;
- (T)getValue;
@end

// Swift impl
@implementation extension Config {
  public func setValue(_ value: Any) {}
  public func getValue() -> Any {}
}

It is up to the implementor to obey the generic contract, just like in old OC ways.

Perhaps @implementation(objc) so that the language is explicit, then? Or are you expecting the compiler can always infer the language correctly?

1 Like

I've opened a new pitch thread; I'll reply to these recent comments there.

2 Likes