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

msime-engine-capi

C ABI of the 水杉输入法 (Metasequoia IME) input engine: a C library (shared and static), a generated header and a pkg-config file.

水杉输入法输入引擎的 C 接口。它把核心层的 msime_engine::Session 包成 33 个带版本的 C 函数,符号一律以 msime_engine_ 开头。v1 覆盖词格整句、拼写纠错、学习、个人词典和 10 个方案(全拼、双拼、五笔、日文、韩文、粤语、注音、越南文、藏文、笔画);九键、触点纠错、神经重排和搭配模型在 Rust crate 里可用,C 接口留到后续版本。设计与取舍见 .agents/notes/implemented/architecture/2026-10-29-engine-capi.md。

头文件 include/msime_engine.h 由 cbindgen 从 src/ 生成并提交,每个函数、结构体和常量的说明都在里面;这份 README 讲它们之间的约定。

构建

用 cargo-c(cargo install cargo-c --locked,要 0.10.26 或更新):

cargo cinstall --locked --release -p msime-engine-capi --prefix /usr/local --libdir lib

装出来的是:

文件 说明
include/msime_engine.h 头文件,#include <msime_engine.h>
lib/libmsime_engine.so.1.0.0、libmsime_engine.so.1(Linux) 共享库,SONAME 是 libmsime_engine.so.1,主版本就是 ABI 主版本
lib/libmsime_engine.1.0.0.dylib(macOS) install name 是 <libdir>/libmsime_engine.1.dylib,compatibility version 是 ABI 主版本
bin/msime_engine.dll、lib/msime_engine.dll.lib(Windows MSVC) DLL 和导入库
lib/libmsime_engine.a、lib/msime_engine.lib(MSVC) 静态库
lib/pkgconfig/msime-engine.pc pkg-config --cflags --libs msime-engine;静态链接用 --static,Libs.private 列出静态库要的系统库。Version 是 ABI 版本(1.0.0),不是 crate 版本,可以写 msime-engine >= 1.0 按 ABI 做编译期开关。没有默认数据目录变量:cargo-c 生成的 .pc 不能加自定义变量,库也没有默认数据目录,四个根目录总由前端传入

静态库要的系统库因平台而异,以 .pc 的 Libs.private 为准:macOS 是 -liconv -lSystem -lc -lm,Linux 是 -lgcc_s -lutil -lrt -lpthread -lm -ldl -lc 一类,Windows 是 kernel32、advapi32、ntdll、userenv、ws2_32 等。

两条链接上的限制:

  • 静态库里带着整份 Rust 标准库和 bundled SQLite 的全局 sqlite3_* 符号。和系统的 libsqlite3 一起链接能链上(C 示例的 static-system-sqlite 变体就是这样),但 C 代码里调用的 sqlite3_* 解析到的是库里自带的那份。一个进程只能链接一个 Rust 静态库:两个都带 Rust 标准库和 SQLite 的静态库放在一起会有重复符号,要用其中一个的共享库形式。
  • 苹果平台上库不剥调试信息(Cargo.toml 的 package.metadata.capi.library.target):Xcode 27 的 strip 剥过的 dylib,链接器报 mis-aligned LINKEDIT string pool 拒绝链接。

改了 src/ 里公开的类型、常量或函数之后重新生成头文件:

bash crates/engine-capi/tests/c-example.sh --update-header

这个脚本也是 CI 的 engine-capi 作业:用 cargo-c 装进 target/engine-capi/stage,核对提交的头文件没有漂移,再用 CMake 把 C 示例 按动态库、静态库(Linux、macOS 上再加一个同时链接系统 SQLite 的静态版本)各编一遍,在 tests/fixture 生成的最小词库上打一句全拼、选词上屏、冲刷学习,再从个人词典读回学到的这句。

生命周期

