xberg Kotlin/Android 插件 API 实战:用 OcrBackendBridge.clearAll() 清除全部 OCR 后端
xberg Kotlin/Android 插件 API 实战:用 OcrBackendBridge.clearAll() 清除全部 OCR 后端
本文围绕 xberg 的 Kotlin(Android)绑定中 OCR 后端注册表的生命周期管理展开,核心讲解 OcrBackendBridge.clearAll() 这一「一键清空全部 OCR 后端」的插件 API 用法。你将掌握该 API 的调用方式、与 register / unregister / list 等配套方法的协作关系,以及从 Kotlin 到 JNI 再到 Rust 全局注册表的完整底层调用链,并理解清除之后内置后端如何「自愈」重新播种的机制。
一、为什么需要「清除全部 OCR 后端」
xberg 以 Rust 为核心实现文档智能解析,其中 OCR(光学字符识别)能力由一套插件式后端注册表统一管理。Tesseract、PaddleOCR、VLM 等后端按 feature 开关被注册进一个进程级全局注册表,宿主语言(Kotlin/Java、Python、Node.js 等)可以通过 trait-bridge 机制注册自定义 OCR 后端,参与 xberg 的 OCR 分派。
在插件化场景中,后端注册表是有状态且进程级共享的:
- 注册了错误的、行为异常的或与当前任务不匹配的后端;
- 需要在运行时整体重置插件环境,为新一轮注册做准备;
- 测试套件需要在每个用例之间清理注册表,保证隔离性;
此时就需要一个「清场」操作。xberg 在 crates/xberg/src/plugins/ocr.rs 中公开了 Rust 层的 clear_ocr_backends(),并在 Kotlin/Android 绑定中通过 OcrBackendBridge.clearAll() 暴露给 Kotlin 调用方。对应 e2e fixture 的说明文字(见 ocr_backends_clear.md)将其定义为:"Clear all OCR backends and verify list is empty"(清除全部 OCR 后端并验证列表为空),side_effect: safe 表明这是一个无副作用风险的安全操作。
二、核心用法:OcrBackendBridge.clearAll()
原文档给出的 Kotlin(Android)调用示例非常简洁,完整继承如下:
import io.xberg.*
fun main() {
OcrBackendBridge.clearAll()
}
这段代码的语义是:把当前进程中注册的全部 OCR 后端一次性移除。clearAll() 无参数、无返回值(Unit),调用后全局 OCR 注册表被清空。
2.1 与单点移除 unregister() 的差异
OcrBackendBridge 提供了「单点移除」与「批量清空」两种粒度,源码见 OcrBackendBridge.kt:
object OcrBackendBridge {
private val registered = mutableMapOf<String, IOcrBackend>()
fun register(impl: IOcrBackend): Unit {
val name = impl.name()
registered[name] = impl
XbergBridge.nativeRegisterOcrBackend(OcrBackendJniDispatcher(impl))
}
fun unregister(name: String): Unit {
registered.remove(name)
XbergBridge.nativeUnregisterOcrBackend(name)
}
fun clearAll(): Unit {
registered.clear()
XbergBridge.nativeClearOcrBackends()
}
fun getAll(): Map<String, IOcrBackend> = registered.toMap()
}
可以清晰看到 clearAll() 做了两件事:
- 清空 Kotlin 侧的本地镜像注册表
registered(mutableMapOf<String, IOcrBackend>); - 调用 JNI 外部函数
XbergBridge.nativeClearOcrBackends(),把清空动作下沉到 Rust 全局注册表。
与之对应,unregister(name) 只按名字移除单个后端。如果你只想去掉某个具体后端(例如只卸载自定义的 my-custom-ocr),应优先用 unregister;只有当需要整体重置时才使用 clearAll()。
2.2 清空后的验证
fixture 名称中的 "verify list is empty" 提示了标准验证姿势——清空后调用 Xberg.listOcrBackends() 确认列表为空。兄弟文档 ocr_backends_list.md 给出了列表查询的标准写法:
import io.xberg.*
fun main() {
val result = Xberg.listOcrBackends()
println(result)
}
listOcrBackends() 的实现在 Xberg.kt:通过 JNI 调用 nativeListOcrBackends() 读取 Rust 侧注册表快照并反序列化为 List<String>。因此一个完整的「清除并验证」流程是:
OcrBackendBridge.clearAll()
val remaining = Xberg.listOcrBackends()
check(remaining.isEmpty()) { "expected empty OCR backend registry, got: $remaining" }
三、配套 API 全景:一次看清 OCR 后端管理方法
clearAll() 是 OCR 后端管理族的一员,Kotlin/Android 绑定围绕 OCR 插件管理共提供以下几组入口,全部集中在 Xberg.kt 与 OcrBackendBridge 中:
| 方法 | 作用 | 备注 |
|---|---|---|
OcrBackendBridge.register(impl) |
注册自定义 OCR 后端 | 传入实现 IOcrBackend 的对象,内部包装为 OcrBackendJniDispatcher 后桥接原生层 |
OcrBackendBridge.unregister(name) |
按名字移除单个后端 | 名字不存在时优雅处理(e2e 用例专门覆盖了不存在后端的场景) |
OcrBackendBridge.clearAll() |
移除全部 OCR 后端 | 本主题核心 API |
OcrBackendBridge.getAll() |
获取 Kotlin 侧已注册后端映射 | 仅返回本地注册的宿主后端 |
Xberg.listOcrBackends() |
列出全局注册表中所有后端名字 | 覆盖内置后端与宿主注册后端 |
Xberg.listOcrBackendCapabilities() |
列出各后端及其语言能力 | 返回结果按名称排序;Tesseract 首次枚举会初始化 tessdata 目录 |
Xberg.listOcrBackendCapabilitiesFor(config) |
在指定 OcrConfig 下报告语言能力 |
设置 tessdata_path 时应使用此带配置版本(见 GH#1857) |
Xberg.ocrBackendSupportsLanguage(backend, lang) |
查询某后端是否支持某语言 | 未注册的后端名会抛异常 |
从源码结构看,这些方法形成了完整的「注册 → 查询 → 单点移除 → 整体清空」生命周期闭环:register 负责接入,listOcrBackends / listOcrBackendCapabilities 负责观测,unregister 负责定点移除,clearAll 负责整体重置。
四、底层调用链:Kotlin → JNI → Rust 注册表
clearAll() 不是一次简单的本地清理,它贯穿了 xberg 多语言绑定的三层架构。完整链路如下:
4.1 第一层:Kotlin 桥接层
clearAll() 调用的 XbergBridge.nativeClearOcrBackends() 声明在 XbergBridge.kt 的 "JNI trait-bridge external funs" 区域:
@Throws(XbergBridgeException::class)
external fun nativeRegisterOcrBackend(impl: io.xberg.OcrBackendJniDispatcher)
@Throws(XbergBridgeException::class)
external fun nativeUnregisterOcrBackend(name: String)
@Throws(XbergBridgeException::class)
external fun nativeClearOcrBackends()
这些 external fun 由 Rust JNI shim 实现,通过 JNI 将调用下沉到 Rust 核心。注意它们都标注了 @Throws(XbergBridgeException::class),说明原生层异常会被包装为 XbergBridgeException 抛回 Kotlin。
4.2 第二层:Rust 公开 API
JNI shim 最终调用 Rust 层公开函数 clear_ocr_backends(),其定义与文档注释见 crates/xberg/src/plugins/ocr.rs:
/// Clear all OCR backends from the global registry.
///
/// Removes all OCR backends and calls their `shutdown()` methods.
///
/// # Returns
///
/// - `Ok(())` if all backends were cleared successfully
/// - `Err(...)` if any shutdown method failed
pub fn clear_ocr_backends() -> crate::Result<()> {
use crate::plugins::registry::get_ocr_backend_registry;
let registry = get_ocr_backend_registry();
let mut registry = registry.write();
registry.shutdown_all()
}
关键信息有二:其一,清空动作要拿注册表的写锁(registry.write()),避免与并发 OCR 分派冲突;其二,返回 crate::Result<()>,若某个后端的 shutdown() 失败会返回 Err。所以 clearAll() 在极端情况下并非绝对静默——后端资源释放失败会被传递到上层。
4.3 第三层:注册表实现
shutdown_all() 定义在 OCR 注册表实现 crates/xberg/src/plugins/registry/ocr.rs:
/// Shutdown all backends and clear the registry.
pub fn shutdown_all(&mut self) -> Result<()> {
let names: Vec<_> = self.backends.keys().cloned().collect();
for name in names {
self.remove(&name)?;
}
Ok(())
}
实现要点:先快照所有后端名字再逐个 remove(),每个被移除的后端都会调用其 shutdown() 方法(见同文件 remove 的实现:backend.shutdown()?),确保模型资源、线程、缓存等被正确释放,而不是简单地把 map 清空。这与文档注释 "Removes all OCR backends and calls their shutdown() methods" 完全吻合。注册表还提供了 clear() 作为 shutdown_all() 的别名,注释明确说明它是 "used by alef trait-bridge codegen"(供 alef 代码生成器使用)。
五、清除之后:内置后端如何「自愈」重新播种
这是使用 clearAll() 时最容易踩坑、也最值得理解的一点:清空并非永久禁用。
xberg 的 OCR 注册表在首次构造时会按 feature 开关播种内置后端(Tesseract、PaddleOCR、VLM 等)。clear_ocr_backends() 清空后,下一次 OCR 分派前,内部自愈路径 ensure_ocr_backends_initialized() 会检查 is_missing_default_backend(),若发现注册表为空或缺少默认后端,就会非破坏性地重新注册内置后端。相关实现见 crates/xberg/src/plugins/ocr.rs:
/// Ensure the global OCR backend registry has its built-in backends registered.
///
/// The global registry is seeded with the built-in backends (Tesseract,
/// PaddleOCR, VLM — gated by feature flags) when it is first constructed.
/// However, [`clear_ocr_backends`] empties the registry, leaving subsequent
/// OCR operations with no backend to dispatch to.
///
/// This function is the self-healing counterpart ... it re-registers the
/// built-in backends whenever the built-in default is missing ...
/// Re-seeding is non-destructive (user-registered backends are kept) ...
pub(crate) fn ensure_ocr_backends_initialized() {
// 读锁下检查 is_missing_default_backend()
// 缺失时 registry.write().ensure_defaults()
}
同时,注册表层 crates/xberg/src/plugins/registry/ocr.rs 对「缺失」的定义非常严谨:
- 注册表完全为空;
- 或编译进 Tesseract 内置时,默认后备后端
tesseract缺席(例如clear()之后只注册了另一个后端,注册表非空但默认配置的 OCR 无后端可用)。
ensure_defaults() 的重新播种是非破坏性的:用户通过 register() 注册的宿主后端会被保留,只补齐缺失的内置默认。
这对你的实战意味着:
clearAll()适合做「临时重置」,适合测试隔离、插件热切换前的清理;- 若你的目标是把 OCR 能力彻底关掉或换成纯自定义后端,需要结合后续分派行为理解——内置默认后端可能在下一次 OCR 调用时自动回归;
- 代码注释明确提到,OCR 管线中已有针对"同一进程内先
clear_ocr_backends()再执行 OCR 分派"的重新播种逻辑(见 pipeline.rs 中的说明),说明这是官方认可并测试覆盖的用法模式。
六、测试验证与 fixture 契约
这个 API 的正确性由仓库中的 e2e 测试与 fixture 契约双重保证。
6.1 Kotlin e2e 测试
Kotlin/Android 侧对应测试位于 OcrBackendManagementTest.kt:
@Test
fun testOcrBackendsClear(): Unit = runBlocking {
// Clear all OCR backends and verify list is empty
val result = OcrBackendBridge.clearAll()
assertNotNull(result, "expected non-null result")
}
同一测试类还覆盖了相邻行为:testOcrBackendsList(列出全部后端)、testOcrBackendCapabilitiesList(列出能力)、testOcrBackendSupportsLanguageUnknownBackend(未注册后端查询语言支持必须抛异常)、testOcrBackendsUnregister(移除不存在的后端需优雅处理)。这些用例共同验证了注册表管理 API 的边界行为。Rust 侧同样有大量测试围绕 clear_ocr_backends() 与重新播种展开,例如 crates/xberg/src/plugins/registry/ocr.rs 的 ensure_defaults_reseeds_when_default_missing_but_registry_nonempty,以及 crates/xberg/src/extractors/pdf/ocr/tests.rs 中"清空注册表后后续 OCR 分派仍需可用的重播种测试"。
6.2 fixture 契约
该 e2e 场景的契约定义在 fixtures/plugin_api/ocr_backends_clear.json,关键字段:
category: "ocr_backend_management"—— 属于 OCR 后端管理类别;call: "clear_ocr_backends"—— 对应 Rust 原生函数名;assertions: [{ "type": "not_error" }]—— 断言清空操作不报错;tags含trait-bridge、plugin_management;skip.languages: ["c"]—— C 语言特意被排除。
关于 C 的排除理由,fixture 的 coverage_exceptions 给出了明确解释:插件注册表接收的是宿主语言回调(host-language callback),C API 没有暴露注册调用,因此也没有与之配对的 clear/unregister 调用,C 语言侧没有可命名的函数,故该 fixture 不为 C 生成文档。这意味着 clearAll() 这类"整体清空"能力是 Kotlin/Java 等具备回调注册能力的宿主绑定所特有的,这也是它被归类为 trait-bridge 能力的原因。
七、注意事项与实践建议
综合源码与测试,使用 OcrBackendBridge.clearAll() 时有几点值得注意:
- 只清 OCR 后端,不影响其他注册表:xberg 的插件体系还有 embedding、reranker、tokenizer、validator、post-processor、renderer、document extractor 等多张注册表,各自有独立的 Bridge 与
clearAll()(如EmbeddingBackendBridge.clearAll()、TokenizerBackendBridge.clearAll()等,见 packages/kotlin-android/src/main/kotlin/io/xberg)。需要全局重置时应逐类调用,OcrBackendBridge.clearAll()不会波及它们。 - 清理成本包含 shutdown:
clearAll()会依次调用每个后端的shutdown()以释放底层资源,注册的后端越多、初始化越重(例如加载了模型的 Tesseract/PaddleOCR),清空耗时越长;若某个shutdown()失败,错误会沿 JNI 链路以XbergBridgeException形式上抛。 - 清空后默认配置的 OCR 仍可用:由于自愈重播种机制,清空后紧接着执行一次默认配置的 OCR 分派,内置后端(如 Tesseract)通常会被自动重新注册,因此
clearAll()更适合理解为「重置到内置默认」而非「彻底禁用 OCR」。 - 验证是否真的清空:清空后请用
Xberg.listOcrBackends()断言返回为空列表,这也是 fixture 语义 "verify list is empty" 所要求的验证动作。
总而言之,OcrBackendBridge.clearAll() 是 xberg Kotlin/Android 插件 API 中实现 OCR 后端整体重置的标准入口:上层一行 Kotlin 代码,底层串联起本地注册表清空、JNI 桥接、Rust 注册表加锁遍历与逐后端 shutdown 的完整链路,并通过 ensure_ocr_backends_initialized 的自愈机制保证清空后的注册表仍可被重新播种。理解这条链路,你就能在插件化 OCR 场景中安全地管理后端生命周期。