[Pitch] Align @objc inference with the semantic model


(Douglas Gregor) #1

Hi all,

Right now, Swift infers @objc (meaning that you don’t have to write it explicitly) in a number of places:

1) When overriding an @objc declaration
2) When implementing a requirement of an @objc protocol (note: this behavior was recently enabled in the Swift compiler)
3) When the declaration is‘@IBOutlet’, or ‘@NSManaged
4) When the declaration is ‘dynamic’
5) When the declaration is non-private and occurs within a subclass of NSObject or an extension thereof, so long as it *can* be expressed in Objective-C

There are different motivations behind the various rules, some of which bear re-examination. Before going into the individual rules, here’s why I’m bringing this up now:

a) There is no easy way to articulate the rules we have. They mostly came from a desire to avoid making Swift programmers write ‘@objc’, but that’s not very principled.

b) When Swift code follows the Swift API design guidelines, the Objective-C names provided by the Swift compiler by default aren’t good names in Objective-C, a topic I brought up a few months ago <http://thread.gmane.org/gmane.comp.lang.swift.evolution/7733>. The upshot is that, for entities that are intended to be used in Objective-C, inferring ‘@objc’ doesn’t help when you’re going to explicit write out the Objective-C selector anyway (‘@objc(objectAtIndex:)’). The Grand Renaming is forcing this on Swift programmers already, so it’s a reasonable time to make a change in the area of @objc inference.

c) The “thunks” generated for the compiler to provide Objective-C entry points have a non-trivial cost. I measured the code size of a handful of Swift projects (all Cocoa[Touch] apps, most Swift-only, some mixed Swift/Objective-C), and 6-8% of code size was in these @objc thunks. We have some ideas to reduce the code size of @objc thunks in general. However, I suspect that most of these @objc thunks are completely unused, although I haven’t done the legwork to prove it, and there is no automatic way to remove them.

d) Removing @objc inference for a particular case can silently break working code, because something invisible to the Swift compiler (e.g., in Objective-C in a mixed-source project, in a system framework, etc.) might be depending on that Objective-C entry point. We can probably tolerate such breakage in Swift 3 where we’re breaking lots of things, but our ability to make sure changes here will be diminish rapidly over time. Hence, we’re talking about this now, and we need to live with our decisions for a long time.

In general, I’d like to remove @objc inference from places where it isn’t needed for the “semantic model”, loosely construed. Let’s go case-by-case with the places we do inference now:

(1) and (2) are geared toward consistency of the semantic model. For example, you need @objc inference for overrides of @objc declarations because otherwise Objective-C code won’t see the override and you’ll get divergent behavior. Similarly when you are implementing a requirement of an @objc protocol: Objective-C callers need to be able to have something to call. I consider it critical that we maintain @objc inference for these cases because maintaining a consistent semantic model across Swift and Objective-C code is fundamental to interoperability. No proposed change here, aside from noting that the ability to infer @objc (including the selector names) when implementing requirements of an @objc protocol was very recently introduced in the Swift compiler.

(3) is about interoperability with certain frameworks and tools. @objc inference helps eliminate some boilerplate today, because these features depend on the Objective-C runtime. Will these features, with their current spelling in Swift, *always* depend on the Objective-C runtime? If so, we can keep the @objc inference. Or is there some probable future where the same features could get implementations that don’t rely on the Objective-C runtime? If so, we might want to eliminate @objc inference for them: today, that means having to write ‘@objc’ explicitly on such declarations, and if that future comes to pass the requirement to add ‘@objc’ will go away (no existing code will break unless a user manually deletes an ‘@objc’ from that code). With ‘@NSManaged’, I feel like the underlying feature would look *very* different if implemented without a backing Objective-C runtime, so I’m inclined to leave @objc inference in place. With ‘@IBOutlet’ it’s a whole lot more murky, and the confusion is magnified by the fact that ‘@IBAction’ doesn’t currently have @objc inference. We should align IBOutlet and IBAction in this regard (so some change is needed), but I don’t have a strong sense of which way to go.

(4) ‘dynamic’ infers ‘@objc’ because the Swift runtime doesn’t have support for replacing a method at runtime. There are two possible futures here: Swift’s runtime gets support for ‘dynamic’ on pure Swift methods (in which case the ‘@objc’ inference would become vestigial cruft that would be hard to take away), or we decide that ‘dynamic’ is an Objective-C compatibility feature that always implies Objective-C. With SE-0070 <https://github.com/apple/swift-evolution/blob/master/proposals/0070-optional-requirements.md>, we started requiring an explicit ‘@objc’ for optional requirements to make it clear that this is an Objective-C compatibility feature, so in a sense both futures point to requiring explicit ‘@objc’ on ‘dynamic’. Therefore, I propose to drop @objc inference for ‘dynamic’ declarations. For now (and maybe forever), that means requiring ‘@objc’ to be written along with ‘dynamic’.

(5) was designed to reduce the need for programmers to explicitly write ‘@objc’. I also suspect it’s the culprit behind many of the unused @objc thunks hypothesized in (c). It is useful in mixed-source projects because you get more of your Swift APIs accessible to your Objective-C code for free, but it’s usefulness in Swift-only projects is far more limited. I propose that we eliminate @objc inference for non-private members of NSObject subclasses and extensions thereof. It means that one will need to be much more explicit for those @objc entry points that are needed (and yes, this will break code), but we will eliminate a (probably significant) number of unnecessary @objc entry points. It also eliminates reflexively inheriting from NSObject “just because it saves typing”, which can help nudge Swift classes toward the more-efficient Swift object model. There is a possible middle path here: eliminate the inference by default, then add a Swift compiler flag that enables @objc inference for these cases. One’s build system would be expected to pass this flag for mixed-source projects.

That’s the meat of the proposal. There are two minor follow-ons:

* Allow one to add @objc (or @nonobjc) to an extension, which will apply to all members of the extension not explicitly marked @objc or @nonobjc, e.g.,

@objc extension MyClass {
  func anObjCMethod() { }
  var anObjCProperty: String { … }
}

This makes it more convenient to add Objective-C entrypoints to a bunch of declarations at once.

* Require one to add @nonobjc to members of extensions of @objc protocols. Members of extensions of @objc protocols cannot be @objc, because there’s no way to implement this without major surgery in the Objective-C runtime or admitting inconsistent behavior (e.g., it only works for subclasses of NSObject). Requiring @nonobjc makes it explicit that these members are only available in Swift. I’ve proposed this before <http://thread.gmane.org/gmane.comp.lang.swift.evolution/2136>, and it didn’t really get a whole lot of support, but I think it’s important for clarifying the model.

What’s everyone think?

  - Doug