Let's say that we have the following Objective-C API:
- (id)foo:(Protocol *)proto;
Which is imported into Swift as:
func foo(_ proto: Protocol) -> Any
Yep, it's one of those things that gives us a proxy object. These tend to be annoying to use in Swift, so let's say we want to make a wrapper around this thing to make it a bit friendlier. First we define a couple of Objective-C-compatible protocols:
@objc protocol Super {}
@objc protocol Sub: Super {}
Now, we define a function that takes a protocol conforming to Super
and passes it along to foo()
, and then we call it with Sub
as the parameter to see if it works:
func bar<P: Super>(proto: P.Type) {
let proxy = foo(proto)
// do whatever with the proxy
}
bar(proto: Sub.self)
Well, this doesn't compile. The error message given is:
error: cannot convert value of type 'P.Type' to expected argument type 'Protocol'
Here's some stuff that does (mostly) compile:
func bar<P: Super>(proto: P.Type) {
// when called with 'Sub.self' as 'proto':
print(type(of: proto)) // Sub.Protocol
print(type(of: Sub.self)) // Sub.Protocol
print(proto == Sub.self) // true
let proxy1 = foo(Sub.self) // compiles, runs, works
let proxy2 = foo(proto) // error: cannot convert value of type 'P.Type' to expected argument type 'Protocol'
}
Okay, it's the same as Sub.self
in almost every way except that I can't pass it to something requiring an Objective-C protocol. Hmm.
The problem is that, although the fact that Sub
conforms to Super
means that it must be an Objective-C protocol, the compiler isn't realizing this. Can we work around that and get it bridged manually? Well, let's take a look at Protocol
's interface, to see if there's anything that we can...
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
@interface Protocol : NSObject
@end
Oh. Hrm.
Well, the fact that Protocol
is a full-fledged NSObject
subclass suggests that this is all probably the work of my bestest favoritest feature, the magical Swift<->Objective-C bridge that performs non-trivial conversions on things without it being obvious what is going on. Well, this gives me an idea, at least; I should be able to manually invoke the bridge by casting to AnyObject
and hopefully get at the Protocol
object that way maybe by as!
ing it or something. Does it work?
print(Sub.self as AnyObject) // <Protocol: 0x012345678>
Well, that's promising. And when I try it on my generic parameter?
print(proto as AnyObject) // Terminated due to signal: SEGMENTATION FAULT (11)
Oh, come on.
I suspect this is probably a bug in the compiler, and I plan to test a few things to determine whether that's the case, but since the Swift sources take a geologic age to compile, I figured I'd post this here while I'm waiting. Anyone have any insight and/or workarounds on what is going on here?
Okay, after investigating this a bit more, I've determined that it is indeed a compiler bug, and have filed a report on it: SR-8129. What appears to be happening is that the Swift compiler falsely assumes that proto
will always be a metatype of a concrete class
type, so it performs the bridging by emitting a call to swift_getObjCClassFromMetadata
, which crashes when it encounters the protocol metatype. When Sub.self
is explicitly cast to AnyObject
, the compiler emits Swift._bridgeAnythingToObjectiveC<A>(A) -> Swift.AnyObject
instead, which appears to dynamically determine the type of the object and bridge it accordingly.
With this in mind, the workaround becomes apparent: cast proto
to Any
first, to destroy the type information associated with the generic and force the compiler to emit Swift._bridgeAnythingToObjectiveC<A>(A) -> Swift.AnyObject
. And indeed, this appears to work:
func bar<P: Super>(proto: P.Type) {
foo(proto as Any as AnyObject as! Protocol)
}
Not the prettiest thing out there, but probably preferable to looking up the protocol via string manipulation.