msime-engine-capi 0.1.0

C ABI of the 水杉输入法 (Metasequoia IME) input engine: sessions, keys, candidates, selection and learning, as a C library with a header and pkg-config file.
Documentation
//! 进程级的函数和不需要会话的个人词典函数。它们都可以在任何线程上同时调用。

use std::ffi::{c_char, CString};
use std::path::{Path, PathBuf};

use msime_engine::{PersonalDictionaryEntry, PersonalDictionaryKind, RuntimePaths};
use serde::{Deserialize, Serialize};

use crate::abi::{
    field_str, guard, input_str, msime_engine_paths, read_sized, write_owned, Failure, Outcome,
};
use crate::{
    MSIME_ENGINE_ABI_MAJOR, MSIME_ENGINE_ABI_MINOR, MSIME_ENGINE_FLUSH_STOP_THREAD,
    MSIME_ENGINE_MODEL_JAPANESE, MSIME_ENGINE_UNSUPPORTED, MSIME_ENGINE_USER_DICT_INCLUDE_LEARNED,
};

// ---- 版本与错误 ----

/// 运行时这份库的 ABI 版本:`(major << 16) | minor`。主版本与编译时的 `MSIME_ENGINE_ABI_MAJOR` 不同就不要用它;次版本比编译时小时,后加的函数不可用。
#[no_mangle]
pub extern "C" fn msime_engine_abi_version() -> u32 {
    (MSIME_ENGINE_ABI_MAJOR << 16) | MSIME_ENGINE_ABI_MINOR
}

/// 库的发布版本,静态的 NUL 结尾字符串,不要释放。
#[no_mangle]
pub extern "C" fn msime_engine_version() -> *const c_char {
    concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr().cast()
}

/// 本线程上一次调用失败的说明(英文,NUL 结尾),没有失败时是空串。有效到本线程下一次调用这个库为止(调它自己除外),不要释放。
#[no_mangle]
pub extern "C" fn msime_engine_last_error() -> *const c_char {
    crate::abi::last_error()
}

/// 释放库交出的字符串(选项以外的 JSON、路径)。NULL 什么也不做。不要用 `free()`,库和调用方可能用不同的分配器。
///
/// # Safety
/// `text` 是 NULL 或这个库交出、还没释放过的字符串。
#[no_mangle]
pub unsafe extern "C" fn msime_engine_string_free(text: *mut c_char) {
    if !text.is_null() {
        // SAFETY: 调用方保证它来自 `CString::into_raw`,只释放一次。
        drop(unsafe { CString::from_raw(text) });
    }
}

// ---- 运行时 ----

/// 读四个根目录里的前三个,`dictionaries` 不碰:`msime_engine_prepare_runtime` 在代次准备好之前还没有代次目录,调用方可以不填这一项。
///
/// # Safety
/// `paths` 里前三个字段的字符串指向各自长度的可读字节。
unsafe fn roots(paths: &msime_engine_paths) -> Outcome<[PathBuf; 3]> {
    // SAFETY: 调用方的前提。
    unsafe {
        Ok([
            PathBuf::from(field_str(paths.resources, "paths.resources")?),
            PathBuf::from(field_str(paths.user_data, "paths.user_data")?),
            PathBuf::from(field_str(paths.cache, "paths.cache")?),
        ])
    }
}

/// 读全部四个根目录,个人词典的三个函数用。
///
/// # Safety
/// `paths` 按 `struct_size` 约定可读,四个字段的字符串指向各自长度的可读字节。
unsafe fn runtime_paths(paths: *const msime_engine_paths) -> Outcome<RuntimePaths> {
    // SAFETY: 调用方的前提。
    let paths = unsafe { read_sized(paths, "paths") }?;
    // SAFETY: 同上。
    let [resources, user_data, cache] = unsafe { roots(&paths) }?;
    // SAFETY: 同上。
    let dictionaries = unsafe { field_str(paths.dictionaries, "paths.dictionaries") }?;
    Ok(RuntimePaths {
        resources,
        user_data,
        cache,
        dictionaries: PathBuf::from(dictionaries),
    })
}

