[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