use std::collections::BTreeMap;
use std::ffi::{c_char, CStr, CString};
use crate::detect::Detector;
use crate::extract;
use crate::store::LanceStore;
use crate::types::SourceType;
fn to_c_string(payload: String) -> *mut c_char {
CString::new(payload)
.unwrap_or_else(|_| CString::new(r#"{"error":"interior NUL in output"}"#).unwrap())
.into_raw()
}
fn error_json(message: &str) -> String {
serde_json::json!({ "error": message }).to_string()
}
fn parse_source_type(raw: &str) -> Option<SourceType> {
serde_json::from_value(serde_json::Value::String(raw.to_string())).ok()
}
#[no_mangle]
pub unsafe extern "C" fn citenexus_extract(
bytes: *const u8,
len: usize,
source_type: *const c_char,
document_id: *const c_char,
) -> *mut c_char {
if bytes.is_null() || source_type.is_null() || document_id.is_null() {
return to_c_string(error_json("null argument"));
}
let data = std::slice::from_raw_parts(bytes, len);
let source_type = match CStr::from_ptr(source_type).to_str() {
Ok(s) => s,
Err(_) => return to_c_string(error_json("source_type is not UTF-8")),
};
let document_id = match CStr::from_ptr(document_id).to_str() {
Ok(s) => s,
Err(_) => return to_c_string(error_json("document_id is not UTF-8")),
};
let Some(kind) = parse_source_type(source_type) else {
return to_c_string(error_json(&format!("unknown source_type: {source_type}")));
};
let payload = match extract::extract(data, kind, document_id, None) {
Ok(doc) => serde_json::to_string(&doc).unwrap_or_else(|e| error_json(&e.to_string())),
Err(message) => error_json(&message),
};
to_c_string(payload)
}
#[no_mangle]
pub unsafe extern "C" fn citenexus_free_string(s: *mut c_char) {
if !s.is_null() {
drop(CString::from_raw(s));
}
}
#[no_mangle]
pub extern "C" fn citenexus_core_version() -> *const c_char {
concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
}
unsafe fn utf8_arg<'a>(ptr: *const c_char, name: &str) -> Result<&'a str, String> {
if ptr.is_null() {
return Err(format!("{name} is null"));
}
CStr::from_ptr(ptr)
.to_str()
.map_err(|_| format!("{name} is not UTF-8"))
}
#[no_mangle]
pub unsafe extern "C" fn citenexus_store_open(
uri: *const c_char,
storage_options_json: *const c_char,
) -> *mut LanceStore {
let Ok(uri) = utf8_arg(uri, "uri") else {
return std::ptr::null_mut();
};
let options: BTreeMap<String, String> = if storage_options_json.is_null() {
BTreeMap::new()
} else {
let Ok(raw) = utf8_arg(storage_options_json, "storage_options_json") else {
return std::ptr::null_mut();
};
match serde_json::from_str(raw) {
Ok(map) => map,
Err(_) => return std::ptr::null_mut(),
}
};
match LanceStore::open(uri, &options) {
Ok(store) => Box::into_raw(Box::new(store)),
Err(_) => std::ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn citenexus_store_upsert(
handle: *mut LanceStore,
rows_json: *const c_char,
) -> *mut c_char {
if handle.is_null() {
return to_c_string(error_json("null store handle"));
}
let rows = match utf8_arg(rows_json, "rows_json") {
Ok(raw) => match serde_json::from_str::<serde_json::Value>(raw) {
Ok(v) => v,
Err(e) => return to_c_string(error_json(&format!("rows_json: {e}"))),
},
Err(msg) => return to_c_string(error_json(&msg)),
};
let payload = match (*handle).upsert(&rows) {
Ok(()) => r#"{"ok":true}"#.to_string(),
Err(message) => error_json(&message),
};
to_c_string(payload)
}
#[no_mangle]
pub unsafe extern "C" fn citenexus_store_search(
handle: *mut LanceStore,
vector_json: *const c_char,
limit: usize,
) -> *mut c_char {
if handle.is_null() {
return to_c_string(error_json("null store handle"));
}
let vector: Vec<f32> = match utf8_arg(vector_json, "vector_json") {
Ok(raw) => match serde_json::from_str(raw) {
Ok(v) => v,
Err(e) => return to_c_string(error_json(&format!("vector_json: {e}"))),
},
Err(msg) => return to_c_string(error_json(&msg)),
};
let payload = match (*handle).search(&vector, limit) {
Ok(rows) => rows.to_string(),
Err(message) => error_json(&message),
};
to_c_string(payload)
}
#[no_mangle]
pub unsafe extern "C" fn citenexus_store_scan(
handle: *mut LanceStore,
limit: i64,
) -> *mut c_char {
if handle.is_null() {
return to_c_string(error_json("null store handle"));
}
let limit = usize::try_from(limit).ok();
let payload = match (*handle).scan(limit) {
Ok(rows) => rows.to_string(),
Err(message) => error_json(&message),
};
to_c_string(payload)
}
#[no_mangle]
pub unsafe extern "C" fn citenexus_store_drop(handle: *mut LanceStore) -> *mut c_char {
if handle.is_null() {
return to_c_string(error_json("null store handle"));
}
let payload = match (*handle).drop_table() {
Ok(()) => r#"{"ok":true}"#.to_string(),
Err(message) => error_json(&message),
};
to_c_string(payload)
}
#[no_mangle]
pub unsafe extern "C" fn citenexus_store_close(handle: *mut LanceStore) {
if !handle.is_null() {
drop(Box::from_raw(handle));
}
}
#[no_mangle]
pub unsafe extern "C" fn citenexus_detector_open(model_path: *const c_char) -> *mut Detector {
let Ok(path) = utf8_arg(model_path, "model_path") else {
return std::ptr::null_mut();
};
match Detector::open(path) {
Ok(detector) => Box::into_raw(Box::new(detector)),
Err(_) => std::ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn citenexus_detect(
handle: *mut Detector,
text: *const c_char,
) -> *mut c_char {
if handle.is_null() {
return to_c_string(error_json("null detector handle"));
}
let text = match utf8_arg(text, "text") {
Ok(t) => t,
Err(msg) => return to_c_string(error_json(&msg)),
};
let payload = match (*handle).detect(text) {
Ok(detection) => {
serde_json::to_string(&detection).unwrap_or_else(|e| error_json(&e.to_string()))
}
Err(message) => error_json(&message),
};
to_c_string(payload)
}
#[no_mangle]
pub unsafe extern "C" fn citenexus_detector_close(handle: *mut Detector) {
if !handle.is_null() {
drop(Box::from_raw(handle));
}
}