Hi all,
Here’s a draft proposal to limit inference of @objc to only those places where
we need it for consistency of the semantic model. It’s in the realm of things
that isn’t *needed* for ABI stability, but if we’re going to make the
source-breaking change here we’d much rather do it in the Swift 4 time-frame
than later. Proposal is at:
https://github.com/DougGregor/swift-evolution/blob/objc-inference/proposals/NNNN-objc-inference.md
<https://github.com/DougGregor/swift-evolution/blob/objc-inference/proposals/NNNN-objc-inference.md>
Introduction
One can explicitly write @objc on any Swift declaration that can be expressed
in Objective-C. As a convenience, Swift also infers @objc in a number of places
to improve interoperability with Objective-C and eliminate boilerplate. This
proposal scales back the inference of @objc to only those cases where the
declaration must be available to Objective-C to maintain semantic coherence of
the model, e.g., when overriding an @objc method or implementing a requirement
of an @objcprotocol. Other cases currently supported (e.g., a method declared
in a subclass of NSObject) would no longer infer @objc, but one could continue
to write it explicitly to produce Objective-C entry points.
Swift-evolution thread: here
<https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160509/017308.html>
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#motivation>Motivation
There are several observations motivating this proposal. The first is that
Swift's rules for inference of @objc are fairly baroque, and it is often
unclear to users when @objc will be inferred. This proposal seeks to make the
inference rules more straightforward. The second observation is that it is
fairly easy to write Swift classes that inadvertently cause Objective-C
selector collisions due to overloading, e.g.,
class MyNumber : NSObject {
init(_ int: Int) { }
init(_ double: Double) { } // error: initializer 'init' with Objective-C
selector 'init:'
// conflicts with previous declaration with the same Objective-C selector
}
The example above also illustrates the third observation, which is that code
following the Swift API Design Guidelines
<https://swift.org/documentation/api-design-guidelines/> will use Swift names
that often translate into very poor Objective-C names that violate the
Objective-C Coding Guidelines for Cocoa
<https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CodingGuidelines/CodingGuidelines.html>.
Specifically, the Objective-C selectors for the initializers above should
include a noun describing the first argument, e.g., initWithInteger: and
initWithDouble:, which requires explicit @objc annotations anyway:
class MyNumber : NSObject {
@objc(initWithInteger:) init(_ int: Int) { }
@objc(initWithDouble:) init(_ double: Double) { }
}
The final observation is that there is a cost for each Objective-C entry point,
because the Swift compiler must create a "thunk" method that maps from the
Objective-C calling convention to the Swift calling convention and is recorded
within Objective-C metadata. This increases the size of the binary (preliminary
tests on some Cocoa[Touch] apps found that 6-8% of binary size was in these
thunks alone, some of which are undoubtedly unused), and can have some impact
on load time (the dynamic linker has to sort through the Objective-C metadata
for these thunks).
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#proposed-solution>Proposed
solution
The proposed solution is to limit the inference of @objc to only those places
where it is required for semantic consistency of the programming model.
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#constructs-that-still-infer-objc>Constructs
that (still) infer @objc
Specifically, @objc will continue to be inferred for a declaration when:
The declaration is an override of an @objc declaration, e.g.,
class Super {
@objc func foo() { }
}
class Sub : Super {
/* inferred @objc */
override func foo() { }
}
This inference is required so that Objective-C callers to the method
Super.foo() will appropriately invoke the overriding method Sub.foo().
The declaration satisfies a requirement of an @objc protocol, e.g.,
@objc protocol MyDelegate {
func bar()
}
class MyClass : MyDelegate {
/* inferred @objc */
func bar() { }
}
This inference is required because anyone calling MyDelegate.bar(), whether
from Objective-C or Swift, will do so via an Objective-C message send, so
conforming to the protocol requires an Objective-C entry point.
The declaration has the @IBAction or @IBOutlet attribute. This inference is
required because the interaction with Interface Builder occurs entirely through
the Objective-C runtime, and therefore depends on the existence of an
Objective-C entrypoint.
The declaration has the @NSManaged attribute. This inference is required
because the interaction with CoreData occurs entirely through the Objective-C
runtime, and therefore depends on the existence of an Objective-C entrypoint.
The list above describes cases where Swift 3 already performs inference of
@objc and will continue to do so if this proposal is accepted.
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#dynamic-no-longer-infers-objc>dynamic
no longer infers @objc
A declaration that is dynamic will no longer infer @objc. For example:
class MyClass {
dynamic func foo() { } // error: 'dynamic' method must be '@objc'
@objc dynamic func bar() { } // okay
}
This change is intended to separate current implementation limitations from
future language evolution: the current implementation supports dynamic by
always using the Objective-C message send mechanism, allowing replacement of
dynamic implementations via the Objective-C runtime (e.g., class_addMethod and
class_replaceMethod). In the future, it is plausible that the Swift language
and runtime will evolve to support dynamic without relying on the Objective-C
runtime, and it's important that we leave the door open for that language
evolution.
This change therefore does two things. First, it makes it clear that the
dynamic behavior is tied to the Objective-C runtime. Second, it means that
well-formed Swift 4 code will continue to work in the same way should Swift
gain the ability to provide dynamic without relying on Objective-C: at that
point, the method foo() above will become well-formed, and the method bar()
will continue to work as it does today through the Objective-C runtime. Indeed,
this change is the right way forward even if Swift never supports dynamic in
its own runtime, following the precedent of SE-0070
<https://github.com/apple/swift-evolution/blob/master/proposals/0070-optional-requirements.md>,
which required the Objective-C-only protocol feature "optional requirements"
to be explicitly marked with @objc.
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#nsobject-derived-classes-no-longer-infer-objc>NSObject-derived
classes no longer infer @objc
A declaration within an NSObject-derived class will no longer infer objc`. For
example:
class MyClass : NSObject {
func foo() { } // not exposed to Objective-C in Swift 4
}
This is the only major change of this proposal, because it means that a large
number of methods that Swift 3 would have exposed to Objective-C (and would,
therefore, be callable from Objective-C code in a mixed project) will no longer
be exposed. On the other hand, this is the most unpredictable part of the Swift
3 model, because such methods infer @objconly when the method can be expressed
in Objective-C. For example:
extension MyClass {
func bar(param: ObjCClass) { } // exposed to Objective-C in Swift 3; not
exposed by this proposal
func baz(param: SwiftStruct) { } // not exposed to Objective-C
}
With this proposal, neither method specifies @objc nor is either required by
the semantic model to expose an Objective-C entrypoint, so they don't infer
@objc: there is no need to reason about the type of the parameter's suitability
in Objective-C.
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#side-benefit-more-reasonable-expectations-for-objc-protocol-extensions>Side
benefit: more reasonable expectations for @objc protocol extensions
Users are often surprised to realize that extensions of @objc protocols do not,
in fact, produce Objective-C entrypoints:
@objc protocol P { }
extension P {
func bar() { }
}
class C : NSObject { }
let c = C()
print(c.respondsToSelector("bar")) // prints "false"
The expectation that P.bar() has an Objective-C entry point is set by the fact
that NSObject-derived Swift classes do implicitly create Objective-C entry
points for declarations within class extensions when possible, but Swift does
not (and, practically speaking, cannot) do the same for protocol extensions.
A previous mini-proposal discussed here
<https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160104/005312.html>
suggested requiring @nonobjc for members of @objc protocol extensions.
However, limiting inference of @objc eliminates the expectation itself,
addressing the problem from a different angle.
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#source-compatibility>Source
compatibility
The two changes that remove inference of @objc are both source-breaking in
different ways. The dynamic change mostly straightforward:
In Swift 4 mode, introduce an error that when a dynamic declaration does not
explicitly state @objc, with a Fix-It to add the @objc.
In Swift 3 compatibility mode, continue to infer @objc for dynamic methods.
However, introduce a warning that such code will be ill-formed in Swift 4,
along with a Fix-It to add the @objc. This
A Swift 3-to-4 migrator could employ the same logic as Swift 3 compatibility
mode to update dynamic declarations appropriately.
The elimination of inference of @objc for declarations in NSObject subclasses
is more complicated. Considering again the three cases:
In Swift 4 mode, do not infer @objc for such declarations. Source-breaking
changes that will be introduced include:
If #selector or #keyPath refers to one such declaration, an error will be
produced on previously-valid code that the declaration is not @objc. In most
cases, a Fix-It will suggest the addition of @objc.
The lack of @objc means that Objective-C code in mixed-source projects won't be
able to call these declarations. Most problems caused by this will result in
warnings or errors from the Objective-C compiler (due to unrecognized
selectors), but some might only be detected at runtime. These latter cases will
be hard-to-detect.
Other tools and frameworks that rely on the presence of Objective-C entrypoints
but do not make use of Swift's facilities for referring to them will fail. This
case is particularly hard to diagnose well, and failures of this sort are
likely to cause runtime failures that only the developer can diagnose and
correct.
In Swift 3 compatibility mode, continue to infer @objc for these declarations.
When @objc is inferred based on this rule, modify the generated header (i.e.,
the header used by Objective-C code to call into Swift code) so that the
declaration contains a "deprecated" attribute indicating that the Swift
declaration should be explicitly marked with @objc. For example:
class MyClass : NSObject {
func foo() { }
}
will produce a generated header that includes:
@interface MyClass : NSObject
-(void)foo NS_DEPRECATED("MyClass.foo() requires an explicit `@objc` in Swift
4");
@end
This way, any reference to that declaration from Objective-C code will produce
a warning about the deprecation. Users can silence the warning by adding an
explicit @objc.
A Swift 3-to-4 migrator is the hardest part of the story. Ideally, the migrator
to only add @objc in places where it is needed, so that we see some of the
expected benefits of code-size reduction. However, there are two problems with
doing so:
Some of the uses that imply the need to add @objc come from Objective-C code,
so a Swift 3-to-4 migrator would also need to compile the Objective-C code
(possibly with a modified version of the Objective-C compiler) and match up the
"deprecated" warnings mentioned in the Swift 3 compatibility mode bullet with
Swift declarations.
The migrator can't reason about dynamically-constructed selectors or the
behavior of other tools that might directly use the Objective-C runtime, so
failing to add a @objc will lead to migrated programs that compile but fail to
execute correctly.
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#overriding-of-declarations-introduced-in-class-extensions>Overriding
of declarations introduced in class extensions
Swift's class model doesn't support overriding of declarations introduced in
class extensions. For example, the following code produces an amusing error
message on the override:
class MySuperclass { }
extension MySuperclass {
func extMethod() { }
}
class MySubclass : MySuperclass { }
extension MySubclass {
override func extMethod() { } // error: declarations in extensions cannot
override yet
}
However, this does work in Swift 3 when the method is @objc, e.g.,
class MySuperclass { }
extension MySuperclass {
@objc func extMethod() { }
}
class MySubclass : MySuperclass { }
extension MySubclass {
override func extMethod() { } // okay! Objective-C message dispatch allows
this
}
Removing @objc inference for NSObject subclasses will therefore break this
correct Swift 3 code:
class MySuperclass { }
extension MySuperclass : NSObject {
func extMethod() { } // implicitly @objc in Swift 3, not @objc in Swift 4
}
class MySubclass : MySuperclass { }
extension MySubclass {
override func extMethod() { } // okay in Swift 3, error in Swift 4:
declarations in extensions cannot override yet
}
There are several potential solutions to this problem, but both are
out-of-scope for this particular proposal:
Require that a non-@objc declaration in a class extension by explicitly
declared final so that it is clear from the source that this declaration cannot
be overridden.
Extend Swift's class model to permit overriding of declarations introduced in
extensions.
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#alternatives-considered>Alternatives
considered
Aside from the obvious alternative of "do nothing", there are ways to address
some of the problems called out in theMotivation
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#motivation>
section without eliminating inference in the cases we're talking about, or to
soften the requirements on some constructs.
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#mangling-objective-c-selectors>Mangling
Objective-C selectors
Some of the problems with Objective-C selector collisions could be addressed by
using "mangled" selector names for Swift-defined declarations. For example,
given:
class MyClass : NSObject {
func print(_ value: Int) { }
}
Instead of choosing the Objective-C selector "print:" by default, which is
likely to conflict, we could use a mangled selector name like
"__MyModule__MyClass__print__Int:" that is unlikely to conflict with anything
else in the program. However, this change would also be source-breaking for the
same reasons that restricting @objc inference is: dynamic behavior that
constructs Objective-C selectors or tools outside of Swift that expect certain
selectors will break at run-time.
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#completely-eliminating-objc-inference>Completely
eliminating @objc inference
Another alternative to this proposal is to go further and completely eliminate
@objc inference. This would simplify the programming model further---it's
exposed to Objective-C only if it's marked @objc---but at the cost of
significantly more boilerplate for applications that use Objective-C
frameworks. For example:
class Sub : Super {
@objc override func foo() { } // @objc is now required
}
class MyClass : MyDelegate {
@objc func bar() { } // @objc is now required
}
I believe that this proposal strikes the right balance already, where @objc is
inferred when it's needed to maintain the semantic model, and can be explicitly
added to document those places where the user is intentionally exposing an
Objective-C entrypoint for some reason. Thus, explicitly writing @objc
indicates intent without creating boilerplate.
<https://github.com/DougGregor/swift-evolution/tree/objc-inference#proposal-add-on-objc-annotations-on-class-definitions-and-extensions>Proposal
add-on: @objc annotations on class definitions and extensions
If the annotation burden of @objc introduced by this proposal is too high, we
could make @objc on a class definition or extension thereof imply @objc on
those members that can be exposed to Objective-C. For example:
@objc extension MyClass {
// implicitly @objc
func f() { }
// Cannot be exposed to Objective-C because tuples aren't representable in
Objective-C
func g() -> (Int, Int) { return (1, 2) }
}
This would reduce (but not eliminate) the annotation burden introduced by this
proposal, allowing developers to group Objective-C-exposed declarations
together under a single @objc annotation. This reduces the amount of
boilerplate. With such a change, we'd need to decide what to do with
MyClass.g(), which could be either:
Make it an error because the context implies that it is @objc, but it cannot
be. The error would be suppressed with an explicit @nonobjc annotation.
Make it implicitly @nonobjc.
Option (1) seems more in line with the rest of the proposal, because it
maintains a predictable model. Regardless, this add-on makes sense if we like
the benefits of the proposal to limit @objc inference but the annotation burden
turns out to be annoyingly high.
- Doug
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution