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

Charles Srstka cocoadev at charlessoft.com
Tue Jan 5 07:41:02 CST 2016


> On Jan 4, 2016, at 10:32 PM, Douglas Gregor via swift-evolution <swift-evolution at 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. 

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

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


More information about the swift-evolution mailing list