[Mini-proposal] Require @nonobjc on members of @objc protocol extensions


(Douglas Gregor) #1

Hi all,

We currently have a bit of a surprise when one extends an @objc protocol:

@objc protocol P { }

extension P {
  func bar() { }
}

class C : NSObject { }

let c = C()
print(c.respondsToSelector("bar")) // prints "false"

because the members of the extension are not exposed to the Objective-C runtime.

There is no direct way to implement Objective-C entry points for protocol extensions. One would effectively have to install a category on every Objective-C root class [*] with the default implementation or somehow intercept all of the operations that might involve that selector.

Alternately, and more simply, we could require @nonobjc on members of @objc protocol extensions, as an explicit indicator that the member is not exposed to Objective-C. It’ll eliminate surprise and, should we ever find both the mechanism and motivation to make default implementations of @objc protocol extension members work, we could easily remove the restriction at that time.

  - Doug

[*] Assuming you can enumerate them, although NSObject and the hidden SwiftObject cover the 99%. Even so, that it’s correct either, because the root class itself might default such a method, and the category version would conflict with it, so...


(Félix Cloutier) #2

For the folks who don't mix Swift and Objective-C that much, extensions on @objc classes are exposed to the Objective-C runtime, so there is a discrepancy here. I'm not passionate about the outcome, just dropping the info.

Félix

···

Le 4 janv. 2016 à 23:32:25, Douglas Gregor via swift-evolution <swift-evolution@swift.org> a écrit :

Hi all,

We currently have a bit of a surprise when one extends an @objc protocol:

@objc protocol P { }

extension P {
  func bar() { }
}

class C : NSObject { }

let c = C()
print(c.respondsToSelector("bar")) // prints "false"

because the members of the extension are not exposed to the Objective-C runtime.

There is no direct way to implement Objective-C entry points for protocol extensions. One would effectively have to install a category on every Objective-C root class [*] with the default implementation or somehow intercept all of the operations that might involve that selector.

Alternately, and more simply, we could require @nonobjc on members of @objc protocol extensions, as an explicit indicator that the member is not exposed to Objective-C. It’ll eliminate surprise and, should we ever find both the mechanism and motivation to make default implementations of @objc protocol extension members work, we could easily remove the restriction at that time.

  - Doug

[*] Assuming you can enumerate them, although NSObject and the hidden SwiftObject cover the 99%. Even so, that it’s correct either, because the root class itself might default such a method, and the category version would conflict with it, so...

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


(Kevin Lundberg) #3

I like this idea, but I would imagine that for an extension with many functions in it, requiring @nonobjc on each one would get tedious very fast. Could it be required (or at least allowed in addition to per-method annotations) at the extension level?:
  
  @objc protocol P {}
  
  @nonobjc extension P {
    func foo() { }
    func bar() { }
    func baz() { }
    func blah() { }
    // etc...
  }

I don’t know if this would have specific implementation ramifications over only doing this on each method, if extensions cannot already be modified with attributes. I can’t think of a case where I’ve seen annotations added to protocol extensions, or any other extensions for that matter.

···

On Jan 4, 2016, at 11:32 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

Hi all,

We currently have a bit of a surprise when one extends an @objc protocol:

@objc protocol P { }

extension P {
  func bar() { }
}

class C : NSObject { }

let c = C()
print(c.respondsToSelector("bar")) // prints "false"

because the members of the extension are not exposed to the Objective-C runtime.

There is no direct way to implement Objective-C entry points for protocol extensions. One would effectively have to install a category on every Objective-C root class [*] with the default implementation or somehow intercept all of the operations that might involve that selector.

Alternately, and more simply, we could require @nonobjc on members of @objc protocol extensions, as an explicit indicator that the member is not exposed to Objective-C. It’ll eliminate surprise and, should we ever find both the mechanism and motivation to make default implementations of @objc protocol extension members work, we could easily remove the restriction at that time.

  - Doug

[*] Assuming you can enumerate them, although NSObject and the hidden SwiftObject cover the 99%. Even so, that it’s correct either, because the root class itself might default such a method, and the category version would conflict with it, so...

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


(Charles Srstka) #4

I can almost do it right now, just hacking with the Objective-C runtime functions, so I’d think that if you were actually working with the compiler sources, it should be doable. The trouble is on the Swift side; currently there aren’t any reflection features that I can find that work on Swift protocols.

If I have a protocol and class, like so:

import Foundation

@objc protocol HasSwiftExtension {}

@objc protocol P: HasSwiftExtension {
    optional func foo()
}

extension P {
    func foo() { print("foo") }
}

class C: NSObject, P {}

(the optional is there because without it, adding the method in an extension causes the compiler to crash on my machine)

And then I have this in Objective-C:

