Swift - 将转义闭包传递给 C API 回调

Mar*_*rry 3 c closures swift

我有我从 Swift 使用的 C API。

在斯威夫特,我有:

enum GetSnapshotResult {
    case success(snapshot: UIImage, String)
    case failure()
}

func getSnapshot(completion: @escaping (GetSnapshotResult) -> Void) {
    CAPIGetSnapshot(nil) { (_) in 
        completion(
            .success(
                snapshot: UIImage(),
                "test"
            )
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

在 C API 中:

void CAPIGetSnapshot(void * ptr, void(*callbackOnFinish)(void *)) {
    //do something in background thread
    //and on its finish, call callbackOnFinish from thread 
    
    callbackOnFinish(ptr);
}
Run Code Online (Sandbox Code Playgroud)

但是,有了这个,我得到:

AC 函数指针不能由捕获上下文的闭包形成

我该如何解决这个问题?

Mar*_*n R 5

您需要一个包装类,以便可以通过 C 函数将指向该实例的空指针传送到回调。的组合passRetained(),并takeRetainedValue()确保包装实例完成功能后解除被调用。

func getSnapshot(completion: @escaping (GetSnapshotResult) -> Void) {
    
    class Wrapper {
        let completion: (GetSnapshotResult) -> Void
        init(completion: @escaping (GetSnapshotResult) -> Void) {
            self.completion = completion
        }
    }

    let wrapper = Wrapper(completion: completion)
    let observer = UnsafeMutableRawPointer(Unmanaged.passRetained(wrapper).toOpaque())

    CAPIGetSnapshot(observer) { theObserver in
        let theWrapper = Unmanaged<Wrapper>.fromOpaque(theObserver!).takeRetainedValue()
        theWrapper.completion(
            .success( snapshot: UIImage(), "test")
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

一些备注:

  • 我假设 C 函数将ptr参数传递给回调。

  • passRetained(wrapper)保留对象。这确保在getSnapshot()函数返回时不会释放包装器实例。

  • takeRetainedValue()在闭包中消耗了保留。因此,当闭包返回时,包装器实例被释放。

  • completion是一个闭包,闭包是引用类型。wrapper.completion只要包装器实例存在,就持有对该闭包的引用。

  • 当然,您可以在闭包中使用相同的变量名(“observer”、“wrapper”)。我在这里选择了不同的名称(“theObserver”、“theWrapper”)只是为了强调它们是不同的变量,即闭包不再捕获上下文。

  • observer需要是一个可变的原始指针只是因为 C 函数的第一个参数被声明为void * ptr. 如果您可以将函数声明更改为

    void CAPIGetSnapshot(const void * ptr, void(*callbackOnFinish)(const void *))
    
    Run Code Online (Sandbox Code Playgroud)

    然后也能let observer = UnsafeRawPointer(...)工作。

  • 有关对象引用与 void 指针之间转换的更多信息,请参见例如如何在 swift 中将 self 强制转换为 UnsafeMutablePointer<Void> 类型

除了自定义包装类之外,您还可以利用这样一个事实,即任意 Swift 值在强制转换为时会自动装箱到类类型中AnyObject(参见例如AnyObject not working in Xcode8 beta6?)。

func getSnapshot(completion: @escaping (GetSnapshotResult) -> Void) {
    
    let wrapper = completion as AnyObject
    let observer = UnsafeRawPointer(Unmanaged.passRetained(wrapper).toOpaque())
    
    CAPIGetSnapshot(observer) { theObserver in
        let theCompletion = Unmanaged<AnyObject>.fromOpaque(theObserver!).takeRetainedValue()
            as! ((GetSnapshotResult) -> Void)
        theCompletion(
            .success( snapshot: UIImage(), "test")
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

强制解包和强制转换在这里是安全的,因为您知道它传递给函数的内容。解包或转换失败将指示编程错误。但我更喜欢第一个版本,而不是依赖这种“魔法”。