We have a generic helper that looks up an ObjC protocol at runtime:
public func getProtocolService<T>(_ type: T.Type) -> T? {
guard let pType = type as Any as AnyObject as? Protocol else {
return nil
}
return lookupProtocol(pType) as? T
}
where lookupProtocol is a C function:
FOUNDATION_EXTERN id _Nullable lookupProtocol(Protocol * _Nullable prot);
Callers use it like:
let service = getProtocolService(MyServiceProtocol.self)
// service: MyServiceProtocol?
We discovered via Instruments that getProtocolService() is very hot in production. The root cause is the as? Protocol cast. The compiler needs Protocol class metadata to perform the dynamic cast, but ObjC's Protocol is classified as ForeignKind::RuntimeOnly internally, so every reference emits an objc_lookUpClass("Protocol") call - which acquires the ObjC runtime lock, causing significant contention under concurrency.
We verified on Swift 6.2 with -O . The generated IR for the function body:
%5 = call ptr @_bridgeAnythingToObjectiveC(...)
%6 = call ptr @objc_lookUpClass(ptr @.str.8.Protocol) ; ← acquires runtime lock
%7 = call ptr @swift_dynamicCastObjCClass(ptr %5, ptr %6)
Note: a simpler type as? Protocol (without the as Any as AnyObject bridge) produces a compiler warning "cast from 'T.Type' to unrelated type 'Protocol' always fails" and gets optimized to nil under -O , making the function silently broken. The as Any as AnyObject as? Protocol workaround avoids this.
We've considered:
- Macro: A
@freestanding(expression)macro that expands#getProtocolService(MyServiceProtocol.self)intolookupProtocol(MyServiceProtocol.self) as? MyServiceProtocol. The macro extracts the protocol name at compile time for both theProtocol-typed parameter (resolved at compile time, noobjc_lookUpClass) and theas? MyServiceProtocolreturn cast. This preserves return type inference but requires a macro implementation. - Compiler-level fix: Adding a lazy cache for
RuntimeOnlyclass lookups. Currently, normal ObjC classes already have a TODO for memoization, but the RuntimeOnly path returns early with a bareobjc_lookUpClassand no Swift-side caching, so every call still goes through the ObjC runtime.
So, the question is: is there a way in Swift to write a function where:
- The parameter accepts a protocol metatype (like
MyProtocol.self) - The return type is inferred as that protocol (
MyProtocol?) from the argument - The compiler resolves the
Protocol*at compile time (noobjc_lookUpClassat runtime)
Today, T.Type gives us the first two but forces the runtime lookup. Protocol gives us the third but loses the first two. Are we missing something, or is this a known limitation?
Any suggestions, whether source-level workarounds, compiler improvements, or macro-based approaches, would be appreciated.