use std::ffi::c_char;
use std::sync::{Mutex, TryLockError};
use msime_engine::{
CandidateEdge, Command, DictionaryAccess, FirstPageRows, KeyResult, LocalInputMode, SchemeType,
Session, SessionSnapshot,
};
use serde_json::{json, Value};
use crate::abi::{
check_output, guard, input_str, msime_engine_candidate, msime_engine_result,
msime_engine_state, msime_engine_str, panic_message, write_owned, write_sized, Failure,
Outcome,
};
use crate::{
MSIME_ENGINE_BUSY, MSIME_ENGINE_CANDIDATE_ANSWERS_KEY, MSIME_ENGINE_CANDIDATE_FUZZY,
MSIME_ENGINE_CANDIDATE_SENTENCE, MSIME_ENGINE_EDGE_FIRST_HAN, MSIME_ENGINE_EDGE_LAST_HAN,
MSIME_ENGINE_KEY_SHIFT_ONLY, MSIME_ENGINE_OPTION_CHINESE_PUNCT,
MSIME_ENGINE_OPTION_DEDICATED_ENGLISH, MSIME_ENGINE_OPTION_FIRST_PAGE_ROWS,
MSIME_ENGINE_OPTION_HELPCODE_ENABLED, MSIME_ENGINE_OPTION_HELPCODE_SCHEMA,
MSIME_ENGINE_OPTION_PAIRED_PUNCT, MSIME_ENGINE_OPTION_PERSONAL_CONTEXT,
MSIME_ENGINE_OPTION_PREDICTION_ALLOWED, MSIME_ENGINE_OPTION_PUNCT_LOCK,
MSIME_ENGINE_OPTION_RESCORING_CONTEXT, MSIME_ENGINE_PANIC, MSIME_ENGINE_STALE,
MSIME_ENGINE_STATE_CANDIDATE_LIST_OPEN, MSIME_ENGINE_STATE_DEDICATED_ENGLISH,
MSIME_ENGINE_STATE_PREDICTION, MSIME_ENGINE_STATE_TAKES_SEPARATOR, MSIME_ENGINE_UNSUPPORTED,
};
pub struct msime_engine_session {
state: Mutex<SessionState>,
}
const _: fn() = || {
fn shared_between_threads<T: Send + Sync>() {}
shared_between_threads::<msime_engine_session>();
};
struct SessionState {
session: Session,
_access: DictionaryAccess,
poisoned: bool,
snapshot: Option<SessionSnapshot>,
result: KeyResult,
}
impl SessionState {
fn begin_change(&mut self) {
self.snapshot = None;
}
fn snapshot(&mut self) -> &SessionSnapshot {
self.snapshot
.get_or_insert_with(|| self.session.display_snapshot())
}
fn check_revision(&self, revision: u64) -> Outcome {
let current = self.session.candidate_revision();
if revision != current {
return Err(Failure::new(
MSIME_ENGINE_STALE,
format!("revision {revision} is stale; the candidates are at revision {current}"),
));
}
Ok(())
}
unsafe fn finish(&mut self, result: KeyResult, output: *mut msime_engine_result) -> Outcome {
self.result = result;
if output.is_null() {
return Ok(());
}
let result = &self.result;
let value = msime_engine_result {
struct_size: 0,
handled: u32::from(result.handled),
has_commit: u32::from(result.commit.is_some()),
diagnostic_code: result
.diagnostic_code()
.map_or(0, |code| u32::from(code.value())),
revision: self.session.candidate_revision(),
commit: msime_engine_str::borrow(result.commit.as_deref().unwrap_or_default()),
diagnostic: msime_engine_str::borrow(result.diagnostic.as_deref().unwrap_or_default()),
};
unsafe { write_sized(output, value, "result") }
}
}
fn with_session(
handle: *const msime_engine_session,
body: impl FnOnce(&mut SessionState) -> Outcome,
) -> i32 {
guard(|| {
let Some(handle) = (unsafe { handle.as_ref() }) else {
return Err(Failure::null("session"));
};
let mut state = match handle.state.try_lock() {
Ok(state) => state,
Err(TryLockError::WouldBlock) => return Err(Failure::new(
MSIME_ENGINE_BUSY,
"another thread is calling this session; calls into one session must not overlap",
)),
Err(TryLockError::Poisoned(_)) => {
return Err(Failure::new(
MSIME_ENGINE_PANIC,
"session is unusable after a panic; free it",
))
}
};
if state.poisoned {
return Err(Failure::new(
MSIME_ENGINE_PANIC,
"session is unusable after a panic; free it",
));
}
let state = &mut *state;
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| body(state))) {
Ok(outcome) => outcome,
Err(payload) => {
state.poisoned = true;
Err(Failure::new(
MSIME_ENGINE_PANIC,
panic_message(payload.as_ref()),
))
}
}
})
}
fn index(value: u32) -> usize {
value as usize
}
fn local_mode_value(mode: LocalInputMode) -> u32 {
match mode {
LocalInputMode::None => crate::MSIME_ENGINE_LOCAL_MODE_NONE,
LocalInputMode::Unicode => crate::MSIME_ENGINE_LOCAL_MODE_UNICODE,
LocalInputMode::DateTime => crate::MSIME_ENGINE_LOCAL_MODE_DATE_TIME,
LocalInputMode::QuickPhrase => crate::MSIME_ENGINE_LOCAL_MODE_QUICK_PHRASE,
LocalInputMode::Emoji => crate::MSIME_ENGINE_LOCAL_MODE_EMOJI,
LocalInputMode::Kaomoji => crate::MSIME_ENGINE_LOCAL_MODE_KAOMOJI,
LocalInputMode::SuperJianpin => crate::MSIME_ENGINE_LOCAL_MODE_SUPER_JIANPIN,
LocalInputMode::TemporaryEnglish => crate::MSIME_ENGINE_LOCAL_MODE_TEMPORARY_ENGLISH,
LocalInputMode::TemporaryJapanese => crate::MSIME_ENGINE_LOCAL_MODE_TEMPORARY_JAPANESE,
LocalInputMode::Expression => crate::MSIME_ENGINE_LOCAL_MODE_EXPRESSION,
LocalInputMode::Command => crate::MSIME_ENGINE_LOCAL_MODE_COMMAND,
LocalInputMode::Mention => crate::MSIME_ENGINE_LOCAL_MODE_MENTION,
LocalInputMode::Url => crate::MSIME_ENGINE_LOCAL_MODE_URL,
LocalInputMode::Chaizi => crate::MSIME_ENGINE_LOCAL_MODE_CHAIZI,
LocalInputMode::Symbol => crate::MSIME_ENGINE_LOCAL_MODE_SYMBOL,
}
}
fn saturating_u32(value: usize) -> u32 {
u32::try_from(value).unwrap_or(u32::MAX)
}
fn bool_option(value: i64) -> Outcome<bool> {
match value {
0 => Ok(false),
1 => Ok(true),
_ => Err(Failure::invalid("a switch option takes 0 or 1")),
}
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_session_new(
options_json: *const c_char,
options_len: usize,
session: *mut *mut msime_engine_session,
) -> i32 {
guard(|| {
if session.is_null() {
return Err(Failure::null("session"));
}
unsafe { session.write(std::ptr::null_mut()) };
let json = unsafe { input_str(options_json, options_len, "options_json") }?;
let options = crate::options::parse(json)?;
options.paths.validate()?;
let access =
DictionaryAccess::try_session(&options.paths.user_data, &options.paths.dictionaries)
.map_err(|error| {
Failure::new(
crate::MSIME_ENGINE_STORAGE,
format!("dictionary lease: {error}"),
)
})?
.ok_or_else(|| {
Failure::new(
MSIME_ENGINE_BUSY,
msime_engine::diagnostics::DICTIONARY_MAINTENANCE_BUSY,
)
})?;
let engine = Session::new(options)?;
let handle = Box::new(msime_engine_session {
state: Mutex::new(SessionState {
session: engine,
_access: access,
poisoned: false,
snapshot: None,
result: KeyResult::default(),
}),
});
unsafe { session.write(Box::into_raw(handle)) };
Ok(())
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_session_free(session: *mut msime_engine_session) -> i32 {
guard(|| {
if session.is_null() {
return Ok(());
}
let busy = match unsafe { &*session }.state.try_lock() {
Ok(_) | Err(TryLockError::Poisoned(_)) => false,
Err(TryLockError::WouldBlock) => true,
};
if busy {
return Err(Failure::new(
MSIME_ENGINE_BUSY,
"another thread is calling this session; free it after that call returns",
));
}
drop(unsafe { Box::from_raw(session) });
Ok(())
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_switch_scheme(
session: *mut msime_engine_session,
scheme: u32,
) -> i32 {
with_session(session, |state| {
let scheme = u8::try_from(scheme)
.ok()
.and_then(SchemeType::from_u8)
.ok_or_else(|| Failure::invalid(format!("unknown scheme {scheme}")))?;
state.begin_change();
state.session.switch_scheme(scheme)?;
Ok(())
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_set_int(
session: *mut msime_engine_session,
option: u32,
value: i64,
) -> i32 {
with_session(session, |state| {
match option {
MSIME_ENGINE_OPTION_CHINESE_PUNCT => {
let enabled = bool_option(value)?;
state.begin_change();
state.session.set_chinese_punctuation_enabled(enabled);
}
MSIME_ENGINE_OPTION_PAIRED_PUNCT => {
let enabled = bool_option(value)?;
state.begin_change();
state.session.set_paired_punctuation_enabled(enabled);
}
MSIME_ENGINE_OPTION_PUNCT_LOCK => {
let lock = i32::try_from(value)
.map_err(|_| Failure::invalid("punctuation lock must be 0..=2"))?;
state.begin_change();
state.session.set_punctuation_lock(lock)?;
}
MSIME_ENGINE_OPTION_DEDICATED_ENGLISH => {
let enabled = bool_option(value)?;
state.begin_change();
state.session.set_dedicated_english(enabled);
}
MSIME_ENGINE_OPTION_HELPCODE_ENABLED => {
let enabled = bool_option(value)?;
state.begin_change();
state.session.set_helpcode_enabled(enabled);
}
MSIME_ENGINE_OPTION_PERSONAL_CONTEXT => {
let enabled = bool_option(value)?;
state.begin_change();
state.session.set_personal_context_enabled(enabled);
}
MSIME_ENGINE_OPTION_PREDICTION_ALLOWED => {
let allowed = bool_option(value)?;
state.begin_change();
state.session.set_prediction_allowed(allowed);
}
MSIME_ENGINE_OPTION_FIRST_PAGE_ROWS => {
if !(1..=255).contains(&value) {
return Err(Failure::invalid("first page rows must be 1..=255"));
}
state.begin_change();
state
.session
.set_first_page_rows(FirstPageRows::new(value as usize));
}
MSIME_ENGINE_OPTION_HELPCODE_SCHEMA | MSIME_ENGINE_OPTION_RESCORING_CONTEXT => {
return Err(Failure::invalid(format!("option {option} takes a string")));
}
_ => {
return Err(Failure::new(
MSIME_ENGINE_UNSUPPORTED,
format!("unknown option {option}"),
))
}
}
Ok(())
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_set_string(
session: *mut msime_engine_session,
option: u32,
value: *const c_char,
value_len: usize,
) -> i32 {
with_session(session, |state| {
let value = unsafe { input_str(value, value_len, "value") }?;
match option {
MSIME_ENGINE_OPTION_HELPCODE_SCHEMA => {
if !Session::is_supported_helpcode_schema(value) {
return Err(Failure::invalid(format!(
"unknown helpcode schema {value:?}"
)));
}
state.begin_change();
if !state.session.set_helpcode_schema(value) {
return Err(Failure::new(
crate::MSIME_ENGINE_FAILED,
format!("helpcode table {value:?} could not be loaded"),
));
}
}
MSIME_ENGINE_OPTION_RESCORING_CONTEXT => {
state.begin_change();
state.session.set_rescoring_context(value);
}
MSIME_ENGINE_OPTION_CHINESE_PUNCT
| MSIME_ENGINE_OPTION_PAIRED_PUNCT
| MSIME_ENGINE_OPTION_PUNCT_LOCK
| MSIME_ENGINE_OPTION_DEDICATED_ENGLISH
| MSIME_ENGINE_OPTION_HELPCODE_ENABLED
| MSIME_ENGINE_OPTION_PERSONAL_CONTEXT
| MSIME_ENGINE_OPTION_PREDICTION_ALLOWED
| MSIME_ENGINE_OPTION_FIRST_PAGE_ROWS => {
return Err(Failure::invalid(format!(
"option {option} takes an integer"
)));
}
_ => {
return Err(Failure::new(
MSIME_ENGINE_UNSUPPORTED,
format!("unknown option {option}"),
))
}
}
Ok(())
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_reset_context(session: *mut msime_engine_session) -> i32 {
with_session(session, |state| {
state.begin_change();
state.session.reset_context();
Ok(())
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_key_char(
session: *mut msime_engine_session,
character: u8,
flags: u32,
result: *mut msime_engine_result,
) -> i32 {
with_session(session, |state| {
if !(character == b'\t' || (0x20..=0x7e).contains(&character)) {
return Err(Failure::invalid(
"key_char takes one printable ASCII character or Tab",
));
}
if flags & !MSIME_ENGINE_KEY_SHIFT_ONLY != 0 {
return Err(Failure::new(
MSIME_ENGINE_UNSUPPORTED,
format!("unknown key flags {flags:#x}"),
));
}
unsafe { check_output(result, "result") }?;
state.begin_change();
let outcome = state
.session
.character(character, flags & MSIME_ENGINE_KEY_SHIFT_ONLY != 0);
unsafe { state.finish(outcome, result) }
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_key_punct(
session: *mut msime_engine_session,
character: u8,
result: *mut msime_engine_result,
) -> i32 {
with_session(session, |state| {
if !character.is_ascii_punctuation() {
return Err(Failure::invalid(
"key_punct takes one ASCII punctuation character",
));
}
unsafe { check_output(result, "result") }?;
state.begin_change();
let outcome = state.session.punctuation(character);
unsafe { state.finish(outcome, result) }
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_command(
session: *mut msime_engine_session,
command: u32,
result: *mut msime_engine_result,
) -> i32 {
with_session(session, |state| {
let command = u8::try_from(command)
.ok()
.and_then(Command::from_u8)
.ok_or_else(|| {
Failure::new(
MSIME_ENGINE_UNSUPPORTED,
format!("unknown command {command}"),
)
})?;
unsafe { check_output(result, "result") }?;
state.begin_change();
let outcome = state.session.command(command);
unsafe { state.finish(outcome, result) }
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_set_caret(
session: *mut msime_engine_session,
caret_byte: i64,
) -> i32 {
with_session(session, |state| {
let caret = usize::try_from(caret_byte).ok();
state.begin_change();
state.session.set_caret(caret);
Ok(())
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_state_get(
session: *mut msime_engine_session,
state: *mut msime_engine_state,
) -> i32 {
with_session(session, |inner| {
if state.is_null() {
return Err(Failure::null("state"));
}
let revision = inner.session.candidate_revision();
let takes_separator = inner.session.takes_local_separator();
let snapshot = inner.snapshot();
let mut flags = 0;
if snapshot.prediction {
flags |= MSIME_ENGINE_STATE_PREDICTION;
}
if snapshot.dedicated_english {
flags |= MSIME_ENGINE_STATE_DEDICATED_ENGLISH;
}
if snapshot.candidate_list_open {
flags |= MSIME_ENGINE_STATE_CANDIDATE_LIST_OPEN;
}
if takes_separator {
flags |= MSIME_ENGINE_STATE_TAKES_SEPARATOR;
}
let value = msime_engine_state {
struct_size: 0,
scheme: u32::from(snapshot.scheme as u8),
revision,
candidate_count: saturating_u32(snapshot.candidates.len()),
flags,
local_mode: local_mode_value(snapshot.local_mode),
caret_byte: saturating_u32(snapshot.caret_position),
conversion_focus_start_scalar: saturating_u32(snapshot.conversion_focus.0),
conversion_focus_end_scalar: saturating_u32(snapshot.conversion_focus.1),
preedit: msime_engine_str::borrow(&snapshot.preedit),
editing_text: msime_engine_str::borrow(&snapshot.editing_text),
conversion: msime_engine_str::borrow(&snapshot.conversion),
spelling_symbols: msime_engine_str::borrow(&snapshot.spelling_symbols),
};
unsafe { write_sized(state, value, "state") }
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_candidate_get(
session: *mut msime_engine_session,
revision: u64,
index: u32,
candidate: *mut msime_engine_candidate,
) -> i32 {
with_session(session, |state| {
if candidate.is_null() {
return Err(Failure::null("candidate"));
}
state.check_revision(revision)?;
let snapshot = state.snapshot();
let row = index as usize;
let Some(item) = snapshot.candidates.get(row) else {
return Err(Failure::invalid(format!(
"candidate {index} is out of range ({} candidates)",
snapshot.candidates.len()
)));
};
let mut flags = 0;
if item.fuzzy {
flags |= MSIME_ENGINE_CANDIDATE_FUZZY;
}
if snapshot
.candidate_answers_key
.get(row)
.copied()
.unwrap_or(false)
{
flags |= MSIME_ENGINE_CANDIDATE_ANSWERS_KEY;
}
if item.sentence_association {
flags |= MSIME_ENGINE_CANDIDATE_SENTENCE;
}
let value = msime_engine_candidate {
struct_size: 0,
index,
source: u32::from(item.source as u8),
scheme: u32::from(item.scheme as u8),
flags,
fixed_position: item.fixed_position,
word: msime_engine_str::borrow(&item.word),
code: msime_engine_str::borrow(&item.pinyin),
annotation: msime_engine_str::borrow(
snapshot
.candidate_annotations
.get(row)
.map_or("", String::as_str),
),
corrected_from: msime_engine_str::borrow(&item.corrected_from),
};
unsafe { write_sized(candidate, value, "candidate") }
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_expand_candidates(
session: *mut msime_engine_session,
grew: *mut u32,
) -> i32 {
with_session(session, |state| {
state.begin_change();
let expanded = state.session.expand_initial_candidates();
if !grew.is_null() {
unsafe { grew.write(u32::from(expanded)) };
}
Ok(())
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_state_json(
session: *mut msime_engine_session,
json: *mut *mut c_char,
) -> i32 {
with_session(session, |state| {
if json.is_null() {
return Err(Failure::null("json"));
}
let revision = state.session.candidate_revision();
let text = debug_json(revision, state.snapshot()).to_string();
unsafe { write_owned(json, text) };
Ok(())
})
}
fn debug_json(revision: u64, snapshot: &SessionSnapshot) -> Value {
let candidates: Vec<Value> = snapshot
.candidates
.iter()
.enumerate()
.map(|(index, item)| {
json!({
"word": item.word.as_str(),
"code": item.pinyin.as_str(),
"canonical_code": item.canonical_pinyin.as_str(),
"source": item.source as u8,
"scheme": item.scheme.name(),
"weight": item.weight,
"fixed_position": item.fixed_position,
"fuzzy": item.fuzzy,
"corrected_from": item.corrected_from.as_str(),
"sentence": item.sentence_association,
"annotation": snapshot.candidate_annotations.get(index).map_or("", String::as_str),
"answers_key": snapshot.candidate_answers_key.get(index).copied().unwrap_or(false),
})
})
.collect();
json!({
"revision": revision,
"scheme": snapshot.scheme.name(),
"local_mode": snapshot.local_mode.name(),
"prediction": snapshot.prediction,
"dedicated_english": snapshot.dedicated_english,
"preedit": snapshot.preedit,
"editing_text": snapshot.editing_text,
"caret_byte": snapshot.caret_position,
"raw_segmentation": snapshot.raw_segmentation,
"normalized_segmentation": snapshot.normalized_segmentation,
"spelling_symbols": snapshot.spelling_symbols,
"shuangpin_profile": snapshot.shuangpin_profile,
"conversion": snapshot.conversion,
"conversion_focus_scalar": [snapshot.conversion_focus.0, snapshot.conversion_focus.1],
"candidate_list_open": snapshot.candidate_list_open,
"nine_key_spellings": snapshot.nine_key_spellings,
"candidates": candidates,
})
}
fn with_candidate(
session: *mut msime_engine_session,
revision: u64,
result: *mut msime_engine_result,
action: impl FnOnce(&mut Session) -> KeyResult,
) -> i32 {
with_session(session, |state| {
state.check_revision(revision)?;
unsafe { check_output(result, "result") }?;
state.begin_change();
let outcome = action(&mut state.session);
unsafe { state.finish(outcome, result) }
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_select(
session: *mut msime_engine_session,
revision: u64,
index: u32,
result: *mut msime_engine_result,
) -> i32 {
with_candidate(session, revision, result, |engine| {
engine.select(self::index(index))
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_select_edge(
session: *mut msime_engine_session,
revision: u64,
index: u32,
edge: u32,
result: *mut msime_engine_result,
) -> i32 {
let edge = match edge {
MSIME_ENGINE_EDGE_FIRST_HAN => CandidateEdge::FirstHan,
MSIME_ENGINE_EDGE_LAST_HAN => CandidateEdge::LastHan,
_ => {
return guard(|| Err(Failure::invalid(format!("unknown edge {edge}"))));
}
};
with_candidate(session, revision, result, |engine| {
engine.select_edge(self::index(index), edge)
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_finish(
session: *mut msime_engine_session,
revision: u64,
first_index: u32,
result: *mut msime_engine_result,
) -> i32 {
with_candidate(session, revision, result, |engine| {
engine.finish(self::index(first_index))
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_pin(
session: *mut msime_engine_session,
revision: u64,
index: u32,
result: *mut msime_engine_result,
) -> i32 {
with_candidate(session, revision, result, |engine| {
engine.pin(self::index(index))
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_remove(
session: *mut msime_engine_session,
revision: u64,
index: u32,
result: *mut msime_engine_result,
) -> i32 {
with_candidate(session, revision, result, |engine| {
engine.remove(self::index(index))
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_fix_position(
session: *mut msime_engine_session,
revision: u64,
index: u32,
position: i32,
result: *mut msime_engine_result,
) -> i32 {
with_candidate(session, revision, result, |engine| {
engine.fix_position(self::index(index), position)
})
}
#[no_mangle]
pub unsafe extern "C" fn msime_engine_clear_position(
session: *mut msime_engine_session,
revision: u64,
index: u32,
result: *mut msime_engine_result,
) -> i32 {
with_candidate(session, revision, result, |engine| {
engine.clear_position(self::index(index))
})
}
#[cfg(test)]
pub(crate) fn lock_for_test(
session: *mut msime_engine_session,
) -> std::sync::MutexGuard<'static, impl Sized> {
unsafe { &*session }.state.lock().expect("session lock")
}
#[cfg(test)]
pub(crate) fn run_for_test(
session: *mut msime_engine_session,
body: impl FnOnce() -> Outcome,
) -> i32 {
with_session(session, |_| body())
}