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

Charles Srstka cocoadev at charlessoft.com
Wed Jan 6 15:55:49 CST 2016


> On Jan 5, 2016, at 8:55 PM, Charles Srstka via swift-evolution <swift-evolution at swift.org> wrote:
> 
>> On Jan 5, 2016, at 8:29 PM, Greg Parker <gparker at apple.com <mailto:gparker at apple.com>> wrote:
>> 
>>> 
>>> On Jan 5, 2016, at 3:37 PM, Charles Srstka via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>> 
>>>> On Jan 5, 2016, at 11:52 AM, Douglas Gregor <dgregor at apple.com <mailto:dgregor at 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.

Okay, here’s a more serious, less devil’s-advocatey sketch. It would require one tweak to the compiler to allow the swiftImplementationForSelector() method to be vtable dispatched. Otherwise, it pretty much works:

The basic protocol:

@objc protocol HasSwiftExtension {}

extension HasSwiftExtension {
    // Problem: This method won't correctly be dispatched, and instead this implementation will always be called.
    // The method cannot be declared in the protocol, because then Swift would try to use the Objective-C runtime
    // to find it (and fail). Some way to declare methods in extensions that are dispatched via the vtable would
    // need to be added for this to work properly.
    func swiftImplementationForSelector(selector: Selector) -> (implementation: IMP, types: String)? {
        return nil
    }
}

extension NSObject {
    class func somePrefixHere_SwizzledResolveInstanceMethod(selector: Selector) -> Bool {
        // Doesn't work as written because of the lack of vtable dispatch.
        // However, if you change "as? HasSwiftExtension" to "as? P" below, it will work in the case of P.
        if let swiftySelf = self as? HasSwiftExtension {
            // Yes, here we are calling an instance method from a class object.
            // It works because the NSObject class is technically also an instance of NSObject.
            // It would be better to use a class method, but Swift doesn't allow declaring those
            // in protocols or extensions.
            if let (implementation: imp, types: types) = swiftySelf.swiftImplementationForSelector(selector) {
                class_addMethod(self, selector, imp, types)
                return true
            }
        }
        
        return somePrefixHere_SwizzledResolveInstanceMethod(selector)
    }
}

The Objective-C shim to do the swizzling:

@interface NSObject(Swizzle)
@end

@implementation NSObject(Swizzle)

+ (void)load {
    Method m1 = class_getClassMethod(self, @selector(resolveInstanceMethod:));
    Method m2 = class_getClassMethod(self, @selector(somePrefixHere_SwizzledResolveInstanceMethod:));
    
    method_exchangeImplementations(m1, m2);
}

@end

Sample protocol and class conforming to this:

// HasSwiftExtension conformance would be added automatically by the compiler.
@objc protocol P: HasSwiftExtension {
    optional func foo() // optional only to avoid that compiler crash
}

class C: NSObject, P {}

extension P {
    // This method would be added automatically by the compiler.
    func swiftImplementationForSelector(selector: Selector) -> (implementation: IMP, types: String)? {
        switch selector {
        case "foo":
            let block: @convention(block) (P) -> () = { $0.foo() }
            let imp = imp_implementationWithBlock(unsafeBitCast(block, AnyObject.self))
            
            return (implementation: imp, types: "v@:")
        default:
            return nil
        }
    }
    
    func foo() { print("foo was called") }
}

Changing “as? HasSwiftExtension” to “as? P” in somePrefixHere_SwizzledResolveInstanceMethod(), and then calling this in main:

let c = C()

c.performSelector("foo")

outputs:

foo was called
Program ended with exit code: 0

I think it could be doable.

Charles

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160106/a8aa71f4/attachment.html>


More information about the swift-evolution mailing list