[Proposal draft] Limiting @objc inference


(Douglas Gregor) #1

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

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


(Rick M) #2

+1

···

On Jan 4, 2017, at 16:50 , Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

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

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

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 will use Swift names that often translate into very poor Objective-C names that violate the Objective-C Coding Guidelines for Cocoa. 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).

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.

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.

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, which required the Objective-C-only protocol feature "optional requirements" to be explicitly marked with @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.

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 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.

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.

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.

Alternatives considered

Aside from the obvious alternative of "do nothing", there are ways to address some of the problems called out in theMotivation section without eliminating inference in the cases we're talking about, or to soften the requirements on some constructs.

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.

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.

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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
Rick Mann
rmann@latencyzero.com


(Timothy Wood) #3

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.

This seems fairly worrisome, but I’m sure how much so. I personally would probably prefer a guaranteed non-breaking migrator (since Swift 4 isn’t supposed to break stuff…) that adds the @objc where it was previously inferred. Sure, this won’t make existing code smaller/faster, but more importantly it won’t break it. The developer than then opt to remove the unneeded annotations over time (allowing for incremental testing and later bisection if a problem crops up).

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 seems like it goes counter to cleaning up the confusion by re-introducting silent non-exposure due to use of Swift-only types. It also seems odd that @objc would cascade to members when `public` and other access levels don’t (though it could do the same inference based on the access level of the types involved.

Overall this looks pretty appealing!

-tim

···

On Jan 4, 2017, at 4:50 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:


(Charlie Monroe) #4

I'm personally coming back and forth on this - my code is now 99% pure Swift, so I don't really need @objc declarations aside for:

- CoreData - which is fairly obvious and @NSManaged will scream if the type isn't @objc-compatible

- UI - and this is a big part. Unfortunately, I've relied on @objc inference in multiple cases when it comes to binding (macOS). Or to be more precise @objc implied dynamic. So for NSObject subclasses

var foo: String = ""

is without further annotations usable with bindings (though IB screams that NSString is expected and it found String). And who has previously dealt with them knows that this change will be hell to debug and discover all the places since Interface Builder usually isn't very helpful in this matter.

In order for the migration to go smoothly, it would require the IB to do some kind of keyPath validations and add annotations to those places as well. Without it, I can't imagine the transition... IB has been a source of great deal of ObjC -> Swift headaches, I wish there weren't any more.

···

On Jan 5, 2017, at 1:50 AM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

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

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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Brian King) #5

+1, I really like the consistency. There's still one potential
inconsistency that I think could be changed to improve things

Overriding of declarations introduced in class extensions

The inference of @objc inside extensions in Swift 3 is more than visibility
to the Obj-c runtime, it also infers `dynamic`. This proposal appears to
retain that, since `@objc` in an extension would allow override via message
dispatch, where as `@objc` in a class declaration would only make the
selector available to the obj-c runtime and retain table dispatch.

Does it make sense to remove the `dynamic` inference too? This would force
all extension methods that can be overridden to be declared `@objc
dynamic`. This clarifies the purpose of @objc since it only manages Obj-C
visibility, and does not modify dispatch behavior.

I know this departs from my previous "NSObject should stay dynamic"
argument earlier, but I was mostly arguing for consistency. Since it is
clear that dynamic behavior should be opted into, I think forcing an
additional `dynamic` keyword seems to make sense. Some developers may rely
on this implicit `dynamic` behavior and encounter issues if a future
version of swift allows overrides in extensions via table dispatch.

Brian King


(Karl) #6

It’s good, but I’d also +1 going even further and removing all implicit @objc.

I don’t really like all the special stuff we do for Obj-C, and I’m in favour of anything which generalises our bridging model (at least the user-facing part) and makes it more predictable. In this case, all entry/exit points from native Swift are clearly marked.

Concerns about boilerplate are taken care of by batch-annotating in extensions. If a method in an extension can’t accept the annotation, it should be an error or require explicit suppression via @nonobjc. However, I don’t believe the compiler currently allows overrides to exist in extensions; that would need to change in order to make the idea useful.

