Search code examples
swiftobjective-c-runtime

Workaround to bridge a *generic* protocol to Objective-C?


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?


Solution

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