Namespace Obj-C names for nested Swift types

While implementing nested protocol support, I discovered that when a nested type in Swift is exposed to Objective-C without a custom name, the compiler-chosen name does not mention the parent type. This leads to two issues:

Issue One: Compiler-chosen name may be ambiguous to humans

Swift declaration:

import Foundation

@objc class A: NSObject {
  @objc class Nested: NSObject {}
}

Generated Obj-C header (snippet):

SWIFT_CLASS("_TtC4Test1A")
@interface A : NSObject
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end


SWIFT_CLASS("_TtCC4Test1A6Nested")
@interface Nested : NSObject
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

The name Nested is lacking important context, leading to a degraded experience using this type from Objective-C.

In Swift, you would generally need to refer to this type as A.Nested (except in certain contexts where unqualified lookup permits it).

Issue Two: Compiler-chosen name may be ambiguous to Objective-C

Swift declaration:

import Foundation

@objc class A: NSObject {
  @objc class Nested: NSObject {}
}

@objc class B: NSObject {
  @objc class Nested: NSObject {}
}

Generated Obj-C header (snippet):

SWIFT_CLASS("_TtC4Test1A")
@interface A : NSObject
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end


SWIFT_CLASS("_TtCC4Test1A6Nested")
@interface Nested : NSObject
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end


SWIFT_CLASS("_TtC4Test1B")
@interface B : NSObject
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end


SWIFT_CLASS("_TtCC4Test1B6Nested")
@interface Nested : NSObject
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

This header cannot be imported because it contains two interface definitions, both named Nested. Instead, users need to manually define names for the nested types.


I'd like to suggest that we fix this. Here are two options for what the new default name could be:

  1. Simple concatenation (ANested and BNested). The resulting names may feel more idiomatic to Obj-C developers.

  2. Use an underscore as a delimiter (A_Nested and B_Nested). Clearer but some may say uglier :balance_scale:

I have a slight preference for option 2, but I figured it would be good to solicit other opinions. You can always override the default name if it isn't to your liking.

This proposal is limited to fixing the names for nested types, using the export rules that already exist today. Any other changes to Obj-C interfaces are strictly out of scope. If anybody else is planning more sweeping changes to how Obj-C interfaces are generated, I'd be happy to give way to them and they can incorporate this change in to their work.

This would be a source-breaking change, so if people are generally happy about doing this, I'd write up a proposal to use these new names when compiling in Swift 6 mode. As far as I can tell there aren't a huge number of complaints about the existing behaviour, so I don't imagine there will be much breakage in practice. Still, even without complaints I think it's clear that the current behaviour is less than ideal, and given that Swift 6 will break source compatibility to some extent, it may be an opportunity to make this change.

There was a brief discussion about this issue back in December 2015.

6 Likes

I think I would prefer a different option: require[1] an explicit name for nested types.


  1. warning in Swift 5, error in Swift 6 ↩ī¸Ž

Why?

Changing the Objective-C name of a type breaks (Objective-C) source and ABI compatibility, and it can even break compatibility with older app versions (e.g. if the type was encoded using NSCoding). I don't think silently having the class name change when using a different compiler version is a good idea.

This wouldn't apply to @objc enums, but I think those have other problems when nested (at least they did when I tried to use them that way a few years ago).

I would suggest 1. If the developer wants underscores (or some other "clearer" separator) they can add it with @objc. If the projects prefix is also set this would generate very natural Obj-C APIs (from Obj-C's point of view). e.g. A.Nested becomes APPANested.

1 Like

I don't think that it does break ABI compatibility or encoded type names; these classes are registered with the Obj-C runtime under different names (the thing in the SWIFT_CLASS annotation), which are already correct because they're just Swift mangled names. They would not change.

For instance, note that in the Obj-C header, Nested from A has a different runtime name to Nested from B:

SWIFT_CLASS("_TtCC4Test1A6Nested")
SWIFT_CLASS("_TtCC4Test1B6Nested")
                        ^

I'll need to double-check all of this, so thanks for bringing it up, but yeah my understanding is that it wouldn't break either of those things.

1 Like

Reading through the generated header, I see that you're correct. I didn't realize that the Objective-C class name wasn't the runtime name, but apparently objc_runtime_name was created at some point (possibly specifically for Swift). Apologies for my mistake.

For easier migration, would it be possible to have a deprecated typedef of the old name, so that the compiler generates a warning with a fixit to the new name (whether it's option 1 or 2)?

2 Likes

Sure, I don't think that would be a problem.