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 或更新):
装出来的是:
| 文件 | 说明 |
|---|---|
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/ 里公开的类型、常量或函数之后重新生成头文件:
这个脚本也是 CI 的 engine-capi 作业:用 cargo-c 装进 target/engine-capi/stage,核对提交的头文件没有漂移,再用 CMake 把 C 示例 按动态库、静态库(Linux、macOS 上再加一个同时链接系统 SQLite 的静态版本)各编一遍,在 tests/fixture 生成的最小词库上打一句全拼、选词上屏、冲刷学习,再从个人词典读回学到的这句。
生命周期
; // 准备代次,交回代次目录
; // 选项 JSON 见下文
; // 按键……
; // 读状态和修订号
; // 按下标取候选
; // 选择,修订号不对就是 STALE
; // 写下学习,交还连接和租约
; // 退出或卸载前
;
- 准备运行时:
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)一样:
- 字母一律
msime_engine_key_char。单独按下的 Shift 加字母(大写字母开本地模式)带MSIME_ENGINE_KEY_SHIFT_ONLY。 - 数字、标点和空格先看
state.spelling_symbols:列在里面的此刻是拼写的一部分(表达式模式的运算符、/和@的入口、注音的声调和空格、拆字的分隔符等),交给msime_engine_key_char。'在state.flags带MSIME_ENGINE_STATE_TAKES_SEPARATOR时也交给msime_engine_key_char。 - 其余标点交给
msime_engine_key_punct:有组字时它以首选结束组字再接上中文标点。前端高亮的不是首选时,先msime_engine_finish(revision, 高亮行),再msime_engine_key_punct。handled为 0(中文标点关着、锁成 ASCII、这个键没有中文标点)时前端自己把字符交给应用,有commit就先交commit。 - 数字 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);候选行没有变,前端手里的下标照旧有效。 - 空格:不在
spelling_symbols里时,有候选就msime_engine_select(revision, 高亮行);联想的候选只在用户明确选择时上屏,空格和回车不上屏它们。 - 编辑键是命令:回车
COMMIT_RAW,EscCANCEL(越南文、藏文和开着的韩文汉字列表第一次只退一步,再发一次才丢弃),退格BACKSPACE,DeleteDELETE_FORWARD,←/→MOVE_LEFT/MOVE_RIGHT(全拼、双拼整句改字用CONVERSION_LEFT/CONVERSION_RIGHT),Home/EndMOVE_HOME/MOVE_END。 - 没有组字(
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。 - 返回码:
OK0、INVALID_ARGUMENT1、FAILED2、STORAGE3、UNSUPPORTED4、STALE5、BUSY6、PANIC7、NULL8。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、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 的依赖图:
在要打包的那个平台上运行,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-core0.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。