- Karl

···

On 5 Jan 2017, at 01:50, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

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

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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Rod Brown) #7

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

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.

+1.

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.

+1. I like this cleanup.

<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.

Yeah, this is definitely a point of concern. I’m not sure that the first part deals with stuff like frameworks and libraries that currently expose functionality to external users in Obj-C. How could you be sure which API was designed to be used by Obj-C when it comes to libraries?

As others have mentioned, perhaps we could have options in the converter? I’m not generally a fan of this type of preference selection but in this case it seems reasonable.

The dynamically constructed selector case seems rare, especially considering that using string-based selectors has been deprecated since Swift 2.2.

<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.

I will definitely be running into this issue if/when this occurs. A lot of users use extensions to break up their code into clear elements for organisational reasons, and also adding functionality to external libraries. Both here would end up with odd behaviour and I really hope we go with Option 2 here.

···

On 5 Jan 2017, at 11:50 am, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:
<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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Micah Hainline) #8

+1. Well thought out, it's bothered me too.

···

On Jan 4, 2017, at 7:34 PM, Rick Mann via swift-evolution <swift-evolution@swift.org> wrote:

+1

On Jan 4, 2017, at 16:50 , Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

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

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

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 will use Swift names that often translate into very poor Objective-C names that violate the Objective-C Coding Guidelines for Cocoa. 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).

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.

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.

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, which required the Objective-C-only protocol feature "optional requirements" to be explicitly marked with @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.

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 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.

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.

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.

Alternatives considered

Aside from the obvious alternative of "do nothing", there are ways to address some of the problems called out in theMotivation section without eliminating inference in the cases we're talking about, or to soften the requirements on some constructs.

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.

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.

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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
Rick Mann
rmann@latencyzero.com

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Douglas Gregor) #9

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.

This seems fairly worrisome, but I’m sure how much so. I personally would probably prefer a guaranteed non-breaking migrator (since Swift 4 isn’t supposed to break stuff…) that adds the @objc where it was previously inferred. Sure, this won’t make existing code smaller/faster, but more importantly it won’t break it. The developer than then opt to remove the unneeded annotations over time (allowing for incremental testing and later bisection if a problem crops up).

It’s the conservative thing for the migrator to do, although it’ll potentially make a mess of migrated source code by littering it with potentially-unnecessary @objc’s.

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 seems like it goes counter to cleaning up the confusion by re-introducting silent non-exposure due to use of Swift-only types. It also seems odd that @objc would cascade to members when `public` and other access levels don’t (though it could do the same inference based on the access level of the types involved.

Yeah, I tend to agree with you that this add-on is likely not worth doing. I put it into the draft this way to provoke discussion, because this has come up before.

  - Doug

···

On Jan 4, 2017, at 8:59 PM, Timothy Wood <tjw@me.com> wrote:

On Jan 4, 2017, at 4:50 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:


(Douglas Gregor) #10

I'm personally coming back and forth on this - my code is now 99% pure Swift, so I don't really need @objc declarations aside for:

- CoreData - which is fairly obvious and @NSManaged will scream if the type isn't @objc-compatible

- UI - and this is a big part. Unfortunately, I've relied on @objc inference in multiple cases when it comes to binding (macOS). Or to be more precise @objc implied dynamic. So for NSObject subclasses

var foo: String = ""

is without further annotations usable with bindings (though IB screams that NSString is expected and it found String). And who has previously dealt with them knows that this change will be hell to debug and discover all the places since Interface Builder usually isn't very helpful in this matter.

In order for the migration to go smoothly, it would require the IB to do some kind of keyPath validations and add annotations to those places as well. Without it, I can't imagine the transition... IB has been a source of great deal of ObjC -> Swift headaches, I wish there weren't any more.

From a migration standpoint, Timothy Wood’s suggestion would properly migrate such code to Swift 4. However, my proposal could make *authoring* such code harder, because you would have to remember to write the @objc… and debugging the failures at runtime could be painful. Maybe there’s some trick in the runtime we could do to provide a better failure when things do go badly here.

  - Doug

···

On Jan 4, 2017, at 9:36 PM, Charlie Monroe <charlie@charliemonroe.net> wrote:

On Jan 5, 2017, at 1:50 AM, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

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

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
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Charlie Monroe) #11

