Hi all,

Here’s a draft proposal to limit inference of @objc to only those places where 
we need it for consistency of the semantic model. It’s in the realm of things 
that isn’t *needed* for ABI stability, but if we’re going to make the 
source-breaking change here we’d much rather do it in the Swift 4 time-frame 
than later. Proposal is at:

        
https://github.com/DougGregor/swift-evolution/blob/objc-inference/proposals/NNNN-objc-inference.md
 
<https://github.com/DougGregor/swift-evolution/blob/objc-inference/proposals/NNNN-objc-inference.md>

Introduction

One can explicitly write @objc on any Swift declaration that can be expressed 
in Objective-C. As a convenience, Swift also infers @objc in a number of places 
to improve interoperability with Objective-C and eliminate boilerplate. This 
proposal scales back the inference of @objc to only those cases where the 
declaration must be available to Objective-C to maintain semantic coherence of 
the model, e.g., when overriding an @objc method or implementing a requirement 
of an @objcprotocol. Other cases currently supported (e.g., a method declared 
in a subclass of NSObject) would no longer infer @objc, but one could continue 
to write it explicitly to produce Objective-C entry points.

Swift-evolution thread: here 
<https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160509/017308.html>
 
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#motivation>Motivation

There are several observations motivating this proposal. The first is that 
Swift's rules for inference of @objc are fairly baroque, and it is often 
unclear to users when @objc will be inferred. This proposal seeks to make the 
inference rules more straightforward. The second observation is that it is 
fairly easy to write Swift classes that inadvertently cause Objective-C 
selector collisions due to overloading, e.g.,

class MyNumber : NSObject {
  init(_ int: Int) { }
  init(_ double: Double) { } // error: initializer 'init' with Objective-C 
selector 'init:' 
      // conflicts with previous declaration with the same Objective-C selector
}
The example above also illustrates the third observation, which is that code 
following the Swift API Design Guidelines 
<https://swift.org/documentation/api-design-guidelines/> will use Swift names 
that often translate into very poor Objective-C names that violate the 
Objective-C Coding Guidelines for Cocoa 
<https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CodingGuidelines/CodingGuidelines.html>.
 Specifically, the Objective-C selectors for the initializers above should 
include a noun describing the first argument, e.g., initWithInteger: and 
initWithDouble:, which requires explicit @objc annotations anyway:

class MyNumber : NSObject {
  @objc(initWithInteger:) init(_ int: Int) { }
  @objc(initWithDouble:) init(_ double: Double) { }
}
The final observation is that there is a cost for each Objective-C entry point, 
because the Swift compiler must create a "thunk" method that maps from the 
Objective-C calling convention to the Swift calling convention and is recorded 
within Objective-C metadata. This increases the size of the binary (preliminary 
tests on some Cocoa[Touch] apps found that 6-8% of binary size was in these 
thunks alone, some of which are undoubtedly unused), and can have some impact 
on load time (the dynamic linker has to sort through the Objective-C metadata 
for these thunks).

 
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#proposed-solution>Proposed
 solution

The proposed solution is to limit the inference of @objc to only those places 
where it is required for semantic consistency of the programming model. 

 
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#constructs-that-still-infer-objc>Constructs
 that (still) infer @objc

Specifically, @objc will continue to be inferred for a declaration when:

The declaration is an override of an @objc declaration, e.g.,

class Super {
  @objc func foo() { }
}

class Sub : Super {
  /* inferred @objc */
  override func foo() { }
}
This inference is required so that Objective-C callers to the method 
Super.foo() will appropriately invoke the overriding method Sub.foo().

The declaration satisfies a requirement of an @objc protocol, e.g.,

@objc protocol MyDelegate {
  func bar()
}

class MyClass : MyDelegate {
  /* inferred @objc */
  func bar() { }
}
This inference is required because anyone calling MyDelegate.bar(), whether 
from Objective-C or Swift, will do so via an Objective-C message send, so 
conforming to the protocol requires an Objective-C entry point.

The declaration has the @IBAction or @IBOutlet attribute. This inference is 
required because the interaction with Interface Builder occurs entirely through 
the Objective-C runtime, and therefore depends on the existence of an 
Objective-C entrypoint.

The declaration has the @NSManaged attribute. This inference is required 
because the interaction with CoreData occurs entirely through the Objective-C 
runtime, and therefore depends on the existence of an Objective-C entrypoint.

The list above describes cases where Swift 3 already performs inference of 
@objc and will continue to do so if this proposal is accepted.

 
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#dynamic-no-longer-infers-objc>dynamic
 no longer infers @objc

A declaration that is dynamic will no longer infer @objc. For example:

class MyClass {
  dynamic func foo() { }       // error: 'dynamic' method must be '@objc'
  @objc dynamic func bar() { } // okay
}
This change is intended to separate current implementation limitations from 
future language evolution: the current implementation supports dynamic by 
always using the Objective-C message send mechanism, allowing replacement of 
dynamic implementations via the Objective-C runtime (e.g., class_addMethod and 
class_replaceMethod). In the future, it is plausible that the Swift language 
and runtime will evolve to support dynamic without relying on the Objective-C 
runtime, and it's important that we leave the door open for that language 
evolution.

