
OpenSuperWhisper源码解析Swift如何安全桥接whisper.cpp的C回调AbortFlag与Unmanaged线程安全设计【免费下载链接】OpenSuperWhispermacOS dictation app项目地址: https://gitcode.com/gh_mirrors/op/OpenSuperWhisperOpenSuperWhisper 是一款 macOS 离线语音速记dictation应用按下快捷键即可说话、松开即生成文字。它的核心是让 Swift 安全地桥接 whisper.cpp 的 C 回调——本文通过 WhisperEngine.swift 源码带你彻底看懂Unmanaged传指针、AbortFlag协作取消和线程安全的完整设计。为什么 Swift 调 C 回调会踩坑 OpenSuperWhisper 的模型加载与推理全部委托给 whisper.cpp通过 libwhisper/CMakeLists.txt 构建头文件经 Bridge.h 引入。而 whisper.cpp 的推理参数里有两个回调回调作用触发线程abort_callback每轮迭代询问是否取消C 推理线程progress_callback汇报 0–100% 进度C 推理线程问题在于Swift 对象由 ARC 自动管理生命周期而 C 回调里只能拿到一个void*原始指针。如果你把一个 Swift 对象地址直接塞进 C 结构体一旦对象被释放C 代码再解引用就是野指针崩溃。这是所有Swift 包 C 库项目共同的隐患。第一招Unmanaged 把 Swift 对象安全地交给 C看进度回调的写法WhisperEngine.swifttypealias WhisperProgressCallback convention(c) (OpaquePointer?, OpaquePointer?, Int32, UnsafeMutableRawPointer?) - Void let progressCallback: WhisperProgressCallback { _, _, progressPercent, userData in guard let userData userData else { return } let ctx UnmanagedProgressContext.fromOpaque(userData).takeUnretainedValue() // 映射进度并切回主线程 DispatchQueue.main.async { ctx.onProgress?(normalizedProgress) } } let progressContextPtr Unmanaged.passUnretained(progressContext!).toOpaque()两个关键动作配合完成安全桥接Unmanaged.passUnretained(ctx).toOpaque()不转移引用计数、只做地址转换。之所以敢用不托管的方式是因为生命周期被defer { progressContext nil }锁死在context.full()这一次调用之内——回调只存在于 C 推理期间不会在对象释放后被调用UnmanagedProgressContext.fromOpaque(userData).takeUnretainedValue()C 侧把指针原样带回来Swift 侧按同一规则还原成强类型对象且takeUnretained不会扰动引用计数杜绝回调里偷偷多持有一个引用的泄漏。 规则就一句话谁创建、谁保证存活。C 指针本身不拥有对象Swift 侧负责让它活得比 C 调用更久。AbortFlag一个永不悬垂的协作取消开关 取消转写比进度回调更微妙Swift 的Task取消了但 C 的推理循环并不认识 Swift 的取消机制。OpenSuperWhisper 的答案是 WhisperEngine.swift 中的AbortFlag/// Thread-safe cancellation flag. Owned by the engine for its whole lifetime, /// so the pointer passed into whispers C callback can never dangle. private final class AbortFlag { private let lock NSLock() private var _isSet false var isSet: Bool { get { lock.lock(); defer { lock.unlock() }; return _isSet } set { lock.lock(); defer { lock.unlock() }; _isSet newValue } } }设计上有三层讲究生命周期绑定引擎abortFlag是引擎成员变量private let abortFlag AbortFlag()与引擎同生同死。C 回调里传入的指针在整个 App 使用期间都不可能悬垂——注释里明说的 can never dangleNSLock 保证可见性Swift 主线程写isSet trueC 推理线程读isSet没有锁的话跨线程读写Bool属于数据竞争。用NSLock包一层 getter/setter读写都串行化语义简单又够用协作式取消C 侧每轮迭代调用abort_callback返回true时 whisper.cpp 立即中断推理无需强杀线程强杀线程会留下未释放的中间状态。取消的完整链路是用户操作 → TranscriptionQueue.swift 触发 → TranscriptionService.swift 转发给当前引擎 →cancelTranscription()只做一件事abortFlag.isSet true。C 推理线程在下一个检查点自行退出Swift 侧则靠try Task.checkCancellation()多处埋点见 WhisperEngine.swift尽快抛出取消。 对比一下同目录的 FluidAudioEngine.swift 使用 Swift 包的 FluidAudio 库取消了就翻转一个Published布尔值即可。正是因为 whisper.cpp 是纯 C 库才需要AbortFlag Unmanaged这套原始指针安全设计——这也是本文标题的重点所在。ProgressContextC 线程进度如何安全地驱动 UI进度回调同样跑在 C 推理线程上直接操作 UI 是违规行为。WhisperEngine.swift 用ProgressContext解决了两件事主线程调度回调内只更新进度值真正调用 UI 闭包前包一层DispatchQueue.main.asyncSwiftUI 只在主线程刷新单调递增保护lastReportedProgress同样用NSLock保护C 线程写、主线程读且只有比上次更大才上报避免进度条回跳。注意它与AbortFlag的分工AbortFlag 是长命的引擎级ProgressContext 是短命的单次转写级用完即置 nil。一长一短两种生命周期恰好示范了passUnretained指针在不同存活期下的两种正确用法。小结把 C 回调接进 Swift 的 3 条军规 ✅指针要还原、计数不动passUnretained/takeUnretainedValue成对出现C 侧永不拥有 Swift 对象生命周期要有主短命对象锁在单次 C 调用内defer兜底长命对象绑定宿主如引擎实例指针永不悬垂跨线程状态必加锁NSLock守护isSet与进度值读多写少的场景成本几乎为零。想继续深入可以从 OpenSuperWhisper/Engines/WhisperEngine.swift 的transcribeAudio完整流程读起再看 OpenSuperWhisper/TranscriptionService.swift 了解队列如何调度多个引擎就能把这套Swift 桥接 C 回调的设计吃透了。【免费下载链接】OpenSuperWhispermacOS dictation app项目地址: https://gitcode.com/gh_mirrors/op/OpenSuperWhisper创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考