I'm personally coming back and forth on this - my code is now 99% pure Swift, so I don't really need @objc declarations aside for:

- CoreData - which is fairly obvious and @NSManaged will scream if the type isn't @objc-compatible

- UI - and this is a big part. Unfortunately, I've relied on @objc inference in multiple cases when it comes to binding (macOS). Or to be more precise @objc implied dynamic. So for NSObject subclasses

var foo: String = ""

is without further annotations usable with bindings (though IB screams that NSString is expected and it found String). And who has previously dealt with them knows that this change will be hell to debug and discover all the places since Interface Builder usually isn't very helpful in this matter.

In order for the migration to go smoothly, it would require the IB to do some kind of keyPath validations and add annotations to those places as well. Without it, I can't imagine the transition... IB has been a source of great deal of ObjC -> Swift headaches, I wish there weren't any more.

From a migration standpoint, Timothy Wood’s suggestion would properly migrate such code to Swift 4. However, my proposal could make *authoring* such code harder, because you would have to remember to write the @objc… and debugging the failures at runtime could be painful. Maybe there’s some trick in the runtime we could do to provide a better failure when things do go badly here.

Debugging it might not be the worst part (if I remember correctly, you will get a log message that class Foo isn't KVO for key bar, so it's easy to fix) - the worst part is testing it. You need to go through the entire UI and make sure it's not crashing somewhere. Easy to do for say Calculator, harder for something in the extent (UI-wise) of Xcode since there are always some places you omit. And then you get run-time crashes. Or actually users do.

Tim's suggestion would solve this, but for most users, it also provides unnnecessary @objc annotations. It's like public -> open migration in Swift 3. Everything is now marked as open and I keep going around and marking it back as public (I have a closed framework that doesn't need any subclassing). Thankfully, it's an easy regex for the rescue, but it's still very annoying and tedious.

It would be much better if the migrator was configurable to some extent on a per-target basis (sorry if this already is possible). For Swift 3 in particular, you could set whether to default to open or public. For this migration, you could choose whether to add @objc annotations everywhere.

I know this is out of scope for Swift team itself, but I think in this particular case, the IB team could also come to rescue and the migrator in Xcode could analyze the XIB files for this migration and suggest changes for known keypaths.

···

On Jan 5, 2017, at 7:06 AM, Douglas Gregor <dgregor@apple.com> wrote:

On Jan 4, 2017, at 9:36 PM, Charlie Monroe <charlie@charliemonroe.net <mailto:charlie@charliemonroe.net>> wrote:

  - Doug

On Jan 5, 2017, at 1:50 AM, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

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

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
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Douglas Gregor) #12

+1, I really like the consistency. There's still one potential inconsistency that I think could be changed to improve things
Overriding of declarations introduced in class extensions

The inference of @objc inside extensions in Swift 3 is more than visibility to the Obj-c runtime, it also infers `dynamic`. This proposal appears to retain that, since `@objc` in an extension would allow override via message dispatch, where as `@objc` in a class declaration would only make the selector available to the obj-c runtime and retain table dispatch.

Excellent point! I’d come across the `dynamic` inference while writing this proposal, but had deluded myself into thinking that it was just an implementation detail. You’re right that it surfaces in the user model.

Does it make sense to remove the `dynamic` inference too? This would force all extension methods that can be overridden to be declared `@objc dynamic`. This clarifies the purpose of @objc since it only manages Obj-C visibility, and does not modify dispatch behavior.

The practical effect of removing `dynamic` inference is that a declaration introduced in a class extension can only be overridden if it is marked

  @objc dynamic

