use std::ffi::CStr;
use std::path::{Path, PathBuf};
use crate::error::{check_error, to_cstring, Error, ErrorCode, Result};
use crate::types::LogLevel;
pub struct ConfigBuilder {
pub memory_limit: u64,
pub num_threads: u32,
pub enable_console_log: bool,
pub fts_brute_force_by_keys_ratio: Option<f32>,
pub jieba_dict_dir: Option<String>,
}
impl ConfigBuilder {
pub fn new() -> Self {
ConfigBuilder {
memory_limit: 0,
num_threads: 0,
enable_console_log: false,
fts_brute_force_by_keys_ratio: None,
jieba_dict_dir: None,
}
}
pub fn memory_limit(mut self, bytes: u64) -> Self {
self.memory_limit = bytes;
self
}
pub fn num_threads(mut self, count: u32) -> Self {
self.num_threads = count;
self
}
pub fn enable_console_log(mut self, enable: bool) -> Self {
self.enable_console_log = enable;
self
}
pub fn fts_brute_force_by_keys_ratio(mut self, ratio: f32) -> Self {
self.fts_brute_force_by_keys_ratio = Some(ratio);
self
}
pub fn jieba_dict_dir(mut self, dir: impl Into<String>) -> Self {
self.jieba_dict_dir = Some(dir.into());
self
}
pub fn build(self) -> Self {
self
}
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self::new()
}
}
pub(crate) struct ConfigData {
pub(crate) handle: *mut zvec_rust_sys::zvec_config_data_t,
}
impl ConfigData {
pub(crate) fn new() -> Result<Self> {
let handle = unsafe { zvec_rust_sys::zvec_config_data_create() };
if handle.is_null() {
return Err(Error {
code: ErrorCode::InternalError,
message: "failed to create config data".into(),
});
}
Ok(ConfigData { handle })
}
pub(crate) fn set_memory_limit(&mut self, bytes: u64) -> Result<()> {
check_error(unsafe { zvec_rust_sys::zvec_config_data_set_memory_limit(self.handle, bytes) })
}
pub(crate) fn set_query_thread_count(&mut self, count: u32) -> Result<()> {
check_error(unsafe {
zvec_rust_sys::zvec_config_data_set_query_thread_count(self.handle, count)
})
}
pub(crate) fn set_optimize_thread_count(&mut self, count: u32) -> Result<()> {
check_error(unsafe {
zvec_rust_sys::zvec_config_data_set_optimize_thread_count(self.handle, count)
})
}
pub(crate) fn set_fts_brute_force_by_keys_ratio(&mut self, ratio: f32) -> Result<()> {
check_error(unsafe {
zvec_rust_sys::zvec_config_data_set_fts_brute_force_by_keys_ratio(self.handle, ratio)
})
}
pub(crate) fn set_jieba_dict_dir(&mut self, dir: &str) -> Result<()> {
let c_dir = to_cstring(dir)?;
check_error(unsafe {
zvec_rust_sys::zvec_config_data_set_jieba_dict_dir(self.handle, c_dir.as_ptr())
})
}
pub(crate) fn set_console_log(&mut self, level: LogLevel) -> Result<()> {
let log_config = unsafe { zvec_rust_sys::zvec_config_log_create_console(level as u32) };
if log_config.is_null() {
return Err(Error {
code: ErrorCode::InternalError,
message: "failed to create console log config".into(),
});
}
let result = check_error(unsafe {
zvec_rust_sys::zvec_config_data_set_log_config(self.handle, log_config)
});
if result.is_err() {
unsafe { zvec_rust_sys::zvec_config_log_destroy(log_config) };
}
result
}
}
impl Drop for ConfigData {
fn drop(&mut self) {
if !self.handle.is_null() {
unsafe { zvec_rust_sys::zvec_config_data_destroy(self.handle) };
}
}
}
pub fn initialize(config: Option<&ConfigBuilder>) -> Result<()> {
match config {
None => {
auto_register_default_jieba_dict_dir();
check_error(unsafe { zvec_rust_sys::zvec_initialize(std::ptr::null()) })
}
Some(builder) => {
let mut cfg = ConfigData::new()?;
if builder.memory_limit > 0 {
cfg.set_memory_limit(builder.memory_limit)?;
}
if builder.num_threads > 0 {
cfg.set_query_thread_count(builder.num_threads)?;
cfg.set_optimize_thread_count(builder.num_threads)?;
}
if builder.enable_console_log {
cfg.set_console_log(LogLevel::Info)?;
}
if let Some(ratio) = builder.fts_brute_force_by_keys_ratio {
cfg.set_fts_brute_force_by_keys_ratio(ratio)?;
}
if let Some(ref dir) = builder.jieba_dict_dir {
cfg.set_jieba_dict_dir(dir)?;
} else {
auto_register_default_jieba_dict_dir();
}
check_error(unsafe { zvec_rust_sys::zvec_initialize(cfg.handle as *const _) })
}
}
}
pub fn set_default_jieba_dict_dir(dir: &str) -> Result<()> {
let c_dir = to_cstring(dir)?;
unsafe { zvec_rust_sys::zvec_set_default_jieba_dict_dir(c_dir.as_ptr()) };
Ok(())
}
pub fn get_default_jieba_dict_dir() -> String {
unsafe {
let ptr = zvec_rust_sys::zvec_get_default_jieba_dict_dir();
if ptr.is_null() {
return String::new();
}
CStr::from_ptr(ptr).to_string_lossy().into_owned()
}
}
fn auto_register_default_jieba_dict_dir() {
if !get_default_jieba_dict_dir().is_empty() {
return;
}
if let Some(dir) = discover_jieba_dict_dir() {
let dir = dir.to_string_lossy().into_owned();
let _ = set_default_jieba_dict_dir(&dir);
}
}
fn discover_jieba_dict_dir() -> Option<PathBuf> {
let mut candidates: Vec<PathBuf> = Vec::new();
if let Some(build_time_dir) = zvec_rust_sys::build_time_jieba_dict_dir() {
candidates.push(PathBuf::from(build_time_dir));
}
if let Some(lib_dir) = loaded_library_dir() {
candidates.push(lib_dir.join("data").join("jieba_dict"));
candidates.push(lib_dir.join("..").join("data").join("jieba_dict"));
}
candidates.into_iter().find(|dir| is_jieba_dict_dir(dir))
}
fn is_jieba_dict_dir(dir: &Path) -> bool {
dir.join("jieba.dict.utf8").is_file() && dir.join("hmm_model.utf8").is_file()
}
#[cfg(unix)]
fn loaded_library_dir() -> Option<PathBuf> {
#[repr(C)]
struct DlInfo {
dli_fname: *const std::ffi::c_char,
dli_fbase: *mut std::ffi::c_void,
dli_sname: *const std::ffi::c_char,
dli_saddr: *mut std::ffi::c_void,
}
extern "C" {
fn dladdr(addr: *const std::ffi::c_void, info: *mut DlInfo) -> std::ffi::c_int;
}
let symbol =
zvec_rust_sys::zvec_get_default_jieba_dict_dir as *const () as *const std::ffi::c_void;
let mut info = DlInfo {
dli_fname: std::ptr::null(),
dli_fbase: std::ptr::null_mut(),
dli_sname: std::ptr::null(),
dli_saddr: std::ptr::null_mut(),
};
let path = unsafe {
if dladdr(symbol, &mut info) == 0 || info.dli_fname.is_null() {
return None;
}
PathBuf::from(CStr::from_ptr(info.dli_fname).to_string_lossy().as_ref())
};
let resolved = std::fs::canonicalize(&path).unwrap_or(path);
resolved.parent().map(PathBuf::from)
}
#[cfg(windows)]
fn loaded_library_dir() -> Option<PathBuf> {
use std::os::windows::ffi::OsStringExt;
type HModule = *mut std::ffi::c_void;
const FROM_ADDRESS: u32 = 0x0000_0004;
const UNCHANGED_REFCOUNT: u32 = 0x0000_0002;
#[link(name = "kernel32")]
extern "system" {
fn GetModuleHandleExW(
flags: u32,
module_name: *mut std::ffi::c_void,
module: *mut HModule,
) -> i32;
fn GetModuleFileNameW(module: HModule, filename: *mut u16, size: u32) -> u32;
}
let symbol =
zvec_rust_sys::zvec_get_default_jieba_dict_dir as *const () as *mut std::ffi::c_void;
unsafe {
let mut module: HModule = std::ptr::null_mut();
if GetModuleHandleExW(FROM_ADDRESS | UNCHANGED_REFCOUNT, symbol, &mut module) == 0
|| module.is_null()
{
return None;
}
let mut size = 260u32; loop {
let mut buf = vec![0u16; size as usize];
let len = GetModuleFileNameW(module, buf.as_mut_ptr(), size);
if len == 0 {
return None;
}
if len < size {
buf.truncate(len as usize);
return PathBuf::from(std::ffi::OsString::from_wide(&buf))
.parent()
.map(PathBuf::from);
}
size = size.checked_mul(2)?; }
}
}
#[cfg(not(any(unix, windows)))]
fn loaded_library_dir() -> Option<PathBuf> {
None
}
#[doc(hidden)]
pub fn shutdown() -> Result<()> {
check_error(unsafe { zvec_rust_sys::zvec_shutdown() })
}
pub fn is_initialized() -> bool {
unsafe { zvec_rust_sys::zvec_is_initialized() }
}
pub fn version() -> String {
unsafe {
let ptr = zvec_rust_sys::zvec_get_version();
if ptr.is_null() {
return String::new();
}
CStr::from_ptr(ptr).to_string_lossy().into_owned()
}
}
pub fn check_version(major: i32, minor: i32, patch: i32) -> bool {
unsafe { zvec_rust_sys::zvec_check_version(major, minor, patch) }
}
pub fn version_major() -> i32 {
unsafe { zvec_rust_sys::zvec_get_version_major() }
}
pub fn version_minor() -> i32 {
unsafe { zvec_rust_sys::zvec_get_version_minor() }
}
pub fn version_patch() -> i32 {
unsafe { zvec_rust_sys::zvec_get_version_patch() }
}
pub fn io_backend_type() -> u32 {
unsafe { zvec_rust_sys::zvec_get_io_backend_type() }
}
pub fn io_backend_type_name(backend_type: u32) -> String {
unsafe {
let ptr = zvec_rust_sys::zvec_get_io_backend_type_name(backend_type);
if ptr.is_null() {
return String::new();
}
CStr::from_ptr(ptr).to_string_lossy().into_owned()
}
}
pub fn io_backend_description() -> String {
unsafe {
let ptr = zvec_rust_sys::zvec_get_io_backend_description();
if ptr.is_null() {
return String::new();
}
CStr::from_ptr(ptr).to_string_lossy().into_owned()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_builder_defaults() {
let builder = ConfigBuilder::new();
assert_eq!(builder.memory_limit, 0);
assert_eq!(builder.num_threads, 0);
assert!(!builder.enable_console_log);
assert!(builder.jieba_dict_dir.is_none());
}
#[test]
fn config_builder_chaining() {
let builder = ConfigBuilder::new()
.memory_limit(1024)
.num_threads(4)
.enable_console_log(true)
.build();
assert_eq!(builder.memory_limit, 1024);
assert_eq!(builder.num_threads, 4);
assert!(builder.enable_console_log);
}
#[test]
fn config_builder_default_trait() {
let builder = ConfigBuilder::default();
assert_eq!(builder.memory_limit, 0);
assert_eq!(builder.num_threads, 0);
assert!(!builder.enable_console_log);
}
#[test]
fn config_builder_memory_limit_setter() {
let builder = ConfigBuilder::new().memory_limit(2048);
assert_eq!(builder.memory_limit, 2048);
}
#[test]
fn config_builder_num_threads_setter() {
let builder = ConfigBuilder::new().num_threads(8);
assert_eq!(builder.num_threads, 8);
}
#[test]
fn config_builder_enable_console_log_setter() {
let builder = ConfigBuilder::new().enable_console_log(true);
assert!(builder.enable_console_log);
}
#[test]
fn config_builder_jieba_dict_dir_setter() {
let builder = ConfigBuilder::new().jieba_dict_dir("/tmp/jieba_dict");
assert_eq!(builder.jieba_dict_dir.as_deref(), Some("/tmp/jieba_dict"));
}
#[test]
fn default_jieba_dict_dir_roundtrip() {
let previous = get_default_jieba_dict_dir();
set_default_jieba_dict_dir("/tmp/zvec_rust_jieba_roundtrip").unwrap();
assert_eq!(
get_default_jieba_dict_dir(),
"/tmp/zvec_rust_jieba_roundtrip"
);
set_default_jieba_dict_dir("").unwrap();
assert_eq!(get_default_jieba_dict_dir(), "");
set_default_jieba_dict_dir(&previous).unwrap();
assert_eq!(get_default_jieba_dict_dir(), previous);
}
#[test]
fn discovered_jieba_dict_dir_contains_dict_files() {
if let Some(dir) = discover_jieba_dict_dir() {
assert!(is_jieba_dict_dir(&dir), "invalid dict dir: {:?}", dir);
}
}
#[test]
fn config_builder_build_returns_self() {
let builder = ConfigBuilder::new()
.memory_limit(4096)
.num_threads(2)
.enable_console_log(true)
.build();
assert_eq!(builder.memory_limit, 4096);
assert_eq!(builder.num_threads, 2);
assert!(builder.enable_console_log);
}
#[test]
fn config_builder_overwrite_values() {
let builder = ConfigBuilder::new()
.memory_limit(1024)
.memory_limit(2048)
.num_threads(4)
.num_threads(8)
.enable_console_log(false)
.enable_console_log(true);
assert_eq!(builder.memory_limit, 2048);
assert_eq!(builder.num_threads, 8);
assert!(builder.enable_console_log);
}
#[test]
fn config_builder_zero_values() {
let builder = ConfigBuilder::new().memory_limit(0).num_threads(0);
assert_eq!(builder.memory_limit, 0);
assert_eq!(builder.num_threads, 0);
}
#[test]
fn config_builder_large_memory_limit() {
let builder = ConfigBuilder::new().memory_limit(u64::MAX);
assert_eq!(builder.memory_limit, u64::MAX);
}
#[test]
fn config_builder_fts_ratio() {
let builder = ConfigBuilder::new();
assert_eq!(builder.fts_brute_force_by_keys_ratio, None);
let builder = ConfigBuilder::new().fts_brute_force_by_keys_ratio(0.5);
assert_eq!(builder.fts_brute_force_by_keys_ratio, Some(0.5));
}
#[test]
fn loaded_library_dir_resolves_zvec_library() {
if let Some(dir) = loaded_library_dir() {
let dir_str = dir.to_string_lossy();
assert!(
dir_str.contains("zvec"),
"unexpected dylib dir: {}",
dir_str
);
}
}
#[test]
fn io_backend_type_name_maps_every_code() {
use zvec_rust_sys::{
ZVEC_IO_BACKEND_TYPE_IO_URING, ZVEC_IO_BACKEND_TYPE_LIBAIO, ZVEC_IO_BACKEND_TYPE_PREAD,
ZVEC_IO_BACKEND_TYPE_WINDOWS_OVERLAPPED,
};
assert_eq!(io_backend_type_name(ZVEC_IO_BACKEND_TYPE_PREAD), "pread");
assert_eq!(io_backend_type_name(ZVEC_IO_BACKEND_TYPE_LIBAIO), "libaio");
assert_eq!(
io_backend_type_name(ZVEC_IO_BACKEND_TYPE_IO_URING),
"io_uring"
);
assert_eq!(
io_backend_type_name(ZVEC_IO_BACKEND_TYPE_WINDOWS_OVERLAPPED),
"windows_overlapped"
);
assert_eq!(io_backend_type_name(u32::MAX), "unknown");
}
#[test]
fn io_backend_introspection_reports_the_active_backend() {
let backend = io_backend_type();
let name = io_backend_type_name(backend);
assert!(
["pread", "libaio", "io_uring", "windows_overlapped"].contains(&name.as_str()),
"unexpected io backend {} (code {})",
name,
backend
);
assert!(!io_backend_description().is_empty());
}
}