@implementation NSObject (Swizzle)
+ (void)load {
    CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
    
    unsigned int classCount = 0;
    Class *classes = objc_copyClassList(&classCount);
    
    Protocol *proto = @protocol(HasSwiftExtension);
    
    for (unsigned int i = 0; i < classCount; i++) {
        Class eachClass = classes[i];
        
        if (class_conformsToProtocol(eachClass, proto)) {
            unsigned int protoCount = 0;
            Protocol * __unsafe_unretained *protocols = class_copyProtocolList(eachClass, &protoCount);
            
            for (unsigned int j = 0; j < protoCount; j++) {
                Protocol *eachProto = protocols[j];
                
                if (protocol_conformsToProtocol(eachProto, proto)) {
                    unsigned int methodCount = 0;
                    // what we would want would be to pass YES for isRequiredMethod; unfortunately,
                    // adding optional methods to an @objc protocol in an extension currently just
                    // crashes the compiler when I try it. So pass NO, for the demonstration.
                    struct objc_method_description *methods = protocol_copyMethodDescriptionList(eachProto, NO, YES, &methodCount);
                    
                    for (unsigned int k = 0; k < methodCount; k++) {
                        struct objc_method_description method = methods[k];
                        
                        if (!class_respondsToSelector(eachClass, method.name)) {
                            [SwizzleWrapper swizzleClass:[eachClass class] protocol:eachProto method:method];
                        }
                    }
                    
                    free(methods);
                }
            }
            
            free(protocols);
        }
    }
    
    free(classes);
    
    NSLog(@"took %f seconds", CFAbsoluteTimeGetCurrent() - startTime);
}
@end

The swizzleClass:protocol:method: method will get called for each missing method, assuming I’ve marked the protocols having an extension by making them conform to my HasSwiftExtension protocol, which the compiler could add automatically. (For the record, the time taken was 0.001501 seconds in my testing, while linking against both Foundation and AppKit).

Unfortunately there’s currently no way to go any further, since AFAIK there’s no way to reflect on a protocol to get a mapping from selector name to method. For this to work, you’d have to store the method names for methods added by extensions to @objc protocols as strings somewhere, and then have a reflection API to access them. However, if you added that, you could just:

class SwizzleWrapper: NSObject {
    class func swizzleClass(aClass: AnyClass, `protocol` aProto: Protocol, method: objc_method_description) {
        let imp: IMP
        
        // now, just add some reflection for protocols to the language so we can
        // figure out what method to call and set imp accordingly, and:
        
        class_addMethod(aClass, method.name, imp, method.types) // ta da!
    }
}

The other obvious disclaimer, of course, is that +load is probably not the right place to do this; you’d need to set things up such that they would run sometime after the Swift runtime has had a chance to finish initializing; the code as above probably isn’t safe if the Swift method being called actually does anything. But with access to the compiler source, you could make sure to get the SetUpStuff() method to run at the appropriate time, so that it could call into Swift and do its setup.

(For the record, I’m not advocating actually using the swizzling method described above; just pointing out that intercepting the selector is possible. Working with the compiler sources, I’d expect more elegant solutions would be possible.)

Charles

···

On Jan 4, 2016, at 10:32 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

There is no direct way to implement Objective-C entry points for protocol extensions. One would effectively have to install a category on every Objective-C root class [*] with the default implementation or somehow intercept all of the operations that might involve that selector.


(Andrey Tarantsov) #5

I'm against this, because I often write extensions on Apple classes (like, say, UIColor) that are only intended to be used from Swift, in a pure-Swift project, and I need no stinking' @nonobjc in there.

How much of a problem can this surprise be? You call a method, the compiler tells you it's not there, you look up the reason, no harm done.

A.

···

On Jan 5, 2016, at 11:32 AM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

Hi all,

We currently have a bit of a surprise when one extends an @objc protocol:

@objc protocol P { }

extension P {
  func bar() { }
}

class C : NSObject { }

let c = C()
print(c.respondsToSelector("bar")) // prints "false"

because the members of the extension are not exposed to the Objective-C runtime.

There is no direct way to implement Objective-C entry points for protocol extensions. One would effectively have to install a category on every Objective-C root class [*] with the default implementation or somehow intercept all of the operations that might involve that selector.

Alternately, and more simply, we could require @nonobjc on members of @objc protocol extensions, as an explicit indicator that the member is not exposed to Objective-C. It’ll eliminate surprise and, should we ever find both the mechanism and motivation to make default implementations of @objc protocol extension members work, we could easily remove the restriction at that time.

  - Doug

[*] Assuming you can enumerate them, although NSObject and the hidden SwiftObject cover the 99%. Even so, that it’s correct either, because the root class itself might default such a method, and the category version would conflict with it, so...

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


(Douglas Gregor) #6

For the folks who don't mix Swift and Objective-C that much, extensions on @objc classes are exposed to the Objective-C runtime, so there is a discrepancy here. I'm not passionate about the outcome, just dropping the info.

Right. I think this is the reason that developers expect members of @objc protocol extensions to show up in Objective-C, because it automatically happens for extensions of @objc classes. (We’ve received a number of bug reports about this)

  - Doug

···

On Jan 4, 2016, at 8:49 PM, Félix Cloutier <felixcca@yahoo.ca> wrote:

Félix

Le 4 janv. 2016 à 23:32:25, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> a écrit :

Hi all,

We currently have a bit of a surprise when one extends an @objc protocol:

@objc protocol P { }

