Kotlin/Android 中查询 xberg OCR 后端注册表:listOcrBackends 使用与底层实现解析

原创2026-10-07 23:58:481,362 阅读
文章标签:后端AI 应用NLP

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 任务的前置步骤,这能让提取管线的失败更早暴露、也更容易诊断。

登录后查看全文
xberg