Why do synchronous protocol function implementations of asynchronous requirements no longer work with @objc annotation?

I am clueless about the difference in the @objc annotation on a protocol and conformance of its implementing types in regard to asynchronous functions. Assuming I have this protocol:

protocol ExampleProtocol {
    func getSomething() async -> String
}

And I have this implementation:

open class ExampleImplementation: ExampleProtocol {
    func getSomething() -> String {
        return "Yo!"
    }
}

This compiles fine. Personally, I would have assumed a conformance violation already because of the missing async in the implementation. But that is not the matter. If I now add the @objc annotation to the protocol declaration, then it breaks the conformance of ExampleImplementation. Why?

Type 'ExampleImplementation' does not conform to protocol 'ExampleProtocol'

Background: We have a protocol which we need to annotate with @objc because of NSXPCInterface.init(with protocol: Protocol). In that XPC service protocol we have some async functions. From the protocol declaration we generated mock code with SwiftyMocky which drops the async keywords on the mock function implementations. That works fine, as long as the protocol is not annotated with @objc. But because it is, this breaks our test builds. That is how I came to this question.

Native Swift protocol conformances are represented by generating a witness table of function pointers, corresponding to each protocol requirement. The compiler emits a thunk for each protocol requirement which calls the concrete type's implementation. This thunk can deal with differences in calling conventions, such as sync vs async, etc.

With @objc protocols however, the situation is different; the conforming class must provide a method implementation for the protocol requirement's selector. An @objc async method is actually presented to Objective-C as taking a callback which is invoked upon completion. So an @objc sync method has the wrong selector name, and wrong contract and cannot fulfill an @objc async requirement. There's no opportunity to emit a thunk here.

2 Likes

Thank you for the clarification. I had to lookup what a thunk is. There even is a Wikipedia article about that.

So in our case I will keep out the async of our XPC service interface and outsource the async/await convenience wrappers into a dedicated Swift type to still have the more convenient API and automatically generated mocks by SwiftyMocky.