1use std::cell::Cell;
4use std::ffi::{c_char, c_void, CString};
5use std::path::{Path, PathBuf};
6use std::sync::{Arc, PoisonError, RwLock};
7
8use msime_engine::{
9 PersonalDictionaryEntry, PersonalDictionaryKind, RuntimePaths, SchemeSet, SchemeType,
10};
11use serde::{Deserialize, Serialize};
12
13use crate::abi::{
14 field_str, guard, input_str, msime_engine_paths, read_sized, write_owned, Failure, Outcome,
15};
16use crate::{
17 MSIME_ENGINE_ABI_MAJOR, MSIME_ENGINE_ABI_MINOR, MSIME_ENGINE_BUSY,
18 MSIME_ENGINE_FLUSH_STOP_THREAD, MSIME_ENGINE_MODEL_JAPANESE, MSIME_ENGINE_UNSUPPORTED,
19 MSIME_ENGINE_USER_DICT_INCLUDE_LEARNED,
20};
21
22#[no_mangle]
26pub extern "C" fn msime_engine_abi_version() -> u32 {
27 (MSIME_ENGINE_ABI_MAJOR << 16) | MSIME_ENGINE_ABI_MINOR
28}
29
30#[no_mangle]
32pub extern "C" fn msime_engine_version() -> *const c_char {
33 concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr().cast()
34}
35
36#[no_mangle]
38pub extern "C" fn msime_engine_last_error() -> *const c_char {
39 crate::abi::last_error()
40}
41
42#[no_mangle]
47pub unsafe extern "C" fn msime_engine_string_free(text: *mut c_char) {
48 if !text.is_null() {
49 drop(unsafe { CString::from_raw(text) });
51 }
52}
53
54pub type msime_engine_log_callback = Option<
58 unsafe extern "C" fn(user_data: *mut c_void, message: *const c_char, message_len: usize),
59>;
60
61struct LogTarget {
63 callback: unsafe extern "C" fn(*mut c_void, *const c_char, usize),
64 user_data: *mut c_void,
65}
66
67unsafe impl Send for LogTarget {}
69unsafe impl Sync for LogTarget {}
71
72static LOG_TARGET: RwLock<Option<LogTarget>> = RwLock::new(None);
74
75thread_local! {
76 static IN_LOG_CALLBACK: Cell<bool> = const { Cell::new(false) };
78}
79
80#[allow(clippy::print_stderr)]
82fn deliver_log_line(line: &str) {
83 if IN_LOG_CALLBACK.get() {
84 return;
85 }
86 let target = LOG_TARGET.read().unwrap_or_else(PoisonError::into_inner);
87 let Some(target) = target.as_ref() else {
88 eprintln!("msime: {line}");
90 return;
91 };
92 let message = CString::new(line.replace('\0', "\u{fffd}")).unwrap_or_default();
93 IN_LOG_CALLBACK.set(true);
94 unsafe { (target.callback)(target.user_data, message.as_ptr(), message.as_bytes().len()) };
96 IN_LOG_CALLBACK.set(false);
97}
98
99#[no_mangle]
108pub unsafe extern "C" fn msime_engine_set_log_callback(
109 callback: msime_engine_log_callback,
110 user_data: *mut c_void,
111) -> i32 {
112 guard(|| {
113 if IN_LOG_CALLBACK.get() {
114 return Err(Failure::new(
115 MSIME_ENGINE_BUSY,
116 "the log callback cannot be replaced from inside the log callback",
117 ));
118 }
119 let mut target = LOG_TARGET.write().unwrap_or_else(PoisonError::into_inner);
120 match callback {
121 Some(callback) => {
122 *target = Some(LogTarget {
123 callback,
124 user_data,
125 });
126 msime_engine::set_log_sink(Some(Arc::new(deliver_log_line)));
127 }
128 None => {
129 *target = None;
130 msime_engine::set_log_sink(None);
131 }
132 }
133 Ok(())
134 })
135}
136
137unsafe fn roots(paths: &msime_engine_paths) -> Outcome<[PathBuf; 3]> {
144 unsafe {
146 Ok([
147 PathBuf::from(field_str(paths.resources, "paths.resources")?),
148 PathBuf::from(field_str(paths.user_data, "paths.user_data")?),
149 PathBuf::from(field_str(paths.cache, "paths.cache")?),
150 ])
151 }
152}
153
154unsafe fn runtime_paths(paths: *const msime_engine_paths) -> Outcome<RuntimePaths> {
159 let paths = unsafe { read_sized(paths, "paths") }?;
161 let [resources, user_data, cache] = unsafe { roots(&paths) }?;
163 let dictionaries = unsafe { field_str(paths.dictionaries, "paths.dictionaries") }?;
165 Ok(RuntimePaths {
166 resources,
167 user_data,
168 cache,
169 dictionaries: PathBuf::from(dictionaries),
170 })
171}
172
173fn utf8_path(path: &Path) -> Outcome<String> {
174 path.to_str()
175 .map(str::to_owned)
176 .ok_or_else(|| Failure::invalid("the path is not UTF-8"))
177}
178
179#[no_mangle]
184pub unsafe extern "C" fn msime_engine_prepare_runtime(
185 paths: *const msime_engine_paths,
186 content_id: *const c_char,
187 content_id_len: usize,
188 dictionaries: *mut *mut c_char,
189) -> i32 {
190 guard(|| unsafe {
192 prepare(
193 paths,
194 content_id,
195 content_id_len,
196 SchemeSet::ALL,
197 dictionaries,
198 )
199 })
200}
201
202#[no_mangle]
207pub unsafe extern "C" fn msime_engine_prepare_runtime_for(
208 paths: *const msime_engine_paths,
209 content_id: *const c_char,
210 content_id_len: usize,
211 schemes: u32,
212 dictionaries: *mut *mut c_char,
213) -> i32 {
214 guard(|| {
215 let schemes = scheme_set(schemes)?;
216 unsafe { prepare(paths, content_id, content_id_len, schemes, dictionaries) }
218 })
219}
220
221unsafe fn prepare(
226 paths: *const msime_engine_paths,
227 content_id: *const c_char,
228 content_id_len: usize,
229 schemes: SchemeSet,
230 dictionaries: *mut *mut c_char,
231) -> Outcome<()> {
232 let paths = unsafe { read_sized(paths, "paths") }?;
234 let [resources, user_data, cache] = unsafe { roots(&paths) }?;
236 let content_id = unsafe { input_str(content_id, content_id_len, "content_id") }?;
238 let prepared = msime_engine::prepare_runtime_paths_for(
239 &resources, &user_data, &cache, content_id, schemes,
240 )?;
241 let directory = utf8_path(&prepared.dictionaries)?;
242 unsafe { write_owned(dictionaries, directory) };
244 Ok(())
245}
246
247fn scheme_set(schemes: u32) -> Outcome<SchemeSet> {
249 if schemes == 0 {
250 return Err(Failure::invalid("schemes must name at least one scheme"));
251 }
252 (0..u32::BITS)
253 .filter(|bit| schemes & (1 << bit) != 0)
254 .try_fold(SchemeSet::EMPTY, |set, bit| {
255 u8::try_from(bit)
256 .ok()
257 .and_then(SchemeType::from_u8)
258 .map(|scheme| set.with(scheme))
259 .ok_or_else(|| Failure::invalid(format!("schemes names no scheme at bit {bit}")))
260 })
261}
262
263#[no_mangle]
265pub extern "C" fn msime_engine_flush_learning(flags: u32) -> i32 {
266 guard(|| {
267 if flags & !MSIME_ENGINE_FLUSH_STOP_THREAD != 0 {
268 return Err(Failure::new(
269 MSIME_ENGINE_UNSUPPORTED,
270 format!("unknown flush flags {flags:#x}"),
271 ));
272 }
273 if flags & MSIME_ENGINE_FLUSH_STOP_THREAD != 0 {
274 msime_engine::shutdown();
275 } else {
276 msime_engine::flush_personal_learning();
277 }
278 Ok(())
279 })
280}
281
282#[no_mangle]
284pub extern "C" fn msime_engine_release_caches() -> i32 {
285 guard(|| {
286 msime_engine::release_japanese_dictionary_cache();
287 msime_engine::release_idle_personal_models();
288 Ok(())
289 })
290}
291
292#[no_mangle]
294pub extern "C" fn msime_engine_close_databases() -> i32 {
295 guard(|| {
296 msime_engine::close_cached_databases();
297 Ok(())
298 })
299}
300
301#[no_mangle]
306pub unsafe extern "C" fn msime_engine_preload_model(
307 kind: u32,
308 path: *const c_char,
309 path_len: usize,
310 bytes: *const u8,
311 bytes_len: usize,
312) -> i32 {
313 guard(|| {
314 if kind != MSIME_ENGINE_MODEL_JAPANESE {
315 return Err(Failure::new(
316 MSIME_ENGINE_UNSUPPORTED,
317 format!("unknown model kind {kind}"),
318 ));
319 }
320 let path = Path::new(unsafe { input_str(path, path_len, "path") }?);
322 if !path.is_absolute() {
323 return Err(Failure::invalid("the model path must be absolute"));
324 }
325 if bytes.is_null() {
326 if bytes_len != 0 {
327 return Err(Failure::null("bytes"));
328 }
329 msime_engine::unload_japanese_dictionary(path);
330 return Ok(());
331 }
332 let copy: Box<[u8]> = unsafe { std::slice::from_raw_parts(bytes, bytes_len) }.into();
334 if !msime_engine::preload_japanese_dictionary(path, copy) {
335 return Err(Failure::invalid("the bytes are not a valid model"));
336 }
337 Ok(())
338 })
339}
340
341#[derive(Serialize, Deserialize)]
345#[serde(deny_unknown_fields)]
346struct EntryJson {
347 kind: KindJson,
348 key: String,
349 value: String,
350 #[serde(default = "default_weight")]
351 weight: i64,
352}
353
354fn default_weight() -> i64 {
355 PersonalDictionaryEntry::DEFAULT_WEIGHT
356}
357
358#[derive(Serialize, Deserialize, Clone, Copy)]
359#[serde(rename_all = "snake_case")]
360enum KindJson {
361 Pinyin,
362 Wubi,
363 Wubi98,
364 QuickPhrase,
365 English,
366}
367
368impl KindJson {
369 fn of(kind: PersonalDictionaryKind) -> Option<Self> {
371 Some(match kind {
372 PersonalDictionaryKind::Pinyin => Self::Pinyin,
373 PersonalDictionaryKind::Wubi => Self::Wubi,
374 PersonalDictionaryKind::Wubi98 => Self::Wubi98,
375 PersonalDictionaryKind::QuickPhrase => Self::QuickPhrase,
376 PersonalDictionaryKind::English => Self::English,
377 _ => return None,
378 })
379 }
380}
381
382impl From<KindJson> for PersonalDictionaryKind {
383 fn from(kind: KindJson) -> Self {
384 match kind {
385 KindJson::Pinyin => Self::Pinyin,
386 KindJson::Wubi => Self::Wubi,
387 KindJson::Wubi98 => Self::Wubi98,
388 KindJson::QuickPhrase => Self::QuickPhrase,
389 KindJson::English => Self::English,
390 }
391 }
392}
393
394impl From<EntryJson> for PersonalDictionaryEntry {
395 fn from(entry: EntryJson) -> Self {
396 Self {
397 kind: entry.kind.into(),
398 key: entry.key,
399 value: entry.value,
400 weight: entry.weight,
401 }
402 }
403}
404
405#[derive(Deserialize)]
406#[serde(deny_unknown_fields)]
407struct EditJson {
408 previous: Option<EntryJson>,
409 replacement: Option<EntryJson>,
410 #[serde(default)]
411 request_id: String,
412}
413
414#[no_mangle]
419pub unsafe extern "C" fn msime_engine_user_dict_list(
420 paths: *const msime_engine_paths,
421 offset: u32,
422 limit: u32,
423 flags: u32,
424 json: *mut *mut c_char,
425) -> i32 {
426 guard(|| {
427 if json.is_null() {
428 return Err(Failure::null("json"));
429 }
430 if flags & !MSIME_ENGINE_USER_DICT_INCLUDE_LEARNED != 0 {
431 return Err(Failure::new(
432 MSIME_ENGINE_UNSUPPORTED,
433 format!("unknown list flags {flags:#x}"),
434 ));
435 }
436 let paths = unsafe { runtime_paths(paths) }?;
438 let limit = if limit == 0 { 100 } else { limit as usize };
439 let page = msime_engine::personal_dictionary_entries(
440 &paths,
441 offset as usize,
442 limit,
443 flags & MSIME_ENGINE_USER_DICT_INCLUDE_LEARNED != 0,
444 )?;
445 let entries: Vec<EntryJson> = page
446 .entries
447 .into_iter()
448 .filter_map(|entry| {
449 Some(EntryJson {
450 kind: KindJson::of(entry.kind)?,
451 key: entry.key,
452 value: entry.value,
453 weight: entry.weight,
454 })
455 })
456 .collect();
457 let text = serde_json::json!({ "entries": entries, "has_more": page.has_more }).to_string();
458 unsafe { write_owned(json, text) };
460 Ok(())
461 })
462}
463
464#[no_mangle]
469pub unsafe extern "C" fn msime_engine_user_dict_validate(
470 entry_json: *const c_char,
471 entry_len: usize,
472 normalized_json: *mut *mut c_char,
473) -> i32 {
474 guard(|| {
475 if normalized_json.is_null() {
476 return Err(Failure::null("normalized_json"));
477 }
478 let text = unsafe { input_str(entry_json, entry_len, "entry_json") }?;
480 let entry: EntryJson = serde_json::from_str(text)
481 .map_err(|error| Failure::invalid(format!("entry: {error}")))?;
482 let normalized = msime_engine::validate_personal_dictionary_entry(
483 &PersonalDictionaryEntry::from(entry),
484 )?;
485 let kind = KindJson::of(normalized.kind)
486 .ok_or_else(|| Failure::invalid("the entry has a kind this ABI cannot name"))?;
487 let text = serde_json::to_string(&EntryJson {
488 kind,
489 key: normalized.key,
490 value: normalized.value,
491 weight: normalized.weight,
492 })
493 .map_err(|error| Failure::invalid(format!("entry: {error}")))?;
494 unsafe { write_owned(normalized_json, text) };
496 Ok(())
497 })
498}
499
500#[no_mangle]
505pub unsafe extern "C" fn msime_engine_user_dict_edit(
506 paths: *const msime_engine_paths,
507 edit_json: *const c_char,
508 edit_len: usize,
509) -> i32 {
510 guard(|| {
511 let paths = unsafe { runtime_paths(paths) }?;
513 let text = unsafe { input_str(edit_json, edit_len, "edit_json") }?;
515 let edit: EditJson = serde_json::from_str(text)
516 .map_err(|error| Failure::invalid(format!("edit: {error}")))?;
517 let previous = edit.previous.map(PersonalDictionaryEntry::from);
518 let replacement = edit.replacement.map(PersonalDictionaryEntry::from);
519 msime_engine::edit_personal_dictionary(
520 &paths,
521 previous.as_ref(),
522 replacement.as_ref(),
523 &edit.request_id,
524 )?;
525 Ok(())
526 })
527}
528
529#[no_mangle]
534pub unsafe extern "C" fn msime_engine_reset_learning(paths: *const msime_engine_paths) -> i32 {
535 guard(|| {
536 let paths = unsafe { runtime_paths(paths) }?;
538 msime_engine::reset_learned_data(&paths)?;
539 Ok(())
540 })
541}
542
543#[no_mangle]
548pub unsafe extern "C" fn msime_engine_reset_learning_for(
549 paths: *const msime_engine_paths,
550 schemes: u32,
551) -> i32 {
552 guard(|| {
553 let schemes = scheme_set(schemes)?;
554 let paths = unsafe { runtime_paths(paths) }?;
556 msime_engine::reset_learned_data_for(&paths, schemes)?;
557 Ok(())
558 })
559}