extension P {
  func bar() { }
}

class C : NSObject { }

let c = C()
print(c.respondsToSelector("bar")) // prints "false"

because the members of the extension are not exposed to the Objective-C runtime.

There is no direct way to implement Objective-C entry points for protocol extensions. One would effectively have to install a category on every Objective-C root class [*] with the default implementation or somehow intercept all of the operations that might involve that selector.

Alternately, and more simply, we could require @nonobjc on members of @objc protocol extensions, as an explicit indicator that the member is not exposed to Objective-C. It’ll eliminate surprise and, should we ever find both the mechanism and motivation to make default implementations of @objc protocol extension members work, we could easily remove the restriction at that time.

  - Doug

[*] Assuming you can enumerate them, although NSObject and the hidden SwiftObject cover the 99%. Even so, that it’s correct either, because the root class itself might default such a method, and the category version would conflict with it, so...

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


(Douglas Gregor) #7

I like this idea, but I would imagine that for an extension with many functions in it, requiring @nonobjc on each one would get tedious very fast. Could it be required (or at least allowed in addition to per-method annotations) at the extension level?:
  
  @objc protocol P {}
  
  @nonobjc extension P {
    func foo() { }
    func bar() { }
    func baz() { }
    func blah() { }
    // etc...
  }

I don’t know if this would have specific implementation ramifications over only doing this on each method, if extensions cannot already be modified with attributes. I can’t think of a case where I’ve seen annotations added to protocol extensions, or any other extensions for that matter.

We have some declaration modifiers (e.g., access-control modifiers) and attributes (e.g., availability) that distribute in this manner from the extension to its members. My only hesitation here is that @objc itself doesn’t distribute in this way, and I’d rather they not be inconsistent.

  - Doug

···

On Jan 4, 2016, at 8:54 PM, Kevin Lundberg <kevin@klundberg.com> wrote:

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

Hi all,

We currently have a bit of a surprise when one extends an @objc protocol:

@objc protocol P { }

extension P {
  func bar() { }
}

class C : NSObject { }

let c = C()
print(c.respondsToSelector("bar")) // prints "false"

because the members of the extension are not exposed to the Objective-C runtime.

There is no direct way to implement Objective-C entry points for protocol extensions. One would effectively have to install a category on every Objective-C root class [*] with the default implementation or somehow intercept all of the operations that might involve that selector.

Alternately, and more simply, we could require @nonobjc on members of @objc protocol extensions, as an explicit indicator that the member is not exposed to Objective-C. It’ll eliminate surprise and, should we ever find both the mechanism and motivation to make default implementations of @objc protocol extension members work, we could easily remove the restriction at that time.

  - Doug

[*] Assuming you can enumerate them, although NSObject and the hidden SwiftObject cover the 99%. Even so, that it’s correct either, because the root class itself might default such a method, and the category version would conflict with it, so...

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


(Douglas Gregor) #8

I'm against this, because I often write extensions on Apple classes (like, say, UIColor) that are only intended to be used from Swift, in a pure-Swift project, and I need no stinking' @nonobjc in there.

You are misreading my proposal. I’m not proposing to change anything about how extensions of classes work. Your extensions of UIColor and other Objective-C classes remain unchanged.

How many Apple *protocols*, such as delegates, have you extended? I expect it’s not that many.

How much of a problem can this surprise be? You call a method, the compiler tells you it's not there, you look up the reason, no harm done.

I’ve seen enough bugs filed and general confusion about this that the answer is “it’s quite a bit of a surprise”. The common case seems to be that people write a protocol extension of a delegate that implements some of its optional members. The only calls to that method occur in some framework code written in Objective-C, so there place for the compiler to tell you it’s not there.

  - Doug

···

On Jan 5, 2016, at 3:51 AM, Andrey Tarantsov <andrey@tarantsov.com> wrote:

A.

On Jan 5, 2016, at 11:32 AM, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Hi all,

We currently have a bit of a surprise when one extends an @objc protocol:

@objc protocol P { }

extension P {
  func bar() { }
}

class C : NSObject { }

let c = C()
print(c.respondsToSelector("bar")) // prints "false"

because the members of the extension are not exposed to the Objective-C runtime.

There is no direct way to implement Objective-C entry points for protocol extensions. One would effectively have to install a category on every Objective-C root class [*] with the default implementation or somehow intercept all of the operations that might involve that selector.

Alternately, and more simply, we could require @nonobjc on members of @objc protocol extensions, as an explicit indicator that the member is not exposed to Objective-C. It’ll eliminate surprise and, should we ever find both the mechanism and motivation to make default implementations of @objc protocol extension members work, we could easily remove the restriction at that time.

  - Doug

[*] Assuming you can enumerate them, although NSObject and the hidden SwiftObject cover the 99%. Even so, that it’s correct either, because the root class itself might default such a method, and the category version would conflict with it, so...

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


(Douglas Gregor) #9

There is no direct way to implement Objective-C entry points for protocol extensions. One would effectively have to install a category on every Objective-C root class [*] with the default implementation or somehow intercept all of the operations that might involve that selector.

