Skip to main content

msime_engine_capi/
library.rs

1//! 进程级的函数和不需要会话的个人词典函数。它们都可以在任何线程上同时调用。
2
3use std::cell::Cell;
4use std::ffi::{c_char, c_void, CString};
5use std::path::{Path, PathBuf};
6use std::sync::{Arc, PoisonError, RwLock};
7
8use msime_engine::{
9    PersonalDictionaryEntry, PersonalDictionaryKind, RuntimePaths, SchemeSet, SchemeType,
10};
11use serde::{Deserialize, Serialize};
12
13use crate::abi::{
14    field_str, guard, input_str, msime_engine_paths, read_sized, write_owned, Failure, Outcome,
15};
16use crate::{
17    MSIME_ENGINE_ABI_MAJOR, MSIME_ENGINE_ABI_MINOR, MSIME_ENGINE_BUSY,
18    MSIME_ENGINE_FLUSH_STOP_THREAD, MSIME_ENGINE_MODEL_JAPANESE, MSIME_ENGINE_UNSUPPORTED,
19    MSIME_ENGINE_USER_DICT_INCLUDE_LEARNED,
20};
21
22// ---- 版本与错误 ----
23
24/// 运行时这份库的 ABI 版本:`(major << 16) | minor`。主版本与编译时的 `MSIME_ENGINE_ABI_MAJOR` 不同就不要用它;次版本比编译时小时,后加的函数不可用。
25#[no_mangle]
26pub extern "C" fn msime_engine_abi_version() -> u32 {
27    (MSIME_ENGINE_ABI_MAJOR << 16) | MSIME_ENGINE_ABI_MINOR
28}
29
30/// 库的发布版本,静态的 NUL 结尾字符串,不要释放。
31#[no_mangle]
32pub extern "C" fn msime_engine_version() -> *const c_char {
33    concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr().cast()
34}
35
36/// 本线程上一次调用失败的说明(英文,NUL 结尾),没有失败时是空串。有效到本线程下一次调用这个库为止(调它自己除外),不要释放。
37#[no_mangle]
38pub extern "C" fn msime_engine_last_error() -> *const c_char {
39    crate::abi::last_error()
40}
41
42/// 释放库交出的字符串(选项以外的 JSON、路径)。NULL 什么也不做。不要用 `free()`,库和调用方可能用不同的分配器。
43///
44/// # Safety
45/// `text` 是 NULL 或这个库交出、还没释放过的字符串。
46#[no_mangle]
47pub unsafe extern "C" fn msime_engine_string_free(text: *mut c_char) {
48    if !text.is_null() {
49        // SAFETY: 调用方保证它来自 `CString::into_raw`,只释放一次。
50        drop(unsafe { CString::from_raw(text) });
51    }
52}
53
54// ---- 日志 ----
55
56/// 日志回调,ABI 1.1 起。`message` 是一行 UTF-8 的警告(不带 `msime: ` 前缀和换行),NUL 结尾,`message_len` 是不含 NUL 的字节数;只在这次调用期间有效。`user_data` 是注册时交来的那个指针,库不碰它。
57pub type msime_engine_log_callback = Option<
58    unsafe extern "C" fn(user_data: *mut c_void, message: *const c_char, message_len: usize),
59>;
60
61/// 注册着的回调和它的 `user_data`。
62struct LogTarget {
63    callback: unsafe extern "C" fn(*mut c_void, *const c_char, usize),
64    user_data: *mut c_void,
65}
66
67// SAFETY: `user_data` 只原样交还给调用方自己的回调;回调会在任何线程上、可能同时被调用,这是写进 `msime_engine_set_log_callback` 文档的约定,线程安全由调用方保证。
68unsafe impl Send for LogTarget {}
69// SAFETY: 同上。
70unsafe impl Sync for LogTarget {}
71
72/// 当前的回调。投递时持读锁、换回调时取写锁,所以 `msime_engine_set_log_callback` 返回时旧回调已经没有在执行的调用,之后也不会再被调用,调用方可以放心释放旧的 `user_data`。
73static LOG_TARGET: RwLock<Option<LogTarget>> = RwLock::new(None);
74
75thread_local! {
76    /// 本线程正在执行日志回调:回调里再进库记的日志丢掉,回调里换回调返回 `BUSY`,免得在自己持着的读锁上等写锁。
77    static IN_LOG_CALLBACK: Cell<bool> = const { Cell::new(false) };
78}
79
80/// 换给引擎的日志出口:把一行交给注册着的回调。
81#[allow(clippy::print_stderr)]
82fn deliver_log_line(line: &str) {
83    if IN_LOG_CALLBACK.get() {
84        return;
85    }
86    let target = LOG_TARGET.read().unwrap_or_else(PoisonError::into_inner);
87    let Some(target) = target.as_ref() else {
88        // 引擎在换回缺省出口之前取到了这个出口:这一行按缺省的样子写到标准错误,不丢。
89        eprintln!("msime: {line}");
90        return;
91    };
92    let message = CString::new(line.replace('\0', "\u{fffd}")).unwrap_or_default();
93    IN_LOG_CALLBACK.set(true);
94    // SAFETY: 回调和 `user_data` 是调用方注册的,调用方保证回调在注册期间可以在任何线程上这样调用。
95    unsafe { (target.callback)(target.user_data, message.as_ptr(), message.as_bytes().len()) };
96    IN_LOG_CALLBACK.set(false);
97}
98
99/// ABI 1.1 起。把引擎的警告日志交给 `callback`,`callback` 为 NULL 时回到缺省:写到标准错误,每行前缀 `msime: `。日志是进程级的:所有会话和库的后台线程记的都交给它,再注册一次就换掉上一个。
100///
101/// 回调可能在任何线程上被调用,也可能在几个线程上同时被调用,要自己处理并发;要尽快返回,不要在里面调这个库的任何函数(在回调里调本函数返回 `MSIME_ENGINE_BUSY`,回调期间同一线程上再记的日志被丢掉)。回调不能抛出 C++ 异常或 `longjmp` 出去。
102///
103/// 本函数返回之后,旧回调没有还在执行的调用,以后也不会再被调用,旧的 `user_data` 可以释放;新回调在注册期间要一直可调,`user_data` 一直有效,直到用别的回调或 NULL 换掉它。进程退出前不必换回缺省。
104///
105/// # Safety
106/// `callback` 是 NULL,或者在它被换掉之前一直可以在任何线程上以 `user_data` 调用的函数。
107#[no_mangle]
108pub unsafe extern "C" fn msime_engine_set_log_callback(
109    callback: msime_engine_log_callback,
110    user_data: *mut c_void,
111) -> i32 {
112    guard(|| {
113        if IN_LOG_CALLBACK.get() {
114            return Err(Failure::new(
115                MSIME_ENGINE_BUSY,
116                "the log callback cannot be replaced from inside the log callback",
117            ));
118        }
119        let mut target = LOG_TARGET.write().unwrap_or_else(PoisonError::into_inner);
120        match callback {
121            Some(callback) => {
122                *target = Some(LogTarget {
123                    callback,
124                    user_data,
125                });
126                msime_engine::set_log_sink(Some(Arc::new(deliver_log_line)));
127            }
128            None => {
129                *target = None;
130                msime_engine::set_log_sink(None);
131            }
132        }
133        Ok(())
134    })
135}
136
137// ---- 运行时 ----
138
139/// 读四个根目录里的前三个,`dictionaries` 不碰:`msime_engine_prepare_runtime` 在代次准备好之前还没有代次目录,调用方可以不填这一项。
140///
141/// # Safety
142/// `paths` 里前三个字段的字符串指向各自长度的可读字节。
143unsafe fn roots(paths: &msime_engine_paths) -> Outcome<[PathBuf; 3]> {
144    // SAFETY: 调用方的前提。
145    unsafe {
146        Ok([
147            PathBuf::from(field_str(paths.resources, "paths.resources")?),
148            PathBuf::from(field_str(paths.user_data, "paths.user_data")?),
149            PathBuf::from(field_str(paths.cache, "paths.cache")?),
150        ])
151    }
152}
153
154/// 读全部四个根目录,个人词典的三个函数用。
155///
156/// # Safety
157/// `paths` 按 `struct_size` 约定可读,四个字段的字符串指向各自长度的可读字节。
158unsafe fn runtime_paths(paths: *const msime_engine_paths) -> Outcome<RuntimePaths> {
159    // SAFETY: 调用方的前提。
160    let paths = unsafe { read_sized(paths, "paths") }?;
161    // SAFETY: 同上。
162    let [resources, user_data, cache] = unsafe { roots(&paths) }?;
163    // SAFETY: 同上。
164    let dictionaries = unsafe { field_str(paths.dictionaries, "paths.dictionaries") }?;
165    Ok(RuntimePaths {
166        resources,
167        user_data,
168        cache,
169        dictionaries: PathBuf::from(dictionaries),
170    })
171}
172
173fn utf8_path(path: &Path) -> Outcome<String> {
174    path.to_str()
175        .map(str::to_owned)
176        .ok_or_else(|| Failure::invalid("the path is not UTF-8"))
177}
178
179/// 准备运行时:在 `user_data/dictionaries/<content_id>` 原子地准备一个代次(从 `resources` 复制工作词库并回放学习日志),已就绪的代次只补回放。`content_id` 是 1 到 128 个 `[0-9A-Za-z_-]`,随包数据变了就换一个;带点的版本号要先换掉点(`dict-v2_0_39`)。三个根目录必须是绝对路径、互不重叠。`paths.dictionaries` 不读。按全部方案准备:`resources` 里必须有 `msime-pinyin.db` 和 `msime-english.db`;只用其中几个方案时用 `msime_engine_prepare_runtime_for`,只带那几个方案的数据。成功时把代次目录写进 `*dictionaries`(可以是 NULL),用 `msime_engine_string_free` 释放;建会话时它就是 `paths.dictionaries`。
180///
181/// # Safety
182/// `paths` 按 `struct_size` 约定可读;`content_id` 指向 `content_id_len` 个可读字节;`dictionaries` 是 NULL 或可写一个指针。
183#[no_mangle]
184pub unsafe extern "C" fn msime_engine_prepare_runtime(
185    paths: *const msime_engine_paths,
186    content_id: *const c_char,
187    content_id_len: usize,
188    dictionaries: *mut *mut c_char,
189) -> i32 {
190    // SAFETY: 调用方的前提。
191    guard(|| unsafe {
192        prepare(
193            paths,
194            content_id,
195            content_id_len,
196            SchemeSet::ALL,
197            dictionaries,
198        )
199    })
200}
201
202/// ABI 1.1 起。同 `msime_engine_prepare_runtime`,只为 `schemes` 里的方案准备:`resources` 只要有这些方案读的词库。`schemes` 的第 n 位是序号为 n 的方案(`1u << MSIME_ENGINE_SCHEME_KOREAN`),要与会话选项的 `enabled_schemes` 相同,不能是 0,不认识的位是 `MSIME_ENGINE_INVALID_ARGUMENT`。有全拼、双拼或五笔时与 `msime_engine_prepare_runtime` 相同,要 `msime-pinyin.db` 和 `msime-english.db`;没有时两者都不要求,`resources` 带了 `msime-english.db` 就照样复制进代次(专用英文的补全与回车学词用它),没带时代次里没有它。日文的 `msime-japanese.dat` 和粤语、注音、笔画的语言库不经过代次,会话从 `resources` 或选项给的路径直接读。
203///
204/// # Safety
205/// 同 `msime_engine_prepare_runtime`。
206#[no_mangle]
207pub unsafe extern "C" fn msime_engine_prepare_runtime_for(
208    paths: *const msime_engine_paths,
209    content_id: *const c_char,
210    content_id_len: usize,
211    schemes: u32,
212    dictionaries: *mut *mut c_char,
213) -> i32 {
214    guard(|| {
215        let schemes = scheme_set(schemes)?;
216        // SAFETY: 调用方的前提。
217        unsafe { prepare(paths, content_id, content_id_len, schemes, dictionaries) }
218    })
219}
220
221/// `msime_engine_prepare_runtime` 与 `_for` 共用的部分。
222///
223/// # Safety
224/// 同 `msime_engine_prepare_runtime`。
225unsafe fn prepare(
226    paths: *const msime_engine_paths,
227    content_id: *const c_char,
228    content_id_len: usize,
229    schemes: SchemeSet,
230    dictionaries: *mut *mut c_char,
231) -> Outcome<()> {
232    // SAFETY: 调用方的前提。
233    let paths = unsafe { read_sized(paths, "paths") }?;
234    // SAFETY: 同上;`dictionaries` 不读。
235    let [resources, user_data, cache] = unsafe { roots(&paths) }?;
236    // SAFETY: 同上。
237    let content_id = unsafe { input_str(content_id, content_id_len, "content_id") }?;
238    let prepared = msime_engine::prepare_runtime_paths_for(
239        &resources, &user_data, &cache, content_id, schemes,
240    )?;
241    let directory = utf8_path(&prepared.dictionaries)?;
242    // SAFETY: 调用方的前提。
243    unsafe { write_owned(dictionaries, directory) };
244    Ok(())
245}
246
247/// 方案位集合:第 n 位是序号为 n 的方案(`MSIME_ENGINE_SCHEME_*`)。空集合和不认识的位都不合法。
248fn scheme_set(schemes: u32) -> Outcome<SchemeSet> {
249    if schemes == 0 {
250        return Err(Failure::invalid("schemes must name at least one scheme"));
251    }
252    (0..u32::BITS)
253        .filter(|bit| schemes & (1 << bit) != 0)
254        .try_fold(SchemeSet::EMPTY, |set, bit| {
255            u8::try_from(bit)
256                .ok()
257                .and_then(SchemeType::from_u8)
258                .map(|scheme| set.with(scheme))
259                .ok_or_else(|| Failure::invalid(format!("schemes names no scheme at bit {bit}")))
260        })
261}
262
263/// 立刻写下所有排着的个人上下文学习(平时延迟约 2 秒写)。`flags` 带 `MSIME_ENGINE_FLUSH_STOP_THREAD` 时再停掉后台写线程并等它结束:卸载库、退出进程前用。
264#[no_mangle]
265pub extern "C" fn msime_engine_flush_learning(flags: u32) -> i32 {
266    guard(|| {
267        if flags & !MSIME_ENGINE_FLUSH_STOP_THREAD != 0 {
268            return Err(Failure::new(
269                MSIME_ENGINE_UNSUPPORTED,
270                format!("unknown flush flags {flags:#x}"),
271            ));
272        }
273        if flags & MSIME_ENGINE_FLUSH_STOP_THREAD != 0 {
274            msime_engine::shutdown();
275        } else {
276            msime_engine::flush_personal_learning();
277        }
278        Ok(())
279    })
280}
281
282/// 内存告警时调:放掉进程级缓存里没有会话在用的日文模型和个人学习模型。正在用的会话不受影响。
283#[no_mangle]
284pub extern "C" fn msime_engine_release_caches() -> i32 {
285    guard(|| {
286        msime_engine::release_japanese_dictionary_cache();
287        msime_engine::release_idle_personal_models();
288        Ok(())
289    })
290}
291
292/// 关掉所有线程上缓存着的空闲数据库连接。删除或替换数据目录之前调;正被某次调用用着的连接在那次调用结束时关。
293#[no_mangle]
294pub extern "C" fn msime_engine_close_databases() -> i32 {
295    guard(|| {
296        msime_engine::close_cached_databases();
297        Ok(())
298    })
299}
300
301/// 把模型的字节直接交给引擎,给没有文件系统或想自己读文件的宿主。`kind` 是 `MSIME_ENGINE_MODEL_*`,`path` 是会话读这个模型的位置(日文是 `resources/msime-japanese.dat` 或选项 `language_dictionaries.japanese`)。`bytes` 为 NULL 且 `bytes_len` 为 0 时撤掉之前交的那份。字节不是合法模型时 `MSIME_ENGINE_INVALID_ARGUMENT`,什么也不改。库复制一份,调用返回后 `bytes` 可以释放。
302///
303/// # Safety
304/// `path` 指向 `path_len` 个可读字节;`bytes` 是 NULL 或指向 `bytes_len` 个可读字节。
305#[no_mangle]
306pub unsafe extern "C" fn msime_engine_preload_model(
307    kind: u32,
308    path: *const c_char,
309    path_len: usize,
310    bytes: *const u8,
311    bytes_len: usize,
312) -> i32 {
313    guard(|| {
314        if kind != MSIME_ENGINE_MODEL_JAPANESE {
315            return Err(Failure::new(
316                MSIME_ENGINE_UNSUPPORTED,
317                format!("unknown model kind {kind}"),
318            ));
319        }
320        // SAFETY: 调用方的前提。
321        let path = Path::new(unsafe { input_str(path, path_len, "path") }?);
322        if !path.is_absolute() {
323            return Err(Failure::invalid("the model path must be absolute"));
324        }
325        if bytes.is_null() {
326            if bytes_len != 0 {
327                return Err(Failure::null("bytes"));
328            }
329            msime_engine::unload_japanese_dictionary(path);
330            return Ok(());
331        }
332        // SAFETY: 调用方保证 `bytes` 指向 `bytes_len` 个可读字节。
333        let copy: Box<[u8]> = unsafe { std::slice::from_raw_parts(bytes, bytes_len) }.into();
334        if !msime_engine::preload_japanese_dictionary(path, copy) {
335            return Err(Failure::invalid("the bytes are not a valid model"));
336        }
337        Ok(())
338    })
339}
340
341// ---- 个人词典(不需要会话) ----
342
343/// 个人词典的一条,JSON 里的样子。
344#[derive(Serialize, Deserialize)]
345#[serde(deny_unknown_fields)]
346struct EntryJson {
347    kind: KindJson,
348    key: String,
349    value: String,
350    #[serde(default = "default_weight")]
351    weight: i64,
352}
353
354fn default_weight() -> i64 {
355    PersonalDictionaryEntry::DEFAULT_WEIGHT
356}
357
358#[derive(Serialize, Deserialize, Clone, Copy)]
359#[serde(rename_all = "snake_case")]
360enum KindJson {
361    Pinyin,
362    Wubi,
363    Wubi98,
364    QuickPhrase,
365    English,
366}
367
368impl KindJson {
369    /// 引擎以后加的种类这个版本的 JSON 还没有名字,列表里先略过,和引擎略过它不认识的词库行一样。
370    fn of(kind: PersonalDictionaryKind) -> Option<Self> {
371        Some(match kind {
372            PersonalDictionaryKind::Pinyin => Self::Pinyin,
373            PersonalDictionaryKind::Wubi => Self::Wubi,
374            PersonalDictionaryKind::Wubi98 => Self::Wubi98,
375            PersonalDictionaryKind::QuickPhrase => Self::QuickPhrase,
376            PersonalDictionaryKind::English => Self::English,
377            _ => return None,
378        })
379    }
380}
381
382impl From<KindJson> for PersonalDictionaryKind {
383    fn from(kind: KindJson) -> Self {
384        match kind {
385            KindJson::Pinyin => Self::Pinyin,
386            KindJson::Wubi => Self::Wubi,
387            KindJson::Wubi98 => Self::Wubi98,
388            KindJson::QuickPhrase => Self::QuickPhrase,
389            KindJson::English => Self::English,
390        }
391    }
392}
393
394impl From<EntryJson> for PersonalDictionaryEntry {
395    fn from(entry: EntryJson) -> Self {
396        Self {
397            kind: entry.kind.into(),
398            key: entry.key,
399            value: entry.value,
400            weight: entry.weight,
401        }
402    }
403}
404
405#[derive(Deserialize)]
406#[serde(deny_unknown_fields)]
407struct EditJson {
408    previous: Option<EntryJson>,
409    replacement: Option<EntryJson>,
410    #[serde(default)]
411    request_id: String,
412}
413
414/// 列出个人词典(用户自己加的词,含自动造的词),按种类、编码、词排序,从第 `offset` 条起最多 `limit` 条(`limit` 为 0 时取 100)。`flags` 是 `MSIME_ENGINE_USER_DICT_*`。结果是 JSON:`{"entries":[{"kind":"pinyin","key":"ni'hao","value":"你好","weight":100000}],"has_more":false}`,用 `msime_engine_string_free` 释放。
415///
416/// # Safety
417/// `paths` 按 `struct_size` 约定可读;`json` 可写一个指针。
418#[no_mangle]
419pub unsafe extern "C" fn msime_engine_user_dict_list(
420    paths: *const msime_engine_paths,
421    offset: u32,
422    limit: u32,
423    flags: u32,
424    json: *mut *mut c_char,
425) -> i32 {
426    guard(|| {
427        if json.is_null() {
428            return Err(Failure::null("json"));
429        }
430        if flags & !MSIME_ENGINE_USER_DICT_INCLUDE_LEARNED != 0 {
431            return Err(Failure::new(
432                MSIME_ENGINE_UNSUPPORTED,
433                format!("unknown list flags {flags:#x}"),
434            ));
435        }
436        // SAFETY: 调用方的前提。
437        let paths = unsafe { runtime_paths(paths) }?;
438        let limit = if limit == 0 { 100 } else { limit as usize };
439        let page = msime_engine::personal_dictionary_entries(
440            &paths,
441            offset as usize,
442            limit,
443            flags & MSIME_ENGINE_USER_DICT_INCLUDE_LEARNED != 0,
444        )?;
445        let entries: Vec<EntryJson> = page
446            .entries
447            .into_iter()
448            .filter_map(|entry| {
449                Some(EntryJson {
450                    kind: KindJson::of(entry.kind)?,
451                    key: entry.key,
452                    value: entry.value,
453                    weight: entry.weight,
454                })
455            })
456            .collect();
457        let text = serde_json::json!({ "entries": entries, "has_more": page.has_more }).to_string();
458        // SAFETY: 上面检查过非空。
459        unsafe { write_owned(json, text) };
460        Ok(())
461    })
462}
463
464/// ABI 1.1 起。按 `msime_engine_user_dict_edit` 收新词条(`replacement`)的同一套规则检查一条词条,不碰任何文件,也不需要路径:输入是一条 `{"kind": "pinyin", "key": "NI HAO", "value": "你好", "weight": 100000}`(`weight` 可以省略),合法时把规范化后的词条写进 `*normalized_json`(编码换成小写,拼音编码里的空格换成 `'`:`NI HAO` 成为 `ni'hao`),用 `msime_engine_string_free` 释放。词条不合法时返回 `MSIME_ENGINE_INVALID_ARGUMENT`,`msime_engine_last_error` 是引擎给的英文原因(例如 `Use complete pinyin syllables separated by apostrophes or spaces`),可以直接显示给用户;`*normalized_json` 不写。给前端在用户填编辑框时就提示,不必等到真的写入。
465///
466/// # Safety
467/// `entry_json` 指向 `entry_len` 个可读字节;`normalized_json` 可写一个指针。
468#[no_mangle]
469pub unsafe extern "C" fn msime_engine_user_dict_validate(
470    entry_json: *const c_char,
471    entry_len: usize,
472    normalized_json: *mut *mut c_char,
473) -> i32 {
474    guard(|| {
475        if normalized_json.is_null() {
476            return Err(Failure::null("normalized_json"));
477        }
478        // SAFETY: 调用方的前提。
479        let text = unsafe { input_str(entry_json, entry_len, "entry_json") }?;
480        let entry: EntryJson = serde_json::from_str(text)
481            .map_err(|error| Failure::invalid(format!("entry: {error}")))?;
482        let normalized = msime_engine::validate_personal_dictionary_entry(
483            &PersonalDictionaryEntry::from(entry),
484        )?;
485        let kind = KindJson::of(normalized.kind)
486            .ok_or_else(|| Failure::invalid("the entry has a kind this ABI cannot name"))?;
487        let text = serde_json::to_string(&EntryJson {
488            kind,
489            key: normalized.key,
490            value: normalized.value,
491            weight: normalized.weight,
492        })
493        .map_err(|error| Failure::invalid(format!("entry: {error}")))?;
494        // SAFETY: 上面检查过非空。
495        unsafe { write_owned(normalized_json, text) };
496        Ok(())
497    })
498}
499
500/// 改个人词典:`{"previous": 条目或 null, "replacement": 条目或 null, "request_id": "..."}`,previous 为 null 是新增,replacement 为 null 是删除,都有是替换(previous 必须与列表里的那条完全相同)。`request_id`(1 到 128 个 `[A-Za-z0-9_-]`,可以省略)让同一次编辑重试时不重复生效。还有会话开着(本进程或别的进程,包括 msime 自己的输入法)时返回 `MSIME_ENGINE_BUSY`,什么也不写:先释放这些路径上的会话。
501///
502/// # Safety
503/// `paths` 按 `struct_size` 约定可读;`edit_json` 指向 `edit_len` 个可读字节。
504#[no_mangle]
505pub unsafe extern "C" fn msime_engine_user_dict_edit(
506    paths: *const msime_engine_paths,
507    edit_json: *const c_char,
508    edit_len: usize,
509) -> i32 {
510    guard(|| {
511        // SAFETY: 调用方的前提。
512        let paths = unsafe { runtime_paths(paths) }?;
513        // SAFETY: 同上。
514        let text = unsafe { input_str(edit_json, edit_len, "edit_json") }?;
515        let edit: EditJson = serde_json::from_str(text)
516            .map_err(|error| Failure::invalid(format!("edit: {error}")))?;
517        let previous = edit.previous.map(PersonalDictionaryEntry::from);
518        let replacement = edit.replacement.map(PersonalDictionaryEntry::from);
519        msime_engine::edit_personal_dictionary(
520            &paths,
521            previous.as_ref(),
522            replacement.as_ref(),
523            &edit.request_id,
524        )?;
525        Ok(())
526    })
527}
528
529/// 清空学到的一切:换回随包的工作词库、清空学习日志。和 `msime_engine_prepare_runtime` 一样要求 `resources` 里有 `msime-pinyin.db` 和 `msime-english.db`;代次是用 `msime_engine_prepare_runtime_for` 准备的,就用 `msime_engine_reset_learning_for` 传同一个方案集合。还有会话开着时返回 `MSIME_ENGINE_BUSY`,什么也不换。
530///
531/// # Safety
532/// `paths` 按 `struct_size` 约定可读。
533#[no_mangle]
534pub unsafe extern "C" fn msime_engine_reset_learning(paths: *const msime_engine_paths) -> i32 {
535    guard(|| {
536        // SAFETY: 调用方的前提。
537        let paths = unsafe { runtime_paths(paths) }?;
538        msime_engine::reset_learned_data(&paths)?;
539        Ok(())
540    })
541}
542
543/// ABI 1.1 起。同 `msime_engine_reset_learning`,只换回 `schemes` 读的词库,规则与 `msime_engine_prepare_runtime_for` 相同:没有全拼、双拼、五笔时不要求 `msime-pinyin.db`,`msime-english.db` 在 `resources` 里有才换回,都没有时只清空日志。`schemes` 的写法同 `msime_engine_prepare_runtime_for`。
544///
545/// # Safety
546/// `paths` 按 `struct_size` 约定可读。
547#[no_mangle]
548pub unsafe extern "C" fn msime_engine_reset_learning_for(
549    paths: *const msime_engine_paths,
550    schemes: u32,
551) -> i32 {
552    guard(|| {
553        let schemes = scheme_set(schemes)?;
554        // SAFETY: 调用方的前提。
555        let paths = unsafe { runtime_paths(paths) }?;
556        msime_engine::reset_learned_data_for(&paths, schemes)?;
557        Ok(())
558    })
559}