fn utf8_path(path: &Path) -> Outcome<String> {
    path.to_str()
        .map(str::to_owned)
        .ok_or_else(|| Failure::invalid("the path is not UTF-8"))
}

/// 准备运行时:在 `user_data/dictionaries/<content_id>` 原子地准备一个代次(从 `resources` 复制工作词库并回放学习日志),已就绪的代次只补回放。`content_id` 是 1 到 128 个 `[0-9A-Za-z_-]`,随包数据变了就换一个;带点的版本号要先换掉点(`dict-v2_0_39`)。三个根目录必须是绝对路径、互不重叠。`paths.dictionaries` 不读。ABI 1.0 不分方案:`resources` 里必须有 `msime-pinyin.db` 和 `msime-english.db`,即使会话只用日文、韩文这类不读拼音词库的方案。成功时把代次目录写进 `*dictionaries`(可以是 NULL),用 `msime_engine_string_free` 释放;建会话时它就是 `paths.dictionaries`。
///
/// # Safety
/// `paths` 按 `struct_size` 约定可读;`content_id` 指向 `content_id_len` 个可读字节;`dictionaries` 是 NULL 或可写一个指针。
#[no_mangle]
pub unsafe extern "C" fn msime_engine_prepare_runtime(
    paths: *const msime_engine_paths,
    content_id: *const c_char,
    content_id_len: usize,
    dictionaries: *mut *mut c_char,
) -> i32 {
    guard(|| {
        // SAFETY: 调用方的前提。
        let paths = unsafe { read_sized(paths, "paths") }?;
        // SAFETY: 同上;`dictionaries` 不读。
        let [resources, user_data, cache] = unsafe { roots(&paths) }?;
        // SAFETY: 同上。
        let content_id = unsafe { input_str(content_id, content_id_len, "content_id") }?;
        let prepared =
            msime_engine::prepare_runtime_paths(&resources, &user_data, &cache, content_id)?;
        let directory = utf8_path(&prepared.dictionaries)?;
        // SAFETY: 调用方的前提。
        unsafe { write_owned(dictionaries, directory) };
        Ok(())
    })
}

/// 立刻写下所有排着的个人上下文学习(平时延迟约 2 秒写)。`flags` 带 `MSIME_ENGINE_FLUSH_STOP_THREAD` 时再停掉后台写线程并等它结束:卸载库、退出进程前用。
#[no_mangle]
pub extern "C" fn msime_engine_flush_learning(flags: u32) -> i32 {
    guard(|| {
        if flags & !MSIME_ENGINE_FLUSH_STOP_THREAD != 0 {
            return Err(Failure::new(
                MSIME_ENGINE_UNSUPPORTED,
                format!("unknown flush flags {flags:#x}"),
            ));
        }
        if flags & MSIME_ENGINE_FLUSH_STOP_THREAD != 0 {
            msime_engine::shutdown();
        } else {
            msime_engine::flush_personal_learning();
        }
        Ok(())
    })
}

/// 内存告警时调:放掉进程级缓存里没有会话在用的日文模型和个人学习模型。正在用的会话不受影响。
#[no_mangle]
pub extern "C" fn msime_engine_release_caches() -> i32 {
    guard(|| {
        msime_engine::release_japanese_dictionary_cache();
        msime_engine::release_idle_personal_models();
        Ok(())
    })
}

/// 关掉所有线程上缓存着的空闲数据库连接。删除或替换数据目录之前调;正被某次调用用着的连接在那次调用结束时关。
#[no_mangle]
pub extern "C" fn msime_engine_close_databases() -> i32 {
    guard(|| {
        msime_engine::close_cached_databases();
        Ok(())
    })
}

