[swift-evolution] [Proposal draft] Limiting @objc inference

Shawn Erickson shawnce at gmail.com
Wed Jan 4 21:56:49 CST 2017


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

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

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





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


>


> +1


>


>> On Jan 4, 2017, at 16:50 , Douglas Gregor via swift-evolution <
swift-evolution at 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- at 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 at swift.org


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


>


>


> --


> Rick Mann


> rmann at latencyzero.com


>


>


> _______________________________________________


> swift-evolution mailing list


> swift-evolution at swift.org


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


_______________________________________________


swift-evolution mailing list


swift-evolution at swift.org


https://lists.swift.org/mailman/listinfo/swift-evolution
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170105/034c681f/attachment.html>


More information about the swift-evolution mailing list