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
425pub fn io_backend_type() -> u32 {
432 unsafe { zvec_rust_sys::zvec_get_io_backend_type() }
433}
434
435pub fn io_backend_type_name(backend_type: u32) -> String {
440 unsafe {
441 let ptr = zvec_rust_sys::zvec_get_io_backend_type_name(backend_type);
442 if ptr.is_null() {
443 return String::new();
444 }
445 CStr::from_ptr(ptr).to_string_lossy().into_owned()
446 }
447}
448
449pub fn io_backend_description() -> String {
455 unsafe {
456 let ptr = zvec_rust_sys::zvec_get_io_backend_description();
457 if ptr.is_null() {
458 return String::new();
459 }
460 CStr::from_ptr(ptr).to_string_lossy().into_owned()
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn config_builder_defaults() {
470 let builder = ConfigBuilder::new();
471 assert_eq!(builder.memory_limit, 0);
472 assert_eq!(builder.num_threads, 0);
473 assert!(!builder.enable_console_log);
474 assert!(builder.jieba_dict_dir.is_none());
475 }
476
477 #[test]
478 fn config_builder_chaining() {
479 let builder = ConfigBuilder::new()
480 .memory_limit(1024)
481 .num_threads(4)
482 .enable_console_log(true)
483 .build();
484 assert_eq!(builder.memory_limit, 1024);
485 assert_eq!(builder.num_threads, 4);
486 assert!(builder.enable_console_log);
487 }
488
489 #[test]
490 fn config_builder_default_trait() {
491 let builder = ConfigBuilder::default();
492 assert_eq!(builder.memory_limit, 0);
493 assert_eq!(builder.num_threads, 0);
494 assert!(!builder.enable_console_log);
495 }
496
497 #[test]
498 fn config_builder_memory_limit_setter() {
499 let builder = ConfigBuilder::new().memory_limit(2048);
500 assert_eq!(builder.memory_limit, 2048);
501 }
502
503 #[test]
504 fn config_builder_num_threads_setter() {
505 let builder = ConfigBuilder::new().num_threads(8);
506 assert_eq!(builder.num_threads, 8);
507 }
508
509 #[test]
510 fn config_builder_enable_console_log_setter() {
511 let builder = ConfigBuilder::new().enable_console_log(true);
512 assert!(builder.enable_console_log);
513 }
514
515 #[test]
516 fn config_builder_jieba_dict_dir_setter() {
517 let builder = ConfigBuilder::new().jieba_dict_dir("/tmp/jieba_dict");
518 assert_eq!(builder.jieba_dict_dir.as_deref(), Some("/tmp/jieba_dict"));
519 }
520
521 #[test]
522 fn default_jieba_dict_dir_roundtrip() {
523 let previous = get_default_jieba_dict_dir();
524 set_default_jieba_dict_dir("/tmp/zvec_rust_jieba_roundtrip").unwrap();
525 assert_eq!(
526 get_default_jieba_dict_dir(),
527 "/tmp/zvec_rust_jieba_roundtrip"
528 );
529 set_default_jieba_dict_dir("").unwrap();
532 assert_eq!(get_default_jieba_dict_dir(), "");
533 set_default_jieba_dict_dir(&previous).unwrap();
534 assert_eq!(get_default_jieba_dict_dir(), previous);
535 }
536
537 #[test]
538 fn discovered_jieba_dict_dir_contains_dict_files() {
539 if let Some(dir) = discover_jieba_dict_dir() {
542 assert!(is_jieba_dict_dir(&dir), "invalid dict dir: {:?}", dir);
543 }
544 }
545
546 #[test]
547 fn config_builder_build_returns_self() {
548 let builder = ConfigBuilder::new()
549 .memory_limit(4096)
550 .num_threads(2)
551 .enable_console_log(true)
552 .build();
553 assert_eq!(builder.memory_limit, 4096);
554 assert_eq!(builder.num_threads, 2);
555 assert!(builder.enable_console_log);
556 }
557
558 #[test]
559 fn config_builder_overwrite_values() {
560 let builder = ConfigBuilder::new()
561 .memory_limit(1024)
562 .memory_limit(2048)
563 .num_threads(4)
564 .num_threads(8)
565 .enable_console_log(false)
566 .enable_console_log(true);
567 assert_eq!(builder.memory_limit, 2048);
568 assert_eq!(builder.num_threads, 8);
569 assert!(builder.enable_console_log);
570 }
571
572 #[test]
573 fn config_builder_zero_values() {
574 let builder = ConfigBuilder::new().memory_limit(0).num_threads(0);
575 assert_eq!(builder.memory_limit, 0);
576 assert_eq!(builder.num_threads, 0);
577 }
578
579 #[test]
580 fn config_builder_large_memory_limit() {
581 let builder = ConfigBuilder::new().memory_limit(u64::MAX);
582 assert_eq!(builder.memory_limit, u64::MAX);
583 }
584
585 #[test]
586 fn config_builder_fts_ratio() {
587 let builder = ConfigBuilder::new();
588 assert_eq!(builder.fts_brute_force_by_keys_ratio, None);
589
590 let builder = ConfigBuilder::new().fts_brute_force_by_keys_ratio(0.5);
591 assert_eq!(builder.fts_brute_force_by_keys_ratio, Some(0.5));
592 }
593
594 #[test]
595 fn loaded_library_dir_resolves_zvec_library() {
596 if let Some(dir) = loaded_library_dir() {
598 let dir_str = dir.to_string_lossy();
599 assert!(
600 dir_str.contains("zvec"),
601 "unexpected dylib dir: {}",
602 dir_str
603 );
604 }
605 }
606
607 #[test]
608 fn io_backend_type_name_maps_every_code() {
609 use zvec_rust_sys::{
610 ZVEC_IO_BACKEND_TYPE_IO_URING, ZVEC_IO_BACKEND_TYPE_LIBAIO, ZVEC_IO_BACKEND_TYPE_PREAD,
611 ZVEC_IO_BACKEND_TYPE_WINDOWS_OVERLAPPED,
612 };
613
614 assert_eq!(io_backend_type_name(ZVEC_IO_BACKEND_TYPE_PREAD), "pread");
615 assert_eq!(io_backend_type_name(ZVEC_IO_BACKEND_TYPE_LIBAIO), "libaio");
616 assert_eq!(
617 io_backend_type_name(ZVEC_IO_BACKEND_TYPE_IO_URING),
618 "io_uring"
619 );
620 assert_eq!(
621 io_backend_type_name(ZVEC_IO_BACKEND_TYPE_WINDOWS_OVERLAPPED),
622 "windows_overlapped"
623 );
624 assert_eq!(io_backend_type_name(u32::MAX), "unknown");
625 }
626
627 #[test]
628 fn io_backend_introspection_reports_the_active_backend() {
629 let backend = io_backend_type();
630 let name = io_backend_type_name(backend);
631 assert!(
632 ["pread", "libaio", "io_uring", "windows_overlapped"].contains(&name.as_str()),
633 "unexpected io backend {} (code {})",
634 name,
635 backend
636 );
637 assert!(!io_backend_description().is_empty());
638 }
639}