objc_lookUpClass("Protocol") emitted for every use of Protocol metatype

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:

  1. Macro: A @freestanding(expression) macro that expands #getProtocolService(MyServiceProtocol.self) into lookupProtocol(MyServiceProtocol.self) as? MyServiceProtocol. The macro extracts the protocol name at compile time for both the Protocol-typed parameter (resolved at compile time, no objc_lookUpClass ) and the as? MyServiceProtocol return cast. This preserves return type inference but requires a macro implementation.
  2. Compiler-level fix: Adding a lazy cache for RuntimeOnly class lookups. Currently, normal ObjC classes already have a TODO for memoization, but the RuntimeOnly path returns early with a bare objc_lookUpClass and 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 (no objc_lookUpClass at 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.

1 Like

Can you use unsafeBitCast(_:to:) to get the result of lookupProtocol back into the Swift type system? The C function would need to take id, but it could verify that is isKindOfClass:[Protocol class] or else return nil.

public func getProtocolService<T>(_ type: T.Type) -> T? {
    if let pType = lookupProtocol(type as Any) {
      return unsafeBitCast(pType, to: (any T).self)
    } else {
        return nil
    }
}