Skip to main content

audio_cpp/
registry.rs

1//! 模型注册表(Registry)。
2//!
3//! 注册表负责枚举已编译进引擎的模型族 / loader / 设备,并加载模型。
4//! 一个 `Registry` 持有 C 侧的不透明句柄,可被多个 `Model` 共享,直到
5//! 所有派生句柄释放后才可释放(见各类型的 `Drop`)。
6
7use std::cell::UnsafeCell;
8use std::ffi::c_char;
9use std::marker::PhantomData;
10use std::ptr;
11use std::sync::Arc;
12
13use audio_cpp_sys::*;
14
15use crate::error::Error;
16use crate::ffi;
17use crate::model::Model;
18use crate::types::{Device, LoaderInfo, ModelFamily, ModelInspection};
19
20/// C 注册表所有权的共享持有者。
21///
22/// `Registry::load` 返回的每个 [`Model`] 都会克隆持有它,因此调用方在
23/// 加载完成后即可释放 `Registry`:C 注册表存活到最后一个派生句柄释放,
24/// 不存在"先 drop registry 即悬垂"。`Send` 而非 `Sync`(C 侧 registry
25/// 内部无锁,并发共享 `&Registry` 仍是未定义行为,与之前一致)。
26#[derive(Clone)]
27// `Arc` 在此仅作共享所有权计数(跨线程只转移所有权、从不并发访问,
28// 与 `Registry: Send + !Sync` 的既有语义一致)。
29pub(crate) struct RegistryGuard {
30    // 仅作所有权计数(Clone/Drop),从不读内容:C 注册表由最后一个
31    // guard 释放,这是刻意设计而非冗余字段。
32    #[allow(dead_code)]
33    inner: Arc<RegistryInner>,
34}
35
36struct RegistryInner {
37    raw: *mut audiocpp_registry,
38    // `*mut` 本身是 Send+Sync,加一个 !Sync 标记保住 `!Sync` 语义。
39    _no_sync: PhantomData<UnsafeCell<()>>,
40}
41
42// SAFETY: guard 只做所有权计数与释放(`Drop` 调 `audiocpp_registry_free`),
43// 不提供并发访问 C 注册表的路径;C 句柄的跨线程转移与之前 `Registry: Send` 一致。
44unsafe impl Send for RegistryGuard {}
45
46impl Drop for RegistryInner {
47    fn drop(&mut self) {
48        unsafe {
49            audiocpp_registry_free(self.raw);
50        }
51    }
52}
53
54/// 模型注册表。
55pub struct Registry {
56    raw: *mut audiocpp_registry,
57    // 自持有:`Clone` 出去给 Model,Registry 先 drop 不影响已加载模型。
58    guard: RegistryGuard,
59}
60
61// C ABI 句柄本身不要求 Send/Sync。C 侧 registry 内部无锁,多个线程共享
62// 同一注册表调用 load() 属于并发访问,行为未定义。因此只实现 Send
63// (所有权可跨线程转移),不实现 Sync(禁止 & 共享跨线程使用)。
64unsafe impl Send for Registry {}
65
66impl Registry {
67    /// 创建默认注册表,包含所有编译进本库的模型族 loader。
68    ///
69    /// # Errors
70    ///
71    /// 底层 C ABI 无法创建注册表时返回 [`Error::NullHandle`]。
72    // `Arc` 仅作所有权计数(见 `RegistryGuard`),`!Send` 属刻意设计。
73    #[allow(clippy::arc_with_non_send_sync)]
74    pub fn new() -> Result<Self, Error> {
75        let raw = unsafe { audiocpp_registry_default() };
76        if raw.is_null() {
77            return Err(Error::NullHandle(ffi::last_error()));
78        }
79        let guard = RegistryGuard {
80            inner: Arc::new(RegistryInner {
81                raw,
82                _no_sync: PhantomData,
83            }),
84        };
85        Ok(Self { raw, guard })
86    }
87
88    /// 已注册的模型族列表,例如 `["silero_vad","qwen3_asr"]`。
89    ///
90    /// # Errors
91    ///
92    /// C ABI 调用失败或返回的 JSON 无法解析时返回对应 [`Error`] 变体。
93    pub fn families(&self) -> Result<Vec<String>, Error> {
94        let mut out: *mut c_char = ptr::null_mut();
95        ffi::check_rc(unsafe { audiocpp_registry_families_json(self.raw, &mut out) })?;
96        let json = unsafe { ffi::take_string(out)? };
97        serde_json::from_str(&json).map_err(Error::from)
98    }
99
100    /// 所有 loader 的声明信息(模型族、任务、端点)。
101    ///
102    /// # Errors
103    ///
104    /// C ABI 调用失败或返回的 JSON 无法解析时返回对应 [`Error`] 变体。
105    pub fn loaders(&self) -> Result<Vec<LoaderInfo>, Error> {
106        let mut out: *mut c_char = ptr::null_mut();
107        ffi::check_rc(unsafe { audiocpp_registry_loaders_json(self.raw, &mut out) })?;
108        let json = unsafe { ffi::take_string(out)? };
109        let root: serde_json::Value = serde_json::from_str(&json)?;
110        let loaders = root.get("loaders").ok_or_else(|| {
111            Error::Other(format!(
112                "注册表 loaders JSON 缺少 `loaders` 顶层字段: {json}"
113            ))
114        })?;
115        serde_json::from_value(loaders.clone()).map_err(Error::from)
116    }
117
118    /// 枚举所有后端可用的计算设备。
119    ///
120    /// # Errors
121    ///
122    /// C ABI 调用失败或返回的 JSON 无法解析时返回对应 [`Error`] 变体。
123    pub fn devices() -> Result<Vec<Device>, Error> {
124        let mut out: *mut c_char = ptr::null_mut();
125        ffi::check_rc(unsafe { audiocpp_registry_devices_json(&mut out) })?;
126        let json = unsafe { ffi::take_string(out)? };
127        serde_json::from_str(&json).map_err(Error::from)
128    }
129
130    /// 判断某个模型族是否已编译进当前引擎。
131    ///
132    /// 用于加载前的预检:返回 `false` 说明该族未启(需对应 `model-*` feature
133    /// 或 `full-models` 重新编译),直接 `load` 会失败。
134    ///
135    /// 注意:`false` 同时涵盖“内部出错”(此时详情见 C 侧 `last_error`,但本
136    /// 签名无法传出);需区分时请用 [`Registry::try_supports_family`]。
137    ///
138    /// # Examples
139    ///
140    /// ```no_run
141    /// use audio_cpp::Registry;
142    /// let registry = Registry::new().unwrap();
143    /// // 默认 core 构建恒定包含 silero_vad;随机字符串必然为 false。
144    /// assert!(registry.supports_family("silero_vad"));
145    /// assert!(!registry.supports_family("definitely_not_a_family"));
146    /// ```
147    pub fn supports_family(&self, family: &str) -> bool {
148        self.try_supports_family(family).unwrap_or(false)
149    }
150
151    /// 与 [`Registry::supports_family`] 同义,但区分“不支持”与“出错”。
152    ///
153    /// C ABI 在传参非法或内部异常时返回 `-1`(`supports_family` 把它折成
154    /// `false`);本方法此时返回 `Err` 并携带 `last_error` 文本。
155    ///
156    /// # Errors
157    ///
158    /// 字符串含 NUL,或 C ABI 内部出错时返回对应 [`Error`] 变体。
159    pub fn try_supports_family(&self, family: &str) -> Result<bool, Error> {
160        let family_c = ffi::cstring(family)?;
161        let rc = unsafe {
162            audiocpp_registry_supports_family(self.raw, family_c.as_ptr() as *const c_char)
163        };
164        if rc < 0 {
165            return Err(Error::Ffi(ffi::last_error()));
166        }
167        Ok(rc != 0)
168    }
169
170    /// 预检模型文件:无需真正加载即可获得 metadata / capabilities / 支持的
171    /// CLI 选项 / 发现的配置与权重资产。
172    ///
173    /// # Examples
174    ///
175    /// ```no_run
176    /// use audio_cpp::Registry;
177    /// let registry = Registry::new().unwrap();
178    /// let info = registry.inspect("./qwen3-asr-q8_0.gguf").unwrap();
179    /// println!("族: {},变体: {}", info.metadata.family, info.metadata.variant);
180    /// for opt in &info.cli.request_options {
181    ///     println!("  请求选项 {}: {}", opt.name, opt.description);
182    /// }
183    /// ```
184    ///
185    /// # Errors
186    ///
187    /// C ABI 调用失败、路径非法或返回 JSON 无法解析时返回对应 [`Error`] 变体。
188    /// 若文件不存在或无法被任何 loader 识别,底层会报错(如 [`Error::NullHandle`])。
189    pub fn inspect(&self, model_path: &str) -> Result<ModelInspection, Error> {
190        let path_c = ffi::cstring(model_path)?;
191        let mut out: *mut c_char = ptr::null_mut();
192        ffi::check_rc(unsafe {
193            audiocpp_registry_inspect_json(self.raw, path_c.as_ptr() as *const c_char, &mut out)
194        })?;
195        let json = unsafe { ffi::take_string(out)? };
196        serde_json::from_str(&json).map_err(Error::from)
197    }
198
199    /// 加载一个模型。
200    ///
201    /// `model_path` 为权重文件路径(如 `.safetensors` / `.gguf`);
202    /// `family_hint` 可选,用于指定模型族(GGUF / NeMo safetensors 无法
203    /// 自动探测族别,须显式指定,见 [`crate::ModelFamily`]);`load_options`
204    /// 可选,例如 `{"weight_id":"..."}`。
205    ///
206    /// 返回的 [`Model`] 共享持有 C 注册表的所有权:本 `Registry` 释放后
207    /// 模型仍可继续使用,无需 `_registry` 三件套持有。
208    ///
209    /// # Errors
210    ///
211    /// 路径含 NUL / 非 UTF-8 返回 [`Error::Nul`] 或 [`Error::NonUtf8Path`];
212    /// 底层加载失败返回 [`Error::NullHandle`]。
213    pub fn load(
214        &self,
215        model_path: &str,
216        family_hint: Option<ModelFamily>,
217        load_options: Option<&str>,
218    ) -> Result<Model, Error> {
219        let path_c = ffi::cstring(model_path)?;
220        let hint_c = family_hint
221            .as_ref()
222            .map(|f| ffi::cstring(f.as_str()))
223            .transpose()?;
224        let options_c = load_options.map(ffi::cstring).transpose()?;
225
226        let raw = unsafe {
227            audiocpp_registry_load(
228                self.raw,
229                path_c.as_ptr() as *const c_char,
230                hint_c
231                    .as_ref()
232                    .map_or(ptr::null(), |s| s.as_ptr() as *const c_char),
233                options_c
234                    .as_ref()
235                    .map_or(ptr::null(), |s| s.as_ptr() as *const c_char),
236            )
237        };
238        if raw.is_null() {
239            return Err(Error::NullHandle(ffi::last_error()));
240        }
241        Ok(Model::from_raw(raw, self.guard.clone()))
242    }
243}
244
245// 注意:`Registry` 没有手动 `Drop` 实现——字段析构即语义:`raw` 是普通指针
246// 无动作,`guard` 的 Arc 计数减一;C 注册表由最后一个存活的 `RegistryGuard`
247// (Registry / Model / Session 任一持有)释放。
248
249#[cfg(test)]
250mod tests {
251    // 测试断言中的 unwrap/expect 是惯用法:失败即测试失败,展开错误链无意义。
252    #![allow(clippy::unwrap_used, clippy::expect_used)]
253    use super::*;
254
255    #[test]
256    fn supports_family_known_and_unknown() {
257        let reg = Registry::new().unwrap();
258        // core 模型集始终包含 silero_vad;明显不存在的族应返回 false 且不报错。
259        assert!(reg.supports_family("silero_vad"));
260        assert!(!reg.supports_family("definitely_not_a_real_family_xyz"));
261    }
262
263    #[test]
264    fn model_outlives_registry() {
265        // G5:Model 共享持有 C 注册表,Registry 先释放后模型仍可继续使用。
266        // 用上游自带的 silero 内置权重(构建本 crate 必需 submodule,路径稳定)。
267        let weight = "../audio-cpp-sys/audio.cpp/assets/framework/models/silero_vad/silero_vad_16k.safetensors";
268        assert!(
269            std::path::Path::new(weight).exists(),
270            "内置权重缺失,submodule 未补齐?{weight}"
271        );
272        let model = {
273            let reg = Registry::new().unwrap();
274            reg.load(weight, None, None).unwrap()
275        };
276        // Registry 已释放:metadata 走 C 侧 loaded-model,与注册表无关。
277        let meta = model.metadata().unwrap();
278        assert_eq!(meta.family, "silero_vad");
279    }
280}