/// 把模型的字节直接交给引擎,给没有文件系统或想自己读文件的宿主。`kind` 是 `MSIME_ENGINE_MODEL_*`,`path` 是会话读这个模型的位置(日文是 `resources/msime-japanese.dat` 或选项 `language_dictionaries.japanese`)。`bytes` 为 NULL 且 `bytes_len` 为 0 时撤掉之前交的那份。字节不是合法模型时 `MSIME_ENGINE_INVALID_ARGUMENT`,什么也不改。库复制一份,调用返回后 `bytes` 可以释放。
///
/// # Safety
/// `path` 指向 `path_len` 个可读字节;`bytes` 是 NULL 或指向 `bytes_len` 个可读字节。
#[no_mangle]
pub unsafe extern "C" fn msime_engine_preload_model(
    kind: u32,
    path: *const c_char,
    path_len: usize,
    bytes: *const u8,
    bytes_len: usize,
) -> i32 {
    guard(|| {
        if kind != MSIME_ENGINE_MODEL_JAPANESE {
            return Err(Failure::new(
                MSIME_ENGINE_UNSUPPORTED,
                format!("unknown model kind {kind}"),
            ));
        }
        // SAFETY: 调用方的前提。
        let path = Path::new(unsafe { input_str(path, path_len, "path") }?);
        if !path.is_absolute() {
            return Err(Failure::invalid("the model path must be absolute"));
        }
        if bytes.is_null() {
            if bytes_len != 0 {
                return Err(Failure::null("bytes"));
            }
            msime_engine::unload_japanese_dictionary(path);
            return Ok(());
        }
        // SAFETY: 调用方保证 `bytes` 指向 `bytes_len` 个可读字节。
        let copy: Box<[u8]> = unsafe { std::slice::from_raw_parts(bytes, bytes_len) }.into();
        if !msime_engine::preload_japanese_dictionary(path, copy) {
            return Err(Failure::invalid("the bytes are not a valid model"));
        }
        Ok(())
    })
}

// ---- 个人词典(不需要会话) ----

/// 个人词典的一条,JSON 里的样子。
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct EntryJson {
    kind: KindJson,
    key: String,
    value: String,
    #[serde(default = "default_weight")]
    weight: i64,
}

fn default_weight() -> i64 {
    PersonalDictionaryEntry::DEFAULT_WEIGHT
}

#[derive(Serialize, Deserialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
enum KindJson {
    Pinyin,
    Wubi,
    Wubi98,
    QuickPhrase,
    English,
}

impl KindJson {
    /// 引擎以后加的种类这个版本的 JSON 还没有名字,列表里先略过,和引擎略过它不认识的词库行一样。
    fn of(kind: PersonalDictionaryKind) -> Option<Self> {
        Some(match kind {
            PersonalDictionaryKind::Pinyin => Self::Pinyin,
            PersonalDictionaryKind::Wubi => Self::Wubi,
            PersonalDictionaryKind::Wubi98 => Self::Wubi98,
            PersonalDictionaryKind::QuickPhrase => Self::QuickPhrase,
            PersonalDictionaryKind::English => Self::English,
            _ => return None,
        })
    }
}

impl From<KindJson> for PersonalDictionaryKind {
    fn from(kind: KindJson) -> Self {
        match kind {
            KindJson::Pinyin => Self::Pinyin,
            KindJson::Wubi => Self::Wubi,
            KindJson::Wubi98 => Self::Wubi98,
            KindJson::QuickPhrase => Self::QuickPhrase,
            KindJson::English => Self::English,
        }
    }
}

