Skip to main content

msime_engine_capi/
session.rs

1//! 会话句柄与会话上的函数:建与释放、设置、按键、状态与候选、选择与逐候选学习。
2//!
3//! 一个句柄里是一把互斥锁护着的会话。同一会话的调用由调用方串行,锁只用 `try_lock`:拿不到就是两个线程同时在调,返回 `MSIME_ENGINE_BUSY`,不等待。核心 `Session` 是 `Send`,所以会话可以在任何线程上用,只是不能并发。会话里的 panic 挡在边界内并作废会话,之后的调用都返回 `MSIME_ENGINE_PANIC`。
4
5use std::ffi::c_char;
6use std::sync::{Mutex, TryLockError};
7
8use msime_engine::{
9    CandidateEdge, Command, DictionaryAccess, FirstPageRows, FrequencyAdjustmentMode, KeyResult,
10    LocalInputMode, SchemeType, Session, SessionSnapshot,
11};
12use serde_json::{json, Value};
13
14use crate::abi::{
15    check_output, guard, input_str, msime_engine_candidate, msime_engine_result,
16    msime_engine_state, msime_engine_str, panic_message, write_owned, write_sized, Failure,
17    Outcome,
18};
19use crate::{
20    MSIME_ENGINE_BUSY, MSIME_ENGINE_CANDIDATE_ANSWERS_KEY, MSIME_ENGINE_CANDIDATE_FUZZY,
21    MSIME_ENGINE_CANDIDATE_SENTENCE, MSIME_ENGINE_EDGE_FIRST_HAN, MSIME_ENGINE_EDGE_LAST_HAN,
22    MSIME_ENGINE_FREQUENCY_DISABLED, MSIME_ENGINE_FREQUENCY_HALVE, MSIME_ENGINE_FREQUENCY_LINEAR,
23    MSIME_ENGINE_FREQUENCY_PIN, MSIME_ENGINE_FREQUENCY_PROMOTE, MSIME_ENGINE_KEY_SHIFT_ONLY,
24    MSIME_ENGINE_OPTION_CHINESE_PUNCT, MSIME_ENGINE_OPTION_DEDICATED_ENGLISH,
25    MSIME_ENGINE_OPTION_FIRST_PAGE_ROWS, MSIME_ENGINE_OPTION_FREQUENCY_LINEAR_STEP,
26    MSIME_ENGINE_OPTION_FREQUENCY_MODE, MSIME_ENGINE_OPTION_FREQUENCY_TRIGGER_COUNT,
27    MSIME_ENGINE_OPTION_HELPCODE_ENABLED, MSIME_ENGINE_OPTION_HELPCODE_SCHEMA,
28    MSIME_ENGINE_OPTION_PAIRED_PUNCT, MSIME_ENGINE_OPTION_PERSONAL_CONTEXT,
29    MSIME_ENGINE_OPTION_PREDICTION_ALLOWED, MSIME_ENGINE_OPTION_PUNCT_LOCK,
30    MSIME_ENGINE_OPTION_RESCORING_CONTEXT, MSIME_ENGINE_OPTION_WUBI_MIXED_PINYIN,
31    MSIME_ENGINE_PANIC, MSIME_ENGINE_STALE, MSIME_ENGINE_STATE_CANDIDATE_LIST_OPEN,
32    MSIME_ENGINE_STATE_DEDICATED_ENGLISH, MSIME_ENGINE_STATE_PREDICTION,
33    MSIME_ENGINE_STATE_TAKES_SEPARATOR, MSIME_ENGINE_UNSUPPORTED,
34};
35
36/// 不透明的会话句柄,由 `msime_engine_session_new` 建、`msime_engine_session_free` 释放。
37pub struct msime_engine_session {
38    state: Mutex<SessionState>,
39}
40
41// 句柄会被交给任意线程:锁里的东西必须能在线程间移动。
42const _: fn() = || {
43    fn shared_between_threads<T: Send + Sync>() {}
44    shared_between_threads::<msime_engine_session>();
45};
46
47/// 字段按声明顺序析构:会话先结束(写下学习、交还日志连接),之后才放掉词库租约,维护在会话写完之前拿不到锁。
48struct SessionState {
49    session: Session,
50    _access: DictionaryAccess,
51    /// 之前的调用在库里 panic 过,会话状态不可信。
52    poisoned: bool,
53    /// 当前修订号下的快照;每次改变状态的调用都清掉,`msime_engine_state_get` 和 `msime_engine_candidate_get` 借出的字符串指向它。
54    snapshot: Option<SessionSnapshot>,
55    /// 上一次改变状态的调用的结果,`msime_engine_result` 借出的 `commit` 和 `diagnostic` 指向它。
56    result: KeyResult,
57}
58
59impl SessionState {
60    /// 改变状态之前:之前借出的字符串到此作废。
61    fn begin_change(&mut self) {
62        self.snapshot = None;
63    }
64
65    /// 宿主看的快照:全拼的 `preedit` 按音节分开,与 msime 自己的宿主一致。
66    fn snapshot(&mut self) -> &SessionSnapshot {
67        self.snapshot
68            .get_or_insert_with(|| self.session.display_snapshot())
69    }
70
71    fn check_revision(&self, revision: u64) -> Outcome {
72        let current = self.session.candidate_revision();
73        if revision != current {
74            return Err(Failure::new(
75                MSIME_ENGINE_STALE,
76                format!("revision {revision} is stale; the candidates are at revision {current}"),
77            ));
78        }
79        Ok(())
80    }
81
82    /// 记下结果并写给调用方(`output` 可以是 NULL)。
83    ///
84    /// # Safety
85    /// 同 [`write_sized`]。
86    unsafe fn finish(&mut self, result: KeyResult, output: *mut msime_engine_result) -> Outcome {
87        self.result = result;
88        if output.is_null() {
89            return Ok(());
90        }
91        let result = &self.result;
92        let value = msime_engine_result {
93            struct_size: 0,
94            handled: u32::from(result.handled),
95            has_commit: u32::from(result.commit.is_some()),
96            diagnostic_code: result
97                .diagnostic_code()
98                .map_or(0, |code| u32::from(code.value())),
99            revision: self.session.candidate_revision(),
100            commit: msime_engine_str::borrow(result.commit.as_deref().unwrap_or_default()),
101            diagnostic: msime_engine_str::borrow(result.diagnostic.as_deref().unwrap_or_default()),
102        };
103        // SAFETY: 调用方的前提。
104        unsafe { write_sized(output, value, "result") }
105    }
106}
107
108/// 在会话上执行一次调用:空句柄、并发调用、已作废的会话各自报错;`body` 里的 panic 作废会话。
109fn with_session(
110    handle: *const msime_engine_session,
111    body: impl FnOnce(&mut SessionState) -> Outcome,
112) -> i32 {
113    guard(|| {
114        // SAFETY: 非空的句柄来自 `msime_engine_session_new`,调用方保证它还没被释放。
115        let Some(handle) = (unsafe { handle.as_ref() }) else {
116            return Err(Failure::null("session"));
117        };
118        let mut state = match handle.state.try_lock() {
119            Ok(state) => state,
120            Err(TryLockError::WouldBlock) => return Err(Failure::new(
121                MSIME_ENGINE_BUSY,
122                "another thread is calling this session; calls into one session must not overlap",
123            )),
124            // `body` 的 panic 在下面就被接住,锁不会因此中毒;真中毒了也只能当作废处理。
125            Err(TryLockError::Poisoned(_)) => {
126                return Err(Failure::new(
127                    MSIME_ENGINE_PANIC,
128                    "session is unusable after a panic; free it",
129                ))
130            }
131        };
132        if state.poisoned {
133            return Err(Failure::new(
134                MSIME_ENGINE_PANIC,
135                "session is unusable after a panic; free it",
136            ));
137        }
138        let state = &mut *state;
139        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| body(state))) {
140            Ok(outcome) => outcome,
141            Err(payload) => {
142                state.poisoned = true;
143                Err(Failure::new(
144                    MSIME_ENGINE_PANIC,
145                    panic_message(payload.as_ref()),
146                ))
147            }
148        }
149    })
150}
151
152/// 下标转成引擎的 `usize`。
153fn index(value: u32) -> usize {
154    value as usize
155}
156
157fn local_mode_value(mode: LocalInputMode) -> u32 {
158    match mode {
159        LocalInputMode::None => crate::MSIME_ENGINE_LOCAL_MODE_NONE,
160        LocalInputMode::Unicode => crate::MSIME_ENGINE_LOCAL_MODE_UNICODE,
161        LocalInputMode::DateTime => crate::MSIME_ENGINE_LOCAL_MODE_DATE_TIME,
162        LocalInputMode::QuickPhrase => crate::MSIME_ENGINE_LOCAL_MODE_QUICK_PHRASE,
163        LocalInputMode::Emoji => crate::MSIME_ENGINE_LOCAL_MODE_EMOJI,
164        LocalInputMode::Kaomoji => crate::MSIME_ENGINE_LOCAL_MODE_KAOMOJI,
165        LocalInputMode::SuperJianpin => crate::MSIME_ENGINE_LOCAL_MODE_SUPER_JIANPIN,
166        LocalInputMode::TemporaryEnglish => crate::MSIME_ENGINE_LOCAL_MODE_TEMPORARY_ENGLISH,
167        LocalInputMode::TemporaryJapanese => crate::MSIME_ENGINE_LOCAL_MODE_TEMPORARY_JAPANESE,
168        LocalInputMode::Expression => crate::MSIME_ENGINE_LOCAL_MODE_EXPRESSION,
169        LocalInputMode::Command => crate::MSIME_ENGINE_LOCAL_MODE_COMMAND,
170        LocalInputMode::Mention => crate::MSIME_ENGINE_LOCAL_MODE_MENTION,
171        LocalInputMode::Url => crate::MSIME_ENGINE_LOCAL_MODE_URL,
172        LocalInputMode::Chaizi => crate::MSIME_ENGINE_LOCAL_MODE_CHAIZI,
173        LocalInputMode::Symbol => crate::MSIME_ENGINE_LOCAL_MODE_SYMBOL,
174    }
175}
176
177fn saturating_u32(value: usize) -> u32 {
178    u32::try_from(value).unwrap_or(u32::MAX)
179}
180
181/// `MSIME_ENGINE_FREQUENCY_*` 到引擎的调频方式;编号冻结,与选项 JSON 的 `frequency.mode` 名字一一对应。
182fn frequency_mode(value: i64) -> Outcome<FrequencyAdjustmentMode> {
183    let mode = u32::try_from(value).ok().and_then(|value| match value {
184        MSIME_ENGINE_FREQUENCY_DISABLED => Some(FrequencyAdjustmentMode::Disabled),
185        MSIME_ENGINE_FREQUENCY_PIN => Some(FrequencyAdjustmentMode::Pin),
186        MSIME_ENGINE_FREQUENCY_HALVE => Some(FrequencyAdjustmentMode::Halve),
187        MSIME_ENGINE_FREQUENCY_LINEAR => Some(FrequencyAdjustmentMode::Linear),
188        MSIME_ENGINE_FREQUENCY_PROMOTE => Some(FrequencyAdjustmentMode::Promote),
189        _ => None,
190    });
191    mode.ok_or_else(|| Failure::invalid(format!("unknown frequency mode {value}")))
192}
193
194/// 调频的次数与步长:1 到 10,与建会话时的检查相同。
195fn frequency_count(value: i64, what: &'static str) -> Outcome<i32> {
196    if !(1..=10).contains(&value) {
197        return Err(Failure::invalid(format!("{what} must be 1..=10")));
198    }
199    Ok(value as i32)
200}
201
202fn bool_option(value: i64) -> Outcome<bool> {
203    match value {
204        0 => Ok(false),
205        1 => Ok(true),
206        _ => Err(Failure::invalid("a switch option takes 0 or 1")),
207    }
208}
209
210// ---- 会话 ----
211
212/// 按选项 JSON(版本 1,键表见 README)建一个会话,成功时把句柄写进 `*session`,失败时写 NULL。会话持有这两个目录上的共享词库租约,直到释放:租约在维护(`msime_engine_user_dict_edit`、`msime_engine_reset_learning`)进行时拿不到,这时返回 `MSIME_ENGINE_BUSY`。
213///
214/// # Safety
215/// `options_json` 指向 `options_len` 个可读字节;`session` 可写一个指针。
216#[no_mangle]
217pub unsafe extern "C" fn msime_engine_session_new(
218    options_json: *const c_char,
219    options_len: usize,
220    session: *mut *mut msime_engine_session,
221) -> i32 {
222    guard(|| {
223        if session.is_null() {
224            return Err(Failure::null("session"));
225        }
226        // SAFETY: 上面检查过非空,调用方保证可写。
227        unsafe { session.write(std::ptr::null_mut()) };
228        // SAFETY: 调用方保证 `options_json` 的长度。
229        let json = unsafe { input_str(options_json, options_len, "options_json") }?;
230        let options = crate::options::parse(json)?;
231        options.paths.validate()?;
232        let access =
233            DictionaryAccess::try_session(&options.paths.user_data, &options.paths.dictionaries)
234                .map_err(|error| {
235                    Failure::new(
236                        crate::MSIME_ENGINE_STORAGE,
237                        format!("dictionary lease: {error}"),
238                    )
239                })?
240                .ok_or_else(|| {
241                    Failure::new(
242                        MSIME_ENGINE_BUSY,
243                        msime_engine::diagnostics::DICTIONARY_MAINTENANCE_BUSY,
244                    )
245                })?;
246        let engine = Session::new(options)?;
247        let handle = Box::new(msime_engine_session {
248            state: Mutex::new(SessionState {
249                session: engine,
250                _access: access,
251                poisoned: false,
252                snapshot: None,
253                result: KeyResult::default(),
254            }),
255        });
256        // SAFETY: 同上。
257        unsafe { session.write(Box::into_raw(handle)) };
258        Ok(())
259    })
260}
261
262/// 结束并释放会话:写下最近的学习,交还日志连接和词库租约。NULL 什么也不做。另一个线程正在调这个会话时返回 `MSIME_ENGINE_BUSY`、不释放;已作废的会话照样可以释放。释放之后句柄不能再用。
263///
264/// # Safety
265/// `session` 是 NULL 或 `msime_engine_session_new` 交出、还没释放过的句柄。
266#[no_mangle]
267pub unsafe extern "C" fn msime_engine_session_free(session: *mut msime_engine_session) -> i32 {
268    guard(|| {
269        if session.is_null() {
270            return Ok(());
271        }
272        // SAFETY: 调用方保证句柄有效。
273        let busy = match unsafe { &*session }.state.try_lock() {
274            Ok(_) | Err(TryLockError::Poisoned(_)) => false,
275            Err(TryLockError::WouldBlock) => true,
276        };
277        if busy {
278            return Err(Failure::new(
279                MSIME_ENGINE_BUSY,
280                "another thread is calling this session; free it after that call returns",
281            ));
282        }
283        // SAFETY: 句柄来自 `Box::into_raw`,调用方保证只释放一次。析构里的 panic 由 `guard` 接住。
284        drop(unsafe { Box::from_raw(session) });
285        Ok(())
286    })
287}
288
289/// 换方案(`MSIME_ENGINE_SCHEME_*`),丢弃组字。方案必须在选项的 `enabled_schemes` 里;新方案的词库打不开时失败,留在原方案、组字不动。
290///
291/// # Safety
292/// `session` 是有效的句柄。
293#[no_mangle]
294pub unsafe extern "C" fn msime_engine_switch_scheme(
295    session: *mut msime_engine_session,
296    scheme: u32,
297) -> i32 {
298    with_session(session, |state| {
299        let scheme = u8::try_from(scheme)
300            .ok()
301            .and_then(SchemeType::from_u8)
302            .ok_or_else(|| Failure::invalid(format!("unknown scheme {scheme}")))?;
303        state.begin_change();
304        state.session.switch_scheme(scheme)?;
305        Ok(())
306    })
307}
308
309/// 设一个整数选项(`MSIME_ENGINE_OPTION_*` 里标着「整数」的)。不认识的编号是 `MSIME_ENGINE_UNSUPPORTED`,字符串选项或越界的值是 `MSIME_ENGINE_INVALID_ARGUMENT`。
310///
311/// # Safety
312/// `session` 是有效的句柄。
313#[no_mangle]
314pub unsafe extern "C" fn msime_engine_set_int(
315    session: *mut msime_engine_session,
316    option: u32,
317    value: i64,
318) -> i32 {
319    with_session(session, |state| {
320        match option {
321            MSIME_ENGINE_OPTION_CHINESE_PUNCT => {
322                let enabled = bool_option(value)?;
323                state.begin_change();
324                state.session.set_chinese_punctuation_enabled(enabled);
325            }
326            MSIME_ENGINE_OPTION_PAIRED_PUNCT => {
327                let enabled = bool_option(value)?;
328                state.begin_change();
329                state.session.set_paired_punctuation_enabled(enabled);
330            }
331            MSIME_ENGINE_OPTION_PUNCT_LOCK => {
332                let lock = i32::try_from(value)
333                    .map_err(|_| Failure::invalid("punctuation lock must be 0..=2"))?;
334                state.begin_change();
335                state.session.set_punctuation_lock(lock)?;
336            }
337            MSIME_ENGINE_OPTION_DEDICATED_ENGLISH => {
338                let enabled = bool_option(value)?;
339                state.begin_change();
340                state.session.set_dedicated_english(enabled);
341            }
342            MSIME_ENGINE_OPTION_HELPCODE_ENABLED => {
343                let enabled = bool_option(value)?;
344                state.begin_change();
345                state.session.set_helpcode_enabled(enabled);
346            }
347            MSIME_ENGINE_OPTION_PERSONAL_CONTEXT => {
348                let enabled = bool_option(value)?;
349                state.begin_change();
350                state.session.set_personal_context_enabled(enabled);
351            }
352            MSIME_ENGINE_OPTION_PREDICTION_ALLOWED => {
353                let allowed = bool_option(value)?;
354                state.begin_change();
355                state.session.set_prediction_allowed(allowed);
356            }
357            MSIME_ENGINE_OPTION_FIRST_PAGE_ROWS => {
358                if !(1..=255).contains(&value) {
359                    return Err(Failure::invalid("first page rows must be 1..=255"));
360                }
361                state.begin_change();
362                state
363                    .session
364                    .set_first_page_rows(FirstPageRows::new(value as usize));
365            }
366            MSIME_ENGINE_OPTION_FREQUENCY_MODE
367            | MSIME_ENGINE_OPTION_FREQUENCY_TRIGGER_COUNT
368            | MSIME_ENGINE_OPTION_FREQUENCY_LINEAR_STEP => {
369                let mut frequency = state.session.frequency_adjustment();
370                match option {
371                    MSIME_ENGINE_OPTION_FREQUENCY_MODE => frequency.mode = frequency_mode(value)?,
372                    MSIME_ENGINE_OPTION_FREQUENCY_TRIGGER_COUNT => {
373                        frequency.trigger_count = frequency_count(value, "trigger count")?;
374                    }
375                    _ => frequency.linear_step = frequency_count(value, "linear step")?,
376                }
377                state.begin_change();
378                state.session.set_frequency_adjustment(frequency)?;
379            }
380            MSIME_ENGINE_OPTION_WUBI_MIXED_PINYIN => {
381                let enabled = bool_option(value)?;
382                state.begin_change();
383                state.session.set_wubi_mixed_pinyin(enabled);
384            }
385            MSIME_ENGINE_OPTION_HELPCODE_SCHEMA | MSIME_ENGINE_OPTION_RESCORING_CONTEXT => {
386                return Err(Failure::invalid(format!("option {option} takes a string")));
387            }
388            _ => {
389                return Err(Failure::new(
390                    MSIME_ENGINE_UNSUPPORTED,
391                    format!("unknown option {option}"),
392                ))
393            }
394        }
395        Ok(())
396    })
397}
398
399/// 设一个字符串选项(`MSIME_ENGINE_OPTION_*` 里标着「字符串」的),`value` 是 `value_len` 个字节的 UTF-8。不认识的编号是 `MSIME_ENGINE_UNSUPPORTED`,整数选项是 `MSIME_ENGINE_INVALID_ARGUMENT`。
400///
401/// # Safety
402/// `session` 是有效的句柄;`value` 指向 `value_len` 个可读字节。
403#[no_mangle]
404pub unsafe extern "C" fn msime_engine_set_string(
405    session: *mut msime_engine_session,
406    option: u32,
407    value: *const c_char,
408    value_len: usize,
409) -> i32 {
410    with_session(session, |state| {
411        // SAFETY: 调用方保证长度。
412        let value = unsafe { input_str(value, value_len, "value") }?;
413        match option {
414            MSIME_ENGINE_OPTION_HELPCODE_SCHEMA => {
415                if !Session::is_supported_helpcode_schema(value) {
416                    return Err(Failure::invalid(format!(
417                        "unknown helpcode schema {value:?}"
418                    )));
419                }
420                state.begin_change();
421                if !state.session.set_helpcode_schema(value) {
422                    return Err(Failure::new(
423                        crate::MSIME_ENGINE_FAILED,
424                        format!("helpcode table {value:?} could not be loaded"),
425                    ));
426                }
427            }
428            MSIME_ENGINE_OPTION_RESCORING_CONTEXT => {
429                state.begin_change();
430                state.session.set_rescoring_context(value);
431            }
432            MSIME_ENGINE_OPTION_CHINESE_PUNCT
433            | MSIME_ENGINE_OPTION_PAIRED_PUNCT
434            | MSIME_ENGINE_OPTION_PUNCT_LOCK
435            | MSIME_ENGINE_OPTION_DEDICATED_ENGLISH
436            | MSIME_ENGINE_OPTION_HELPCODE_ENABLED
437            | MSIME_ENGINE_OPTION_PERSONAL_CONTEXT
438            | MSIME_ENGINE_OPTION_PREDICTION_ALLOWED
439            | MSIME_ENGINE_OPTION_FIRST_PAGE_ROWS
440            | MSIME_ENGINE_OPTION_FREQUENCY_MODE
441            | MSIME_ENGINE_OPTION_FREQUENCY_TRIGGER_COUNT
442            | MSIME_ENGINE_OPTION_FREQUENCY_LINEAR_STEP
443            | MSIME_ENGINE_OPTION_WUBI_MIXED_PINYIN => {
444                return Err(Failure::invalid(format!(
445                    "option {option} takes an integer"
446                )));
447            }
448            _ => {
449                return Err(Failure::new(
450                    MSIME_ENGINE_UNSUPPORTED,
451                    format!("unknown option {option}"),
452                ))
453            }
454        }
455        Ok(())
456    })
457}
458
459/// 忘掉上屏的上下文(焦点或应用变了、宿主自己插入了文字),清掉上屏后联想。
460///
461/// # Safety
462/// `session` 是有效的句柄。
463#[no_mangle]
464pub unsafe extern "C" fn msime_engine_reset_context(session: *mut msime_engine_session) -> i32 {
465    with_session(session, |state| {
466        state.begin_change();
467        state.session.reset_context();
468        Ok(())
469    })
470}
471
472// ---- 输入 ----
473
474/// 一个 ASCII 字符(0x20 到 0x7E,另收 Tab 0x09):字母、拼写用的数字,以及 `spelling_symbols` 里列出的符号。`flags` 是 `MSIME_ENGINE_KEY_*`。结果写进 `result`(可以是 NULL)。按键路由见 README。
475///
476/// # Safety
477/// `session` 是有效的句柄;`result` 是 NULL 或按 `struct_size` 约定可写。
478#[no_mangle]
479pub unsafe extern "C" fn msime_engine_key_char(
480    session: *mut msime_engine_session,
481    character: u8,
482    flags: u32,
483    result: *mut msime_engine_result,
484) -> i32 {
485    with_session(session, |state| {
486        if !(character == b'\t' || (0x20..=0x7e).contains(&character)) {
487            return Err(Failure::invalid(
488                "key_char takes one printable ASCII character or Tab",
489            ));
490        }
491        if flags & !MSIME_ENGINE_KEY_SHIFT_ONLY != 0 {
492            return Err(Failure::new(
493                MSIME_ENGINE_UNSUPPORTED,
494                format!("unknown key flags {flags:#x}"),
495            ));
496        }
497        // SAFETY: 调用方的前提。
498        unsafe { check_output(result, "result") }?;
499        state.begin_change();
500        let outcome = state
501            .session
502            .character(character, flags & MSIME_ENGINE_KEY_SHIFT_ONLY != 0);
503        // SAFETY: 调用方的前提。
504        unsafe { state.finish(outcome, result) }
505    })
506}
507
508/// ABI 1.1 起。数字键当选择键,引擎自己的数字选词入口:有组字时 `1`–`9` 上屏整张候选表的第 `digit - 1` 行(也就是第一页,`msime_engine_select` 的那套学习照常);越南文 VNI 和注音此刻拼写用的数字交给拼写;没有组字时不处理(`handled` 为 0),并且忘掉上屏的上下文,因为这个数字由宿主自己交给应用;上屏后联想的候选不接数字,按下时被清掉。`digit` 只收 ASCII 数字 `0`–`9`,其余是 `MSIME_ENGINE_INVALID_ARGUMENT`。不带修订号:它选的是调用时的候选。Unicode、日期、计算这些用数字拼写的本地模式里它照样选词,所以按键路由仍是先 `msime_engine_key_char`,没被处理时再用它(只显示第一页的前端)或 `msime_engine_select`(翻过页的前端),见 README 按键路由第 4 条。
509///
510/// # Safety
511/// 同 `msime_engine_key_char`。
512#[no_mangle]
513pub unsafe extern "C" fn msime_engine_candidate_key(
514    session: *mut msime_engine_session,
515    digit: u8,
516    result: *mut msime_engine_result,
517) -> i32 {
518    with_session(session, |state| {
519        if !digit.is_ascii_digit() {
520            return Err(Failure::invalid("candidate_key takes one ASCII digit 0-9"));
521        }
522        // SAFETY: 调用方的前提。
523        unsafe { check_output(result, "result") }?;
524        state.begin_change();
525        let outcome = state.session.candidate_key(digit);
526        // SAFETY: 调用方的前提。
527        unsafe { state.finish(outcome, result) }
528    })
529}
530
531/// 一个 ASCII 标点:有组字时以首选结束组字,再接上中文标点;中文标点关着、锁成 ASCII 或这个键没有中文标点时不处理(`handled` 为 0),宿主自己输出这个键。宿主高亮了首选以外的行时,先用 `msime_engine_finish` 以那一行结束,再调这个函数。
532///
533/// # Safety
534/// 同 `msime_engine_key_char`。
535#[no_mangle]
536pub unsafe extern "C" fn msime_engine_key_punct(
537    session: *mut msime_engine_session,
538    character: u8,
539    result: *mut msime_engine_result,
540) -> i32 {
541    with_session(session, |state| {
542        if !character.is_ascii_punctuation() {
543            return Err(Failure::invalid(
544                "key_punct takes one ASCII punctuation character",
545            ));
546        }
547        // SAFETY: 调用方的前提。
548        unsafe { check_output(result, "result") }?;
549        state.begin_change();
550        let outcome = state.session.punctuation(character);
551        // SAFETY: 调用方的前提。
552        unsafe { state.finish(outcome, result) }
553    })
554}
555
556/// 一个编辑命令(`MSIME_ENGINE_COMMAND_*`)。
557///
558/// # Safety
559/// 同 `msime_engine_key_char`。
560#[no_mangle]
561pub unsafe extern "C" fn msime_engine_command(
562    session: *mut msime_engine_session,
563    command: u32,
564    result: *mut msime_engine_result,
565) -> i32 {
566    with_session(session, |state| {
567        let command = u8::try_from(command)
568            .ok()
569            .and_then(Command::from_u8)
570            .ok_or_else(|| {
571                Failure::new(
572                    MSIME_ENGINE_UNSUPPORTED,
573                    format!("unknown command {command}"),
574                )
575            })?;
576        // SAFETY: 调用方的前提。
577        unsafe { check_output(result, "result") }?;
578        state.begin_change();
579        let outcome = state.session.command(command);
580        // SAFETY: 调用方的前提。
581        unsafe { state.finish(outcome, result) }
582    })
583}
584
585/// 把组字光标放到 `editing_text` 的第 `caret_byte` 个字节前;负数(`MSIME_ENGINE_CARET_END`)回到末尾。位置限制在可编辑的范围内,九键组码时什么也不做。
586///
587/// # Safety
588/// `session` 是有效的句柄。
589#[no_mangle]
590pub unsafe extern "C" fn msime_engine_set_caret(
591    session: *mut msime_engine_session,
592    caret_byte: i64,
593) -> i32 {
594    with_session(session, |state| {
595        let caret = usize::try_from(caret_byte).ok();
596        state.begin_change();
597        state.session.set_caret(caret);
598        Ok(())
599    })
600}
601
602// ---- 状态 ----
603
604/// 读组字状态。只读:不改变会话,之前借出的字符串照样有效。
605///
606/// # Safety
607/// `session` 是有效的句柄;`state` 按 `struct_size` 约定可写。
608#[no_mangle]
609pub unsafe extern "C" fn msime_engine_state_get(
610    session: *mut msime_engine_session,
611    state: *mut msime_engine_state,
612) -> i32 {
613    with_session(session, |inner| {
614        if state.is_null() {
615            return Err(Failure::null("state"));
616        }
617        let revision = inner.session.candidate_revision();
618        let takes_separator = inner.session.takes_local_separator();
619        let snapshot = inner.snapshot();
620        let mut flags = 0;
621        if snapshot.prediction {
622            flags |= MSIME_ENGINE_STATE_PREDICTION;
623        }
624        if snapshot.dedicated_english {
625            flags |= MSIME_ENGINE_STATE_DEDICATED_ENGLISH;
626        }
627        if snapshot.candidate_list_open {
628            flags |= MSIME_ENGINE_STATE_CANDIDATE_LIST_OPEN;
629        }
630        if takes_separator {
631            flags |= MSIME_ENGINE_STATE_TAKES_SEPARATOR;
632        }
633        let value = msime_engine_state {
634            struct_size: 0,
635            scheme: u32::from(snapshot.scheme as u8),
636            revision,
637            candidate_count: saturating_u32(snapshot.candidates.len()),
638            flags,
639            local_mode: local_mode_value(snapshot.local_mode),
640            caret_byte: saturating_u32(snapshot.caret_position),
641            conversion_focus_start_scalar: saturating_u32(snapshot.conversion_focus.0),
642            conversion_focus_end_scalar: saturating_u32(snapshot.conversion_focus.1),
643            preedit: msime_engine_str::borrow(&snapshot.preedit),
644            editing_text: msime_engine_str::borrow(&snapshot.editing_text),
645            conversion: msime_engine_str::borrow(&snapshot.conversion),
646            spelling_symbols: msime_engine_str::borrow(&snapshot.spelling_symbols),
647        };
648        // SAFETY: 调用方的前提。
649        unsafe { write_sized(state, value, "state") }
650    })
651}
652
653/// 读第 `index` 行候选。`revision` 必须是当前的候选修订号,否则 `MSIME_ENGINE_STALE`;越界是 `MSIME_ENGINE_INVALID_ARGUMENT`。只读,可以先取状态再逐个取一页候选。
654///
655/// # Safety
656/// `session` 是有效的句柄;`candidate` 按 `struct_size` 约定可写。
657#[no_mangle]
658pub unsafe extern "C" fn msime_engine_candidate_get(
659    session: *mut msime_engine_session,
660    revision: u64,
661    index: u32,
662    candidate: *mut msime_engine_candidate,
663) -> i32 {
664    with_session(session, |state| {
665        if candidate.is_null() {
666            return Err(Failure::null("candidate"));
667        }
668        state.check_revision(revision)?;
669        let snapshot = state.snapshot();
670        let row = index as usize;
671        let Some(item) = snapshot.candidates.get(row) else {
672            return Err(Failure::invalid(format!(
673                "candidate {index} is out of range ({} candidates)",
674                snapshot.candidates.len()
675            )));
676        };
677        let mut flags = 0;
678        if item.fuzzy {
679            flags |= MSIME_ENGINE_CANDIDATE_FUZZY;
680        }
681        if snapshot
682            .candidate_answers_key
683            .get(row)
684            .copied()
685            .unwrap_or(false)
686        {
687            flags |= MSIME_ENGINE_CANDIDATE_ANSWERS_KEY;
688        }
689        if item.sentence_association {
690            flags |= MSIME_ENGINE_CANDIDATE_SENTENCE;
691        }
692        let value = msime_engine_candidate {
693            struct_size: 0,
694            index,
695            source: u32::from(item.source as u8),
696            scheme: u32::from(item.scheme as u8),
697            flags,
698            fixed_position: item.fixed_position,
699            word: msime_engine_str::borrow(&item.word),
700            code: msime_engine_str::borrow(&item.pinyin),
701            annotation: msime_engine_str::borrow(
702                snapshot
703                    .candidate_annotations
704                    .get(row)
705                    .map_or("", String::as_str),
706            ),
707            corrected_from: msime_engine_str::borrow(&item.corrected_from),
708        };
709        // SAFETY: 调用方的前提。
710        unsafe { write_sized(candidate, value, "candidate") }
711    })
712}
713
714/// 单字母全拼只先给出一部分候选;宿主翻过第一页时调它要回其余的。`*grew` 写 1 表示列表变长了(修订号随之改变),否则写 0。会改变状态。
715///
716/// # Safety
717/// `session` 是有效的句柄;`grew` 是 NULL 或可写。
718#[no_mangle]
719pub unsafe extern "C" fn msime_engine_expand_candidates(
720    session: *mut msime_engine_session,
721    grew: *mut u32,
722) -> i32 {
723    with_session(session, |state| {
724        state.begin_change();
725        let expanded = state.session.expand_initial_candidates();
726        if !grew.is_null() {
727            // SAFETY: 调用方保证可写。
728            unsafe { grew.write(u32::from(expanded)) };
729        }
730        Ok(())
731    })
732}
733
734/// 整个状态和全部候选的 JSON,给调试和日志用;格式不属于 ABI,随版本变化。返回的字符串用 `msime_engine_string_free` 释放。只读。
735///
736/// # Safety
737/// `session` 是有效的句柄;`json` 可写一个指针。
738#[no_mangle]
739pub unsafe extern "C" fn msime_engine_state_json(
740    session: *mut msime_engine_session,
741    json: *mut *mut c_char,
742) -> i32 {
743    with_session(session, |state| {
744        if json.is_null() {
745            return Err(Failure::null("json"));
746        }
747        let revision = state.session.candidate_revision();
748        let text = debug_json(revision, state.snapshot()).to_string();
749        // SAFETY: 上面检查过非空。
750        unsafe { write_owned(json, text) };
751        Ok(())
752    })
753}
754
755fn debug_json(revision: u64, snapshot: &SessionSnapshot) -> Value {
756    let candidates: Vec<Value> = snapshot
757        .candidates
758        .iter()
759        .enumerate()
760        .map(|(index, item)| {
761            json!({
762                "word": item.word.as_str(),
763                "code": item.pinyin.as_str(),
764                "canonical_code": item.canonical_pinyin.as_str(),
765                "source": item.source as u8,
766                "scheme": item.scheme.name(),
767                "weight": item.weight,
768                "fixed_position": item.fixed_position,
769                "fuzzy": item.fuzzy,
770                "corrected_from": item.corrected_from.as_str(),
771                "sentence": item.sentence_association,
772                "annotation": snapshot.candidate_annotations.get(index).map_or("", String::as_str),
773                "answers_key": snapshot.candidate_answers_key.get(index).copied().unwrap_or(false),
774            })
775        })
776        .collect();
777    json!({
778        "revision": revision,
779        "scheme": snapshot.scheme.name(),
780        "local_mode": snapshot.local_mode.name(),
781        "prediction": snapshot.prediction,
782        "dedicated_english": snapshot.dedicated_english,
783        "preedit": snapshot.preedit,
784        "editing_text": snapshot.editing_text,
785        "caret_byte": snapshot.caret_position,
786        "raw_segmentation": snapshot.raw_segmentation,
787        "normalized_segmentation": snapshot.normalized_segmentation,
788        "spelling_symbols": snapshot.spelling_symbols,
789        "shuangpin_profile": snapshot.shuangpin_profile,
790        "conversion": snapshot.conversion,
791        "conversion_focus_scalar": [snapshot.conversion_focus.0, snapshot.conversion_focus.1],
792        "candidate_list_open": snapshot.candidate_list_open,
793        "nine_key_spellings": snapshot.nine_key_spellings,
794        "candidates": candidates,
795    })
796}
797
798// ---- 选择与逐候选学习 ----
799//
800// 都带 `revision`:不是当前修订号时 `MSIME_ENGINE_STALE`,什么也不做。下标是引擎顺序里的行号,与 `msime_engine_candidate_get` 相同。
801
802/// 在核对修订号和输出结构体之后执行一次选择类调用;被拒绝的调用不碰会话。
803fn with_candidate(
804    session: *mut msime_engine_session,
805    revision: u64,
806    result: *mut msime_engine_result,
807    action: impl FnOnce(&mut Session) -> KeyResult,
808) -> i32 {
809    with_session(session, |state| {
810        state.check_revision(revision)?;
811        // SAFETY: 调用方的前提(每个公开函数的 Safety 段)。
812        unsafe { check_output(result, "result") }?;
813        state.begin_change();
814        let outcome = action(&mut state.session);
815        // SAFETY: 调用方的前提(每个公开函数的 Safety 段)。
816        unsafe { state.finish(outcome, result) }
817    })
818}
819
820/// 选第 `index` 行上屏(或消耗它覆盖的那段编码,剩下的接着组字),并照常学习。
821///
822/// # Safety
823/// `session` 是有效的句柄;`result` 是 NULL 或按 `struct_size` 约定可写。
824#[no_mangle]
825pub unsafe extern "C" fn msime_engine_select(
826    session: *mut msime_engine_session,
827    revision: u64,
828    index: u32,
829    result: *mut msime_engine_result,
830) -> i32 {
831    with_candidate(session, revision, result, |engine| {
832        engine.select(self::index(index))
833    })
834}
835
836/// 以词定字:上屏第 `index` 行的第一个或最后一个汉字(`MSIME_ENGINE_EDGE_*`),丢掉其余组字。
837///
838/// # Safety
839/// 同 `msime_engine_select`。
840#[no_mangle]
841pub unsafe extern "C" fn msime_engine_select_edge(
842    session: *mut msime_engine_session,
843    revision: u64,
844    index: u32,
845    edge: u32,
846    result: *mut msime_engine_result,
847) -> i32 {
848    let edge = match edge {
849        MSIME_ENGINE_EDGE_FIRST_HAN => CandidateEdge::FirstHan,
850        MSIME_ENGINE_EDGE_LAST_HAN => CandidateEdge::LastHan,
851        _ => {
852            return guard(|| Err(Failure::invalid(format!("unknown edge {edge}"))));
853        }
854    };
855    with_candidate(session, revision, result, |engine| {
856        engine.select_edge(self::index(index), edge)
857    })
858}
859
860/// 结束组字:从第 `first_index` 行(宿主高亮的那一行)起上屏,用于失焦、标点前或宿主自己的结束键。
861///
862/// # Safety
863/// 同 `msime_engine_select`。
864#[no_mangle]
865pub unsafe extern "C" fn msime_engine_finish(
866    session: *mut msime_engine_session,
867    revision: u64,
868    first_index: u32,
869    result: *mut msime_engine_result,
870) -> i32 {
871    with_candidate(session, revision, result, |engine| {
872        engine.finish(self::index(first_index))
873    })
874}
875
876/// 把第 `index` 行(词库里的词)提到前面,不上屏。不支持的行不处理(`handled` 为 0)。
877///
878/// # Safety
879/// 同 `msime_engine_select`。
880#[no_mangle]
881pub unsafe extern "C" fn msime_engine_pin(
882    session: *mut msime_engine_session,
883    revision: u64,
884    index: u32,
885    result: *mut msime_engine_result,
886) -> i32 {
887    with_candidate(session, revision, result, |engine| {
888        engine.pin(self::index(index))
889    })
890}
891
892/// 删掉第 `index` 行:用户自己的词从个人词典删除,随包的词不再出现。
893///
894/// # Safety
895/// 同 `msime_engine_select`。
896#[no_mangle]
897pub unsafe extern "C" fn msime_engine_remove(
898    session: *mut msime_engine_session,
899    revision: u64,
900    index: u32,
901    result: *mut msime_engine_result,
902) -> i32 {
903    with_candidate(session, revision, result, |engine| {
904        engine.remove(self::index(index))
905    })
906}
907
908/// 把第 `index` 行固定在第 `position` 位(1..=5)。
909///
910/// # Safety
911/// 同 `msime_engine_select`。
912#[no_mangle]
913pub unsafe extern "C" fn msime_engine_fix_position(
914    session: *mut msime_engine_session,
915    revision: u64,
916    index: u32,
917    position: i32,
918    result: *mut msime_engine_result,
919) -> i32 {
920    with_candidate(session, revision, result, |engine| {
921        engine.fix_position(self::index(index), position)
922    })
923}
924
925/// 取消第 `index` 行的固定位置。
926///
927/// # Safety
928/// 同 `msime_engine_select`。
929#[no_mangle]
930pub unsafe extern "C" fn msime_engine_clear_position(
931    session: *mut msime_engine_session,
932    revision: u64,
933    index: u32,
934    result: *mut msime_engine_result,
935) -> i32 {
936    with_candidate(session, revision, result, |engine| {
937        engine.clear_position(self::index(index))
938    })
939}
940
941#[cfg(test)]
942pub(crate) fn lock_for_test(
943    session: *mut msime_engine_session,
944) -> std::sync::MutexGuard<'static, impl Sized> {
945    // SAFETY: 测试里句柄一直有效,直到测试释放它。
946    unsafe { &*session }.state.lock().expect("session lock")
947}
948
949#[cfg(test)]
950pub(crate) fn run_for_test(
951    session: *mut msime_engine_session,
952    body: impl FnOnce() -> Outcome,
953) -> i32 {
954    with_session(session, |_| body())
955}