1use std::ffi::CStr;
2use std::path::{Path, PathBuf};
3
4use crate::error::{check_error, to_cstring, Error, ErrorCode, Result};
5use crate::types::LogLevel;
6
7pub struct ConfigBuilder {
23 pub memory_limit: u64,
25 pub num_threads: u32,
27 pub enable_console_log: bool,
29 pub fts_brute_force_by_keys_ratio: Option<f32>,
31 pub jieba_dict_dir: Option<String>,
34}
35
36impl ConfigBuilder {
37 pub fn new() -> Self {
39 ConfigBuilder {
40 memory_limit: 0,
41 num_threads: 0,
42 enable_console_log: false,
43 fts_brute_force_by_keys_ratio: None,
44 jieba_dict_dir: None,
45 }
46 }
47
48 pub fn memory_limit(mut self, bytes: u64) -> Self {
50 self.memory_limit = bytes;
51 self
52 }
53
54 pub fn num_threads(mut self, count: u32) -> Self {
56 self.num_threads = count;
57 self
58 }
59
60 pub fn enable_console_log(mut self, enable: bool) -> Self {
62 self.enable_console_log = enable;
63 self
64 }
65
66 pub fn fts_brute_force_by_keys_ratio(mut self, ratio: f32) -> Self {
68 self.fts_brute_force_by_keys_ratio = Some(ratio);
69 self
70 }
71
72 pub fn jieba_dict_dir(mut self, dir: impl Into<String>) -> Self {
80 self.jieba_dict_dir = Some(dir.into());
81 self
82 }
83
84 pub fn build(self) -> Self {
90 self
91 }
92}
93
94impl Default for ConfigBuilder {
95 fn default() -> Self {
96 Self::new()
97 }
98}
99
100pub(crate) struct ConfigData {
102 pub(crate) handle: *mut zvec_rust_sys::zvec_config_data_t,
103}
104
105impl ConfigData {
106 pub(crate) fn new() -> Result<Self> {
107 let handle = unsafe { zvec_rust_sys::zvec_config_data_create() };
108 if handle.is_null() {
109 return Err(Error {
110 code: ErrorCode::InternalError,
111 message: "failed to create config data".into(),
112 });
113 }
114 Ok(ConfigData { handle })
115 }
116
117 pub(crate) fn set_memory_limit(&mut self, bytes: u64) -> Result<()> {
118 check_error(unsafe { zvec_rust_sys::zvec_config_data_set_memory_limit(self.handle, bytes) })
119 }
120
121 pub(crate) fn set_query_thread_count(&mut self, count: u32) -> Result<()> {
122 check_error(unsafe {
123 zvec_rust_sys::zvec_config_data_set_query_thread_count(self.handle, count)
124 })
125 }
126
127 pub(crate) fn set_optimize_thread_count(&mut self, count: u32) -> Result<()> {
128 check_error(unsafe {
129 zvec_rust_sys::zvec_config_data_set_optimize_thread_count(self.handle, count)
130 })
131 }
132
133 pub(crate) fn set_fts_brute_force_by_keys_ratio(&mut self, ratio: f32) -> Result<()> {
134 check_error(unsafe {
135 zvec_rust_sys::zvec_config_data_set_fts_brute_force_by_keys_ratio(self.handle, ratio)
136 })
137 }
138
139 pub(crate) fn set_jieba_dict_dir(&mut self, dir: &str) -> Result<()> {
140 let c_dir = to_cstring(dir)?;
141 check_error(unsafe {
142 zvec_rust_sys::zvec_config_data_set_jieba_dict_dir(self.handle, c_dir.as_ptr())
143 })
144 }
145
146 pub(crate) fn set_console_log(&mut self, level: LogLevel) -> Result<()> {
147 let log_config = unsafe { zvec_rust_sys::zvec_config_log_create_console(level as u32) };
148 if log_config.is_null() {
149 return Err(Error {
150 code: ErrorCode::InternalError,
151 message: "failed to create console log config".into(),
152 });
153 }
154 let result = check_error(unsafe {
157 zvec_rust_sys::zvec_config_data_set_log_config(self.handle, log_config)
158 });
159 if result.is_err() {
160 unsafe { zvec_rust_sys::zvec_config_log_destroy(log_config) };
161 }
162 result
163 }
164}
165
166impl Drop for ConfigData {
167 fn drop(&mut self) {
168 if !self.handle.is_null() {
169 unsafe { zvec_rust_sys::zvec_config_data_destroy(self.handle) };
171 }
172 }
173}
174
175pub fn initialize(config: Option<&ConfigBuilder>) -> Result<()> {
197 match config {
198 None => {
199 auto_register_default_jieba_dict_dir();
200 check_error(unsafe { zvec_rust_sys::zvec_initialize(std::ptr::null()) })
201 }
202 Some(builder) => {
203 let mut cfg = ConfigData::new()?;
204 if builder.memory_limit > 0 {
205 cfg.set_memory_limit(builder.memory_limit)?;
206 }
207 if builder.num_threads > 0 {
208 cfg.set_query_thread_count(builder.num_threads)?;
209 cfg.set_optimize_thread_count(builder.num_threads)?;
210 }
211 if builder.enable_console_log {
212 cfg.set_console_log(LogLevel::Info)?;
213 }
214 if let Some(ratio) = builder.fts_brute_force_by_keys_ratio {
215 cfg.set_fts_brute_force_by_keys_ratio(ratio)?;
216 }
217 if let Some(ref dir) = builder.jieba_dict_dir {
218 cfg.set_jieba_dict_dir(dir)?;
219 } else {
220 auto_register_default_jieba_dict_dir();
221 }
222 check_error(unsafe { zvec_rust_sys::zvec_initialize(cfg.handle as *const _) })
223 }
224 }
225}
226
227pub fn set_default_jieba_dict_dir(dir: &str) -> Result<()> {
238 let c_dir = to_cstring(dir)?;
239 unsafe { zvec_rust_sys::zvec_set_default_jieba_dict_dir(c_dir.as_ptr()) };
240 Ok(())
241}
242
243pub fn get_default_jieba_dict_dir() -> String {
245 unsafe {
246 let ptr = zvec_rust_sys::zvec_get_default_jieba_dict_dir();
247 if ptr.is_null() {
248 return String::new();
249 }
250 CStr::from_ptr(ptr).to_string_lossy().into_owned()
251 }
252}
253
254fn auto_register_default_jieba_dict_dir() {
260 if !get_default_jieba_dict_dir().is_empty() {
261 return;
262 }
263 if let Some(dir) = discover_jieba_dict_dir() {
264 let dir = dir.to_string_lossy().into_owned();
265 let _ = set_default_jieba_dict_dir(&dir);
266 }
267}
268
269fn discover_jieba_dict_dir() -> Option<PathBuf> {
272 let mut candidates: Vec<PathBuf> = Vec::new();
273
274 if let Some(build_time_dir) = zvec_rust_sys::build_time_jieba_dict_dir() {
277 candidates.push(PathBuf::from(build_time_dir));
278 }
279
280 if let Some(lib_dir) = loaded_library_dir() {
284 candidates.push(lib_dir.join("data").join("jieba_dict"));
285 candidates.push(lib_dir.join("..").join("data").join("jieba_dict"));
286 }
287
288 candidates.into_iter().find(|dir| is_jieba_dict_dir(dir))
289}
290
291fn is_jieba_dict_dir(dir: &Path) -> bool {
292 dir.join("jieba.dict.utf8").is_file() && dir.join("hmm_model.utf8").is_file()
293}
294
295#[cfg(unix)]
297fn loaded_library_dir() -> Option<PathBuf> {
298 #[repr(C)]
299 struct DlInfo {
300 dli_fname: *const std::ffi::c_char,
301 dli_fbase: *mut std::ffi::c_void,
302 dli_sname: *const std::ffi::c_char,
303 dli_saddr: *mut std::ffi::c_void,
304 }
305 extern "C" {
306 fn dladdr(addr: *const std::ffi::c_void, info: *mut DlInfo) -> std::ffi::c_int;
307 }
308
309 let symbol =
314 zvec_rust_sys::zvec_get_default_jieba_dict_dir as *const () as *const std::ffi::c_void;
315
316 let mut info = DlInfo {
317 dli_fname: std::ptr::null(),
318 dli_fbase: std::ptr::null_mut(),
319 dli_sname: std::ptr::null(),
320 dli_saddr: std::ptr::null_mut(),
321 };
322 let path = unsafe {
323 if dladdr(symbol, &mut info) == 0 || info.dli_fname.is_null() {
324 return None;
325 }
326 PathBuf::from(CStr::from_ptr(info.dli_fname).to_string_lossy().as_ref())
327 };
328 let resolved = std::fs::canonicalize(&path).unwrap_or(path);
329 resolved.parent().map(PathBuf::from)
330}
331
332#[cfg(windows)]
334fn loaded_library_dir() -> Option<PathBuf> {
335 use std::os::windows::ffi::OsStringExt;
336
337 type HModule = *mut std::ffi::c_void;
338 const FROM_ADDRESS: u32 = 0x0000_0004;
339 const UNCHANGED_REFCOUNT: u32 = 0x0000_0002;
340
341 #[link(name = "kernel32")]
342 extern "system" {
343 fn GetModuleHandleExW(
344 flags: u32,
345 module_name: *mut std::ffi::c_void,
346 module: *mut HModule,
347 ) -> i32;
348 fn GetModuleFileNameW(module: HModule, filename: *mut u16, size: u32) -> u32;
349 }
350
351 let symbol =
352 zvec_rust_sys::zvec_get_default_jieba_dict_dir as *const () as *mut std::ffi::c_void;
353 unsafe {
354 let mut module: HModule = std::ptr::null_mut();
355 if GetModuleHandleExW(FROM_ADDRESS | UNCHANGED_REFCOUNT, symbol, &mut module) == 0
356 || module.is_null()
357 {
358 return None;
359 }
360 let mut size = 260u32; loop {
362 let mut buf = vec![0u16; size as usize];
363 let len = GetModuleFileNameW(module, buf.as_mut_ptr(), size);
364 if len == 0 {
365 return None;
366 }
367 if len < size {
368 buf.truncate(len as usize);
369 return PathBuf::from(std::ffi::OsString::from_wide(&buf))
370 .parent()
371 .map(PathBuf::from);
372 }
373 size = size.checked_mul(2)?; }
375 }
376}
377
378#[cfg(not(any(unix, windows)))]
379fn loaded_library_dir() -> Option<PathBuf> {
380 None
381}
382
383#[doc(hidden)]
385pub fn shutdown() -> Result<()> {
386 check_error(unsafe { zvec_rust_sys::zvec_shutdown() })
387}
388
389pub fn is_initialized() -> bool {
391 unsafe { zvec_rust_sys::zvec_is_initialized() }
392}
393
394pub fn version() -> String {
396 unsafe {
397 let ptr = zvec_rust_sys::zvec_get_version();
398 if ptr.is_null() {
399 return String::new();
400 }
401 CStr::from_ptr(ptr).to_string_lossy().into_owned()
402 }
403}
404
405pub fn check_version(major: i32, minor: i32, patch: i32) -> bool {
407 unsafe { zvec_rust_sys::zvec_check_version(major, minor, patch) }
408}
409
410pub fn version_major() -> i32 {
412 unsafe { zvec_rust_sys::zvec_get_version_major() }
413}
414
415pub fn version_minor() -> i32 {
417 unsafe { zvec_rust_sys::zvec_get_version_minor() }
418}
419
420pub fn version_patch() -> i32 {
422 unsafe { zvec_rust_sys::zvec_get_version_patch() }
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 #[test]
430 fn config_builder_defaults() {
431 let builder = ConfigBuilder::new();
432 assert_eq!(builder.memory_limit, 0);
433 assert_eq!(builder.num_threads, 0);
434 assert!(!builder.enable_console_log);
435 assert!(builder.jieba_dict_dir.is_none());
436 }
437
438 #[test]
439 fn config_builder_chaining() {
440 let builder = ConfigBuilder::new()
441 .memory_limit(1024)
442 .num_threads(4)
443 .enable_console_log(true)
444 .build();
445 assert_eq!(builder.memory_limit, 1024);
446 assert_eq!(builder.num_threads, 4);
447 assert!(builder.enable_console_log);
448 }
449
450 #[test]
451 fn config_builder_default_trait() {
452 let builder = ConfigBuilder::default();
453 assert_eq!(builder.memory_limit, 0);
454 assert_eq!(builder.num_threads, 0);
455 assert!(!builder.enable_console_log);
456 }
457
458 #[test]
459 fn config_builder_memory_limit_setter() {
460 let builder = ConfigBuilder::new().memory_limit(2048);
461 assert_eq!(builder.memory_limit, 2048);
462 }
463
464 #[test]
465 fn config_builder_num_threads_setter() {
466 let builder = ConfigBuilder::new().num_threads(8);
467 assert_eq!(builder.num_threads, 8);
468 }
469
470 #[test]
471 fn config_builder_enable_console_log_setter() {
472 let builder = ConfigBuilder::new().enable_console_log(true);
473 assert!(builder.enable_console_log);
474 }
475
476 #[test]
477 fn config_builder_jieba_dict_dir_setter() {
478 let builder = ConfigBuilder::new().jieba_dict_dir("/tmp/jieba_dict");
479 assert_eq!(builder.jieba_dict_dir.as_deref(), Some("/tmp/jieba_dict"));
480 }
481
482 #[test]
483 fn default_jieba_dict_dir_roundtrip() {
484 let previous = get_default_jieba_dict_dir();
485 set_default_jieba_dict_dir("/tmp/zvec_rust_jieba_roundtrip").unwrap();
486 assert_eq!(
487 get_default_jieba_dict_dir(),
488 "/tmp/zvec_rust_jieba_roundtrip"
489 );
490 set_default_jieba_dict_dir("").unwrap();
493 assert_eq!(get_default_jieba_dict_dir(), "");
494 set_default_jieba_dict_dir(&previous).unwrap();
495 assert_eq!(get_default_jieba_dict_dir(), previous);
496 }
497
498 #[test]
499 fn discovered_jieba_dict_dir_contains_dict_files() {
500 if let Some(dir) = discover_jieba_dict_dir() {
503 assert!(is_jieba_dict_dir(&dir), "invalid dict dir: {:?}", dir);
504 }
505 }
506
507 #[test]
508 fn config_builder_build_returns_self() {
509 let builder = ConfigBuilder::new()
510 .memory_limit(4096)
511 .num_threads(2)
512 .enable_console_log(true)
513 .build();
514 assert_eq!(builder.memory_limit, 4096);
515 assert_eq!(builder.num_threads, 2);
516 assert!(builder.enable_console_log);
517 }
518
519 #[test]
520 fn config_builder_overwrite_values() {
521 let builder = ConfigBuilder::new()
522 .memory_limit(1024)
523 .memory_limit(2048)
524 .num_threads(4)
525 .num_threads(8)
526 .enable_console_log(false)
527 .enable_console_log(true);
528 assert_eq!(builder.memory_limit, 2048);
529 assert_eq!(builder.num_threads, 8);
530 assert!(builder.enable_console_log);
531 }
532
533 #[test]
534 fn config_builder_zero_values() {
535 let builder = ConfigBuilder::new().memory_limit(0).num_threads(0);
536 assert_eq!(builder.memory_limit, 0);
537 assert_eq!(builder.num_threads, 0);
538 }
539
540 #[test]
541 fn config_builder_large_memory_limit() {
542 let builder = ConfigBuilder::new().memory_limit(u64::MAX);
543 assert_eq!(builder.memory_limit, u64::MAX);
544 }
545
546 #[test]
547 fn config_builder_fts_ratio() {
548 let builder = ConfigBuilder::new();
549 assert_eq!(builder.fts_brute_force_by_keys_ratio, None);
550
551 let builder = ConfigBuilder::new().fts_brute_force_by_keys_ratio(0.5);
552 assert_eq!(builder.fts_brute_force_by_keys_ratio, Some(0.5));
553 }
554
555 #[test]
556 fn loaded_library_dir_resolves_zvec_library() {
557 if let Some(dir) = loaded_library_dir() {
559 let dir_str = dir.to_string_lossy();
560 assert!(
561 dir_str.contains("zvec"),
562 "unexpected dylib dir: {}",
563 dir_str
564 );
565 }
566 }
567}