impl From<EntryJson> for PersonalDictionaryEntry {
    fn from(entry: EntryJson) -> Self {
        Self {
            kind: entry.kind.into(),
            key: entry.key,
            value: entry.value,
            weight: entry.weight,
        }
    }
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct EditJson {
    previous: Option<EntryJson>,
    replacement: Option<EntryJson>,
    #[serde(default)]
    request_id: String,
}

/// 列出个人词典(用户自己加的词,含自动造的词),按种类、编码、词排序,从第 `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` 释放。
///
/// # Safety
/// `paths` 按 `struct_size` 约定可读;`json` 可写一个指针。
#[no_mangle]
pub unsafe extern "C" fn msime_engine_user_dict_list(
    paths: *const msime_engine_paths,
    offset: u32,
    limit: u32,
    flags: u32,
    json: *mut *mut c_char,
) -> i32 {
    guard(|| {
        if json.is_null() {
            return Err(Failure::null("json"));
        }
        if flags & !MSIME_ENGINE_USER_DICT_INCLUDE_LEARNED != 0 {
            return Err(Failure::new(
                MSIME_ENGINE_UNSUPPORTED,
                format!("unknown list flags {flags:#x}"),
            ));
        }
        // SAFETY: 调用方的前提。
        let paths = unsafe { runtime_paths(paths) }?;
        let limit = if limit == 0 { 100 } else { limit as usize };
        let page = msime_engine::personal_dictionary_entries(
            &paths,
            offset as usize,
            limit,
            flags & MSIME_ENGINE_USER_DICT_INCLUDE_LEARNED != 0,
        )?;
        let entries: Vec<EntryJson> = page
            .entries
            .into_iter()
            .filter_map(|entry| {
                Some(EntryJson {
                    kind: KindJson::of(entry.kind)?,
                    key: entry.key,
                    value: entry.value,
                    weight: entry.weight,
                })
            })
            .collect();
        let text = serde_json::json!({ "entries": entries, "has_more": page.has_more }).to_string();
        // SAFETY: 上面检查过非空。
        unsafe { write_owned(json, text) };
        Ok(())
    })
}

/// 改个人词典:`{"previous": 条目或 null, "replacement": 条目或 null, "request_id": "..."}`,previous 为 null 是新增,replacement 为 null 是删除,都有是替换(previous 必须与列表里的那条完全相同)。`request_id`(1 到 128 个 `[A-Za-z0-9_-]`,可以省略)让同一次编辑重试时不重复生效。还有会话开着(本进程或别的进程,包括 msime 自己的输入法)时返回 `MSIME_ENGINE_BUSY`,什么也不写:先释放这些路径上的会话。
///
/// # Safety
/// `paths` 按 `struct_size` 约定可读;`edit_json` 指向 `edit_len` 个可读字节。
#[no_mangle]
pub unsafe extern "C" fn msime_engine_user_dict_edit(
    paths: *const msime_engine_paths,
    edit_json: *const c_char,
    edit_len: usize,
) -> i32 {
    guard(|| {
        // SAFETY: 调用方的前提。
        let paths = unsafe { runtime_paths(paths) }?;
        // SAFETY: 同上。
        let text = unsafe { input_str(edit_json, edit_len, "edit_json") }?;
        let edit: EditJson = serde_json::from_str(text)
            .map_err(|error| Failure::invalid(format!("edit: {error}")))?;
        let previous = edit.previous.map(PersonalDictionaryEntry::from);
        let replacement = edit.replacement.map(PersonalDictionaryEntry::from);
        msime_engine::edit_personal_dictionary(
            &paths,
            previous.as_ref(),
            replacement.as_ref(),
            &edit.request_id,
        )?;
        Ok(())
    })
}

/// 清空学到的一切:换回随包的工作词库、清空学习日志。和 `msime_engine_prepare_runtime` 一样要求 `resources` 里有 `msime-pinyin.db`。还有会话开着时返回 `MSIME_ENGINE_BUSY`,什么也不换。
///
/// # Safety
/// `paths` 按 `struct_size` 约定可读。
#[no_mangle]
pub unsafe extern "C" fn msime_engine_reset_learning(paths: *const msime_engine_paths) -> i32 {
    guard(|| {
        // SAFETY: 调用方的前提。
        let paths = unsafe { runtime_paths(paths) }?;
        msime_engine::reset_learned_data(&paths)?;
        Ok(())
    })
}