use std::ffi::{CStr, CString};
use std::os::raw::c_char;
#[repr(C)]
pub struct ElidMatch {
pub index: usize,
pub score: f64,
}
#[repr(C)]
pub struct ElidMatchArray {
pub matches: *mut ElidMatch,
pub length: usize,
}
unsafe fn c_str_to_rust(c_str: *const c_char) -> Option<&'static str> {
if c_str.is_null() {
return None;
}
CStr::from_ptr(c_str).to_str().ok()
}
#[no_mangle]
pub unsafe extern "C" fn elid_levenshtein(a: *const c_char, b: *const c_char) -> usize {
let a_str = match c_str_to_rust(a) {
Some(s) => s,
None => return 0,
};
let b_str = match c_str_to_rust(b) {
Some(s) => s,
None => return 0,
};
crate::levenshtein(a_str, b_str)
}
#[no_mangle]
pub unsafe extern "C" fn elid_normalized_levenshtein(a: *const c_char, b: *const c_char) -> f64 {
let a_str = match c_str_to_rust(a) {
Some(s) => s,
None => return 0.0,
};
let b_str = match c_str_to_rust(b) {
Some(s) => s,
None => return 0.0,
};
crate::normalized_levenshtein(a_str, b_str)
}
#[no_mangle]
pub unsafe extern "C" fn elid_jaro(a: *const c_char, b: *const c_char) -> f64 {
let a_str = match c_str_to_rust(a) {
Some(s) => s,
None => return 0.0,
};
let b_str = match c_str_to_rust(b) {
Some(s) => s,
None => return 0.0,
};
crate::jaro(a_str, b_str)
}
#[no_mangle]
pub unsafe extern "C" fn elid_jaro_winkler(a: *const c_char, b: *const c_char) -> f64 {
let a_str = match c_str_to_rust(a) {
Some(s) => s,
None => return 0.0,
};
let b_str = match c_str_to_rust(b) {
Some(s) => s,
None => return 0.0,
};
crate::jaro_winkler(a_str, b_str)
}
#[no_mangle]
pub unsafe extern "C" fn elid_hamming(a: *const c_char, b: *const c_char) -> i64 {
let a_str = match c_str_to_rust(a) {
Some(s) => s,
None => return -1,
};
let b_str = match c_str_to_rust(b) {
Some(s) => s,
None => return -1,
};
match crate::hamming(a_str, b_str) {
Some(dist) => dist as i64,
None => -1,
}
}
#[no_mangle]
pub unsafe extern "C" fn elid_osa_distance(a: *const c_char, b: *const c_char) -> usize {
let a_str = match c_str_to_rust(a) {
Some(s) => s,
None => return 0,
};
let b_str = match c_str_to_rust(b) {
Some(s) => s,
None => return 0,
};
crate::osa_distance(a_str, b_str)
}
#[no_mangle]
pub unsafe extern "C" fn elid_best_match(a: *const c_char, b: *const c_char) -> f64 {
let a_str = match c_str_to_rust(a) {
Some(s) => s,
None => return 0.0,
};
let b_str = match c_str_to_rust(b) {
Some(s) => s,
None => return 0.0,
};
crate::best_match(a_str, b_str)
}
#[no_mangle]
pub unsafe extern "C" fn elid_simhash(text: *const c_char) -> u64 {
let text_str = match c_str_to_rust(text) {
Some(s) => s,
None => return 0,
};
crate::simhash(text_str)
}
#[no_mangle]
pub extern "C" fn elid_simhash_distance(hash1: u64, hash2: u64) -> u32 {
crate::simhash_distance(hash1, hash2)
}
#[no_mangle]
pub unsafe extern "C" fn elid_simhash_similarity(a: *const c_char, b: *const c_char) -> f64 {
let a_str = match c_str_to_rust(a) {
Some(s) => s,
None => return 0.0,
};
let b_str = match c_str_to_rust(b) {
Some(s) => s,
None => return 0.0,
};
crate::simhash_similarity(a_str, b_str)
}
#[no_mangle]
pub unsafe extern "C" fn elid_free_string(s: *mut c_char) {
if !s.is_null() {
drop(CString::from_raw(s));
}
}
#[no_mangle]
pub unsafe extern "C" fn elid_free_match_array(array: ElidMatchArray) {
if !array.matches.is_null() && array.length > 0 {
drop(Vec::from_raw_parts(
array.matches,
array.length,
array.length,
));
}
}
#[no_mangle]
pub extern "C" fn elid_version() -> *const c_char {
#[allow(clippy::manual_c_str_literals)]
{
"0.4.24\0".as_ptr() as *const c_char
}
}
#[cfg(feature = "embeddings")]
mod embeddings_ffi {
use super::*;
use crate::embeddings::{
decode_to_embedding, encode, hamming_distance, is_reversible, Profile,
};
#[no_mangle]
pub unsafe extern "C" fn elid_encode_lossless(
embedding: *const f32,
len: usize,
) -> *mut c_char {
if embedding.is_null() || !(64..=2048).contains(&len) {
return std::ptr::null_mut();
}
let slice = std::slice::from_raw_parts(embedding, len);
let profile = Profile::lossless();
match encode(slice, &profile) {
Ok(elid) => match CString::new(elid.as_str()) {
Ok(cs) => cs.into_raw(),
Err(_) => std::ptr::null_mut(),
},
Err(_) => std::ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn elid_encode_compressed(
embedding: *const f32,
len: usize,
retention_pct: f32,
) -> *mut c_char {
if embedding.is_null() || !(64..=2048).contains(&len) {
return std::ptr::null_mut();
}
let slice = std::slice::from_raw_parts(embedding, len);
let profile = Profile::compressed(retention_pct, len as u16);
match encode(slice, &profile) {
Ok(elid) => match CString::new(elid.as_str()) {
Ok(cs) => cs.into_raw(),
Err(_) => std::ptr::null_mut(),
},
Err(_) => std::ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn elid_encode_max_length(
embedding: *const f32,
len: usize,
max_chars: usize,
) -> *mut c_char {
if embedding.is_null() || !(64..=2048).contains(&len) {
return std::ptr::null_mut();
}
let slice = std::slice::from_raw_parts(embedding, len);
let profile = Profile::max_length(max_chars, len as u16);
match encode(slice, &profile) {
Ok(elid) => match CString::new(elid.as_str()) {
Ok(cs) => cs.into_raw(),
Err(_) => std::ptr::null_mut(),
},
Err(_) => std::ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn elid_encode_cross_dimensional(
embedding: *const f32,
len: usize,
common_dims: u16,
) -> *mut c_char {
if embedding.is_null() || !(64..=2048).contains(&len) {
return std::ptr::null_mut();
}
let slice = std::slice::from_raw_parts(embedding, len);
let profile = Profile::cross_dimensional(common_dims);
match encode(slice, &profile) {
Ok(elid) => match CString::new(elid.as_str()) {
Ok(cs) => cs.into_raw(),
Err(_) => std::ptr::null_mut(),
},
Err(_) => std::ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn elid_decode_to_embedding(
elid: *const c_char,
out_len: *mut usize,
) -> *mut f32 {
if elid.is_null() || out_len.is_null() {
return std::ptr::null_mut();
}
let elid_str = match c_str_to_rust(elid) {
Some(s) => s,
None => return std::ptr::null_mut(),
};
let elid_obj = match crate::embeddings::Elid::from_string(elid_str.to_string()) {
Ok(e) => e,
Err(_) => return std::ptr::null_mut(),
};
match decode_to_embedding(&elid_obj) {
Ok((embedding, _metadata)) => {
let len = embedding.len();
let mut boxed = embedding.into_boxed_slice();
let ptr = boxed.as_mut_ptr();
std::mem::forget(boxed);
*out_len = len;
ptr
}
Err(_) => std::ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn elid_is_reversible(elid: *const c_char) -> i32 {
if elid.is_null() {
return -1;
}
let elid_str = match c_str_to_rust(elid) {
Some(s) => s,
None => return -1,
};
let elid_obj = match crate::embeddings::Elid::from_string(elid_str.to_string()) {
Ok(e) => e,
Err(_) => return -1,
};
if is_reversible(&elid_obj) {
1
} else {
0
}
}
#[no_mangle]
pub unsafe extern "C" fn elid_embedding_hamming_distance(
elid1: *const c_char,
elid2: *const c_char,
) -> i32 {
if elid1.is_null() || elid2.is_null() {
return -1;
}
let elid1_str = match c_str_to_rust(elid1) {
Some(s) => s,
None => return -1,
};
let elid2_str = match c_str_to_rust(elid2) {
Some(s) => s,
None => return -1,
};
let elid1_obj = match crate::embeddings::Elid::from_string(elid1_str.to_string()) {
Ok(e) => e,
Err(_) => return -1,
};
let elid2_obj = match crate::embeddings::Elid::from_string(elid2_str.to_string()) {
Ok(e) => e,
Err(_) => return -1,
};
match hamming_distance(&elid1_obj, &elid2_obj) {
Ok(dist) => dist as i32,
Err(_) => -1,
}
}
#[no_mangle]
pub unsafe extern "C" fn elid_free_embedding(ptr: *mut f32, len: usize) {
if !ptr.is_null() && len > 0 {
drop(Vec::from_raw_parts(ptr, len, len));
}
}
}
#[cfg(feature = "embeddings")]
pub use embeddings_ffi::*;
#[cfg(feature = "models-text")]
#[no_mangle]
pub unsafe extern "C" fn elid_embed_text(text: *const c_char, out_len: *mut usize) -> *mut f32 {
if text.is_null() || out_len.is_null() {
return std::ptr::null_mut();
}
let text_str = match c_str_to_rust(text) {
Some(s) => s,
None => return std::ptr::null_mut(),
};
match crate::models::embed_text(text_str) {
Ok(embedding) => {
let len = embedding.len();
let mut boxed = embedding.into_boxed_slice();
let ptr = boxed.as_mut_ptr();
std::mem::forget(boxed);
*out_len = len;
ptr
}
Err(_) => std::ptr::null_mut(),
}
}
#[cfg(feature = "models-image")]
#[no_mangle]
pub unsafe extern "C" fn elid_embed_image(
image_bytes: *const u8,
image_len: usize,
out_len: *mut usize,
) -> *mut f32 {
if image_bytes.is_null() || out_len.is_null() || image_len == 0 {
return std::ptr::null_mut();
}
let bytes = std::slice::from_raw_parts(image_bytes, image_len);
match crate::models::embed_image(bytes) {
Ok(embedding) => {
let len = embedding.len();
let mut boxed = embedding.into_boxed_slice();
let ptr = boxed.as_mut_ptr();
std::mem::forget(boxed);
*out_len = len;
ptr
}
Err(_) => std::ptr::null_mut(),
}
}
#[cfg(feature = "embeddings")]
#[no_mangle]
pub unsafe extern "C" fn elid_embedding_to_bands(
embedding: *const f32,
len: usize,
num_bands: u8,
seed: u64,
) -> *mut c_char {
if embedding.is_null() || len == 0 {
return std::ptr::null_mut();
}
if num_bands == 0 || 16 % num_bands != 0 {
return std::ptr::null_mut();
}
let slice = std::slice::from_raw_parts(embedding, len);
let bands = crate::embeddings::embedding_to_bands(slice, num_bands, seed);
if bands.is_empty() {
return std::ptr::null_mut();
}
let result = bands.join("\n");
match CString::new(result) {
Ok(cs) => cs.into_raw(),
Err(_) => std::ptr::null_mut(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
use std::ptr;
#[test]
fn test_ffi_levenshtein() {
let a = CString::new("kitten").unwrap();
let b = CString::new("sitting").unwrap();
unsafe {
let dist = elid_levenshtein(a.as_ptr(), b.as_ptr());
assert_eq!(dist, 3);
}
}
#[test]
fn test_ffi_normalized_levenshtein() {
let a = CString::new("hello").unwrap();
let b = CString::new("hello").unwrap();
unsafe {
let sim = elid_normalized_levenshtein(a.as_ptr(), b.as_ptr());
assert_eq!(sim, 1.0);
}
}
#[test]
fn test_ffi_jaro_winkler() {
let a = CString::new("martha").unwrap();
let b = CString::new("marhta").unwrap();
unsafe {
let sim = elid_jaro_winkler(a.as_ptr(), b.as_ptr());
assert!(sim > 0.9);
}
}
#[test]
fn test_ffi_hamming() {
let a = CString::new("karolin").unwrap();
let b = CString::new("kathrin").unwrap();
unsafe {
let dist = elid_hamming(a.as_ptr(), b.as_ptr());
assert_eq!(dist, 3);
}
}
#[test]
fn test_ffi_simhash() {
let text = CString::new("iPhone 14").unwrap();
unsafe {
let hash = elid_simhash(text.as_ptr());
assert!(hash > 0);
}
}
#[test]
fn test_ffi_simhash_distance() {
let hash1 = 0b1010101010101010u64;
let hash2 = 0b1010101010101011u64;
let dist = elid_simhash_distance(hash1, hash2);
assert_eq!(dist, 1);
}
#[test]
fn test_ffi_null_safety() {
unsafe {
assert_eq!(elid_levenshtein(ptr::null(), ptr::null()), 0);
assert_eq!(elid_normalized_levenshtein(ptr::null(), ptr::null()), 0.0);
assert_eq!(elid_jaro(ptr::null(), ptr::null()), 0.0);
assert_eq!(elid_hamming(ptr::null(), ptr::null()), -1);
}
}
}
#[cfg(all(test, feature = "embeddings"))]
mod embeddings_tests {
use super::*;
use std::ffi::CString;
use std::ptr;
#[test]
fn test_ffi_encode_lossless() {
let embedding: Vec<f32> = (0..128).map(|i| (i as f32 / 64.0) - 1.0).collect();
unsafe {
let elid = elid_encode_lossless(embedding.as_ptr(), embedding.len());
assert!(!elid.is_null(), "Should encode successfully");
elid_free_string(elid);
}
}
#[test]
fn test_ffi_encode_lossless_null() {
unsafe {
let elid = elid_encode_lossless(ptr::null(), 128);
assert!(elid.is_null(), "Should return NULL for null input");
}
}
#[test]
fn test_ffi_encode_lossless_invalid_len() {
let embedding: Vec<f32> = vec![0.1; 32]; unsafe {
let elid = elid_encode_lossless(embedding.as_ptr(), embedding.len());
assert!(elid.is_null(), "Should return NULL for invalid length");
}
}
#[test]
fn test_ffi_encode_compressed() {
let embedding: Vec<f32> = (0..768).map(|i| (i as f32 / 384.0) - 1.0).collect();
unsafe {
let elid = elid_encode_compressed(embedding.as_ptr(), embedding.len(), 0.5);
assert!(!elid.is_null(), "Should encode successfully");
elid_free_string(elid);
}
}
#[test]
fn test_ffi_encode_max_length() {
let embedding: Vec<f32> = (0..768).map(|i| (i as f32 / 384.0) - 1.0).collect();
let max_chars = 100;
unsafe {
let elid = elid_encode_max_length(embedding.as_ptr(), embedding.len(), max_chars);
assert!(!elid.is_null(), "Should encode successfully");
let elid_str = CStr::from_ptr(elid).to_str().unwrap();
assert!(
elid_str.len() <= max_chars,
"ELID length {} exceeds max {}",
elid_str.len(),
max_chars
);
elid_free_string(elid);
}
}
#[test]
fn test_ffi_encode_cross_dimensional() {
let embedding: Vec<f32> = (0..768).map(|i| (i as f32 / 384.0) - 1.0).collect();
unsafe {
let elid = elid_encode_cross_dimensional(embedding.as_ptr(), embedding.len(), 128);
assert!(!elid.is_null(), "Should encode successfully");
elid_free_string(elid);
}
}
#[test]
fn test_ffi_encode_decode_roundtrip() {
let embedding: Vec<f32> = (0..128).map(|i| (i as f32 / 64.0) - 1.0).collect();
unsafe {
let elid = elid_encode_lossless(embedding.as_ptr(), embedding.len());
assert!(!elid.is_null());
let is_rev = elid_is_reversible(elid);
assert_eq!(is_rev, 1, "Lossless encoding should be reversible");
let mut out_len: usize = 0;
let decoded = elid_decode_to_embedding(elid, &mut out_len);
assert!(!decoded.is_null(), "Should decode successfully");
assert_eq!(out_len, embedding.len());
let decoded_slice = std::slice::from_raw_parts(decoded, out_len);
for (i, (&orig, &dec)) in embedding.iter().zip(decoded_slice.iter()).enumerate() {
assert_eq!(orig, dec, "Mismatch at index {}: {} vs {}", i, orig, dec);
}
elid_free_embedding(decoded, out_len);
elid_free_string(elid);
}
}
#[test]
fn test_ffi_decode_null_safety() {
unsafe {
let mut out_len: usize = 0;
let result = elid_decode_to_embedding(ptr::null(), &mut out_len);
assert!(result.is_null());
let elid = CString::new("invalid").unwrap();
let result = elid_decode_to_embedding(elid.as_ptr(), ptr::null_mut());
assert!(result.is_null());
}
}
#[test]
fn test_ffi_is_reversible_null() {
unsafe {
let result = elid_is_reversible(ptr::null());
assert_eq!(result, -1, "Should return -1 for NULL");
}
}
#[test]
fn test_ffi_is_reversible_invalid() {
let invalid = CString::new("xyz_invalid").unwrap();
unsafe {
let result = elid_is_reversible(invalid.as_ptr());
assert_eq!(result, -1, "Should return -1 for invalid ELID");
}
}
#[test]
fn test_ffi_embedding_hamming_distance_null() {
unsafe {
assert_eq!(
elid_embedding_hamming_distance(ptr::null(), ptr::null()),
-1
);
let valid = CString::new("test").unwrap();
assert_eq!(
elid_embedding_hamming_distance(valid.as_ptr(), ptr::null()),
-1
);
assert_eq!(
elid_embedding_hamming_distance(ptr::null(), valid.as_ptr()),
-1
);
}
}
#[test]
fn test_ffi_free_embedding_null() {
unsafe {
elid_free_embedding(ptr::null_mut(), 0);
elid_free_embedding(ptr::null_mut(), 100);
}
}
#[test]
fn test_ffi_embedding_to_bands() {
let embedding: Vec<f32> = (0..256).map(|i| (i as f32 / 128.0) - 1.0).collect();
let seed = 0x454c4944_53494d48u64;
unsafe {
let bands = elid_embedding_to_bands(embedding.as_ptr(), embedding.len(), 4, seed);
assert!(!bands.is_null(), "Should generate bands successfully");
let bands_str = CStr::from_ptr(bands).to_str().unwrap();
let band_vec: Vec<&str> = bands_str.split('\n').collect();
assert_eq!(band_vec.len(), 4, "Should have 4 bands");
for band in &band_vec {
assert!(
band.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()),
"Band should be base32hex encoded: {}",
band
);
}
elid_free_string(bands);
}
}
#[test]
fn test_ffi_embedding_to_bands_null_safety() {
unsafe {
let result = elid_embedding_to_bands(ptr::null(), 128, 4, 0);
assert!(result.is_null(), "Should return NULL for null embedding");
let embedding: Vec<f32> = vec![0.1; 128];
let result = elid_embedding_to_bands(embedding.as_ptr(), 0, 4, 0);
assert!(result.is_null(), "Should return NULL for zero length");
}
}
#[test]
fn test_ffi_embedding_to_bands_invalid_num_bands() {
let embedding: Vec<f32> = (0..256).map(|i| (i as f32 / 128.0) - 1.0).collect();
unsafe {
let result = elid_embedding_to_bands(embedding.as_ptr(), embedding.len(), 0, 0);
assert!(result.is_null(), "Should return NULL for num_bands=0");
let result = elid_embedding_to_bands(embedding.as_ptr(), embedding.len(), 3, 0);
assert!(result.is_null(), "Should return NULL for num_bands=3");
let result = elid_embedding_to_bands(embedding.as_ptr(), embedding.len(), 5, 0);
assert!(result.is_null(), "Should return NULL for num_bands=5");
let result = elid_embedding_to_bands(embedding.as_ptr(), embedding.len(), 1, 0);
assert!(!result.is_null(), "Should succeed for num_bands=1");
elid_free_string(result);
let result = elid_embedding_to_bands(embedding.as_ptr(), embedding.len(), 2, 0);
assert!(!result.is_null(), "Should succeed for num_bands=2");
elid_free_string(result);
let result = elid_embedding_to_bands(embedding.as_ptr(), embedding.len(), 8, 0);
assert!(!result.is_null(), "Should succeed for num_bands=8");
elid_free_string(result);
let result = elid_embedding_to_bands(embedding.as_ptr(), embedding.len(), 16, 0);
assert!(!result.is_null(), "Should succeed for num_bands=16");
elid_free_string(result);
}
}
#[test]
fn test_ffi_embedding_to_bands_deterministic() {
let embedding: Vec<f32> = (0..256).map(|i| (i as f32 / 128.0) - 1.0).collect();
let seed = 0x12345678u64;
unsafe {
let bands1 = elid_embedding_to_bands(embedding.as_ptr(), embedding.len(), 4, seed);
let bands2 = elid_embedding_to_bands(embedding.as_ptr(), embedding.len(), 4, seed);
assert!(!bands1.is_null());
assert!(!bands2.is_null());
let str1 = CStr::from_ptr(bands1).to_str().unwrap();
let str2 = CStr::from_ptr(bands2).to_str().unwrap();
assert_eq!(
str1, str2,
"Same embedding and seed should produce same bands"
);
elid_free_string(bands1);
elid_free_string(bands2);
}
}
}
#[cfg(all(test, feature = "models-text"))]
mod models_text_tests {
use super::*;
use std::ptr;
#[test]
fn test_ffi_embed_text_null_safety() {
unsafe {
let mut out_len: usize = 0;
let result = elid_embed_text(ptr::null(), &mut out_len);
assert!(result.is_null(), "Should return NULL for null text");
let text = CString::new("Hello, world!").unwrap();
let result = elid_embed_text(text.as_ptr(), ptr::null_mut());
assert!(result.is_null(), "Should return NULL for null out_len");
}
}
#[test]
fn test_ffi_embed_text_not_implemented() {
let text = CString::new("Hello, world!").unwrap();
unsafe {
let mut out_len: usize = 0;
let result = elid_embed_text(text.as_ptr(), &mut out_len);
assert!(result.is_null(), "Should return NULL when model not loaded");
}
}
}
#[cfg(all(test, feature = "models-image"))]
mod models_image_tests {
use super::*;
use std::ptr;
#[test]
fn test_ffi_embed_image_null_safety() {
unsafe {
let mut out_len: usize = 0;
let result = elid_embed_image(ptr::null(), 100, &mut out_len);
assert!(result.is_null(), "Should return NULL for null image_bytes");
let bytes = vec![0u8; 100];
let result = elid_embed_image(bytes.as_ptr(), 0, &mut out_len);
assert!(result.is_null(), "Should return NULL for zero length");
let result = elid_embed_image(bytes.as_ptr(), bytes.len(), ptr::null_mut());
assert!(result.is_null(), "Should return NULL for null out_len");
}
}
#[test]
fn test_ffi_embed_image_not_implemented() {
let dummy_bytes = vec![0u8; 100];
unsafe {
let mut out_len: usize = 0;
let result = elid_embed_image(dummy_bytes.as_ptr(), dummy_bytes.len(), &mut out_len);
assert!(result.is_null(), "Should return NULL when model not loaded");
}
}
}