1use 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
36pub struct msime_engine_session {
38 state: Mutex<SessionState>,
39}
40
41const _: fn() = || {
43 fn shared_between_threads<T: Send + Sync>() {}
44 shared_between_threads::<msime_engine_session>();
45};
46
47struct SessionState {
49 session: Session,
50 _access: DictionaryAccess,
51 poisoned: bool,
53 snapshot: Option<SessionSnapshot>,
55 result: KeyResult,
57}
58
59impl SessionState {
60 fn begin_change(&mut self) {
62 self.snapshot = None;
63 }
64
65 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 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 unsafe { write_sized(output, value, "result") }
105 }
106}
107
108fn with_session(
110 handle: *const msime_engine_session,
111 body: impl FnOnce(&mut SessionState) -> Outcome,
112) -> i32 {
113 guard(|| {
114 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 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
152fn 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
181fn 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
194fn 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#[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 unsafe { session.write(std::ptr::null_mut()) };
228 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 unsafe { session.write(Box::into_raw(handle)) };
258 Ok(())
259 })
260}
261
262#[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 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 drop(unsafe { Box::from_raw(session) });
285 Ok(())
286 })
287}
288
289#[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#[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#[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 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#[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#[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 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 unsafe { state.finish(outcome, result) }
505 })
506}
507
508#[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 unsafe { check_output(result, "result") }?;
524 state.begin_change();
525 let outcome = state.session.candidate_key(digit);
526 unsafe { state.finish(outcome, result) }
528 })
529}
530
531#[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 unsafe { check_output(result, "result") }?;
549 state.begin_change();
550 let outcome = state.session.punctuation(character);
551 unsafe { state.finish(outcome, result) }
553 })
554}
555
556#[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 unsafe { check_output(result, "result") }?;
578 state.begin_change();
579 let outcome = state.session.command(command);
580 unsafe { state.finish(outcome, result) }
582 })
583}
584
585#[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#[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 unsafe { write_sized(state, value, "state") }
650 })
651}
652
653#[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 unsafe { write_sized(candidate, value, "candidate") }
711 })
712}
713
714#[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 unsafe { grew.write(u32::from(expanded)) };
729 }
730 Ok(())
731 })
732}
733
734#[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 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
798fn 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 unsafe { check_output(result, "result") }?;
813 state.begin_change();
814 let outcome = action(&mut state.session);
815 unsafe { state.finish(outcome, result) }
817 })
818}
819
820#[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#[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#[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#[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#[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#[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#[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 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}