Overriding declarations won’t have to repeat this utterance (“override” suffices), so the boilerplate is restricted to the introduction of the first overridable declaration.

I guess the specific rule would be that @objc declarations introduced in a class extension must either be ‘dynamic’ or ‘final’, with the subtle-but-understandable annoyance that ‘final’ methods can still be swizzled by the Objective-C runtime.

I definitely need to mention this in the proposal (thank you!), but I’m on the fence regarding whether to propose doing anything about it in this specific proposal. I already intentionally subsetted out the other part of the overrides-of-declarations-in-extensions issue (see the section “Overriding of declarations introduced in class extensions”), and it feels like the issues should be tackled together.

Part of me also wonders whether it’s worth this level of boilerplate now to save on an expected-to-be-small amount of breakage later. Specifically, we’re talking about cases where the user swizzled a method but didn’t mark it ‘dynamic’. Or, swizzled a method that was explicitly marked ‘final’, which the runtime doesn’t know. There are numerous other failure cases here where one has swizzled a method via the Objective-C runtime such that only Objective-C callers invoke the new method, but Swift callers invoke the pre-swizzled version. Personally, I think we can address this problem holistically by teaching the Objective-C runtime about Swift’s thunks so it can warn when swizzling non-‘dynamic’ ones, which would obviate the need for explicit ‘dynamic’ in these cases.

I know this departs from my previous "NSObject should stay dynamic" argument earlier, but I was mostly arguing for consistency. Since it is clear that dynamic behavior should be opted into, I think forcing an additional `dynamic` keyword seems to make sense. Some developers may rely on this implicit `dynamic` behavior and encounter issues if a future version of swift allows overrides in extensions via table dispatch.

We’re both seeking a more consistent and predictable model, but we’re going in different directions. I feel that my proposal is more in line with the direction Swift has been going.

  - Doug

···

On Jan 5, 2017, at 9:30 AM, Brian King <brianaking@gmail.com> wrote:


(Jordan Rose) #13

Good^W Bad news: this never worked. @objc does not imply 'dynamic', so you're just dropping updates on the floor. Unfortunately the bindings system can't detect this case and warn you about it.

Jordan

···

On Jan 4, 2017, at 21:36, Charlie Monroe via swift-evolution <swift-evolution@swift.org> wrote:

I'm personally coming back and forth on this - my code is now 99% pure Swift, so I don't really need @objc declarations aside for:

- CoreData - which is fairly obvious and @NSManaged will scream if the type isn't @objc-compatible

- UI - and this is a big part. Unfortunately, I've relied on @objc inference in multiple cases when it comes to binding (macOS). Or to be more precise @objc implied dynamic. So for NSObject subclasses

var foo: String = ""

is without further annotations usable with bindings (though IB screams that NSString is expected and it found String). And who has previously dealt with them knows that this change will be hell to debug and discover all the places since Interface Builder usually isn't very helpful in this matter.

In order for the migration to go smoothly, it would require the IB to do some kind of keyPath validations and add annotations to those places as well. Without it, I can't imagine the transition... IB has been a source of great deal of ObjC -> Swift headaches, I wish there weren't any more.


(Douglas Gregor) #14

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/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.

Yeah, this is definitely a point of concern. I’m not sure that the first part deals with stuff like frameworks and libraries that currently expose functionality to external users in Obj-C. How could you be sure which API was designed to be used by Obj-C when it comes to libraries?

It is *very* hard to know.

As others have mentioned, perhaps we could have options in the converter? I’m not generally a fan of this type of preference selection but in this case it seems reasonable.

Yeah, maybe. Designing the migrator itself isn’t really in the scope of the Swift evolution process, but we do appreciate hearing people’s ideas here. It’s a tough thing to migrate well.

The dynamically constructed selector case seems rare, especially considering that using string-based selectors has been deprecated since Swift 2.2.

I suspect that you are right that failures would be rare. It’s the

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

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.

I will definitely be running into this issue if/when this occurs. A lot of users use extensions to break up their code into clear elements for organisational reasons, and also adding functionality to external libraries. Both here would end up with odd behaviour and I really hope we go with Option 2 here.