msime_engine_prepare_runtime(&paths, "dict-v2_0_39", 12, &dictionaries); // 准备代次,交回代次目录
msime_engine_session_new(options_json, len, &session);                 // 选项 JSON 见下文
msime_engine_key_char(session, 'n', 0, &result);                       // 按键……
msime_engine_state_get(session, &state);                               // 读状态和修订号
msime_engine_candidate_get(session, state.revision, i, &candidate);    // 按下标取候选
msime_engine_select(session, state.revision, i, &result);              // 选择,修订号不对就是 STALE
msime_engine_session_free(session);                                    // 写下学习,交还连接和租约
msime_engine_flush_learning(MSIME_ENGINE_FLUSH_STOP_THREAD);           // 退出或卸载前
msime_engine_string_free(dictionaries);
  • 准备运行时:msime_engine_prepare_runtime 在 user_data/dictionaries/<content_id> 原子地准备一个代次:从 resources 复制工作词库,回放学习日志。content_id 是 1 到 128 个 [0-9A-Za-z_-],随包数据一变就换一个(例如数据发布的版本号,点要换掉:dict-v2.0.39 写成 dict-v2_0_39);同一个 id 再调只补回放。paths.dictionaries 这时还没有,不读也不用填。ABI 1.0 准备代次时不分方案,resources 里必须有 msime-pinyin.db 和 msime-english.db(见要带的数据文件)。它交回的代次目录就是建会话时的 paths.dictionaries。三个根目录都要是绝对路径、互不重叠:resources 只读,user_data 是用户数据(学习日志 msime_user.db 和代次),cache 随时可以清空。
  • 会话:msime_engine_session_new 建会话时取这两个目录(user_data 和代次)上的共享词库租约,msime_engine_session_free 时交还。会话结束时写下最近约 2 秒排着的上下文学习、关掉自己的日志连接。
  • 维护:msime_engine_user_dict_edit 和 msime_engine_reset_learning 要独占租约,同一份数据上还有会话开着(本进程、别的进程,包括 msime 自己的输入法)时返回 MSIME_ENGINE_BUSY、什么也不改:先释放会话。msime_engine_user_dict_list 只读,随时可以调。进行中的维护也让 msime_engine_session_new 返回 MSIME_ENGINE_BUSY。
  • 退出与卸载:会话都释放之后调 msime_engine_flush_learning(MSIME_ENGINE_FLUSH_STOP_THREAD) 写完并停掉后台写线程。删除或替换数据目录之前调 msime_engine_close_databases。引擎还有进程级的缓存和单位换算的后台线程,没有办法全部收回,所以不要 dlclose 这个库后再重新加载,把它当作每个进程一份。

按键路由

引擎收的是语义按键,键位映射是前端的事:字母、数字、标点按 ASCII 交来,编辑键是命令。哪个键走哪个入口由前端决定,规则和 msime 自己的宿主(crates/input-runtime)一样:

  1. 字母一律 msime_engine_key_char。单独按下的 Shift 加字母(大写字母开本地模式)带 MSIME_ENGINE_KEY_SHIFT_ONLY。
  2. 数字、标点和空格先看 state.spelling_symbols:列在里面的此刻是拼写的一部分(表达式模式的运算符、/ 和 @ 的入口、注音的声调和空格、拆字的分隔符等),交给 msime_engine_key_char。' 在 state.flags 带 MSIME_ENGINE_STATE_TAKES_SEPARATOR 时也交给 msime_engine_key_char。
  3. 其余标点交给 msime_engine_key_punct:有组字时它以首选结束组字再接上中文标点。前端高亮的不是首选时,先 msime_engine_finish(revision, 高亮行),再 msime_engine_key_punct。handled 为 0(中文标点关着、锁成 ASCII、这个键没有中文标点)时前端自己把字符交给应用,有 commit 就先交 commit。
  4. 数字 1–9 先交 msime_engine_key_char(Unicode 模式、日期模式等会自己用数字)。它没处理(handled 为 0、没有 commit)、按键之前有候选且不是联想(MSIME_ENGINE_STATE_PREDICTION)时,前端把它当作本页的选择键:msime_engine_select(result.revision, 本页起点 + 数字 - 1)。修订号要用这次 msime_engine_key_char 交回的 result.revision:交给引擎的每个字符都让修订号变,没处理的也一样,按键之前读到的 state.revision 这时已经过期(MSIME_ENGINE_STALE);候选行没有变,前端手里的下标照旧有效。
  5. 空格:不在 spelling_symbols 里时,有候选就 msime_engine_select(revision, 高亮行);联想的候选只在用户明确选择时上屏,空格和回车不上屏它们。
  6. 编辑键是命令:回车 COMMIT_RAW,Esc CANCEL(越南文、藏文和开着的韩文汉字列表第一次只退一步,再发一次才丢弃),退格 BACKSPACE,Delete DELETE_FORWARD,←/→ MOVE_LEFT/MOVE_RIGHT(全拼、双拼整句改字用 CONVERSION_LEFT/CONVERSION_RIGHT),Home/End MOVE_HOME/MOVE_END。
  7. 没有组字(state.editing_text 为空、没有候选)时引擎不处理的键,前端照常交给应用。

分页归前端。 引擎给出整张候选表,state.candidate_count 是行数,前端按自己的每页行数用 msime_engine_candidate_get 按下标取,并把每页行数经 MSIME_ENGINE_OPTION_FIRST_PAGE_ROWS 告诉引擎(它决定首页并入多少补全词)。单字母全拼一开始只给出一部分候选,前端翻过第一页时调 msime_engine_expand_candidates 要回其余的。

下标和修订号。 候选下标是引擎顺序里的行号。每次可能改变候选的调用都让修订号(state.revision、result.revision)变;msime_engine_candidate_get 和选择类函数都要交回修订号,不是当前的就返回 MSIME_ENGINE_STALE、什么也不做,这时重新读状态。光标、字节与字的单位写在字段名里:caret_byte 按 editing_text 的字节计,conversion_focus_*_scalar 按 conversion 的 Unicode 标量计。

线程

  • 会话可以在任何线程上用,也可以在两次调用之间换线程,但同一个会话的调用不能并发。库里只做 try_lock:另一个线程正在调同一个会话时返回 MSIME_ENGINE_BUSY,不等待。msime_engine_session_free 同样如此,释放之后句柄不能再用,释放与别的线程的调用之间的先后由前端保证。
  • 不同的会话可以在不同线程上同时用。
  • 进程级函数(版本、prepare_runtime、flush_learning、release_caches、close_databases、preload_model、个人词典三个)可以在任何线程上同时调用。
  • 库里任何 panic 都挡在边界内。会话里的 panic 让会话作废:这一次和之后的调用都返回 MSIME_ENGINE_PANIC,只能释放它;别的会话不受影响。
  • msime_engine_last_error 是线程局部的:它说明的是本线程上一次调用,有效到本线程下一次调用这个库。

内存与字符串

  • 输入一律是借用的 (ptr, len) UTF-8,不要求 NUL 结尾,调用返回后库不再读它。ptr 为 NULL 时 len 必须是 0(当作空串)。
  • 热路径输出(msime_engine_result、msime_engine_state、msime_engine_candidate 里的 msime_engine_str)借自会话:len 个字节,不以 NUL 结尾,空串时 ptr 也不是 NULL。它们有效到同一会话下一次改变状态的调用:按键、命令、选择与逐候选学习、set_*、switch_scheme、set_caret、reset_context、expand_candidates。state_get、candidate_get、state_json 这类只读调用不让它们失效,所以可以先取状态再逐个取一页候选,上屏文字在这期间也一直有效。
  • 库分配的字符串(state_json、user_dict_list 的 JSON、prepare_runtime 交回的路径)用 msime_engine_string_free 释放,不要用 free()。msime_engine_version 和 msime_engine_last_error 不要释放。
  • 只有冷路径用 JSON:建会话的选项、调试快照、个人词典。按键路径上没有 JSON,库也不为交回的结果另外分配(引擎内部的分配不在这个承诺里)。

版本与结构体

  • msime_engine_abi_version() 返回 (major << 16) | minor。主版本与头文件的 MSIME_ENGINE_ABI_MAJOR 不同就不要用这份库;次版本小于头文件的 MSIME_ENGINE_ABI_MINOR 时,后加的函数不可用。同一个主版本内只追加函数、常量、标志位和结构体尾部的字段,已有的编号与含义冻结;方案、命令和候选来源沿用核心层的序号(命令与 host-api 的编号不同)。
  • 每个进出的结构体都以 uint32_t struct_size 开头,调用前设为 sizeof。输出时库写 min(调用方大小, 库内大小) 个字节,把 struct_size 改成写入的大小,调用方多出的部分清零;输入时库只读这么多,读不到的字段按零。比 1.0 的布局还小是 MSIME_ENGINE_INVALID_ARGUMENT。
  • 返回码:OK 0、INVALID_ARGUMENT 1、FAILED 2、STORAGE 3、UNSUPPORTED 4、STALE 5、BUSY 6、PANIC 7、NULL 8。1、2、3、6 与引擎的 ErrorCode 共用编号。msime_engine_result.diagnostic_code 是引擎的诊断码(学习没能存下之类,键照样处理了),0 是没有,0xFFFF 是表外的文字。

会话选项

msime_engine_session_new 的选项是一个 JSON 对象,版本 1。只有 version 和 paths 必填,其余缺省取引擎库的默认值(SessionOptions::new:全拼、小鹤双拼、学习和个人上下文开、中文标点和成对标点开、辅助码关)。不认识的键、取值不对都是 MSIME_ENGINE_INVALID_ARGUMENT。

{
  "version": 1,
  "paths": {"resources": "/abs/res", "user_data": "/abs/user", "cache": "/abs/cache", "dictionaries": "/abs/user/dictionaries/dict-v2_0_39"},
  "scheme": "quanpin",
  "enabled_schemes": ["quanpin", "shuangpin", "wubi"],
  "shuangpin": {"profile": "xiaohe", "preedit_shows_keys": true},
  "fuzzy_pinyin": ["z_zh", "in_ing"],
  "autocorrect": ["transposition", "neighbor"]
}
键 取值
version 1
paths resources、user_data、cache、dictionaries 四个绝对路径,dictionaries 是 msime_engine_prepare_runtime 交回的代次目录
scheme 起始方案:quanpin、shuangpin、wubi、japanese、korean、cantonese、zhuyin、vietnamese、tibetan、stroke
enabled_schemes 会话允许的方案,缺省全部;scheme 和之后的 msime_engine_switch_scheme 都要在里面,只为其中的方案读词库
shuangpin.profile xiaohe、ziranma、shoudao、microsoft、smart_abc、custom
shuangpin.custom profile 为 custom 时的键位表:{"initials": {"zh": "v", …}, "finals": {"iang": "l", …}, "zero_initials": {"a": "aa", …}};键是小写字母或 ;,ü 写作 v,表不合法时建会话失败
shuangpin.preedit_shows_keys 预编辑显示按键原文(缺省 true)还是还原出的全拼
wubi profile(wubi86、wubi98)、mixed_pinyin(码表没有时按全拼出字)、z_wildcard(z 匹配任一码位)
vietnamese input_method(telex、vni)、tone_style(modern 声调在第二个元音上、classic)
korean.layout dubeolsik、romaja
helpcode enabled、schema(resources/helpcodes/ 下的表名,例如 wubi86)
punctuation chinese、paired、lock(follow、chinese、ascii)
learning、personal_context 学习词频与用户词、学习词序与搭配
next_word_prediction 上屏后联想,缺省关;MSIME_ENGINE_OPTION_PREDICTION_ALLOWED 是它的即时门禁
fuzzy_pinyin 模糊音规则:z_zh、c_ch、s_sh、n_l、f_h、r_l、an_ang、en_eng、in_ing、ian_iang、uan_uang
autocorrect 全拼纠错:transposition、neighbor、deletion、insertion
english mixed_candidates(中文里混入英文单词)、minimum_prefix(1–8)、first_page_count(1–3)
first_page_rows 每页候选数,1–255
language_dictionaries cantonese、zhuyin、stroke、japanese 各自数据库的路径;没有时对应方案激活失败(日文只出假名)

建好之后能改的选项经 msime_engine_set_int、msime_engine_set_string,编号是头文件里的 MSIME_ENGINE_OPTION_*。

个人词典

不需要会话。msime_engine_user_dict_list 交回 {"entries":[{"kind":"pinyin","key":"ni'hao","value":"你好","weight":100000}],"has_more":false},kind 是 pinyin、wubi、wubi98、quick_phrase、english。msime_engine_user_dict_edit 收 {"previous": 条目或 null, "replacement": 条目或 null, "request_id": "add-1"}:只有 replacement 是新增,只有 previous 是删除(它要和列表里的那条完全一致),两个都有是替换;weight 可以省略。request_id 让同一次编辑重试时不重复生效。

要带的数据文件

库本身不带词库。运行时文件放在 resources 里,可以从 msime-dictionary 的 dict-v* 发布取,也可以自己按同样的格式生成。数据许可证跟着数据走,不随引擎:采用方按自己的发布方式挑选数据。

ABI 1.0 的 msime_engine_prepare_runtime 和 msime_engine_reset_learning 不分方案,所以每个方案都要 msime-pinyin.db 和 msime-english.db,包括不读拼音词库的日文、韩文、越南文和藏文。引擎内部能按方案只准备 msime-english.db(msime 自己的宿主这样用),但它还不是公开接口,C 接口 1.0 也没有这个选项;以后要加就在次版本里追加一个带方案集合的函数,已有的两个不变。