I can almost do it right now, just hacking with the Objective-C runtime functions, so I’d think that if you were actually working with the compiler sources, it should be doable. The trouble is on the Swift side; currently there aren’t any reflection features that I can find that work on Swift protocols.

The compiler isn’t the limitation here, it’s the Objective-C runtime. That’s somewhat malleable, but making changes there to support a Swift feature affects backward deployment.

@implementation NSObject (Swizzle)

Note that all approaches based on adding categories to a root class require you to enumerate root classes, as I noted in my original message. That’s unfortunate and requires more trickery.

+ (void)load {
    CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
    
    unsigned int classCount = 0;
    Class *classes = objc_copyClassList(&classCount);

Doing it this way won’t handle protocol conformances or classes loaded later via a dlopen’d dylib.

    Protocol *proto = @protocol(HasSwiftExtension);
    
    for (unsigned int i = 0; i < classCount; i++) {
        Class eachClass = classes[i];
        
        if (class_conformsToProtocol(eachClass, proto)) {
            unsigned int protoCount = 0;
            Protocol * __unsafe_unretained *protocols = class_copyProtocolList(eachClass, &protoCount);
            
            for (unsigned int j = 0; j < protoCount; j++) {
                Protocol *eachProto = protocols[j];
                
                if (protocol_conformsToProtocol(eachProto, proto)) {
                    unsigned int methodCount = 0;
                    // what we would want would be to pass YES for isRequiredMethod; unfortunately,
                    // adding optional methods to an @objc protocol in an extension currently just
                    // crashes the compiler when I try it. So pass NO, for the demonstration.

The crash is a bug; please file it.

                    struct objc_method_description *methods = protocol_copyMethodDescriptionList(eachProto, NO, YES, &methodCount);
                    
                    for (unsigned int k = 0; k < methodCount; k++) {
                        struct objc_method_description method = methods[k];
                        
                        if (!class_respondsToSelector(eachClass, method.name)) {
                            [SwizzleWrapper swizzleClass:[eachClass class] protocol:eachProto method:method];
                        }
                    }
                    
                    free(methods);
                }
            }
            
            free(protocols);
        }
    }
    
    free(classes);
    
    NSLog(@"took %f seconds", CFAbsoluteTimeGetCurrent() - startTime);
}
@end

[snip]

(For the record, I’m not advocating actually using the swizzling method described above; just pointing out that intercepting the selector is possible. Working with the compiler sources, I’d expect more elegant solutions would be possible.)

There are better mechanisms for this than +load. But one would have to deal with the dylib loading issue and the need to enumerate root classes to get to a complete implementation. Frankly, I don’t think this level of Objective-C runtime hackery is worth the effort, hence my suggestion to make the existing behavior explicit.

  - Doug

···

On Jan 5, 2016, at 5:41 AM, Charles Srstka <cocoadev@charlessoft.com> wrote:

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


(Félix Cloutier) #10

Extensions on classes already work and I can't see them requiring @objc or @nonobjc. It's extensions on protocols that don't work from Objective-C. The way I understand it, Doug suggests a warning/error for extensions on @objc protocols, and a @nonobjc attribute to shut it up.

Your point may still stand if you use @objc protocols in your code.

Félix

···

Le 5 janv. 2016 à 06:51:27, Andrey Tarantsov via swift-evolution <swift-evolution@swift.org> a écrit :

I'm against this, because I often write extensions on Apple classes (like, say, UIColor) that are only intended to be used from Swift, in a pure-Swift project, and I need no stinking' @nonobjc in there.

How much of a problem can this surprise be? You call a method, the compiler tells you it's not there, you look up the reason, no harm done.

A.

On Jan 5, 2016, at 11:32 AM, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Hi all,

We currently have a bit of a surprise when one extends an @objc protocol:

@objc protocol P { }

extension P {
  func bar() { }
}

class C : NSObject { }

let c = C()
print(c.respondsToSelector("bar")) // prints "false"

because the members of the extension are not exposed to the Objective-C runtime.

There is no direct way to implement Objective-C entry points for protocol extensions. One would effectively have to install a category on every Objective-C root class [*] with the default implementation or somehow intercept all of the operations that might involve that selector.

Alternately, and more simply, we could require @nonobjc on members of @objc protocol extensions, as an explicit indicator that the member is not exposed to Objective-C. It’ll eliminate surprise and, should we ever find both the mechanism and motivation to make default implementations of @objc protocol extension members work, we could easily remove the restriction at that time.

  - Doug

[*] Assuming you can enumerate them, although NSObject and the hidden SwiftObject cover the 99%. Even so, that it’s correct either, because the root class itself might default such a method, and the category version would conflict with it, so...

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

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


(John Joyce) #11

Would it not be possible to do the relative analog of Objective-C nullability macro sandwiches in Swift?
And then allowing exceptions within the file to be called out explicitly with @nonobjc or @objc ?
@begin_assume_nonobjc
@end_assume_nonobjc
@begin_assume_objc
@begin_assume_objc

···

On Jan 5, 2016, at 1:54 PM, Kevin Lundberg via swift-evolution <swift-evolution@swift.org> wrote:

I like this idea, but I would imagine that for an extension with many functions in it, requiring @nonobjc on each one would get tedious very fast. Could it be required (or at least allowed in addition to per-method annotations) at the extension level?:
  
  @objc protocol P {}
  
  @nonobjc extension P {
    func foo() { }
    func bar() { }
    func baz() { }
    func blah() { }
    // etc...
  }

I don’t know if this would have specific implementation ramifications over only doing this on each method, if extensions cannot already be modified with attributes. I can’t think of a case where I’ve seen annotations added to protocol extensions, or any other extensions for that matter.

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

Hi all,

We currently have a bit of a surprise when one extends an @objc protocol:

@objc protocol P { }

extension P {
  func bar() { }
}

class C : NSObject { }

let c = C()
print(c.respondsToSelector("bar")) // prints "false"

because the members of the extension are not exposed to the Objective-C runtime.

There is no direct way to implement Objective-C entry points for protocol extensions. One would effectively have to install a category on every Objective-C root class [*] with the default implementation or somehow intercept all of the operations that might involve that selector.

Alternately, and more simply, we could require @nonobjc on members of @objc protocol extensions, as an explicit indicator that the member is not exposed to Objective-C. It’ll eliminate surprise and, should we ever find both the mechanism and motivation to make default implementations of @objc protocol extension members work, we could easily remove the restriction at that time.

  - Doug

[*] Assuming you can enumerate them, although NSObject and the hidden SwiftObject cover the 99%. Even so, that it’s correct either, because the root class itself might default such a method, and the category version would conflict with it, so...

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

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


(Jordan Rose) #12

I'd be much happier with this if we could do "@nonobjc extension", and I have no problems with "@objc extension" (even if it's rare). I suppose that would be a separate proposal.

Jordan

···

On Jan 4, 2016, at 20:59, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

On Jan 4, 2016, at 8:54 PM, Kevin Lundberg <kevin@klundberg.com <mailto:kevin@klundberg.com>> wrote:

I like this idea, but I would imagine that for an extension with many functions in it, requiring @nonobjc on each one would get tedious very fast. Could it be required (or at least allowed in addition to per-method annotations) at the extension level?:
  
  @objc protocol P {}
  
  @nonobjc extension P {
    func foo() { }
    func bar() { }
    func baz() { }
    func blah() { }
    // etc...
  }

I don’t know if this would have specific implementation ramifications over only doing this on each method, if extensions cannot already be modified with attributes. I can’t think of a case where I’ve seen annotations added to protocol extensions, or any other extensions for that matter.

We have some declaration modifiers (e.g., access-control modifiers) and attributes (e.g., availability) that distribute in this manner from the extension to its members. My only hesitation here is that @objc itself doesn’t distribute in this way, and I’d rather they not be inconsistent.


(Douglas Gregor) #13

Would it not be possible to do the relative analog of Objective-C nullability macro sandwiches in Swift?
And then allowing exceptions within the file to be called out explicitly with @nonobjc or @objc ?
@begin_assume_nonobjc
@end_assume_nonobjc
@begin_assume_objc
@begin_assume_objc

Ick :slight_smile:

If we need to annotate several things at once, doing it an extension granularity is good enough, because there’s essentially no cost to merging or breaking apart extensions as needed.

  - Doug

···

On Jan 4, 2016, at 8:58 PM, John Joyce <uchuugaka@icloud.com> wrote:

On Jan 5, 2016, at 1:54 PM, Kevin Lundberg via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I like this idea, but I would imagine that for an extension with many functions in it, requiring @nonobjc on each one would get tedious very fast. Could it be required (or at least allowed in addition to per-method annotations) at the extension level?:
  
  @objc protocol P {}
  
  @nonobjc extension P {
    func foo() { }
    func bar() { }
    func baz() { }
    func blah() { }
    // etc...
  }

I don’t know if this would have specific implementation ramifications over only doing this on each method, if extensions cannot already be modified with attributes. I can’t think of a case where I’ve seen annotations added to protocol extensions, or any other extensions for that matter.

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

Hi all,

We currently have a bit of a surprise when one extends an @objc protocol:

@objc protocol P { }

extension P {
  func bar() { }
}

class C : NSObject { }

let c = C()
print(c.respondsToSelector("bar")) // prints "false"

because the members of the extension are not exposed to the Objective-C runtime.

There is no direct way to implement Objective-C entry points for protocol extensions. One would effectively have to install a category on every Objective-C root class [*] with the default implementation or somehow intercept all of the operations that might involve that selector.

Alternately, and more simply, we could require @nonobjc on members of @objc protocol extensions, as an explicit indicator that the member is not exposed to Objective-C. It’ll eliminate surprise and, should we ever find both the mechanism and motivation to make default implementations of @objc protocol extension members work, we could easily remove the restriction at that time.

  - Doug

[*] Assuming you can enumerate them, although NSObject and the hidden SwiftObject cover the 99%. Even so, that it’s correct either, because the root class itself might default such a method, and the category version would conflict with it, so...

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

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


(Charles Srstka) #14

Yeah, +load was just to throw together a quick-and-dirty demonstration, and not what you’d actually use. You have a point about libraries and bundles; you’d have to hook into that and rescan each time new code was dynamically loaded. However, the enumeration of classes only seems to take around 0.001 seconds, so I don’t think it’s terrible.

Although what might be easier would be just to include this in NSObject’s default implementation of +resolveInstanceMethod: (I don’t think you’d need to do it for every root class, since NSObjects are almost always what you’re dealing with in cases where you’d want this, and I don’t think Swift allows you to create non-NSObject @objc classes anymore anyway). This would lazily create the Objective-C selectors as they are needed, so there would be no need for iterating through classes. You could either do this with the disclaimer that the functionality required some OS X 10.x, or if you wanted it to be backward compatible you could always swizzle it in on older OS X versions (or do what KVO does and stick a dynamically-created subclass in NSObject’s place).

The thing that makes it a shame that this doesn’t work currently (and, indeed, crashing the compiler if the function isn’t optional) is that Swift’s protocol-oriented-programming concept is very appealing and could solve a lot of problems, but especially when you’re dealing with UI programming, you’re often working with code that will only take NSObjects. So if your model objects are going into something like a view-based NSTableView, which is pretty awkward to use without using bindings, using composition to build the model classes doesn’t work very well.

Charles

···

On Jan 5, 2016, at 11:52 AM, Douglas Gregor <dgregor@apple.com> wrote:

There are better mechanisms for this than +load. But one would have to deal with the dylib loading issue and the need to enumerate root classes to get to a complete implementation. Frankly, I don’t think this level of Objective-C runtime hackery is worth the effort, hence my suggestion to make the existing behavior explicit.


(Andrew Bennett) #15

I'm on the fence here, I think it's a good solution if a project has
ongoing objc dependencies.

However I have a few issues/suggestions:

1) If a project is iteratively migrating from objc to swift, as I'm sure
many are (a large project I'm working on included), then it would make that
job much more tedious and increase the objc footprint in the swift code.

2) A linter could help me slowly remove unnecessary side-effects of this
proposal, but it's not ideal, and it does not solve the large amounts of
annotations needed to do so.

3) If this proposal went ahead would existing code be migrated to add the
annotation everywhere? (to retain equivalence)

4) Have you explored any alternatives?
* instead of defaulting to @objc require an explicit @objc or @nonobjc,
otherwise have a compile-time warning; default to @nonobjc still (for
consistency).
* @objc(inherit=false)

when put on the protocol it disables what you propose, inherit is true by
default.

* compile time feedback:
    + An objc compile time warning/error suggesting you add @objc if
there's no matching selector, but there is a swift method.

This may only work in whole module optimisation, also the dynamic nature of
objc may prevent it from being deterministic.

    + A swift compile time warning/error suggesting you add @objc if
there's an objc protocol and an unannotated swift implementation.

If I remember correctly something like this may already exist.

···

On Tue, Jan 5, 2016 at 4:02 PM, Douglas Gregor via swift-evolution < swift-evolution@swift.org> wrote:

On Jan 4, 2016, at 8:58 PM, John Joyce <uchuugaka@icloud.com> wrote:

Would it not be possible to do the relative analog of Objective-C
nullability macro sandwiches in Swift?
And then allowing exceptions within the file to be called out explicitly
with @nonobjc or @objc ?
@begin_assume_nonobjc
@end_assume_nonobjc
@begin_assume_objc
@begin_assume_objc

Ick :slight_smile:

If we need to annotate several things at once, doing it an extension
granularity is good enough, because there’s essentially no cost to merging
or breaking apart extensions as needed.

- Doug

On Jan 5, 2016, at 1:54 PM, Kevin Lundberg via swift-evolution < > swift-evolution@swift.org> wrote:

I like this idea, but I would imagine that for an extension with many
functions in it, requiring @nonobjc on each one would get tedious very
fast. Could it be required (or at least allowed in addition to per-method
annotations) at the extension level?:
@objc protocol P {}
@nonobjc extension P {
func foo() { }
func bar() { }
func baz() { }
func blah() { }
// etc...
}

I don’t know if this would have specific implementation ramifications over
only doing this on each method, if extensions cannot already be modified
with attributes. I can’t think of a case where I’ve seen annotations added
to protocol extensions, or any other extensions for that matter.

On Jan 4, 2016, at 11:32 PM, Douglas Gregor via swift-evolution < > swift-evolution@swift.org> wrote:

Hi all,

We currently have a bit of a surprise when one extends an @objc protocol:

@objc protocol P { }

extension P {
  func bar() { }
}

class C : NSObject { }

let c = C()
print(c.respondsToSelector("bar")) // prints "false"

because the members of the extension are not exposed to the Objective-C
runtime.

There is no direct way to implement Objective-C entry points for protocol
extensions. One would effectively have to install a category on every
Objective-C root class [*] with the default implementation or somehow
intercept all of the operations that might involve that selector.

Alternately, and more simply, we could require @nonobjc on members of
@objc protocol extensions, as an explicit indicator that the member is not
exposed to Objective-C. It’ll eliminate surprise and, should we ever find
both the mechanism and motivation to make default implementations of @objc
protocol extension members work, we could easily remove the restriction at
that time.

- Doug

[*] Assuming you can enumerate them, although NSObject and the hidden
SwiftObject cover the 99%. Even so, that it’s correct either, because the
root class itself might default such a method, and the category version
would conflict with it, so...
_______________________________________________
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

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


(Chris Lattner) #16

I agree with Jordan on both points: would be useful, but should be a separate proposal.

-Chris

···

On Jan 5, 2016, at 4:09 PM, Jordan Rose via swift-evolution <swift-evolution@swift.org> wrote:

We have some declaration modifiers (e.g., access-control modifiers) and attributes (e.g., availability) that distribute in this manner from the extension to its members. My only hesitation here is that @objc itself doesn’t distribute in this way, and I’d rather they not be inconsistent.

I'd be much happier with this if we could do "@nonobjc extension", and I have no problems with "@objc extension" (even if it's rare). I suppose that would be a separate proposal.


(Douglas Gregor) #17

I'm on the fence here, I think it's a good solution if a project has ongoing objc dependencies.

If the Objective-C runtime is there, you have ongoing Objective-C dependencies: we call them Cocoa and Cocoa Touch :wink:

However I have a few issues/suggestions:

1) If a project is iteratively migrating from objc to swift, as I'm sure many are (a large project I'm working on included), then it would make that job much more tedious and increase the objc footprint in the swift code.

I don’t know what you mean by this. Your Objective-C code doesn’t contain protocol extensions, and the incremental cost to writing @nonobjc for members of extensions of @objc protocols (or accepting the Fix-It) is trivial.

2) A linter could help me slowly remove unnecessary side-effects of this proposal, but it's not ideal, and it does not solve the large amounts of annotations needed to do so.

You’re worried about migrating an @objc protocol to a non-@objc protocol, and it has so many protocol extensions that the cost of removing the @nonobjc (or accept the Fix-It) is prohibitive? That seems very, very unlikely to me.

3) If this proposal went ahead would existing code be migrated to add the annotation everywhere? (to retain equivalence)

Yes, where “everywhere” == “extensions of @objc protocols”.

4) Have you explored any alternatives?
* instead of defaulting to @objc require an explicit @objc or @nonobjc, otherwise have a compile-time warning; default to @nonobjc still (for consistency).
* @objc(inherit=false)
when put on the protocol it disables what you propose, inherit is true by default.
* compile time feedback:
    + An objc compile time warning/error suggesting you add @objc if there's no matching selector, but there is a swift method.
This may only work in whole module optimisation, also the dynamic nature of objc may prevent it from being deterministic.
    + A swift compile time warning/error suggesting you add @objc if there's an objc protocol and an unannotated swift implementation.
If I remember correctly something like this may already exist.

The idea of eliminating the blanket @objc default in subclasses of Objective-C-defined classes has come up. It would require us to infer ‘@objc’ in more places (and you noted), and would obviate the need for my proposal because the expectation of @objc-by-default would go away. That is a *much* larger and more interesting discussion.

  - Doug

···

On Jan 5, 2016, at 3:40 AM, Andrew Bennett <cacoyi@gmail.com> wrote:

On Tue, Jan 5, 2016 at 4:02 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Jan 4, 2016, at 8:58 PM, John Joyce <uchuugaka@icloud.com <mailto:uchuugaka@icloud.com>> wrote:

Would it not be possible to do the relative analog of Objective-C nullability macro sandwiches in Swift?
And then allowing exceptions within the file to be called out explicitly with @nonobjc or @objc ?
@begin_assume_nonobjc
@end_assume_nonobjc
@begin_assume_objc
@begin_assume_objc

Ick :slight_smile:

If we need to annotate several things at once, doing it an extension granularity is good enough, because there’s essentially no cost to merging or breaking apart extensions as needed.

  - Doug

On Jan 5, 2016, at 1:54 PM, Kevin Lundberg via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I like this idea, but I would imagine that for an extension with many functions in it, requiring @nonobjc on each one would get tedious very fast. Could it be required (or at least allowed in addition to per-method annotations) at the extension level?:
  
  @objc protocol P {}
  
  @nonobjc extension P {
    func foo() { }
    func bar() { }
    func baz() { }
    func blah() { }
    // etc...
  }

I don’t know if this would have specific implementation ramifications over only doing this on each method, if extensions cannot already be modified with attributes. I can’t think of a case where I’ve seen annotations added to protocol extensions, or any other extensions for that matter.

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

Hi all,

We currently have a bit of a surprise when one extends an @objc protocol:

@objc protocol P { }

extension P {
  func bar() { }
}

class C : NSObject { }

let c = C()
print(c.respondsToSelector("bar")) // prints "false"

because the members of the extension are not exposed to the Objective-C runtime.

There is no direct way to implement Objective-C entry points for protocol extensions. One would effectively have to install a category on every Objective-C root class [*] with the default implementation or somehow intercept all of the operations that might involve that selector.