Either option requires a proposal; Option 2 can certainly have ABI impact because it could effect the ABI of Swift classes.

As an aside, we/I realized belatedly that this proposal obviously doesn’t fit Swift 4 stage 1, so we’ll hold off on the review until we open up stage 2.

  - Doug

···

On Jan 6, 2017, at 3:46 AM, Rod Brown <rodney.brown6@icloud.com> wrote:

On 5 Jan 2017, at 11:50 am, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:


(Georgios Moschovitis) #15

I don’t really like all the special stuff we do for Obj-C,

big +1


(Alexis) #16

I’m a big fan of making @objc less implicit — I’ve frequently been left wondering when it’s actually necessary or not. I think the standard library kinda just uses it haphazardly in its bridging stuff, leaving me to wonder when it actually does anything (this is probably due to how things once worked in the Long Long Ago).

I don’t consider my opinion to be too valuable here though, as someone who’s never really done meaningful ObjC work.

What I do have a stronger opinion on is that I’m pretty scared about implicitly breaking tons of code in late-binding ways. I’d be more sympathetic to this if we hadn’t declared source stability, but we have, and it doesn’t seem like this breakage meets the bar. Especially since there's a 100% safe migration solution we can implement, if I’m understanding correctly. Adding @objc everywhere it was being inferred seems fairly reasonable to me. This means no immediate benefits for existing code bases, but it still means:

* All new code bases will benefit.

* Old code bases will have a very clear opportunity to audit, if they deem it worth their time. Realistically they probably won’t, but it’s not like their code will be any slower/bloated than it was.


(Shawn Erickson) #17

+1. Yeah well thought out and explained. All for consistency and favor
being explicit.

+1. Well thought out, it's bothered me too.

+1

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

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

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 will use Swift names that
often translate into very poor Objective-C names that violate the
Objective-C Coding Guidelines for Cocoa. 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).

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.

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.

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, which
required the Objective-C-only protocol feature "optional requirements" to
be explicitly marked with @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.

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 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.

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.

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.

Alternatives considered

Aside from the obvious alternative of "do nothing", there are ways to

address some of the problems called out in theMotivation section without
eliminating inference in the cases we're talking about, or to soften the
requirements on some constructs.

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.

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.

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.

···

On Wed, Jan 4, 2017 at 7:01 PM Micah Hainline via swift-evolution < swift-evolution@swift.org> wrote:

On Jan 4, 2017, at 7:34 PM, Rick Mann via swift-evolution < swift-evolution@swift.org> wrote:

On Jan 4, 2017, at 16:50 , Douglas Gregor via swift-evolution < swift-evolution@swift.org> wrote:

   - Doug

_______________________________________________

swift-evolution mailing list

swift-evolution@swift.org

https://lists.swift.org/mailman/listinfo/swift-evolution

--

Rick Mann

rmann@latencyzero.com

_______________________________________________

swift-evolution mailing list

swift-evolution@swift.org

https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________

swift-evolution mailing list

swift-evolution@swift.org

https://lists.swift.org/mailman/listinfo/swift-evolution


(Freak Show) #18

When you can write CoreData and the rest of Cocoa in pure Swift, you can maybe start shoving Objective C to the door. Until then, a whole community of developers still needs to get things done.

As someone very heavily invested in Objective C and is still using it to pay the bills, I can say the same WRT Swift. I don't need nor desire annotating everything with nullability specifications - yet now Xcode colors all my code yellow demanding I do it. Nil isn't an error in Objective C philosophy and its a zero value activity and yet I'm finding my bread and butter code (I write iOS apps for a living) increasingly encumbered by Swifty cruft that make no sense in a dynamic world .

So -1.

···

On Jan 6, 2017, at 03:22, Georgios Moschovitis via swift-evolution <swift-evolution@swift.org> wrote:

I don’t really like all the special stuff we do for Obj-C,

big +1
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution