Kotlin/Android 中查询 xberg OCR 后端注册表:listOcrBackends 使用与底层实现解析
Kotlin/Android 中查询 xberg OCR 后端注册表:listOcrBackends 使用与底层实现解析
本文以 xberg 仓库中 Kotlin(Android)语言的
list_ocr_backends代码片段为主线,讲解如何在 Android/Kotlin 应用中查询当前进程内已注册的全部 OCR 后端,并深入到 Rust 核心的 OCR 后端注册表(registry)实现、内置后端清单、注册/注销机制以及配套的能力查询 API。读完本文,你将掌握Xberg.listOcrBackends()的完整用法、其从 Kotlin 到 JNI 再到 Rust 的调用链,以及如何结合listOcrBackendCapabilities与ocr_backend_supports_language做一次"运行前体检"。
一、为什么需要"列出 OCR 后端"
xberg 是一个以 Rust 为核心的文档智能引擎,OCR(光学字符识别)是它处理扫描件、图片型 PDF 时的关键能力。为了让 OCR 具备可插拔性,xberg 在插件体系中设计了一个全局 OCR 后端注册表(OCR backend registry):每个后端(Tesseract、PaddleOCR、Sceptre、VLM 等)都以插件形式注册进这个注册表,文档提取管线在需要 OCR 时按名称从注册表中取用后端。
在实际开发中有三个高频场景需要"列出已注册的 OCR 后端":
- 运行前体检:确认目标后端(例如
tesseract或paddle-ocr)是否真的注册成功,避免在提取时才因"backend not found"报错; - 插件生命周期管理:在注册、注销、清空等操作之后验证注册表状态是否符合预期;
- 能力上报/诊断:把可用后端列表展示给用户或写进日志,便于排查问题。
本次关联文档提供的正是这一场景在 Kotlin(Android)语言绑定下的最小可运行示例(list_ocr_backends.md):
import io.xberg.*
fun main() {
val result = Xberg.listOcrBackends()
println(result)
}
这个片段虽然只有三行,但它对应的是一条从 Kotlin 绑定直达 Rust 核心注册表的完整链路。下面我们沿着这条链路逐层展开。
二、Kotlin 绑定层:Xberg.listOcrBackends()
2.1 方法签名与返回类型
在 Kotlin/Android 包中,入口统一封装在 Xberg.kt 里。listOcrBackends() 的源码定义如下:
/**
* List all registered OCR backends.
*
* Returns the names of all OCR backends currently registered in the global registry.
*
* **Returns:**
*
* A vector of OCR backend names.
*/
fun listOcrBackends(): List<String> {
val resultJson = XbergBridge.nativeListOcrBackends()
return mapper.readValue(resultJson, object : TypeReference<List<String>>() {})
}
关键点:
- 返回类型是
List<String>,即后端名称列表;顺序由 Rust 侧统一排序保证(见 4.2 节)。 - 调用是同步的:该方法直接调用 JNI 原生函数并把返回的 JSON 反序列化为 Kotlin 集合。从 XbergBridge.kt 可以看到对应的 JNI 声明:
external fun nativeListOcrBackends(): String
- 线程安全:底层注册表本身是线程安全的(见第 4 节),因此该查询在并发场景下也可以安全调用。
2.2 同族的注册表查询 API
listOcrBackends() 并不是孤立存在的,它属于 Kotlin 绑定中"注册表查询"一族。在 Xberg.kt 中可以看到与之并列的:
| Kotlin 方法 | 返回类型 | 语义 |
|---|---|---|
listOcrBackends() |
List<String> |
列出全部已注册 OCR 后端名称 |
listOcrBackendCapabilities() |
List<OcrBackendCapabilities> |
列出各后端的名称与声明的语言能力 |
listOcrBackendCapabilitiesFor(config) |
List<OcrBackendCapabilities> |
在指定 OcrConfig 下评估各后端能力(如 config.tessdata_path 生效时的语言清单) |
listPostProcessors() |
List<String> |
列出全部后处理器名称 |
listRenderers() |
List<String> |
列出全部渲染器名称 |
listRerankerBackends() |
List<String> |
列出全部重排序后端名称 |
其中 listOcrBackendCapabilities 与 listOcrBackends 是"能力枚举"与"名称枚举"的对应关系(Rust 侧文档原话是 "the capability-enumeration counterpart to list_ocr_backends"),详见第 6 节。
三、Kotlin → JNI → Rust 的完整调用链
从上面的源码可以还原出 Xberg.listOcrBackends() 的完整调用链:
Xberg.listOcrBackends() // Kotlin API 入口
└─ XbergBridge.nativeListOcrBackends() // JNI external fun,返回 JSON 字符串
└─ Rust: xberg::plugins::list_ocr_backends() // 读注册表只读锁并列出名称
└─ OcrBackendRegistry::list() // 收集键名并排序
- Kotlin 层负责类型映射与序列化(Jackson
mapper.readValue),不感知注册表内部结构; - JNI 桥层(
XbergBridge)负责把调用转发给原生侧; - Rust 核心的 ocr.rs 中的
list_ocr_backends()是真正的实现所在(见第 4 节)。
这条链路也解释了为什么文档片段里只需要 Xberg.listOcrBackends() 一行:所有细节都被绑定层封装了。
四、Rust 核心:注册表与 list_ocr_backends()
4.1 函数实现:读锁 + 列表
Rust 侧的实现位于 ocr.rs:
/// List all registered OCR backends.
///
/// Returns the names of all OCR backends currently registered in the global registry.
///
/// # Returns
///
/// A vector of OCR backend names.
pub fn list_ocr_backends() -> crate::Result<Vec<String>> {
use crate::plugins::registry::get_ocr_backend_registry();
let registry = get_ocr_backend_registry();
let registry = registry.read();
Ok(registry.list())
}
实现要点:
- 全局注册表:
get_ocr_backend_registry()返回一个全局单例注册表;registry.read()获取只读锁后调用list()。 - 只读操作:与
register_ocr_backend、unregister_ocr_backend不同,列举只拿读锁,不阻塞其他并发的注册操作,因此是高并发场景下的廉价查询。
4.2 注册表数据结构与排序保证
注册表本体定义在 registry/ocr.rs,内部使用 AHashMap(ahash 哈希表)以名称到 Arc<dyn OcrBackend> 的映射保存后端。list() 的实现为:
/// List all registered backend names.
pub fn list(&self) -> Vec<String> {
let mut names: Vec<_> = self.backends.keys().cloned().collect();
names.sort_unstable();
names
}
这意味着 返回的名称列表是经过排序的(sort_unstable),Kotlin 侧拿到的 List<String> 顺序确定,便于稳定对比与快照测试。注册表本身是线程安全的,支持多线程并发访问(源码注释明确说明 "The registry is thread-safe and can be accessed concurrently from multiple threads")。
4.3 内置后端:由 feature 门控的注册清单
注册表在构造时(OcrBackendRegistry::new())会调用 register_defaults() 把当前编译特性(Cargo features)启用的内置后端注册进去。从 builtin_ocr_backend_names() 可以看到按 feature 门控的候选清单(registry/ocr.rs):
| 后端名称 | 启用条件(feature) |
|---|---|
tesseract |
ocr / ocr-wasm |
paddle-ocr |
paddle_ocr |
sceptre |
sceptre_ocr(非 wasm32) |
vlm |
liter-llm(非 wasm32) |
candle-trocr |
candle-trocr |
candle-paddleocr-vl |
candle-paddleocr-vl |
candle-glm-ocr |
candle-glm-ocr |
candle-deepseek-ocr |
candle-deepseek-ocr(非 wasm32) |
因此,Xberg.listOcrBackends() 在不同构建配置下会返回不同的列表——它反映的是当前构建真实可用的后端,而不是代码库声明的全部后端。这是"列出"这个 API 最实用的意义:你看到的就是当前进程里真正能用的。
另外,每个后端的注册是相互独立的:register_defaults() 中如果某个后端初始化失败,会以 warning 跳过,其他后端照常注册(源码注释 "if one fails to initialize it is skipped with a warning so the remaining backends still register")。也就是说,列表里缺了某个后端,往往意味着该后端初始化失败或未编译进当前构建。
4.4 注册与注销:列表从何而来
注册表除了 list() 之外还有 register() 与 unregister_ocr_backend()(公开函数在 ocr.rs,注册表方法在 registry/ocr.rs):
register(backend: Arc<dyn OcrBackend>):校验插件名(validate_plugin_name)→ 调用backend.initialize()→ 插入哈希表。初始化失败则注册失败。unregister_ocr_backend(name):按名称从注册表中移除;移除不存在的后端会返回错误。clear_all()(Kotlin 侧OcrBackendBridge.clearAll()):清空整个注册表。
还有一个值得一提的细节:名称规范化(canonical name)。canonical_ocr_backend_name() 会把用户传入的名称统一小写,并把 "paddleocr" 规范化为 "paddle-ocr"(registry/ocr.rs)。这保证了能力查询和内部派发对同一个名字的理解是一致的。
五、在 Kotlin/Android 工程中实际使用
5.1 最小可运行示例
把关联文档的示例稍作展开,就得到一个完整的可运行片段:
import io.xberg.Xberg
fun main() {
// 列出当前进程内所有已注册的 OCR 后端
val backends = Xberg.listOcrBackends()
println("Registered OCR backends: $backends")
// 常见配套:判断某个后端是否可用
val hasTesseract = backends.contains("tesseract")
val hasPaddle = backends.contains("paddle-ocr")
println("tesseract available = $hasTesseract")
println("paddle-ocr available = $hasPaddle")
}
注意 import io.xberg.* 中 * 导入会引入 Xberg 等全部公开类型;显式 import io.xberg.Xberg 也是等价的。文档片段使用 import io.xberg.*,两种写法皆可。
5.2 实战:运行前的后端可用性检查
在提交 OCR 任务之前,可以先查询列表并做可用性判断,避免任务运行到一半才失败:
fun ensureOcrBackend(required: String): Boolean {
val available = Xberg.listOcrBackends()
if (!available.contains(required)) {
println("Warning: required OCR backend '$required' is not registered. Available: $available")
return false
}
return true
}
fun main() {
if (ensureOcrBackend("tesseract")) {
// 继续执行 OCR 提取……
}
}
需要说明的是,"列出名称"只代表后端已注册;具体某个语言是否可用,还要用能力查询(第 6 节)来判断。
5.3 与插件注册/注销配合的验证流程
结合 OcrBackendBridge(在 OcrBackendBridge.kt),一个典型的"注册 → 验证 → 注销"流程可以这样写:
// 注册自定义后端后验证其出现在列表中
// (假设 customBackend 实现了 io.xberg.IOcrBackend)
// OcrBackendBridge.register(customBackend)
val afterRegister = Xberg.listOcrBackends()
println(afterRegister)
// 注销后再次验证
// OcrBackendBridge.unregister("my-custom-ocr")
val afterUnregister = Xberg.listOcrBackends()
println(afterUnregister)
e2e 测试 OcrBackendManagementTest.kt 正是这样组织的:testOcrBackendsList() 调用 Xberg.listOcrBackends() 并断言结果非空;testOcrBackendsClear() 清空注册表;testOcrBackendsUnregister() 注销一个不存在的后端验证其优雅处理。这些测试用例可以作为你编写自己集成测试时的参考模板。
六、能力枚举:与 listOcrBackendCapabilities 的关系
只拿到后端名字往往不够——你通常还想知道"这个后端支持哪些语言"。Rust 侧文档明确说明 list_ocr_backend_capabilities 是 list_ocr_backends 的能力枚举对应物(capability-enumeration counterpart)。
在 Kotlin 绑定中对应:
val caps = Xberg.listOcrBackendCapabilities()
for (c in caps) {
println("${c.name}: languages=${c.supportedLanguages}")
}
Rust 侧 OcrBackendCapabilities 结构(ocr.rs)包含两个字段:
name:后端注册名,即list_ocr_backends()返回的名称;supported_languages:后端声明支持的语言列表。
重要约定(源码明确提示):supported_languages 为空列表不代表后端不支持任何语言——它只是说该后端没有重写 supported_languages() 这个默认方法(默认返回 vec![])。例如 VLM 后端接受任意语言,却继承了空的默认值。因此:
- 判断"某个语言是否可用"要用
ocr_backend_supports_language(backend, language)(Kotlin/Rust 均有对应绑定),不要从空列表推断"不支持"; - 判断"某个后端在指定配置下支持哪些语言"用
listOcrBackendCapabilitiesFor(config)(Kotlin 侧方法),它能反映config.tessdata_path等配置生效后的真实语言集合。
Kotlin 侧 OcrBackendCapabilities 类型定义在 OcrBackendCapabilities.kt。
七、测试与契约:如何验证"列出"行为的正确性
7.1 契约夹具(fixture)
仓库的 e2e 契约目录 fixtures/registry/list_ocr_backends.json 定义了该操作的行为契约:
{
"id": "list_ocr_backends",
"category": "registry",
"description": "List OCR backends",
"docs": {
"topic": "registry",
"title": "List OCR backends",
"description": "List OCR backends",
"side_effects": "safe"
},
"tags": ["registry"],
"call": "list_ocr_backends",
"input": {},
"assertions": [
{
"type": "not_error"
}
]
}
从中可以读出三条契约信息:
call:底层公开调用名为list_ocr_backends(对应 Kotlin 的Xberg.listOcrBackends());side_effects: "safe":这是一个无副作用的只读查询;input: {}:不需要任何输入参数;assertions: [{ "type": "not_error" }]:对任意状态调用都不应报错。
7.2 多语言 e2e 测试
list_ocr_backends 是多语言共用的契约,Kotlin/Android 的 e2e 测试位于:
- OcrBackendManagementTest.kt:
testOcrBackendsList()直接调用Xberg.listOcrBackends()并断言结果非空; - RegistryTest.kt:同样在注册表主题下调用
Xberg.listOcrBackends()做冒烟验证。
Rust 侧的单元测试(plugins/ocr/tests.rs)还会把 list_ocr_backends() 的结果与能力枚举对比,验证两者对同一注册表状态的一致性。这些测试共同保证了:无论从哪种语言绑定调用,拿到的列表语义都是一致的。
八、常见疑问与注意事项
Q1:返回的列表顺序稳定吗?
稳定。Rust 侧 OcrBackendRegistry::list() 对名称做了 sort_unstable() 排序,Kotlin 侧按 JSON 顺序反序列化,因此相同注册状态下两次调用结果一致。
Q2:列表里没有某个后端,是不是出错了?
不一定。后端是否出现取决于两件事:当前构建是否启用了对应 feature(见 4.3 节表格),以及该后端初始化是否成功(初始化失败会被 warning 跳过)。可结合日志中的 OCR backend registered / Failed to register 追踪信息排查。
Q3:listOcrBackends 和 listOcrBackendCapabilities 应该用哪个?
- 只要名字:用
listOcrBackends(); - 需要名字 + 声明语言:用
listOcrBackendCapabilities()/listOcrBackendCapabilitiesFor(config); - 判断单个语言是否可用:用
ocr_backend_supports_language(...),并注意"空语言列表 ≠ 不支持任何语言"的约定。
Q4:这个查询有副作用吗?
没有。契约夹具标注 side_effects: "safe",实现上只获取注册表读锁并构造排序后的名称副本,不影响注册表状态。
九、小结
从一份三行的 Kotlin 片段出发,我们完整走完了 xberg 的 OCR 后端列举链路:
- API 层:
Xberg.listOcrBackends()返回List<String>,是注册表名称枚举的 Kotlin/Android 入口(Xberg.kt); - 桥接层:
XbergBridge.nativeListOcrBackends()以 JNI external fun 对接原生侧(XbergBridge.kt); - 核心层:
plugins::list_ocr_backends()获取全局注册表读锁并调用排序后的list()(ocr.rs、registry/ocr.rs); - 插件模型:内置后端由 Cargo features 门控注册,支持运行时注册/注销/清空,名称经
canonical_ocr_backend_name规范化; - 配套 API:
listOcrBackendCapabilities(含For(config)变体)提供语言能力枚举,ocr_backend_supports_language用于单语言判断; - 验证手段:契约夹具 fixtures/registry/list_ocr_backends.json 与 e2e 测试 OcrBackendManagementTest.kt 提供了跨语言一致性的保障。
在实际项目中,建议把"运行前用 listOcrBackends() 做后端可用性检查 + 用 listOcrBackendCapabilitiesFor(config) 做语言能力确认"作为 OCR 任务的前置步骤,这能让提取管线的失败更早暴露、也更容易诊断。