> 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. 

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

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

Reply via email to