Does dynamic lookup of @objc async methods break cancellation?

Not sure how if it is possible to skip ObjC interface without violating LSP.

Would it be possible to allocate two "slots" in the interface for such methods, and use second one to smuggle async entry point?

Something like this:

@protocol Loader <NSObject>
- (void)loadWithCompletionHandler:(void(^)(NSString *result, NSError *error))completionHandler;
@optional
/// Extra indirection just to make sure that async calling convention doesn't interfere with ObjC runtime.
/// If that is not a concern, we could store pointer to async func as IMP of this method.
- (void*)__get_async_load_impl;
@end
func use(loader: Loader) async throws {
    let result: String
    
    if loader.responds(to: #selector(__get_async_load)) {
          let impl = unsafeBitCast(loader.__get_async_load_impl(), to: (@convention(thin) (AnyObject) async throws -> String).self) 
          result = try await impl(loader)
    } else {
         result = try await withUnsafeThrowingContinuation { cont in
            ...
         }
    }

    print(result)
}

Another approach would be to embrace cancellation in ObjC interoperability and generate the following:

@protocol Loader <NSObject>
- (void)loadWithCanceller:(NSCanceller *)canceller completionHandler:(void(^)(NSString *result, NSError *error))completionHandler;
@end
extension LoaderImpl {
    @objc func load(canceller: NSCanceller, completionHandler:  @convention(block) (NSString?, NSError?) {
        let t = Task {
            do {
                let t = try await self.load()
                completionHandler(t as NSString, nil)
            } catch {
                completionHandler(nil, error as NSError)
            }
        }
        canceller.addHandler(t.cancel)
    }
}

func use(loader: Loader) async throws {
     let c = NSCanceller()
     let r = try await withCancellationHandler(c.cancel) {
         try await withUnsafeThrowingContinuation { cont in
             self.load(canceller: c) { result, error in
                 ...
             }
         }
     }
     print(try await loader.load())
}

The latter approach would work well even in cases when we need to do double bridging with arbitrary complex code in between.

And there seems to be some demand for standard thread-safe canceller type anyway - How to use withTaskCancellationHandler properly?