方案 至少要有 数据许可证
每个方案 msime-pinyin.db、msime-english.db,以及 msime-english.db 的 SCOWL 版权文件 msime-scowl_Copyright.txt(分发 msime-english.db 时必须一同携带) msime-pinyin.db 是 GPL-3.0(rime-ice 加补充);msime-english.db 是 GPL-3.0(rime-ice-en 加补充),英文词表含 SCOWL 的词
全拼、双拼 排序与 msime 一样还要 msime-bigram.bin、msime-trigram.bin n-gram 的计数来自 zhwiki(CC-BY-SA-4.0),词表来自 GPL-3.0 的 msime-pinyin.db
五笔 再加 msime-wubi.db 混有 Apache-2.0(86 极点码表)、GPL-3.0(rime-ice 词条与人工词条补入的词)、GPL-3.0-or-later(Fcitx 的 98 表)、Unlicense(98 五笔小组的表)和 WTFPL(区划名称)的来源,逐项见 resources/licenses/msime-engine-dictionary-NOTICE.md
日文 再加 msime-japanese.dat Mozc 开源词库(BSD-3-Clause 加词典附加条款),带它的 README 与 LICENSE(msime-mozc_dictionary_oss_README.txt、msime-mozc_LICENSE.txt)
韩文、越南文、藏文 不用再加 只有第一行的两个库
粤语、注音、笔画 再加 msime-cantonese.db、msime-zhuyin.db、msime-stroke.db(经 language_dictionaries) 粤语 CC BY 4.0;注音 LGPL-2.1-or-later 加 MIT;笔画 LGPL-3.0-only 加 GPL-3.0-only(笔顺码来自 rime-stroke,排序权重来自 rime-ice),带 resources/licenses/ 下对应的许可证文件
辅助码 resources/helpcodes/ 下的表 只有 wubi86 有来源与许可说明;仓库里其余几张没有再分发授权,不要随产品分发

数据格式的规格文档还没有写完(引擎库方案的 P5),在那之前以 crates/engine 的读端为准。

二进制归档与第三方声明

C 库的二进制静态链接了整张 Rust 依赖图和 bundled SQLite,每个二进制归档都要带上这些依赖的声明。用 Linux 打包同一个收集脚本,只走 msime-engine-capi 的依赖图:

python3 -X utf8 platforms/linux/collect-notices.py cargo dist/THIRD_PARTY_NOTICES.txt msime-engine-capi

在要打包的那个平台上运行,Cargo 按该平台解析依赖图;-X utf8 是给 Windows 的,那里 Python 默认按 cp1252 读 cargo 的输出。它逐字复制每个 crate 自带的许可证、NOTICE 和版权文件(包括带 SQLite 的 libsqlite3-sys),本仓库自己的 crate 不列;自己不带许可证文件的 crate(现在是 ewts、exmex、rink-core)只列出声明的许可证表达式。归档里另外要放:

  • 引擎的许可证:crates/engine/LICENSE(MPL-2.0 全文与 Exhibit A 声明,引擎、msime-path-trust 和 rink-core 用),以及这个目录的 LICENSE(同一份 MPL-2.0 全文,这个 crate 用)。
  • rink-core 的源码位置(MPL-2.0 §3.2):crates.io 上的 rink-core 0.9,上游是 codeberg.org/tiffany/rink。
  • ewts 的许可证:resources/licenses/ewts-MIT.txt。crate 自己不带许可证文件,这份 MIT 全文末尾还附着它转换表来源 ewts-js 的 Apache-2.0 署名。
  • 编进引擎的数据表和改编代码的许可证与署名:逐个文件见 crates/engine/README.md 的「许可证」一节,表里「全文」一列的文件都在 resources/licenses/ 下,原样放进归档。

CI 的 engine-capi 作业每次都跑一遍上面的命令,核对它能为这张依赖图生成声明,并且不带许可证文件的 crate 仍只有上面列出的三个:依赖图里多出一个时作业失败,先在这里补上它的许可证文本。

不在这个 ABI 里

在线、云与 AI 候选;插件指令;翻译与释义;宿主喂入的表(指令表、联系人、快捷短语);同步与备份;只为 msime 自己的基准测试公开的接口;排序内部与重排模型;实验性能力(多语言、日文混输、触点与滑行、九键与 14 键、手写、emoji 目录、英文补全与音标)。整句只给引擎认为最好的那一种读法(sentence_alternatives 固定为假),候选是引擎原始的顺序。引擎的警告日志写到标准错误,前缀 msime: ;ABI 1.0 没有日志回调。

许可证

这个目录里的代码(src/、include/、examples/、tests/)以 Mozilla Public License 2.0 发布,全文在 LICENSE,不附 Exhibit B(与 GPL 兼容):

This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.

向这个目录提交的贡献同样以 MPL-2.0 授权。

它链接的 msime-engine 和 msime-path-trust 同样是 MPL-2.0,所以从这里构建出的二进制整体按 MPL-2.0 分发;链接进去的第三方 crate 和编进引擎的数据表保留各自的许可证。仓库其余部分仍是 GPL-3.0-only。