This change therefore does two things. First, it makes it clear that the 
dynamic behavior is tied to the Objective-C runtime. Second, it means that 
well-formed Swift 4 code will continue to work in the same way should Swift 
gain the ability to provide dynamic without relying on Objective-C: at that 
point, the method foo() above will become well-formed, and the method bar() 
will continue to work as it does today through the Objective-C runtime. Indeed, 
this change is the right way forward even if Swift never supports dynamic in 
its own runtime, following the precedent of SE-0070 
<https://github.com/apple/swift-evolution/blob/master/proposals/0070-optional-requirements.md>,
 which required the Objective-C-only protocol feature "optional requirements" 
to be explicitly marked with @objc.

 
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#nsobject-derived-classes-no-longer-infer-objc>NSObject-derived
 classes no longer infer @objc

A declaration within an NSObject-derived class will no longer infer objc`. For 
example:

class MyClass : NSObject {
  func foo() { } // not exposed to Objective-C in Swift 4
}
This is the only major change of this proposal, because it means that a large 
number of methods that Swift 3 would have exposed to Objective-C (and would, 
therefore, be callable from Objective-C code in a mixed project) will no longer 
be exposed. On the other hand, this is the most unpredictable part of the Swift 
3 model, because such methods infer @objconly when the method can be expressed 
in Objective-C. For example:

extension MyClass {
  func bar(param: ObjCClass) { } // exposed to Objective-C in Swift 3; not 
exposed by this proposal
  func baz(param: SwiftStruct) { } // not exposed to Objective-C
}
With this proposal, neither method specifies @objc nor is either required by 
the semantic model to expose an Objective-C entrypoint, so they don't infer 
@objc: there is no need to reason about the type of the parameter's suitability 
in Objective-C.

 
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#side-benefit-more-reasonable-expectations-for-objc-protocol-extensions>Side
 benefit: more reasonable expectations for @objc protocol extensions

Users are often surprised to realize that extensions of @objc protocols do not, 
in fact, produce Objective-C entrypoints:

@objc protocol P { }

extension P {
  func bar() { }
}

class C : NSObject { }

let c = C()
print(c.respondsToSelector("bar")) // prints "false"
The expectation that P.bar() has an Objective-C entry point is set by the fact 
that NSObject-derived Swift classes do implicitly create Objective-C entry 
points for declarations within class extensions when possible, but Swift does 
not (and, practically speaking, cannot) do the same for protocol extensions.

A previous mini-proposal discussed here 
<https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160104/005312.html>
 suggested requiring @nonobjc for members of @objc protocol extensions. 
However, limiting inference of @objc eliminates the expectation itself, 
addressing the problem from a different angle.

 
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#source-compatibility>Source
 compatibility

The two changes that remove inference of @objc are both source-breaking in 
different ways. The dynamic change mostly straightforward:

In Swift 4 mode, introduce an error that when a dynamic declaration does not 
explicitly state @objc, with a Fix-It to add the @objc.

In Swift 3 compatibility mode, continue to infer @objc for dynamic methods. 
However, introduce a warning that such code will be ill-formed in Swift 4, 
along with a Fix-It to add the @objc. This

A Swift 3-to-4 migrator could employ the same logic as Swift 3 compatibility 
mode to update dynamic declarations appropriately.

The elimination of inference of @objc for declarations in NSObject subclasses 
is more complicated. Considering again the three cases:

In Swift 4 mode, do not infer @objc for such declarations. Source-breaking 
changes that will be introduced include:

If #selector or #keyPath refers to one such declaration, an error will be 
produced on previously-valid code that the declaration is not @objc. In most 
cases, a Fix-It will suggest the addition of @objc.

The lack of @objc means that Objective-C code in mixed-source projects won't be 
able to call these declarations. Most problems caused by this will result in 
warnings or errors from the Objective-C compiler (due to unrecognized 
selectors), but some might only be detected at runtime. These latter cases will 
be hard-to-detect.

Other tools and frameworks that rely on the presence of Objective-C entrypoints 
but do not make use of Swift's facilities for referring to them will fail. This 
case is particularly hard to diagnose well, and failures of this sort are 
likely to cause runtime failures that only the developer can diagnose and 
correct.

In Swift 3 compatibility mode, continue to infer @objc for these declarations. 
When @objc is inferred based on this rule, modify the generated header (i.e., 
the header used by Objective-C code to call into Swift code) so that the 
declaration contains a "deprecated" attribute indicating that the Swift 
declaration should be explicitly marked with @objc. For example:

class MyClass : NSObject {
  func foo() { }
}
will produce a generated header that includes:

@interface MyClass : NSObject
-(void)foo NS_DEPRECATED("MyClass.foo() requires an explicit `@objc` in Swift 
4");
@end
This way, any reference to that declaration from Objective-C code will produce 
a warning about the deprecation. Users can silence the warning by adding an 
explicit @objc.

A Swift 3-to-4 migrator is the hardest part of the story. Ideally, the migrator 
to only add @objc in places where it is needed, so that we see some of the 
expected benefits of code-size reduction. However, there are two problems with 
doing so:

Some of the uses that imply the need to add @objc come from Objective-C code, 
so a Swift 3-to-4 migrator would also need to compile the Objective-C code 
(possibly with a modified version of the Objective-C compiler) and match up the 
"deprecated" warnings mentioned in the Swift 3 compatibility mode bullet with 
Swift declarations.

The migrator can't reason about dynamically-constructed selectors or the 
behavior of other tools that might directly use the Objective-C runtime, so 
failing to add a @objc will lead to migrated programs that compile but fail to 
execute correctly.

 
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#overriding-of-declarations-introduced-in-class-extensions>Overriding
 of declarations introduced in class extensions

Swift's class model doesn't support overriding of declarations introduced in 
class extensions. For example, the following code produces an amusing error 
message on the override:

class MySuperclass { }

extension MySuperclass {
  func extMethod() { }
}

class MySubclass : MySuperclass { }

extension MySubclass {
  override func extMethod() { }   // error: declarations in extensions cannot 
override yet
}
However, this does work in Swift 3 when the method is @objc, e.g.,

class MySuperclass { }

extension MySuperclass {
  @objc func extMethod() { }
}

class MySubclass : MySuperclass { }

extension MySubclass {
  override func extMethod() { }   // okay! Objective-C message dispatch allows 
this
}
Removing @objc inference for NSObject subclasses will therefore break this 
correct Swift 3 code:

class MySuperclass { }

extension MySuperclass : NSObject {
  func extMethod() { } // implicitly @objc in Swift 3, not @objc in Swift 4
}

class MySubclass : MySuperclass { }

extension MySubclass {
  override func extMethod() { }   // okay in Swift 3, error in Swift 4: 
declarations in extensions cannot override yet
}
There are several potential solutions to this problem, but both are 
out-of-scope for this particular proposal:

Require that a non-@objc declaration in a class extension by explicitly 
declared final so that it is clear from the source that this declaration cannot 
be overridden.

Extend Swift's class model to permit overriding of declarations introduced in 
extensions.

 
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#alternatives-considered>Alternatives
 considered

Aside from the obvious alternative of "do nothing", there are ways to address 
some of the problems called out in theMotivation 
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#motivation> 
section without eliminating inference in the cases we're talking about, or to 
soften the requirements on some constructs.

 
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#mangling-objective-c-selectors>Mangling
 Objective-C selectors

Some of the problems with Objective-C selector collisions could be addressed by 
using "mangled" selector names for Swift-defined declarations. For example, 
given:

class MyClass : NSObject {
  func print(_ value: Int) { }
}
Instead of choosing the Objective-C selector "print:" by default, which is 
likely to conflict, we could use a mangled selector name like 
"__MyModule__MyClass__print__Int:" that is unlikely to conflict with anything 
else in the program. However, this change would also be source-breaking for the 
same reasons that restricting @objc inference is: dynamic behavior that 
constructs Objective-C selectors or tools outside of Swift that expect certain 
selectors will break at run-time.

 
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#completely-eliminating-objc-inference>Completely
 eliminating @objc inference

Another alternative to this proposal is to go further and completely eliminate 
@objc inference. This would simplify the programming model further---it's 
exposed to Objective-C only if it's marked @objc---but at the cost of 
significantly more boilerplate for applications that use Objective-C 
frameworks. For example:

class Sub : Super {
  @objc override func foo() { }  // @objc is now required
}

class MyClass : MyDelegate {
  @objc func bar() { }  // @objc is now required
}
I believe that this proposal strikes the right balance already, where @objc is 
inferred when it's needed to maintain the semantic model, and can be explicitly 
added to document those places where the user is intentionally exposing an 
Objective-C entrypoint for some reason. Thus, explicitly writing @objc 
indicates intent without creating boilerplate.

 
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#proposal-add-on-objc-annotations-on-class-definitions-and-extensions>Proposal
 add-on: @objc annotations on class definitions and extensions

If the annotation burden of @objc introduced by this proposal is too high, we 
could make @objc on a class definition or extension thereof imply @objc on 
those members that can be exposed to Objective-C. For example:

@objc extension MyClass {
  // implicitly @objc
  func f() { }

  // Cannot be exposed to Objective-C because tuples aren't representable in 
Objective-C
  func g() -> (Int, Int) { return (1, 2) }
}
This would reduce (but not eliminate) the annotation burden introduced by this 
proposal, allowing developers to group Objective-C-exposed declarations 
together under a single @objc annotation. This reduces the amount of 
boilerplate. With such a change, we'd need to decide what to do with 
MyClass.g(), which could be either:

Make it an error because the context implies that it is @objc, but it cannot 
be. The error would be suppressed with an explicit @nonobjc annotation.

Make it implicitly @nonobjc.

Option (1) seems more in line with the rest of the proposal, because it 
maintains a predictable model. Regardless, this add-on makes sense if we like 
the benefits of the proposal to limit @objc inference but the annotation burden 
turns out to be annoyingly high.


        - Doug


_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to