Alternately, and more simply, we could require @nonobjc on members of @objc protocol extensions, as an explicit indicator that the member is not exposed to Objective-C. It’ll eliminate surprise and, should we ever find both the mechanism and motivation to make default implementations of @objc protocol extension members work, we could easily remove the restriction at that time.

  - Doug

[*] Assuming you can enumerate them, although NSObject and the hidden SwiftObject cover the 99%. Even so, that it’s correct either, because the root class itself might default such a method, and the category version would conflict with it, so...

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

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

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


(Greg Parker) #18

Enumeration of classes is terrible: it forces the runtime to perform lots of work that it tries very hard to perform lazily otherwise. I would expect your measured cost to be much higher if you had linked to more high-level libraries (UIKit, MapKit, etc).

···

On Jan 5, 2016, at 3:37 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

On Jan 5, 2016, at 11:52 AM, Douglas Gregor <dgregor@apple.com <mailto:dgregor@apple.com>> wrote:

There are better mechanisms for this than +load. But one would have to deal with the dylib loading issue and the need to enumerate root classes to get to a complete implementation. Frankly, I don’t think this level of Objective-C runtime hackery is worth the effort, hence my suggestion to make the existing behavior explicit.

Yeah, +load was just to throw together a quick-and-dirty demonstration, and not what you’d actually use. You have a point about libraries and bundles; you’d have to hook into that and rescan each time new code was dynamically loaded. However, the enumeration of classes only seems to take around 0.001 seconds, so I don’t think it’s terrible.

--
Greg Parker gparker@apple.com Runtime Wrangler


(Charles Srstka) #19

That was my gut reaction to the idea also, when I had it, but it seems to run pretty fast no matter what I do. I just tried dragging every framework from /System/Library/Frameworks into the project, removing only the Java frameworks, Kernel.framework, Message.framework, and vecLib.framework. Time taken was 0.004260 seconds.

It is, of course, ugly and hacky as hell, and that might make a pretty good reason not to do it. :-/ What do you think about the other idea, of adding to NSObject’s default implementation of +resolveInstanceMethod:? That *would* be done lazily, and would avoid all the problems involving dynamically loaded code.

Charles

···

On Jan 5, 2016, at 8:29 PM, Greg Parker <gparker@apple.com> wrote:

On Jan 5, 2016, at 3:37 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Jan 5, 2016, at 11:52 AM, Douglas Gregor <dgregor@apple.com <mailto:dgregor@apple.com>> wrote:

There are better mechanisms for this than +load. But one would have to deal with the dylib loading issue and the need to enumerate root classes to get to a complete implementation. Frankly, I don’t think this level of Objective-C runtime hackery is worth the effort, hence my suggestion to make the existing behavior explicit.

Yeah, +load was just to throw together a quick-and-dirty demonstration, and not what you’d actually use. You have a point about libraries and bundles; you’d have to hook into that and rescan each time new code was dynamically loaded. However, the enumeration of classes only seems to take around 0.001 seconds, so I don’t think it’s terrible.

Enumeration of classes is terrible: it forces the runtime to perform lots of work that it tries very hard to perform lazily otherwise. I would expect your measured cost to be much higher if you had linked to more high-level libraries (UIKit, MapKit, etc).


(Félix Cloutier) #20

The linker is smart enough to get rid of frameworks that you don't actually use.

Félix

···

Le 5 janv. 2016 à 21:55:17, Charles Srstka via swift-evolution <swift-evolution@swift.org> a écrit :

On Jan 5, 2016, at 8:29 PM, Greg Parker <gparker@apple.com <mailto:gparker@apple.com>> wrote:

On Jan 5, 2016, at 3:37 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Jan 5, 2016, at 11:52 AM, Douglas Gregor <dgregor@apple.com <mailto:dgregor@apple.com>> wrote:

There are better mechanisms for this than +load. But one would have to deal with the dylib loading issue and the need to enumerate root classes to get to a complete implementation. Frankly, I don’t think this level of Objective-C runtime hackery is worth the effort, hence my suggestion to make the existing behavior explicit.

Yeah, +load was just to throw together a quick-and-dirty demonstration, and not what you’d actually use. You have a point about libraries and bundles; you’d have to hook into that and rescan each time new code was dynamically loaded. However, the enumeration of classes only seems to take around 0.001 seconds, so I don’t think it’s terrible.

Enumeration of classes is terrible: it forces the runtime to perform lots of work that it tries very hard to perform lazily otherwise. I would expect your measured cost to be much higher if you had linked to more high-level libraries (UIKit, MapKit, etc).

That was my gut reaction to the idea also, when I had it, but it seems to run pretty fast no matter what I do. I just tried dragging every framework from /System/Library/Frameworks into the project, removing only the Java frameworks, Kernel.framework, Message.framework, and vecLib.framework. Time taken was 0.004260 seconds.

It is, of course, ugly and hacky as hell, and that might make a pretty good reason not to do it. :-/ What do you think about the other idea, of adding to NSObject’s default implementation of +resolveInstanceMethod:? That *would* be done lazily, and would avoid all the problems involving dynamically loaded code.

Charles
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution