synadb/ffi.rs
1// Copyright (c) 2025 SynaDB Contributors
2// Licensed under the SynaDB License. See LICENSE file for details.
3
4//! C-ABI Foreign Function Interface for Syna database.
5//!
6//! This module provides extern "C" functions for cross-language access.
7//! All functions use `catch_unwind` to prevent Rust panics from unwinding
8//! into foreign code.
9
10// FFI functions intentionally take raw pointers without being marked unsafe
11// because they handle null checks and use catch_unwind for safety
12#![allow(clippy::not_unsafe_ptr_arg_deref)]
13// Using slice::from_raw_parts_mut with Box::from_raw is intentional for FFI memory management
14#![allow(clippy::cast_slice_from_raw_parts)]
15
16use std::ffi::CStr;
17use std::os::raw::c_char;
18
19use crate::engine::{close_db, free_tensor, open_db, open_db_with_config, with_db, DbConfig};
20use crate::error::{
21 ERR_GENERIC, ERR_INTERNAL_PANIC, ERR_INVALID_PATH, ERR_KEY_NOT_FOUND, ERR_SUCCESS,
22 ERR_TYPE_MISMATCH,
23};
24use crate::types::Atom;
25
26/// Opens a database at the given path and registers it in the global registry.
27///
28/// # Arguments
29/// * `path` - Null-terminated C string containing the path to the database file
30///
31/// # Returns
32/// * `1` (ERR_SUCCESS) - Database opened successfully or was already open
33/// * `0` (ERR_GENERIC) - Generic error during database open
34/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
35/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
36///
37/// # Safety
38/// * `path` must be a valid null-terminated C string or null
39///
40/// _Requirements: 6.2, 6.3, 9.4_
41#[no_mangle]
42pub extern "C" fn SYNA_open(path: *const c_char) -> i32 {
43 std::panic::catch_unwind(|| {
44 // Check for null pointer
45 if path.is_null() {
46 return ERR_INVALID_PATH;
47 }
48
49 // Convert C string to Rust string
50 let c_str = unsafe { CStr::from_ptr(path) };
51 let path_str = match c_str.to_str() {
52 Ok(s) => s,
53 Err(_) => return ERR_INVALID_PATH,
54 };
55
56 // Open the database
57 match open_db(path_str) {
58 Ok(_) => ERR_SUCCESS,
59 Err(_) => ERR_GENERIC,
60 }
61 })
62 .unwrap_or(ERR_INTERNAL_PANIC)
63}
64
65/// Opens a database with sync_on_write disabled for high-throughput writes.
66///
67/// This is optimized for bulk ingestion scenarios where durability can be
68/// traded for speed. Data is still written to disk but not fsynced after
69/// each write, achieving 100K+ ops/sec instead of ~100 ops/sec.
70///
71/// # Arguments
72/// * `path` - Null-terminated C string containing the path to the database file
73/// * `sync_on_write` - 1 for sync after each write (durable), 0 for no sync (fast)
74///
75/// # Returns
76/// * `1` (ERR_SUCCESS) - Database opened successfully or was already open
77/// * `0` (ERR_GENERIC) - Generic error during database open
78/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
79/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
80///
81/// # Safety
82/// * `path` must be a valid null-terminated C string or null
83#[no_mangle]
84pub extern "C" fn SYNA_open_with_config(path: *const c_char, sync_on_write: i32) -> i32 {
85 std::panic::catch_unwind(|| {
86 // Check for null pointer
87 if path.is_null() {
88 return ERR_INVALID_PATH;
89 }
90
91 // Convert C string to Rust string
92 let c_str = unsafe { CStr::from_ptr(path) };
93 let path_str = match c_str.to_str() {
94 Ok(s) => s,
95 Err(_) => return ERR_INVALID_PATH,
96 };
97
98 // Create config with sync_on_write setting
99 let config = DbConfig {
100 sync_on_write: sync_on_write != 0,
101 ..DbConfig::default()
102 };
103
104 // Open the database with config
105 match open_db_with_config(path_str, config) {
106 Ok(_) => ERR_SUCCESS,
107 Err(_) => ERR_GENERIC,
108 }
109 })
110 .unwrap_or(ERR_INTERNAL_PANIC)
111}
112
113/// Closes a database and removes it from the global registry.
114///
115/// # Arguments
116/// * `path` - Null-terminated C string containing the path to the database file
117///
118/// # Returns
119/// * `1` (ERR_SUCCESS) - Database closed successfully
120/// * `0` (ERR_GENERIC) - Generic error during database close
121/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
122/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
123/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
124///
125/// # Safety
126/// * `path` must be a valid null-terminated C string or null
127///
128/// _Requirements: 6.2_
129#[no_mangle]
130pub extern "C" fn SYNA_close(path: *const c_char) -> i32 {
131 std::panic::catch_unwind(|| {
132 // Check for null pointer
133 if path.is_null() {
134 return ERR_INVALID_PATH;
135 }
136
137 // Convert C string to Rust string
138 let c_str = unsafe { CStr::from_ptr(path) };
139 let path_str = match c_str.to_str() {
140 Ok(s) => s,
141 Err(_) => return ERR_INVALID_PATH,
142 };
143
144 // Close the database
145 match close_db(path_str) {
146 Ok(_) => ERR_SUCCESS,
147 Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND,
148 Err(_) => ERR_GENERIC,
149 }
150 })
151 .unwrap_or(ERR_INTERNAL_PANIC)
152}
153
154/// Writes a float value to the database.
155///
156/// # Arguments
157/// * `path` - Null-terminated C string containing the path to the database file
158/// * `key` - Null-terminated C string containing the key
159/// * `value` - The f64 value to store
160///
161/// # Returns
162/// * Positive value - The byte offset where the entry was written
163/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
164/// * `-2` (ERR_INVALID_PATH) - Path or key is null or invalid UTF-8
165/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
166///
167/// # Safety
168/// * `path` and `key` must be valid null-terminated C strings or null
169///
170/// _Requirements: 6.2, 6.3_
171#[no_mangle]
172pub extern "C" fn SYNA_put_float(path: *const c_char, key: *const c_char, value: f64) -> i64 {
173 std::panic::catch_unwind(|| {
174 // Validate pointers
175 if path.is_null() || key.is_null() {
176 return ERR_INVALID_PATH as i64;
177 }
178
179 // Convert C strings to Rust strings
180 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
181 Ok(s) => s,
182 Err(_) => return ERR_INVALID_PATH as i64,
183 };
184
185 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
186 Ok(s) => s,
187 Err(_) => return ERR_INVALID_PATH as i64,
188 };
189
190 // Call with_db to append the value
191 match with_db(path_str, |db| db.append(key_str, Atom::Float(value))) {
192 Ok(offset) => offset as i64,
193 Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND as i64,
194 Err(_) => ERR_GENERIC as i64,
195 }
196 })
197 .unwrap_or(ERR_INTERNAL_PANIC as i64)
198}
199
200/// Writes an integer value to the database.
201///
202/// # Arguments
203/// * `path` - Null-terminated C string containing the path to the database file
204/// * `key` - Null-terminated C string containing the key
205/// * `value` - The i64 value to store
206///
207/// # Returns
208/// * Positive value - The byte offset where the entry was written
209/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
210/// * `-2` (ERR_INVALID_PATH) - Path or key is null or invalid UTF-8
211/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
212///
213/// # Safety
214/// * `path` and `key` must be valid null-terminated C strings or null
215///
216/// _Requirements: 6.2, 6.3_
217#[no_mangle]
218pub extern "C" fn SYNA_put_int(path: *const c_char, key: *const c_char, value: i64) -> i64 {
219 std::panic::catch_unwind(|| {
220 // Validate pointers
221 if path.is_null() || key.is_null() {
222 return ERR_INVALID_PATH as i64;
223 }
224
225 // Convert C strings to Rust strings
226 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
227 Ok(s) => s,
228 Err(_) => return ERR_INVALID_PATH as i64,
229 };
230
231 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
232 Ok(s) => s,
233 Err(_) => return ERR_INVALID_PATH as i64,
234 };
235
236 // Call with_db to append the value
237 match with_db(path_str, |db| db.append(key_str, Atom::Int(value))) {
238 Ok(offset) => offset as i64,
239 Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND as i64,
240 Err(_) => ERR_GENERIC as i64,
241 }
242 })
243 .unwrap_or(ERR_INTERNAL_PANIC as i64)
244}
245
246/// Writes a text value to the database.
247///
248/// # Arguments
249/// * `path` - Null-terminated C string containing the path to the database file
250/// * `key` - Null-terminated C string containing the key
251/// * `value` - Null-terminated C string containing the text value to store
252///
253/// # Returns
254/// * Positive value - The byte offset where the entry was written
255/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
256/// * `-2` (ERR_INVALID_PATH) - Path, key, or value is null or invalid UTF-8
257/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
258///
259/// # Safety
260/// * `path`, `key`, and `value` must be valid null-terminated C strings or null
261///
262/// _Requirements: 6.2, 6.3_
263#[no_mangle]
264pub extern "C" fn SYNA_put_text(
265 path: *const c_char,
266 key: *const c_char,
267 value: *const c_char,
268) -> i64 {
269 std::panic::catch_unwind(|| {
270 // Validate pointers
271 if path.is_null() || key.is_null() || value.is_null() {
272 return ERR_INVALID_PATH as i64;
273 }
274
275 // Convert C strings to Rust strings
276 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
277 Ok(s) => s,
278 Err(_) => return ERR_INVALID_PATH as i64,
279 };
280
281 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
282 Ok(s) => s,
283 Err(_) => return ERR_INVALID_PATH as i64,
284 };
285
286 let value_str = match unsafe { CStr::from_ptr(value) }.to_str() {
287 Ok(s) => s,
288 Err(_) => return ERR_INVALID_PATH as i64,
289 };
290
291 // Call with_db to append the value
292 match with_db(path_str, |db| {
293 db.append(key_str, Atom::Text(value_str.to_string()))
294 }) {
295 Ok(offset) => offset as i64,
296 Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND as i64,
297 Err(_) => ERR_GENERIC as i64,
298 }
299 })
300 .unwrap_or(ERR_INTERNAL_PANIC as i64)
301}
302
303/// Writes a byte array to the database.
304///
305/// # Arguments
306/// * `path` - Null-terminated C string containing the path to the database file
307/// * `key` - Null-terminated C string containing the key
308/// * `data` - Pointer to the byte array to store
309/// * `len` - Length of the byte array
310///
311/// # Returns
312/// * Positive value - The byte offset where the entry was written
313/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
314/// * `-2` (ERR_INVALID_PATH) - Path, key, or data is null or invalid UTF-8
315/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
316///
317/// # Safety
318/// * `path` and `key` must be valid null-terminated C strings or null
319/// * `data` must be a valid pointer to at least `len` bytes, or null
320///
321/// _Requirements: 6.2, 6.4_
322#[no_mangle]
323pub extern "C" fn SYNA_put_bytes(
324 path: *const c_char,
325 key: *const c_char,
326 data: *const u8,
327 len: usize,
328) -> i64 {
329 std::panic::catch_unwind(|| {
330 // Validate pointers
331 if path.is_null() || key.is_null() || (data.is_null() && len > 0) {
332 return ERR_INVALID_PATH as i64;
333 }
334
335 // Convert C strings to Rust strings
336 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
337 Ok(s) => s,
338 Err(_) => return ERR_INVALID_PATH as i64,
339 };
340
341 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
342 Ok(s) => s,
343 Err(_) => return ERR_INVALID_PATH as i64,
344 };
345
346 // Create Vec<u8> from raw pointer and length
347 let bytes = if len == 0 {
348 Vec::new()
349 } else {
350 unsafe { std::slice::from_raw_parts(data, len) }.to_vec()
351 };
352
353 // Call with_db to append the value
354 match with_db(path_str, |db| db.append(key_str, Atom::Bytes(bytes))) {
355 Ok(offset) => offset as i64,
356 Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND as i64,
357 Err(_) => ERR_GENERIC as i64,
358 }
359 })
360 .unwrap_or(ERR_INTERNAL_PANIC as i64)
361}
362
363/// Writes multiple float values to the database in a single batch operation.
364///
365/// This is optimized for high-throughput ingestion scenarios. All values are
366/// written under the same key, building up a history that can be extracted
367/// as a tensor with `SYNA_get_history_tensor()`.
368///
369/// # Arguments
370/// * `path` - Null-terminated C string containing the path to the database file
371/// * `key` - Null-terminated C string containing the key
372/// * `values` - Pointer to array of f64 values
373/// * `count` - Number of values in the array
374///
375/// # Returns
376/// * Positive value - Number of values written
377/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
378/// * `-2` (ERR_INVALID_PATH) - Path, key, or values is null or invalid UTF-8
379/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
380///
381/// # Safety
382/// * `path` and `key` must be valid null-terminated C strings or null
383/// * `values` must be a valid pointer to at least `count` f64 values, or null if count is 0
384///
385/// # Performance
386/// This function is significantly faster than calling `SYNA_put_float()` in a loop:
387/// - Single FFI boundary crossing
388/// - Single mutex lock for all writes
389/// - Single fsync at the end (if sync_on_write is enabled)
390#[no_mangle]
391pub extern "C" fn SYNA_put_floats_batch(
392 path: *const c_char,
393 key: *const c_char,
394 values: *const f64,
395 count: usize,
396) -> i64 {
397 std::panic::catch_unwind(|| {
398 // Validate pointers
399 if path.is_null() || key.is_null() || (values.is_null() && count > 0) {
400 return ERR_INVALID_PATH as i64;
401 }
402
403 // Convert C strings to Rust strings
404 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
405 Ok(s) => s,
406 Err(_) => return ERR_INVALID_PATH as i64,
407 };
408
409 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
410 Ok(s) => s,
411 Err(_) => return ERR_INVALID_PATH as i64,
412 };
413
414 // Create slice from raw pointer
415 let slice = if count == 0 {
416 &[]
417 } else {
418 unsafe { std::slice::from_raw_parts(values, count) }
419 };
420
421 // Call with_db to batch append
422 match with_db(path_str, |db| db.append_floats_batch(key_str, slice)) {
423 Ok(n) => n as i64,
424 Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND as i64,
425 Err(_) => ERR_GENERIC as i64,
426 }
427 })
428 .unwrap_or(ERR_INTERNAL_PANIC as i64)
429}
430
431/// Reads a float value from the database.
432///
433/// # Arguments
434/// * `path` - Null-terminated C string containing the path to the database file
435/// * `key` - Null-terminated C string containing the key
436/// * `out` - Pointer to write the f64 value to
437///
438/// # Returns
439/// * `1` (ERR_SUCCESS) - Value read successfully
440/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
441/// * `-2` (ERR_INVALID_PATH) - Path, key, or out is null or invalid UTF-8
442/// * `-5` (ERR_KEY_NOT_FOUND) - Key not found in database
443/// * `-6` (ERR_TYPE_MISMATCH) - Value exists but is not a Float
444/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
445///
446/// # Safety
447/// * `path` and `key` must be valid null-terminated C strings or null
448/// * `out` must be a valid pointer to an f64
449///
450/// _Requirements: 6.2, 6.4_
451#[no_mangle]
452pub extern "C" fn SYNA_get_float(path: *const c_char, key: *const c_char, out: *mut f64) -> i32 {
453 std::panic::catch_unwind(|| {
454 // Validate pointers
455 if path.is_null() || key.is_null() || out.is_null() {
456 return ERR_INVALID_PATH;
457 }
458
459 // Convert C strings to Rust strings
460 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
461 Ok(s) => s,
462 Err(_) => return ERR_INVALID_PATH,
463 };
464
465 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
466 Ok(s) => s,
467 Err(_) => return ERR_INVALID_PATH,
468 };
469
470 // Call with_db to get the value
471 match with_db(path_str, |db| db.get(key_str)) {
472 Ok(Some(Atom::Float(f))) => {
473 // Write value to out pointer
474 unsafe { *out = f };
475 ERR_SUCCESS
476 }
477 Ok(Some(_)) => {
478 // Value exists but is not a Float
479 ERR_TYPE_MISMATCH
480 }
481 Ok(None) => {
482 // Key not found
483 ERR_KEY_NOT_FOUND
484 }
485 Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND,
486 Err(_) => ERR_GENERIC,
487 }
488 })
489 .unwrap_or(ERR_INTERNAL_PANIC)
490}
491
492/// Reads an integer value from the database.
493///
494/// # Arguments
495/// * `path` - Null-terminated C string containing the path to the database file
496/// * `key` - Null-terminated C string containing the key
497/// * `out` - Pointer to write the i64 value to
498///
499/// # Returns
500/// * `1` (ERR_SUCCESS) - Value read successfully
501/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
502/// * `-2` (ERR_INVALID_PATH) - Path, key, or out is null or invalid UTF-8
503/// * `-5` (ERR_KEY_NOT_FOUND) - Key not found in database
504/// * `-6` (ERR_TYPE_MISMATCH) - Value exists but is not an Int
505/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
506///
507/// # Safety
508/// * `path` and `key` must be valid null-terminated C strings or null
509/// * `out` must be a valid pointer to an i64
510///
511/// _Requirements: 6.2, 6.4_
512#[no_mangle]
513pub extern "C" fn SYNA_get_int(path: *const c_char, key: *const c_char, out: *mut i64) -> i32 {
514 std::panic::catch_unwind(|| {
515 // Validate pointers
516 if path.is_null() || key.is_null() || out.is_null() {
517 return ERR_INVALID_PATH;
518 }
519
520 // Convert C strings to Rust strings
521 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
522 Ok(s) => s,
523 Err(_) => return ERR_INVALID_PATH,
524 };
525
526 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
527 Ok(s) => s,
528 Err(_) => return ERR_INVALID_PATH,
529 };
530
531 // Call with_db to get the value
532 match with_db(path_str, |db| db.get(key_str)) {
533 Ok(Some(Atom::Int(i))) => {
534 // Write value to out pointer
535 unsafe { *out = i };
536 ERR_SUCCESS
537 }
538 Ok(Some(_)) => {
539 // Value exists but is not an Int
540 ERR_TYPE_MISMATCH
541 }
542 Ok(None) => {
543 // Key not found
544 ERR_KEY_NOT_FOUND
545 }
546 Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND,
547 Err(_) => ERR_GENERIC,
548 }
549 })
550 .unwrap_or(ERR_INTERNAL_PANIC)
551}
552
553/// Retrieves the complete history of float values for a key as a contiguous array.
554///
555/// This function is designed for AI/ML workloads where you need to feed time-series
556/// data directly to frameworks like PyTorch or TensorFlow.
557///
558/// # Arguments
559/// * `path` - Null-terminated C string containing the path to the database file
560/// * `key` - Null-terminated C string containing the key
561/// * `out_len` - Pointer to write the array length to
562///
563/// # Returns
564/// * Non-null pointer to contiguous f64 array on success
565/// * Null pointer on error (check out_len for error code)
566///
567/// # Error Codes (written to out_len on error)
568/// * `0` - Empty history (no float values for this key)
569///
570/// # Safety
571/// * `path` and `key` must be valid null-terminated C strings or null
572/// * `out_len` must be a valid pointer to a usize
573/// * The returned pointer MUST be freed using `SYNA_free_tensor()` to avoid memory leaks
574///
575/// _Requirements: 4.2, 6.4_
576#[no_mangle]
577pub extern "C" fn SYNA_get_history_tensor(
578 path: *const c_char,
579 key: *const c_char,
580 out_len: *mut usize,
581) -> *mut f64 {
582 std::panic::catch_unwind(|| {
583 // Validate pointers
584 if path.is_null() || key.is_null() || out_len.is_null() {
585 return std::ptr::null_mut();
586 }
587
588 // Convert C strings to Rust strings
589 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
590 Ok(s) => s,
591 Err(_) => return std::ptr::null_mut(),
592 };
593
594 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
595 Ok(s) => s,
596 Err(_) => return std::ptr::null_mut(),
597 };
598
599 // Call with_db to get the history tensor
600 match with_db(path_str, |db| db.get_history_tensor(key_str)) {
601 Ok((ptr, len)) => {
602 // Write length to out_len pointer
603 unsafe { *out_len = len };
604 ptr
605 }
606 Err(_) => {
607 // Set out_len to 0 on error
608 unsafe { *out_len = 0 };
609 std::ptr::null_mut()
610 }
611 }
612 })
613 .unwrap_or(std::ptr::null_mut())
614}
615
616/// Frees memory allocated by `SYNA_get_history_tensor()`.
617///
618/// # Arguments
619/// * `ptr` - Pointer returned by `SYNA_get_history_tensor()`
620/// * `len` - Length returned by `SYNA_get_history_tensor()`
621///
622/// # Safety
623/// * `ptr` must have been returned by `SYNA_get_history_tensor()`
624/// * `len` must be the length returned alongside the pointer
625/// * This function must only be called once per pointer
626/// * Calling with a null pointer or zero length is safe (no-op)
627///
628/// _Requirements: 4.3, 6.5_
629#[no_mangle]
630pub extern "C" fn SYNA_free_tensor(ptr: *mut f64, len: usize) {
631 std::panic::catch_unwind(|| {
632 // Call internal free_tensor (safe wrapper)
633 unsafe { free_tensor(ptr, len) };
634 })
635 .ok(); // Ignore panic result - we don't want to propagate panics from free
636}
637
638/// Deletes a key from the database by appending a tombstone entry.
639///
640/// # Arguments
641/// * `path` - Null-terminated C string containing the path to the database file
642/// * `key` - Null-terminated C string containing the key to delete
643///
644/// # Returns
645/// * `1` (ERR_SUCCESS) - Key deleted successfully
646/// * `0` (ERR_GENERIC) - Generic error during delete
647/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
648/// * `-2` (ERR_INVALID_PATH) - Path or key is null or invalid UTF-8
649/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
650///
651/// # Safety
652/// * `path` and `key` must be valid null-terminated C strings or null
653///
654/// _Requirements: 10.1_
655#[no_mangle]
656pub extern "C" fn SYNA_delete(path: *const c_char, key: *const c_char) -> i32 {
657 std::panic::catch_unwind(|| {
658 // Validate pointers
659 if path.is_null() || key.is_null() {
660 return ERR_INVALID_PATH;
661 }
662
663 // Convert C strings to Rust strings
664 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
665 Ok(s) => s,
666 Err(_) => return ERR_INVALID_PATH,
667 };
668
669 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
670 Ok(s) => s,
671 Err(_) => return ERR_INVALID_PATH,
672 };
673
674 // Call with_db to delete the key
675 match with_db(path_str, |db| db.delete(key_str)) {
676 Ok(_) => ERR_SUCCESS,
677 Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND,
678 Err(_) => ERR_GENERIC,
679 }
680 })
681 .unwrap_or(ERR_INTERNAL_PANIC)
682}
683
684/// Checks if a key exists in the database and is not deleted.
685///
686/// # Arguments
687/// * `path` - Null-terminated C string containing the path to the database file
688/// * `key` - Null-terminated C string containing the key to check
689///
690/// # Returns
691/// * `1` - Key exists and is not deleted
692/// * `0` - Key does not exist or is deleted
693/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
694/// * `-2` (ERR_INVALID_PATH) - Path or key is null or invalid UTF-8
695/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
696///
697/// # Safety
698/// * `path` and `key` must be valid null-terminated C strings or null
699///
700/// _Requirements: 10.2_
701#[no_mangle]
702pub extern "C" fn SYNA_exists(path: *const c_char, key: *const c_char) -> i32 {
703 std::panic::catch_unwind(|| {
704 // Validate pointers
705 if path.is_null() || key.is_null() {
706 return ERR_INVALID_PATH;
707 }
708
709 // Convert C strings to Rust strings
710 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
711 Ok(s) => s,
712 Err(_) => return ERR_INVALID_PATH,
713 };
714
715 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
716 Ok(s) => s,
717 Err(_) => return ERR_INVALID_PATH,
718 };
719
720 // Call with_db to check if key exists
721 match with_db(path_str, |db| Ok(db.exists(key_str))) {
722 Ok(true) => 1,
723 Ok(false) => 0,
724 Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND,
725 Err(_) => ERR_GENERIC,
726 }
727 })
728 .unwrap_or(ERR_INTERNAL_PANIC)
729}
730
731/// Compacts the database by rewriting only the latest non-deleted entries.
732///
733/// This operation reclaims disk space by removing deleted entries and old versions.
734/// After compaction, `get_history()` will only return the latest value for each key.
735///
736/// # Arguments
737/// * `path` - Null-terminated C string containing the path to the database file
738///
739/// # Returns
740/// * `1` (ERR_SUCCESS) - Compaction completed successfully
741/// * `0` (ERR_GENERIC) - Generic error during compaction
742/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
743/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
744/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
745///
746/// # Safety
747/// * `path` must be a valid null-terminated C string or null
748///
749/// _Requirements: 11.1_
750#[no_mangle]
751pub extern "C" fn SYNA_compact(path: *const c_char) -> i32 {
752 std::panic::catch_unwind(|| {
753 // Validate pointer
754 if path.is_null() {
755 return ERR_INVALID_PATH;
756 }
757
758 // Convert C string to Rust string
759 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
760 Ok(s) => s,
761 Err(_) => return ERR_INVALID_PATH,
762 };
763
764 // Call with_db to compact the database
765 match with_db(path_str, |db| db.compact()) {
766 Ok(_) => ERR_SUCCESS,
767 Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND,
768 Err(_) => ERR_GENERIC,
769 }
770 })
771 .unwrap_or(ERR_INTERNAL_PANIC)
772}
773
774/// Returns a list of all non-deleted keys in the database.
775///
776/// # Arguments
777/// * `path` - Null-terminated C string containing the path to the database file
778/// * `out_len` - Pointer to write the number of keys to
779///
780/// # Returns
781/// * Non-null pointer to array of null-terminated C strings on success
782/// * Null pointer on error
783///
784/// # Safety
785/// * `path` must be a valid null-terminated C string or null
786/// * `out_len` must be a valid pointer to a usize
787/// * The returned pointer MUST be freed using `SYNA_free_keys()` to avoid memory leaks
788///
789/// _Requirements: 10.5_
790#[no_mangle]
791pub extern "C" fn SYNA_keys(path: *const c_char, out_len: *mut usize) -> *mut *mut c_char {
792 std::panic::catch_unwind(|| {
793 // Validate pointers
794 if path.is_null() || out_len.is_null() {
795 return std::ptr::null_mut();
796 }
797
798 // Convert C string to Rust string
799 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
800 Ok(s) => s,
801 Err(_) => return std::ptr::null_mut(),
802 };
803
804 // Call with_db to get keys
805 match with_db(path_str, |db| Ok(db.keys())) {
806 Ok(keys) => {
807 let len = keys.len();
808
809 if len == 0 {
810 // No keys - return null with length 0
811 unsafe { *out_len = 0 };
812 return std::ptr::null_mut();
813 }
814
815 // Allocate array of C string pointers
816 let mut c_strings: Vec<*mut c_char> = Vec::with_capacity(len);
817
818 for key in keys {
819 // Convert each key to a C string
820 // Add null terminator
821 let mut bytes = key.into_bytes();
822 bytes.push(0); // null terminator
823
824 // Allocate memory for the C string
825 let c_str = bytes.into_boxed_slice();
826 let ptr = Box::into_raw(c_str) as *mut c_char;
827 c_strings.push(ptr);
828 }
829
830 // Write length to out_len
831 unsafe { *out_len = len };
832
833 // Convert Vec to boxed slice and leak for FFI
834 let boxed = c_strings.into_boxed_slice();
835 Box::into_raw(boxed) as *mut *mut c_char
836 }
837 Err(_) => {
838 unsafe { *out_len = 0 };
839 std::ptr::null_mut()
840 }
841 }
842 })
843 .unwrap_or(std::ptr::null_mut())
844}
845
846/// Frees memory allocated by `SYNA_keys()`.
847///
848/// # Arguments
849/// * `keys` - Pointer returned by `SYNA_keys()`
850/// * `len` - Length returned by `SYNA_keys()`
851///
852/// # Safety
853/// * `keys` must have been returned by `SYNA_keys()`
854/// * `len` must be the length returned alongside the pointer
855/// * This function must only be called once per pointer
856/// * Calling with a null pointer or zero length is safe (no-op)
857///
858/// _Requirements: 6.5_
859#[no_mangle]
860pub extern "C" fn SYNA_free_keys(keys: *mut *mut c_char, len: usize) {
861 std::panic::catch_unwind(|| {
862 if keys.is_null() || len == 0 {
863 return;
864 }
865
866 unsafe {
867 // Reconstruct the slice of pointers
868 let key_slice = std::slice::from_raw_parts_mut(keys, len);
869
870 // Free each individual string
871 for key_ptr in key_slice.iter() {
872 if !key_ptr.is_null() {
873 // Find the length of the C string (including null terminator)
874 let c_str = CStr::from_ptr(*key_ptr);
875 let str_len = c_str.to_bytes_with_nul().len();
876
877 // Reconstruct the box and drop it
878 let _ =
879 Box::from_raw(std::slice::from_raw_parts_mut(*key_ptr as *mut u8, str_len));
880 }
881 }
882
883 // Free the array itself
884 let _ = Box::from_raw(std::slice::from_raw_parts_mut(keys, len));
885 }
886 })
887 .ok(); // Ignore panic result - we don't want to propagate panics from free
888}
889
890/// Reads a text value from the database.
891///
892/// # Arguments
893/// * `path` - Null-terminated C string containing the path to the database file
894/// * `key` - Null-terminated C string containing the key
895/// * `out_len` - Pointer to write the string length to (excluding null terminator)
896///
897/// # Returns
898/// * Non-null pointer to null-terminated C string on success
899/// * Null pointer on error
900///
901/// # Error Codes (check return value)
902/// * Null with out_len = 0 - Key not found or type mismatch
903///
904/// # Safety
905/// * `path` and `key` must be valid null-terminated C strings or null
906/// * `out_len` must be a valid pointer to a usize
907/// * The returned pointer MUST be freed using `SYNA_free_text()` to avoid memory leaks
908///
909/// _Requirements: 6.2, 6.4_
910#[no_mangle]
911pub extern "C" fn SYNA_get_text(
912 path: *const c_char,
913 key: *const c_char,
914 out_len: *mut usize,
915) -> *mut c_char {
916 std::panic::catch_unwind(|| {
917 // Validate pointers
918 if path.is_null() || key.is_null() || out_len.is_null() {
919 return std::ptr::null_mut();
920 }
921
922 // Convert C strings to Rust strings
923 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
924 Ok(s) => s,
925 Err(_) => return std::ptr::null_mut(),
926 };
927
928 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
929 Ok(s) => s,
930 Err(_) => return std::ptr::null_mut(),
931 };
932
933 // Call with_db to get the value
934 match with_db(path_str, |db| db.get(key_str)) {
935 Ok(Some(Atom::Text(s))) => {
936 let len = s.len();
937 unsafe { *out_len = len };
938
939 // Convert to C string with null terminator
940 let mut bytes = s.into_bytes();
941 bytes.push(0); // null terminator
942
943 let c_str = bytes.into_boxed_slice();
944 Box::into_raw(c_str) as *mut c_char
945 }
946 Ok(Some(_)) => {
947 // Value exists but is not Text
948 unsafe { *out_len = 0 };
949 std::ptr::null_mut()
950 }
951 Ok(None) => {
952 // Key not found
953 unsafe { *out_len = 0 };
954 std::ptr::null_mut()
955 }
956 Err(_) => {
957 unsafe { *out_len = 0 };
958 std::ptr::null_mut()
959 }
960 }
961 })
962 .unwrap_or(std::ptr::null_mut())
963}
964
965/// Frees memory allocated by `SYNA_get_text()`.
966///
967/// # Arguments
968/// * `ptr` - Pointer returned by `SYNA_get_text()`
969/// * `len` - Length returned by `SYNA_get_text()` (excluding null terminator)
970///
971/// # Safety
972/// * `ptr` must have been returned by `SYNA_get_text()`
973/// * `len` must be the length returned alongside the pointer
974/// * This function must only be called once per pointer
975/// * Calling with a null pointer is safe (no-op)
976///
977/// _Requirements: 6.5_
978#[no_mangle]
979pub extern "C" fn SYNA_free_text(ptr: *mut c_char, len: usize) {
980 std::panic::catch_unwind(|| {
981 if ptr.is_null() {
982 return;
983 }
984
985 unsafe {
986 // Reconstruct the box (len + 1 for null terminator)
987 let _ = Box::from_raw(std::slice::from_raw_parts_mut(ptr as *mut u8, len + 1));
988 }
989 })
990 .ok();
991}
992
993/// Reads a byte array from the database.
994///
995/// # Arguments
996/// * `path` - Null-terminated C string containing the path to the database file
997/// * `key` - Null-terminated C string containing the key
998/// * `out_len` - Pointer to write the array length to
999///
1000/// # Returns
1001/// * Non-null pointer to byte array on success
1002/// * Null pointer on error
1003///
1004/// # Error Codes (check return value)
1005/// * Null with out_len = 0 - Key not found or type mismatch
1006///
1007/// # Safety
1008/// * `path` and `key` must be valid null-terminated C strings or null
1009/// * `out_len` must be a valid pointer to a usize
1010/// * The returned pointer MUST be freed using `SYNA_free_bytes()` to avoid memory leaks
1011///
1012/// _Requirements: 6.2, 6.4_
1013#[no_mangle]
1014pub extern "C" fn SYNA_get_bytes(
1015 path: *const c_char,
1016 key: *const c_char,
1017 out_len: *mut usize,
1018) -> *mut u8 {
1019 std::panic::catch_unwind(|| {
1020 // Validate pointers
1021 if path.is_null() || key.is_null() || out_len.is_null() {
1022 return std::ptr::null_mut();
1023 }
1024
1025 // Convert C strings to Rust strings
1026 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1027 Ok(s) => s,
1028 Err(_) => return std::ptr::null_mut(),
1029 };
1030
1031 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
1032 Ok(s) => s,
1033 Err(_) => return std::ptr::null_mut(),
1034 };
1035
1036 // Call with_db to get the value
1037 match with_db(path_str, |db| db.get(key_str)) {
1038 Ok(Some(Atom::Bytes(bytes))) => {
1039 let len = bytes.len();
1040
1041 if len == 0 {
1042 unsafe { *out_len = 0 };
1043 return std::ptr::null_mut();
1044 }
1045
1046 unsafe { *out_len = len };
1047
1048 // Convert to boxed slice and leak for FFI
1049 let boxed = bytes.into_boxed_slice();
1050 Box::into_raw(boxed) as *mut u8
1051 }
1052 Ok(Some(_)) => {
1053 // Value exists but is not Bytes
1054 unsafe { *out_len = 0 };
1055 std::ptr::null_mut()
1056 }
1057 Ok(None) => {
1058 // Key not found
1059 unsafe { *out_len = 0 };
1060 std::ptr::null_mut()
1061 }
1062 Err(_) => {
1063 unsafe { *out_len = 0 };
1064 std::ptr::null_mut()
1065 }
1066 }
1067 })
1068 .unwrap_or(std::ptr::null_mut())
1069}
1070
1071/// Frees memory allocated by `SYNA_get_bytes()`.
1072///
1073/// # Arguments
1074/// * `ptr` - Pointer returned by `SYNA_get_bytes()`
1075/// * `len` - Length returned by `SYNA_get_bytes()`
1076///
1077/// # Safety
1078/// * `ptr` must have been returned by `SYNA_get_bytes()`
1079/// * `len` must be the length returned alongside the pointer
1080/// * This function must only be called once per pointer
1081/// * Calling with a null pointer or zero length is safe (no-op)
1082///
1083/// _Requirements: 6.5_
1084#[no_mangle]
1085pub extern "C" fn SYNA_free_bytes(ptr: *mut u8, len: usize) {
1086 std::panic::catch_unwind(|| {
1087 if ptr.is_null() || len == 0 {
1088 return;
1089 }
1090
1091 unsafe {
1092 let _ = Box::from_raw(std::slice::from_raw_parts_mut(ptr, len));
1093 }
1094 })
1095 .ok();
1096}
1097
1098/* ============================================================================
1099 * Vector Functions (AI/ML Embeddings)
1100 * ============================================================================ */
1101
1102/// Stores a vector (embedding) in the database.
1103///
1104/// Vectors are stored as `Atom::Vector(Vec<f32>, u16)` where the second
1105/// element is the dimensionality. This is optimized for AI/ML embedding
1106/// storage and similarity search.
1107///
1108/// # Arguments
1109/// * `path` - Null-terminated C string containing the path to the database file
1110/// * `key` - Null-terminated C string containing the key
1111/// * `data` - Pointer to f32 array containing the vector data
1112/// * `dimensions` - Number of dimensions (elements) in the vector
1113///
1114/// # Returns
1115/// * Positive value - The byte offset where the entry was written
1116/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
1117/// * `-2` (ERR_INVALID_PATH) - Path, key, or data is null or invalid UTF-8
1118/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1119///
1120/// # Safety
1121/// * `path` and `key` must be valid null-terminated C strings or null
1122/// * `data` must be a valid pointer to at least `dimensions` f32 values, or null
1123///
1124/// _Requirements: 1.1_
1125#[no_mangle]
1126pub extern "C" fn SYNA_put_vector(
1127 path: *const c_char,
1128 key: *const c_char,
1129 data: *const f32,
1130 dimensions: u16,
1131) -> i64 {
1132 std::panic::catch_unwind(|| {
1133 // Validate pointers
1134 if path.is_null() || key.is_null() || (data.is_null() && dimensions > 0) {
1135 return ERR_INVALID_PATH as i64;
1136 }
1137
1138 // Convert C strings to Rust strings
1139 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1140 Ok(s) => s,
1141 Err(_) => return ERR_INVALID_PATH as i64,
1142 };
1143
1144 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
1145 Ok(s) => s,
1146 Err(_) => return ERR_INVALID_PATH as i64,
1147 };
1148
1149 // Create Vec<f32> from raw pointer and dimensions
1150 let vector = if dimensions == 0 {
1151 Vec::new()
1152 } else {
1153 unsafe { std::slice::from_raw_parts(data, dimensions as usize) }.to_vec()
1154 };
1155
1156 // Call with_db to append the vector
1157 match with_db(path_str, |db| {
1158 db.append(key_str, Atom::Vector(vector, dimensions))
1159 }) {
1160 Ok(offset) => offset as i64,
1161 Err(crate::error::SynaError::NotFound(_)) => crate::error::ERR_DB_NOT_FOUND as i64,
1162 Err(_) => ERR_GENERIC as i64,
1163 }
1164 })
1165 .unwrap_or(ERR_INTERNAL_PANIC as i64)
1166}
1167
1168/// Retrieves a vector (embedding) from the database.
1169///
1170/// # Arguments
1171/// * `path` - Null-terminated C string containing the path to the database file
1172/// * `key` - Null-terminated C string containing the key
1173/// * `out_data` - Pointer to store the allocated f32 array pointer
1174/// * `out_dimensions` - Pointer to store the number of dimensions
1175///
1176/// # Returns
1177/// * `1` (ERR_SUCCESS) - Vector retrieved successfully
1178/// * `0` (ERR_GENERIC) - Generic error
1179/// * `-1` (ERR_DB_NOT_FOUND) - Database not found in registry
1180/// * `-2` (ERR_INVALID_PATH) - Path, key, or output pointers are null or invalid UTF-8
1181/// * `-5` (ERR_KEY_NOT_FOUND) - Key not found in database
1182/// * `-6` (ERR_TYPE_MISMATCH) - Value exists but is not a Vector
1183/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1184///
1185/// # Safety
1186/// * `path` and `key` must be valid null-terminated C strings or null
1187/// * `out_data` must be a valid pointer to a `*mut f32`
1188/// * `out_dimensions` must be a valid pointer to a `u16`
1189/// * The returned data pointer MUST be freed using `SYNA_free_vector()` to avoid memory leaks
1190///
1191/// _Requirements: 1.1_
1192#[no_mangle]
1193pub extern "C" fn SYNA_get_vector(
1194 path: *const c_char,
1195 key: *const c_char,
1196 out_data: *mut *mut f32,
1197 out_dimensions: *mut u16,
1198) -> i32 {
1199 std::panic::catch_unwind(|| {
1200 // Validate pointers
1201 if path.is_null() || key.is_null() || out_data.is_null() || out_dimensions.is_null() {
1202 return ERR_INVALID_PATH;
1203 }
1204
1205 // Convert C strings to Rust strings
1206 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1207 Ok(s) => s,
1208 Err(_) => return ERR_INVALID_PATH,
1209 };
1210
1211 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
1212 Ok(s) => s,
1213 Err(_) => return ERR_INVALID_PATH,
1214 };
1215
1216 // Call with_db to get the value
1217 match with_db(path_str, |db| db.get(key_str)) {
1218 Ok(Some(Atom::Vector(vec_data, dims))) => {
1219 // Write dimensions to output
1220 unsafe { *out_dimensions = dims };
1221
1222 if vec_data.is_empty() {
1223 // Empty vector - return null pointer with 0 dimensions
1224 unsafe { *out_data = std::ptr::null_mut() };
1225 return ERR_SUCCESS;
1226 }
1227
1228 // Convert to boxed slice and leak for FFI
1229 let boxed = vec_data.into_boxed_slice();
1230 unsafe { *out_data = Box::into_raw(boxed) as *mut f32 };
1231
1232 ERR_SUCCESS
1233 }
1234 Ok(Some(_)) => {
1235 // Value exists but is not a Vector
1236 unsafe {
1237 *out_data = std::ptr::null_mut();
1238 *out_dimensions = 0;
1239 }
1240 ERR_TYPE_MISMATCH
1241 }
1242 Ok(None) => {
1243 // Key not found
1244 unsafe {
1245 *out_data = std::ptr::null_mut();
1246 *out_dimensions = 0;
1247 }
1248 ERR_KEY_NOT_FOUND
1249 }
1250 Err(crate::error::SynaError::NotFound(_)) => {
1251 unsafe {
1252 *out_data = std::ptr::null_mut();
1253 *out_dimensions = 0;
1254 }
1255 crate::error::ERR_DB_NOT_FOUND
1256 }
1257 Err(_) => {
1258 unsafe {
1259 *out_data = std::ptr::null_mut();
1260 *out_dimensions = 0;
1261 }
1262 ERR_GENERIC
1263 }
1264 }
1265 })
1266 .unwrap_or(ERR_INTERNAL_PANIC)
1267}
1268
1269/// Frees memory allocated by `SYNA_get_vector()`.
1270///
1271/// # Arguments
1272/// * `data` - Pointer returned by `SYNA_get_vector()` in `out_data`
1273/// * `dimensions` - Dimensions returned by `SYNA_get_vector()` in `out_dimensions`
1274///
1275/// # Safety
1276/// * `data` must have been returned by `SYNA_get_vector()`
1277/// * `dimensions` must be the dimensions returned alongside the pointer
1278/// * This function must only be called once per pointer
1279/// * Calling with a null pointer or zero dimensions is safe (no-op)
1280///
1281/// _Requirements: 1.1_
1282#[no_mangle]
1283pub extern "C" fn SYNA_free_vector(data: *mut f32, dimensions: u16) {
1284 std::panic::catch_unwind(|| {
1285 if data.is_null() || dimensions == 0 {
1286 return;
1287 }
1288
1289 unsafe {
1290 let _ = Box::from_raw(std::slice::from_raw_parts_mut(data, dimensions as usize));
1291 }
1292 })
1293 .ok(); // Ignore panic result - we don't want to propagate panics from free
1294}
1295
1296// =============================================================================
1297// VectorStore FFI Functions
1298// =============================================================================
1299
1300use std::collections::HashMap;
1301use std::ffi::CString;
1302use std::path::{Path, PathBuf};
1303
1304use once_cell::sync::Lazy;
1305use parking_lot::Mutex;
1306
1307use crate::distance::DistanceMetric;
1308use crate::vector::{VectorConfig, VectorStore};
1309
1310/// Thread-safe global registry for managing open VectorStore instances.
1311/// Uses canonicalized paths as keys to ensure uniqueness.
1312static VECTOR_STORE_REGISTRY: Lazy<Mutex<HashMap<String, VectorStore>>> =
1313 Lazy::new(|| Mutex::new(HashMap::new()));
1314
1315/// Canonicalizes a path to an absolute path string for consistent registry keys.
1316///
1317/// If the path doesn't exist yet (for new databases), we use the parent directory's
1318/// canonical path combined with the filename.
1319fn canonicalize_vector_path(path: &str) -> Option<String> {
1320 let path_buf = PathBuf::from(path);
1321
1322 // Try to canonicalize directly (works if file exists)
1323 if let Ok(canonical) = std::fs::canonicalize(&path_buf) {
1324 return Some(canonical.to_string_lossy().to_string());
1325 }
1326
1327 // File doesn't exist yet - canonicalize parent and append filename
1328 let parent = path_buf.parent().unwrap_or(Path::new("."));
1329 let filename = path_buf.file_name()?;
1330
1331 // Canonicalize parent directory
1332 let canonical_parent = if parent.as_os_str().is_empty() || parent == Path::new(".") {
1333 std::env::current_dir().ok()?
1334 } else {
1335 std::fs::canonicalize(parent).ok()?
1336 };
1337
1338 let canonical = canonical_parent.join(filename);
1339 Some(canonical.to_string_lossy().to_string())
1340}
1341
1342/// Creates a new vector store at the given path.
1343///
1344/// # Arguments
1345/// * `path` - Null-terminated C string containing the path to the database file
1346/// * `dimensions` - Number of dimensions for vectors (64-8192)
1347/// * `metric` - Distance metric: 0=Cosine, 1=Euclidean, 2=DotProduct
1348///
1349/// # Returns
1350/// * `1` (ERR_SUCCESS) - Vector store created successfully
1351/// * `0` (ERR_GENERIC) - Generic error during creation
1352/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
1353/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1354///
1355/// # Safety
1356/// * `path` must be a valid null-terminated C string or null
1357///
1358/// _Requirements: 8.1_
1359#[no_mangle]
1360pub extern "C" fn SYNA_vector_store_new(path: *const c_char, dimensions: u16, metric: i32) -> i32 {
1361 std::panic::catch_unwind(|| {
1362 // Check for null pointer
1363 if path.is_null() {
1364 return ERR_INVALID_PATH;
1365 }
1366
1367 // Convert C string to Rust string
1368 let c_str = unsafe { CStr::from_ptr(path) };
1369 let path_str = match c_str.to_str() {
1370 Ok(s) => s,
1371 Err(_) => return ERR_INVALID_PATH,
1372 };
1373
1374 // Convert metric integer to DistanceMetric enum
1375 let distance_metric = match metric {
1376 0 => DistanceMetric::Cosine,
1377 1 => DistanceMetric::Euclidean,
1378 2 => DistanceMetric::DotProduct,
1379 _ => DistanceMetric::Cosine, // Default to Cosine for invalid values
1380 };
1381
1382 // Create config
1383 let config = VectorConfig {
1384 dimensions,
1385 metric: distance_metric,
1386 ..Default::default()
1387 };
1388
1389 // Canonicalize path for consistent registry keys
1390 let canonical_path = match canonicalize_vector_path(path_str) {
1391 Some(p) => p,
1392 None => return ERR_INVALID_PATH,
1393 };
1394
1395 // Create the vector store
1396 match VectorStore::new(path_str, config) {
1397 Ok(store) => {
1398 // Register in global registry with canonicalized path
1399 let mut registry = VECTOR_STORE_REGISTRY.lock();
1400 registry.insert(canonical_path, store);
1401 ERR_SUCCESS
1402 }
1403 Err(_) => ERR_GENERIC,
1404 }
1405 })
1406 .unwrap_or(ERR_INTERNAL_PANIC)
1407}
1408
1409/// Creates a new vector store with sync_on_write configuration.
1410///
1411/// # Arguments
1412/// * `path` - Null-terminated C string containing the path to the database file
1413/// * `dimensions` - Number of dimensions for vectors (64-8192)
1414/// * `metric` - Distance metric: 0=Cosine, 1=Euclidean, 2=DotProduct
1415/// * `sync_on_write` - 1 for sync after each write (durable), 0 for no sync (fast)
1416///
1417/// # Returns
1418/// * `1` (ERR_SUCCESS) - Vector store created successfully
1419/// * `0` (ERR_GENERIC) - Generic error during creation
1420/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
1421/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1422///
1423/// # Safety
1424/// * `path` must be a valid null-terminated C string or null
1425#[no_mangle]
1426pub extern "C" fn SYNA_vector_store_new_with_config(
1427 path: *const c_char,
1428 dimensions: u16,
1429 metric: i32,
1430 sync_on_write: i32,
1431) -> i32 {
1432 std::panic::catch_unwind(|| {
1433 // Check for null pointer
1434 if path.is_null() {
1435 return ERR_INVALID_PATH;
1436 }
1437
1438 // Convert C string to Rust string
1439 let c_str = unsafe { CStr::from_ptr(path) };
1440 let path_str = match c_str.to_str() {
1441 Ok(s) => s,
1442 Err(_) => return ERR_INVALID_PATH,
1443 };
1444
1445 // Convert metric integer to DistanceMetric enum
1446 let distance_metric = match metric {
1447 0 => DistanceMetric::Cosine,
1448 1 => DistanceMetric::Euclidean,
1449 2 => DistanceMetric::DotProduct,
1450 _ => DistanceMetric::Cosine,
1451 };
1452
1453 // Create config with sync_on_write setting
1454 let config = VectorConfig {
1455 dimensions,
1456 metric: distance_metric,
1457 sync_on_write: sync_on_write != 0,
1458 ..Default::default()
1459 };
1460
1461 // Canonicalize path for consistent registry keys
1462 let canonical_path = match canonicalize_vector_path(path_str) {
1463 Some(p) => p,
1464 None => return ERR_INVALID_PATH,
1465 };
1466
1467 // Create the vector store
1468 match VectorStore::new(path_str, config) {
1469 Ok(store) => {
1470 // Register in global registry with canonicalized path
1471 let mut registry = VECTOR_STORE_REGISTRY.lock();
1472 registry.insert(canonical_path, store);
1473 ERR_SUCCESS
1474 }
1475 Err(_) => ERR_GENERIC,
1476 }
1477 })
1478 .unwrap_or(ERR_INTERNAL_PANIC)
1479}
1480
1481/// Inserts a vector into the store.
1482///
1483/// # Arguments
1484/// * `path` - Null-terminated C string containing the path to the database file
1485/// * `key` - Null-terminated C string containing the key
1486/// * `data` - Pointer to the f32 vector data
1487/// * `dimensions` - Number of dimensions in the vector
1488///
1489/// # Returns
1490/// * `1` (ERR_SUCCESS) - Vector inserted successfully
1491/// * `0` (ERR_GENERIC) - Generic error during insertion
1492/// * `-1` (ERR_DB_NOT_FOUND) - Vector store not found in registry
1493/// * `-2` (ERR_INVALID_PATH) - Path, key, or data is null or invalid UTF-8
1494/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1495///
1496/// # Safety
1497/// * `path` and `key` must be valid null-terminated C strings or null
1498/// * `data` must be a valid pointer to at least `dimensions` f32 values
1499///
1500/// _Requirements: 8.1_
1501#[no_mangle]
1502pub extern "C" fn SYNA_vector_store_insert(
1503 path: *const c_char,
1504 key: *const c_char,
1505 data: *const f32,
1506 dimensions: u16,
1507) -> i32 {
1508 std::panic::catch_unwind(|| {
1509 // Validate pointers
1510 if path.is_null() || key.is_null() || (data.is_null() && dimensions > 0) {
1511 return ERR_INVALID_PATH;
1512 }
1513
1514 // Convert C strings to Rust strings
1515 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1516 Ok(s) => s,
1517 Err(_) => return ERR_INVALID_PATH,
1518 };
1519
1520 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
1521 Ok(s) => s,
1522 Err(_) => return ERR_INVALID_PATH,
1523 };
1524
1525 // Canonicalize path for consistent registry keys
1526 let canonical_path = match canonicalize_vector_path(path_str) {
1527 Some(p) => p,
1528 None => return ERR_INVALID_PATH,
1529 };
1530
1531 // Create vector from raw pointer
1532 let vector = if dimensions == 0 {
1533 Vec::new()
1534 } else {
1535 unsafe { std::slice::from_raw_parts(data, dimensions as usize) }.to_vec()
1536 };
1537
1538 // Get the vector store from registry using canonicalized path
1539 let mut registry = VECTOR_STORE_REGISTRY.lock();
1540 match registry.get_mut(&canonical_path) {
1541 Some(store) => match store.insert(key_str, &vector) {
1542 Ok(_) => ERR_SUCCESS,
1543 Err(_) => ERR_GENERIC,
1544 },
1545 None => crate::error::ERR_DB_NOT_FOUND,
1546 }
1547 })
1548 .unwrap_or(ERR_INTERNAL_PANIC)
1549}
1550
1551/// Inserts multiple vectors in a single batch operation.
1552///
1553/// This is significantly faster than calling `SYNA_vector_store_insert()` in a loop:
1554/// - Single FFI boundary crossing for all vectors
1555/// - Deferred index building until after all vectors are inserted
1556/// - Reduced lock contention
1557///
1558/// # Arguments
1559/// * `path` - Null-terminated C string containing the path to the database file
1560/// * `keys` - Array of null-terminated C strings (keys for each vector)
1561/// * `data` - Pointer to contiguous f32 array containing all vectors (row-major)
1562/// * `dimensions` - Number of dimensions per vector
1563/// * `count` - Number of vectors to insert
1564///
1565/// # Returns
1566/// * Non-negative value - Number of vectors successfully inserted
1567/// * `-1` (ERR_DB_NOT_FOUND) - Vector store not found in registry
1568/// * `-2` (ERR_INVALID_PATH) - Path, keys, or data is null or invalid
1569/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1570///
1571/// # Safety
1572/// * `path` must be a valid null-terminated C string
1573/// * `keys` must be a valid pointer to `count` null-terminated C strings
1574/// * `data` must be a valid pointer to `count * dimensions` f32 values
1575///
1576/// # Example (C)
1577/// ```c
1578/// const char* keys[] = {"doc1", "doc2", "doc3"};
1579/// float data[3 * 768] = { ... }; // 3 vectors of 768 dimensions
1580/// int32_t inserted = SYNA_vector_store_insert_batch("vectors.db", keys, data, 768, 3);
1581/// ```
1582///
1583/// _Requirements: 8.1_
1584#[no_mangle]
1585pub extern "C" fn SYNA_vector_store_insert_batch(
1586 path: *const c_char,
1587 keys: *const *const c_char,
1588 data: *const f32,
1589 dimensions: u16,
1590 count: usize,
1591) -> i32 {
1592 std::panic::catch_unwind(|| {
1593 // Validate pointers
1594 if path.is_null() || keys.is_null() || (data.is_null() && count > 0 && dimensions > 0) {
1595 return ERR_INVALID_PATH;
1596 }
1597
1598 // Convert path
1599 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1600 Ok(s) => s,
1601 Err(_) => return ERR_INVALID_PATH,
1602 };
1603
1604 // Canonicalize path
1605 let canonical_path = match canonicalize_vector_path(path_str) {
1606 Some(p) => p,
1607 None => return ERR_INVALID_PATH,
1608 };
1609
1610 // Convert keys array
1611 let keys_slice = unsafe { std::slice::from_raw_parts(keys, count) };
1612 let mut key_strings: Vec<&str> = Vec::with_capacity(count);
1613 for key_ptr in keys_slice {
1614 if key_ptr.is_null() {
1615 return ERR_INVALID_PATH;
1616 }
1617 match unsafe { CStr::from_ptr(*key_ptr) }.to_str() {
1618 Ok(s) => key_strings.push(s),
1619 Err(_) => return ERR_INVALID_PATH,
1620 }
1621 }
1622
1623 // Create vector slices from contiguous data
1624 let total_floats = count * dimensions as usize;
1625 let data_slice = if total_floats == 0 {
1626 &[]
1627 } else {
1628 unsafe { std::slice::from_raw_parts(data, total_floats) }
1629 };
1630
1631 // Split into individual vectors
1632 let vectors: Vec<&[f32]> = data_slice.chunks(dimensions as usize).collect();
1633
1634 // Get the vector store from registry
1635 let mut registry = VECTOR_STORE_REGISTRY.lock();
1636 match registry.get_mut(&canonical_path) {
1637 Some(store) => match store.insert_batch(&key_strings, &vectors) {
1638 Ok(n) => n as i32,
1639 Err(_) => ERR_GENERIC,
1640 },
1641 None => crate::error::ERR_DB_NOT_FOUND,
1642 }
1643 })
1644 .unwrap_or(ERR_INTERNAL_PANIC)
1645}
1646
1647/// Inserts multiple vectors without updating the index (maximum write speed).
1648///
1649/// This is the fastest way to bulk-load vectors. Vectors are written to storage
1650/// but NOT added to the HNSW index. Call `SYNA_vector_store_build_index()` after
1651/// all inserts to build the index.
1652///
1653/// # Arguments
1654/// * `path` - Null-terminated C string containing the path to the database file
1655/// * `keys` - Array of null-terminated C strings (keys for each vector)
1656/// * `data` - Pointer to contiguous f32 array containing all vectors (row-major)
1657/// * `dimensions` - Number of dimensions per vector
1658/// * `count` - Number of vectors to insert
1659///
1660/// # Returns
1661/// * Non-negative value - Number of vectors successfully inserted
1662/// * `-1` (ERR_DB_NOT_FOUND) - Vector store not found in registry
1663/// * `-2` (ERR_INVALID_PATH) - Path, keys, or data is null or invalid
1664/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1665///
1666/// # Performance
1667/// This function achieves 100K+ inserts/sec by skipping index updates.
1668/// After bulk loading, call `SYNA_vector_store_build_index()` to enable fast search.
1669///
1670/// _Requirements: 8.1_
1671#[no_mangle]
1672pub extern "C" fn SYNA_vector_store_insert_batch_fast(
1673 path: *const c_char,
1674 keys: *const *const c_char,
1675 data: *const f32,
1676 dimensions: u16,
1677 count: usize,
1678) -> i32 {
1679 std::panic::catch_unwind(|| {
1680 if path.is_null() || keys.is_null() || (data.is_null() && count > 0 && dimensions > 0) {
1681 return ERR_INVALID_PATH;
1682 }
1683
1684 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1685 Ok(s) => s,
1686 Err(_) => return ERR_INVALID_PATH,
1687 };
1688
1689 let canonical_path = match canonicalize_vector_path(path_str) {
1690 Some(p) => p,
1691 None => return ERR_INVALID_PATH,
1692 };
1693
1694 let keys_slice = unsafe { std::slice::from_raw_parts(keys, count) };
1695 let mut key_strings: Vec<&str> = Vec::with_capacity(count);
1696 for key_ptr in keys_slice {
1697 if key_ptr.is_null() {
1698 return ERR_INVALID_PATH;
1699 }
1700 match unsafe { CStr::from_ptr(*key_ptr) }.to_str() {
1701 Ok(s) => key_strings.push(s),
1702 Err(_) => return ERR_INVALID_PATH,
1703 }
1704 }
1705
1706 let total_floats = count * dimensions as usize;
1707 let data_slice = if total_floats == 0 {
1708 &[]
1709 } else {
1710 unsafe { std::slice::from_raw_parts(data, total_floats) }
1711 };
1712
1713 let vectors: Vec<&[f32]> = data_slice.chunks(dimensions as usize).collect();
1714
1715 let mut registry = VECTOR_STORE_REGISTRY.lock();
1716 match registry.get_mut(&canonical_path) {
1717 Some(store) => match store.insert_batch_fast(&key_strings, &vectors, false) {
1718 Ok(n) => n as i32,
1719 Err(_) => ERR_GENERIC,
1720 },
1721 None => crate::error::ERR_DB_NOT_FOUND,
1722 }
1723 })
1724 .unwrap_or(ERR_INTERNAL_PANIC)
1725}
1726
1727/// Searches for k nearest neighbors in the vector store.
1728///
1729/// Returns a JSON array of results with the following structure:
1730/// ```json
1731/// [
1732/// {"key": "doc1", "score": 0.123, "vector": [0.1, 0.2, ...]},
1733/// {"key": "doc2", "score": 0.456, "vector": [0.3, 0.4, ...]}
1734/// ]
1735/// ```
1736///
1737/// # Arguments
1738/// * `path` - Null-terminated C string containing the path to the database file
1739/// * `query` - Pointer to the f32 query vector
1740/// * `dimensions` - Number of dimensions in the query vector
1741/// * `k` - Number of nearest neighbors to return
1742/// * `out_json` - Pointer to write the JSON result string to
1743///
1744/// # Returns
1745/// * Non-negative value - Number of results found
1746/// * `-1` (ERR_DB_NOT_FOUND) - Vector store not found in registry
1747/// * `-2` (ERR_INVALID_PATH) - Path, query, or out_json is null or invalid
1748/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1749///
1750/// # Safety
1751/// * `path` must be a valid null-terminated C string or null
1752/// * `query` must be a valid pointer to at least `dimensions` f32 values
1753/// * `out_json` must be a valid pointer to a `*mut c_char`
1754/// * The returned JSON string MUST be freed using `SYNA_free_json()` to avoid memory leaks
1755///
1756/// _Requirements: 8.1_
1757#[no_mangle]
1758pub extern "C" fn SYNA_vector_store_search(
1759 path: *const c_char,
1760 query: *const f32,
1761 dimensions: u16,
1762 k: usize,
1763 out_json: *mut *mut c_char,
1764) -> i32 {
1765 std::panic::catch_unwind(|| {
1766 // Validate pointers
1767 if path.is_null() || query.is_null() || out_json.is_null() {
1768 return ERR_INVALID_PATH;
1769 }
1770
1771 // Convert C string to Rust string
1772 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1773 Ok(s) => s,
1774 Err(_) => return ERR_INVALID_PATH,
1775 };
1776
1777 // Canonicalize path for consistent registry keys
1778 let canonical_path = match canonicalize_vector_path(path_str) {
1779 Some(p) => p,
1780 None => return ERR_INVALID_PATH,
1781 };
1782
1783 // Create query vector from raw pointer
1784 let query_vec: Vec<f32> =
1785 unsafe { std::slice::from_raw_parts(query, dimensions as usize) }.to_vec();
1786
1787 // Get the vector store from registry using canonicalized path
1788 let mut registry = VECTOR_STORE_REGISTRY.lock();
1789 match registry.get_mut(&canonical_path) {
1790 Some(store) => match store.search(&query_vec, k) {
1791 Ok(results) => {
1792 // Convert results to JSON
1793 let json_results: Vec<serde_json::Value> = results
1794 .iter()
1795 .map(|r| {
1796 serde_json::json!({
1797 "key": r.key,
1798 "score": r.score,
1799 "vector": r.vector
1800 })
1801 })
1802 .collect();
1803
1804 let json_str =
1805 serde_json::to_string(&json_results).unwrap_or_else(|_| "[]".to_string());
1806 let result_count = results.len() as i32;
1807
1808 // Convert to C string
1809 match CString::new(json_str) {
1810 Ok(c_string) => {
1811 unsafe { *out_json = c_string.into_raw() };
1812 result_count
1813 }
1814 Err(_) => ERR_GENERIC,
1815 }
1816 }
1817 Err(_) => ERR_GENERIC,
1818 },
1819 None => crate::error::ERR_DB_NOT_FOUND,
1820 }
1821 })
1822 .unwrap_or(ERR_INTERNAL_PANIC)
1823}
1824
1825/// Builds the HNSW index for a vector store.
1826///
1827/// This function manually triggers HNSW index construction for faster search.
1828/// The index is built automatically when vector count exceeds the threshold,
1829/// but this function allows explicit control.
1830///
1831/// # Arguments
1832/// * `path` - Null-terminated C string containing the path to the vector store
1833///
1834/// # Returns
1835/// * `1` (ERR_SUCCESS) - Index built successfully
1836/// * `0` (ERR_GENERIC) - Generic error during build
1837/// * `-1` (ERR_DB_NOT_FOUND) - Vector store not found in registry
1838/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
1839/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1840///
1841/// # Safety
1842/// * `path` must be a valid null-terminated C string or null
1843///
1844/// _Requirements: 8.1_
1845#[no_mangle]
1846pub extern "C" fn SYNA_vector_store_build_index(path: *const c_char) -> i32 {
1847 std::panic::catch_unwind(|| {
1848 // Check for null pointer
1849 if path.is_null() {
1850 return ERR_INVALID_PATH;
1851 }
1852
1853 // Convert path to Rust string
1854 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1855 Ok(s) => s,
1856 Err(_) => return ERR_INVALID_PATH,
1857 };
1858
1859 // Canonicalize path for consistent registry keys
1860 let canonical_path = match canonicalize_vector_path(path_str) {
1861 Some(p) => p,
1862 None => return ERR_INVALID_PATH,
1863 };
1864
1865 // Get the vector store from registry
1866 let mut registry = VECTOR_STORE_REGISTRY.lock();
1867 match registry.get_mut(&canonical_path) {
1868 Some(store) => match store.build_index() {
1869 Ok(()) => crate::error::ERR_SUCCESS,
1870 Err(_) => ERR_GENERIC,
1871 },
1872 None => crate::error::ERR_DB_NOT_FOUND,
1873 }
1874 })
1875 .unwrap_or(ERR_INTERNAL_PANIC)
1876}
1877
1878/// Returns whether a vector store has an HNSW index built.
1879///
1880/// # Arguments
1881/// * `path` - Null-terminated C string containing the path to the vector store
1882///
1883/// # Returns
1884/// * `1` - Index is built
1885/// * `0` - Index is not built
1886/// * `-1` (ERR_DB_NOT_FOUND) - Vector store not found in registry
1887/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
1888/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1889///
1890/// # Safety
1891/// * `path` must be a valid null-terminated C string or null
1892///
1893/// _Requirements: 8.1_
1894#[no_mangle]
1895pub extern "C" fn SYNA_vector_store_has_index(path: *const c_char) -> i32 {
1896 std::panic::catch_unwind(|| {
1897 // Check for null pointer
1898 if path.is_null() {
1899 return ERR_INVALID_PATH;
1900 }
1901
1902 // Convert path to Rust string
1903 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1904 Ok(s) => s,
1905 Err(_) => return ERR_INVALID_PATH,
1906 };
1907
1908 // Canonicalize path for consistent registry keys
1909 let canonical_path = match canonicalize_vector_path(path_str) {
1910 Some(p) => p,
1911 None => return ERR_INVALID_PATH,
1912 };
1913
1914 // Get the vector store from registry
1915 let registry = VECTOR_STORE_REGISTRY.lock();
1916 match registry.get(&canonical_path) {
1917 Some(store) => {
1918 if store.has_index() {
1919 1
1920 } else {
1921 0
1922 }
1923 }
1924 None => crate::error::ERR_DB_NOT_FOUND,
1925 }
1926 })
1927 .unwrap_or(ERR_INTERNAL_PANIC)
1928}
1929
1930/// Closes a vector store and saves any pending changes.
1931///
1932/// This function removes the vector store from the global registry and
1933/// triggers the Drop implementation, which saves any dirty index to disk.
1934///
1935/// # Arguments
1936/// * `path` - Null-terminated C string containing the path to the database file
1937///
1938/// # Returns
1939/// * `1` (ERR_SUCCESS) - Store closed successfully
1940/// * `-1` (ERR_DB_NOT_FOUND) - Store not found in registry
1941/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
1942/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1943///
1944/// # Safety
1945/// * `path` must be a valid null-terminated C string or null
1946///
1947/// _Requirements: 8.1_
1948#[no_mangle]
1949pub extern "C" fn SYNA_vector_store_close(path: *const c_char) -> i32 {
1950 std::panic::catch_unwind(|| {
1951 // Check for null pointer
1952 if path.is_null() {
1953 return ERR_INVALID_PATH;
1954 }
1955
1956 // Convert C string to Rust string
1957 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
1958 Ok(s) => s,
1959 Err(_) => return ERR_INVALID_PATH,
1960 };
1961
1962 // Canonicalize path for consistent lookup
1963 let canonical_path = match std::fs::canonicalize(path_str) {
1964 Ok(p) => p.to_string_lossy().to_string(),
1965 Err(_) => path_str.to_string(),
1966 };
1967
1968 // Remove from registry (this triggers Drop which saves the index)
1969 let mut registry = VECTOR_STORE_REGISTRY.lock();
1970 match registry.remove(&canonical_path) {
1971 Some(_store) => {
1972 // Store is dropped here, triggering index save
1973 ERR_SUCCESS
1974 }
1975 None => crate::error::ERR_DB_NOT_FOUND,
1976 }
1977 })
1978 .unwrap_or(ERR_INTERNAL_PANIC)
1979}
1980
1981/// Flushes any pending changes to disk without closing the store.
1982///
1983/// This saves the HNSW index if it has unsaved changes.
1984///
1985/// # Arguments
1986/// * `path` - Null-terminated C string containing the path to the database file
1987///
1988/// # Returns
1989/// * `1` (ERR_SUCCESS) - Flush successful
1990/// * `-1` (ERR_DB_NOT_FOUND) - Store not found in registry
1991/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
1992/// * `0` (ERR_GENERIC) - Flush failed
1993/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
1994///
1995/// # Safety
1996/// * `path` must be a valid null-terminated C string or null
1997///
1998/// _Requirements: 8.1_
1999#[no_mangle]
2000pub extern "C" fn SYNA_vector_store_flush(path: *const c_char) -> i32 {
2001 std::panic::catch_unwind(|| {
2002 // Check for null pointer
2003 if path.is_null() {
2004 return ERR_INVALID_PATH;
2005 }
2006
2007 // Convert C string to Rust string
2008 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2009 Ok(s) => s,
2010 Err(_) => return ERR_INVALID_PATH,
2011 };
2012
2013 // Canonicalize path for consistent lookup
2014 let canonical_path = match std::fs::canonicalize(path_str) {
2015 Ok(p) => p.to_string_lossy().to_string(),
2016 Err(_) => path_str.to_string(),
2017 };
2018
2019 // Get store from registry
2020 let mut registry = VECTOR_STORE_REGISTRY.lock();
2021 match registry.get_mut(&canonical_path) {
2022 Some(store) => match store.flush() {
2023 Ok(_) => ERR_SUCCESS,
2024 Err(_) => ERR_GENERIC,
2025 },
2026 None => crate::error::ERR_DB_NOT_FOUND,
2027 }
2028 })
2029 .unwrap_or(ERR_INTERNAL_PANIC)
2030}
2031
2032/// Frees a JSON string allocated by `SYNA_vector_store_search()`.
2033///
2034/// # Arguments
2035/// * `json` - Pointer returned by `SYNA_vector_store_search()` in `out_json`
2036///
2037/// # Safety
2038/// * `json` must have been returned by `SYNA_vector_store_search()`
2039/// * This function must only be called once per pointer
2040/// * Calling with a null pointer is safe (no-op)
2041///
2042/// _Requirements: 8.1_
2043#[no_mangle]
2044pub extern "C" fn SYNA_free_json(json: *mut c_char) {
2045 std::panic::catch_unwind(|| {
2046 if json.is_null() {
2047 return;
2048 }
2049
2050 unsafe {
2051 // Reconstruct the CString and drop it
2052 let _ = CString::from_raw(json);
2053 }
2054 })
2055 .ok(); // Ignore panic result - we don't want to propagate panics from free
2056}
2057
2058// =============================================================================
2059// Model Registry FFI Functions
2060// =============================================================================
2061
2062use crate::model_registry::{ModelRegistry, ModelStage};
2063
2064/// Thread-safe global registry for managing open ModelRegistry instances.
2065static MODEL_REGISTRY: Lazy<Mutex<HashMap<String, ModelRegistry>>> =
2066 Lazy::new(|| Mutex::new(HashMap::new()));
2067
2068/// Opens or creates a model registry at the given path.
2069///
2070/// # Arguments
2071/// * `path` - Null-terminated C string containing the path to the database file
2072///
2073/// # Returns
2074/// * `1` (ERR_SUCCESS) - Registry opened successfully
2075/// * `0` (ERR_GENERIC) - Generic error during open
2076/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
2077/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2078///
2079/// # Safety
2080/// * `path` must be a valid null-terminated C string or null
2081///
2082/// _Requirements: 8.1_
2083#[no_mangle]
2084pub extern "C" fn SYNA_model_registry_open(path: *const c_char) -> i32 {
2085 std::panic::catch_unwind(|| {
2086 if path.is_null() {
2087 return ERR_INVALID_PATH;
2088 }
2089
2090 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2091 Ok(s) => s,
2092 Err(_) => return ERR_INVALID_PATH,
2093 };
2094
2095 match ModelRegistry::new(path_str) {
2096 Ok(registry) => {
2097 let mut reg = MODEL_REGISTRY.lock();
2098 reg.insert(path_str.to_string(), registry);
2099 ERR_SUCCESS
2100 }
2101 Err(_) => ERR_GENERIC,
2102 }
2103 })
2104 .unwrap_or(ERR_INTERNAL_PANIC)
2105}
2106
2107/// Saves a model to the registry with automatic versioning.
2108///
2109/// # Arguments
2110/// * `path` - Null-terminated C string containing the path to the registry
2111/// * `name` - Null-terminated C string containing the model name
2112/// * `data` - Pointer to the model data bytes
2113/// * `data_len` - Length of the model data
2114/// * `metadata_json` - Null-terminated JSON string with metadata (can be null for empty)
2115/// * `out_version` - Pointer to write the assigned version number
2116/// * `out_checksum` - Pointer to write the checksum string (caller must free with SYNA_free_text)
2117/// * `out_checksum_len` - Pointer to write the checksum string length
2118///
2119/// # Returns
2120/// * `1` (ERR_SUCCESS) - Model saved successfully
2121/// * `0` (ERR_GENERIC) - Generic error during save
2122/// * `-1` (ERR_DB_NOT_FOUND) - Registry not found
2123/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2124/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2125///
2126/// _Requirements: 8.1_
2127#[no_mangle]
2128pub extern "C" fn SYNA_model_save(
2129 path: *const c_char,
2130 name: *const c_char,
2131 data: *const u8,
2132 data_len: usize,
2133 metadata_json: *const c_char,
2134 out_version: *mut u32,
2135 out_checksum: *mut *mut c_char,
2136 out_checksum_len: *mut usize,
2137) -> i64 {
2138 std::panic::catch_unwind(|| {
2139 // Validate required pointers
2140 if path.is_null() || name.is_null() || (data.is_null() && data_len > 0) {
2141 return ERR_INVALID_PATH as i64;
2142 }
2143 if out_version.is_null() || out_checksum.is_null() || out_checksum_len.is_null() {
2144 return ERR_INVALID_PATH as i64;
2145 }
2146
2147 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2148 Ok(s) => s,
2149 Err(_) => return ERR_INVALID_PATH as i64,
2150 };
2151
2152 let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
2153 Ok(s) => s,
2154 Err(_) => return ERR_INVALID_PATH as i64,
2155 };
2156
2157 // Parse metadata JSON if provided
2158 let metadata: std::collections::HashMap<String, String> = if metadata_json.is_null() {
2159 std::collections::HashMap::new()
2160 } else {
2161 match unsafe { CStr::from_ptr(metadata_json) }.to_str() {
2162 Ok(json_str) => serde_json::from_str(json_str).unwrap_or_default(),
2163 Err(_) => std::collections::HashMap::new(),
2164 }
2165 };
2166
2167 // Create data slice
2168 let model_data = if data_len == 0 {
2169 &[]
2170 } else {
2171 unsafe { std::slice::from_raw_parts(data, data_len) }
2172 };
2173
2174 // Get registry and save model
2175 let mut reg = MODEL_REGISTRY.lock();
2176 match reg.get_mut(path_str) {
2177 Some(registry) => {
2178 match registry.save_model(name_str, model_data, metadata) {
2179 Ok(version) => {
2180 unsafe { *out_version = version.version };
2181
2182 // Return checksum as C string
2183 let checksum_len = version.checksum.len();
2184 unsafe { *out_checksum_len = checksum_len };
2185
2186 let mut bytes = version.checksum.into_bytes();
2187 bytes.push(0);
2188 let c_str = bytes.into_boxed_slice();
2189 unsafe { *out_checksum = Box::into_raw(c_str) as *mut c_char };
2190
2191 ERR_SUCCESS as i64
2192 }
2193 Err(_) => ERR_GENERIC as i64,
2194 }
2195 }
2196 None => crate::error::ERR_DB_NOT_FOUND as i64,
2197 }
2198 })
2199 .unwrap_or(ERR_INTERNAL_PANIC as i64)
2200}
2201
2202/// Loads a model from the registry with checksum verification.
2203///
2204/// # Arguments
2205/// * `path` - Null-terminated C string containing the path to the registry
2206/// * `name` - Null-terminated C string containing the model name
2207/// * `version` - Version number to load (0 for latest)
2208/// * `out_data` - Pointer to write the model data pointer
2209/// * `out_data_len` - Pointer to write the model data length
2210/// * `out_meta_json` - Pointer to write the metadata JSON string
2211/// * `out_meta_len` - Pointer to write the metadata JSON length
2212///
2213/// # Returns
2214/// * `1` (ERR_SUCCESS) - Model loaded successfully
2215/// * `0` (ERR_GENERIC) - Generic error during load
2216/// * `-1` (ERR_DB_NOT_FOUND) - Registry not found
2217/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2218/// * `-5` (ERR_KEY_NOT_FOUND) - Model not found
2219/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2220///
2221/// _Requirements: 8.1_
2222#[no_mangle]
2223pub extern "C" fn SYNA_model_load(
2224 path: *const c_char,
2225 name: *const c_char,
2226 version: u32,
2227 out_data: *mut *mut u8,
2228 out_data_len: *mut usize,
2229 out_meta_json: *mut *mut c_char,
2230 out_meta_len: *mut usize,
2231) -> i32 {
2232 std::panic::catch_unwind(|| {
2233 if path.is_null() || name.is_null() || out_data.is_null() || out_data_len.is_null() {
2234 return ERR_INVALID_PATH;
2235 }
2236 if out_meta_json.is_null() || out_meta_len.is_null() {
2237 return ERR_INVALID_PATH;
2238 }
2239
2240 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2241 Ok(s) => s,
2242 Err(_) => return ERR_INVALID_PATH,
2243 };
2244
2245 let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
2246 Ok(s) => s,
2247 Err(_) => return ERR_INVALID_PATH,
2248 };
2249
2250 let version_opt = if version == 0 { None } else { Some(version) };
2251
2252 let mut reg = MODEL_REGISTRY.lock();
2253 match reg.get_mut(path_str) {
2254 Some(registry) => {
2255 match registry.load_model(name_str, version_opt) {
2256 Ok((data, version_info)) => {
2257 // Return model data
2258 let data_len = data.len();
2259 unsafe { *out_data_len = data_len };
2260
2261 if data_len > 0 {
2262 let boxed = data.into_boxed_slice();
2263 unsafe { *out_data = Box::into_raw(boxed) as *mut u8 };
2264 } else {
2265 unsafe { *out_data = std::ptr::null_mut() };
2266 }
2267
2268 // Return metadata as JSON
2269 let meta_json = serde_json::to_string(&version_info)
2270 .unwrap_or_else(|_| "{}".to_string());
2271 let meta_len = meta_json.len();
2272 unsafe { *out_meta_len = meta_len };
2273
2274 let mut bytes = meta_json.into_bytes();
2275 bytes.push(0);
2276 let c_str = bytes.into_boxed_slice();
2277 unsafe { *out_meta_json = Box::into_raw(c_str) as *mut c_char };
2278
2279 ERR_SUCCESS
2280 }
2281 Err(crate::error::SynaError::ModelNotFound(_)) => ERR_KEY_NOT_FOUND,
2282 Err(crate::error::SynaError::ChecksumMismatch { .. }) => ERR_GENERIC,
2283 Err(_) => ERR_GENERIC,
2284 }
2285 }
2286 None => crate::error::ERR_DB_NOT_FOUND,
2287 }
2288 })
2289 .unwrap_or(ERR_INTERNAL_PANIC)
2290}
2291
2292/// Lists all versions of a model.
2293///
2294/// # Arguments
2295/// * `path` - Null-terminated C string containing the path to the registry
2296/// * `name` - Null-terminated C string containing the model name
2297/// * `out_json` - Pointer to write the JSON array of versions
2298/// * `out_len` - Pointer to write the JSON string length
2299///
2300/// # Returns
2301/// * Non-negative value - Number of versions found
2302/// * `-1` (ERR_DB_NOT_FOUND) - Registry not found
2303/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2304/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2305///
2306/// _Requirements: 8.1_
2307#[no_mangle]
2308pub extern "C" fn SYNA_model_list(
2309 path: *const c_char,
2310 name: *const c_char,
2311 out_json: *mut *mut c_char,
2312 out_len: *mut usize,
2313) -> i32 {
2314 std::panic::catch_unwind(|| {
2315 if path.is_null() || name.is_null() || out_json.is_null() || out_len.is_null() {
2316 return ERR_INVALID_PATH;
2317 }
2318
2319 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2320 Ok(s) => s,
2321 Err(_) => return ERR_INVALID_PATH,
2322 };
2323
2324 let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
2325 Ok(s) => s,
2326 Err(_) => return ERR_INVALID_PATH,
2327 };
2328
2329 let mut reg = MODEL_REGISTRY.lock();
2330 match reg.get_mut(path_str) {
2331 Some(registry) => match registry.list_versions(name_str) {
2332 Ok(versions) => {
2333 let count = versions.len() as i32;
2334 let json =
2335 serde_json::to_string(&versions).unwrap_or_else(|_| "[]".to_string());
2336
2337 unsafe { *out_len = json.len() };
2338 match CString::new(json) {
2339 Ok(c_string) => {
2340 unsafe { *out_json = c_string.into_raw() };
2341 count
2342 }
2343 Err(_) => ERR_GENERIC,
2344 }
2345 }
2346 Err(_) => ERR_GENERIC,
2347 },
2348 None => crate::error::ERR_DB_NOT_FOUND,
2349 }
2350 })
2351 .unwrap_or(ERR_INTERNAL_PANIC)
2352}
2353
2354/// Sets the deployment stage for a model version.
2355///
2356/// # Arguments
2357/// * `path` - Null-terminated C string containing the path to the registry
2358/// * `name` - Null-terminated C string containing the model name
2359/// * `version` - Version number to update
2360/// * `stage` - Stage: 0=Development, 1=Staging, 2=Production, 3=Archived
2361///
2362/// # Returns
2363/// * `1` (ERR_SUCCESS) - Stage updated successfully
2364/// * `0` (ERR_GENERIC) - Generic error
2365/// * `-1` (ERR_DB_NOT_FOUND) - Registry not found
2366/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2367/// * `-5` (ERR_KEY_NOT_FOUND) - Model/version not found
2368/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2369///
2370/// _Requirements: 8.1_
2371#[no_mangle]
2372pub extern "C" fn SYNA_model_set_stage(
2373 path: *const c_char,
2374 name: *const c_char,
2375 version: u32,
2376 stage: i32,
2377) -> i32 {
2378 std::panic::catch_unwind(|| {
2379 if path.is_null() || name.is_null() {
2380 return ERR_INVALID_PATH;
2381 }
2382
2383 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2384 Ok(s) => s,
2385 Err(_) => return ERR_INVALID_PATH,
2386 };
2387
2388 let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
2389 Ok(s) => s,
2390 Err(_) => return ERR_INVALID_PATH,
2391 };
2392
2393 let model_stage = match stage {
2394 0 => ModelStage::Development,
2395 1 => ModelStage::Staging,
2396 2 => ModelStage::Production,
2397 3 => ModelStage::Archived,
2398 _ => ModelStage::Development,
2399 };
2400
2401 let mut reg = MODEL_REGISTRY.lock();
2402 match reg.get_mut(path_str) {
2403 Some(registry) => match registry.set_stage(name_str, version, model_stage) {
2404 Ok(_) => ERR_SUCCESS,
2405 Err(crate::error::SynaError::ModelNotFound(_)) => ERR_KEY_NOT_FOUND,
2406 Err(_) => ERR_GENERIC,
2407 },
2408 None => crate::error::ERR_DB_NOT_FOUND,
2409 }
2410 })
2411 .unwrap_or(ERR_INTERNAL_PANIC)
2412}
2413
2414// =============================================================================
2415// Experiment Tracking FFI Functions
2416// =============================================================================
2417
2418use crate::experiment::{ExperimentTracker, RunStatus};
2419
2420/// Thread-safe global registry for managing open ExperimentTracker instances.
2421static EXPERIMENT_REGISTRY: Lazy<Mutex<HashMap<String, ExperimentTracker>>> =
2422 Lazy::new(|| Mutex::new(HashMap::new()));
2423
2424/// Opens or creates an experiment tracker at the given path.
2425///
2426/// # Arguments
2427/// * `path` - Null-terminated C string containing the path to the database file
2428///
2429/// # Returns
2430/// * `1` (ERR_SUCCESS) - Tracker opened successfully
2431/// * `0` (ERR_GENERIC) - Generic error during open
2432/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
2433/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2434///
2435/// _Requirements: 8.1_
2436#[no_mangle]
2437pub extern "C" fn SYNA_exp_tracker_open(path: *const c_char) -> i32 {
2438 std::panic::catch_unwind(|| {
2439 if path.is_null() {
2440 return ERR_INVALID_PATH;
2441 }
2442
2443 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2444 Ok(s) => s,
2445 Err(_) => return ERR_INVALID_PATH,
2446 };
2447
2448 match ExperimentTracker::new(path_str) {
2449 Ok(tracker) => {
2450 let mut reg = EXPERIMENT_REGISTRY.lock();
2451 reg.insert(path_str.to_string(), tracker);
2452 ERR_SUCCESS
2453 }
2454 Err(_) => ERR_GENERIC,
2455 }
2456 })
2457 .unwrap_or(ERR_INTERNAL_PANIC)
2458}
2459
2460/// Starts a new experiment run.
2461///
2462/// # Arguments
2463/// * `path` - Null-terminated C string containing the path to the tracker
2464/// * `experiment` - Null-terminated C string containing the experiment name
2465/// * `tags_json` - Null-terminated JSON array of tags (can be null for empty)
2466/// * `out_run_id` - Pointer to write the run ID string
2467/// * `out_run_id_len` - Pointer to write the run ID length
2468///
2469/// # Returns
2470/// * `1` (ERR_SUCCESS) - Run started successfully
2471/// * `0` (ERR_GENERIC) - Generic error
2472/// * `-1` (ERR_DB_NOT_FOUND) - Tracker not found
2473/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2474/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2475///
2476/// _Requirements: 8.1_
2477#[no_mangle]
2478pub extern "C" fn SYNA_exp_start_run(
2479 path: *const c_char,
2480 experiment: *const c_char,
2481 tags_json: *const c_char,
2482 out_run_id: *mut *mut c_char,
2483 out_run_id_len: *mut usize,
2484) -> i32 {
2485 std::panic::catch_unwind(|| {
2486 if path.is_null()
2487 || experiment.is_null()
2488 || out_run_id.is_null()
2489 || out_run_id_len.is_null()
2490 {
2491 return ERR_INVALID_PATH;
2492 }
2493
2494 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2495 Ok(s) => s,
2496 Err(_) => return ERR_INVALID_PATH,
2497 };
2498
2499 let exp_str = match unsafe { CStr::from_ptr(experiment) }.to_str() {
2500 Ok(s) => s,
2501 Err(_) => return ERR_INVALID_PATH,
2502 };
2503
2504 let tags: Vec<String> = if tags_json.is_null() {
2505 Vec::new()
2506 } else {
2507 match unsafe { CStr::from_ptr(tags_json) }.to_str() {
2508 Ok(json_str) => serde_json::from_str(json_str).unwrap_or_default(),
2509 Err(_) => Vec::new(),
2510 }
2511 };
2512
2513 let mut reg = EXPERIMENT_REGISTRY.lock();
2514 match reg.get_mut(path_str) {
2515 Some(tracker) => match tracker.start_run(exp_str, tags) {
2516 Ok(run_id) => {
2517 let run_id_len = run_id.len();
2518 unsafe { *out_run_id_len = run_id_len };
2519
2520 let mut bytes = run_id.into_bytes();
2521 bytes.push(0);
2522 let c_str = bytes.into_boxed_slice();
2523 unsafe { *out_run_id = Box::into_raw(c_str) as *mut c_char };
2524
2525 ERR_SUCCESS
2526 }
2527 Err(_) => ERR_GENERIC,
2528 },
2529 None => crate::error::ERR_DB_NOT_FOUND,
2530 }
2531 })
2532 .unwrap_or(ERR_INTERNAL_PANIC)
2533}
2534
2535/// Logs a parameter for a run.
2536///
2537/// # Arguments
2538/// * `path` - Null-terminated C string containing the path to the tracker
2539/// * `run_id` - Null-terminated C string containing the run ID
2540/// * `key` - Null-terminated C string containing the parameter name
2541/// * `value` - Null-terminated C string containing the parameter value
2542///
2543/// # Returns
2544/// * `1` (ERR_SUCCESS) - Parameter logged successfully
2545/// * `0` (ERR_GENERIC) - Generic error
2546/// * `-1` (ERR_DB_NOT_FOUND) - Tracker not found
2547/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2548/// * `-5` (ERR_KEY_NOT_FOUND) - Run not found
2549/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2550///
2551/// _Requirements: 8.1_
2552#[no_mangle]
2553pub extern "C" fn SYNA_exp_log_param(
2554 path: *const c_char,
2555 run_id: *const c_char,
2556 key: *const c_char,
2557 value: *const c_char,
2558) -> i32 {
2559 std::panic::catch_unwind(|| {
2560 if path.is_null() || run_id.is_null() || key.is_null() || value.is_null() {
2561 return ERR_INVALID_PATH;
2562 }
2563
2564 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2565 Ok(s) => s,
2566 Err(_) => return ERR_INVALID_PATH,
2567 };
2568
2569 let run_id_str = match unsafe { CStr::from_ptr(run_id) }.to_str() {
2570 Ok(s) => s,
2571 Err(_) => return ERR_INVALID_PATH,
2572 };
2573
2574 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
2575 Ok(s) => s,
2576 Err(_) => return ERR_INVALID_PATH,
2577 };
2578
2579 let value_str = match unsafe { CStr::from_ptr(value) }.to_str() {
2580 Ok(s) => s,
2581 Err(_) => return ERR_INVALID_PATH,
2582 };
2583
2584 let mut reg = EXPERIMENT_REGISTRY.lock();
2585 match reg.get_mut(path_str) {
2586 Some(tracker) => match tracker.log_param(run_id_str, key_str, value_str) {
2587 Ok(_) => ERR_SUCCESS,
2588 Err(crate::error::SynaError::RunNotFound(_)) => ERR_KEY_NOT_FOUND,
2589 Err(crate::error::SynaError::RunAlreadyEnded(_)) => ERR_GENERIC,
2590 Err(_) => ERR_GENERIC,
2591 },
2592 None => crate::error::ERR_DB_NOT_FOUND,
2593 }
2594 })
2595 .unwrap_or(ERR_INTERNAL_PANIC)
2596}
2597
2598/// Logs a metric value for a run.
2599///
2600/// # Arguments
2601/// * `path` - Null-terminated C string containing the path to the tracker
2602/// * `run_id` - Null-terminated C string containing the run ID
2603/// * `key` - Null-terminated C string containing the metric name
2604/// * `value` - The metric value (f64)
2605/// * `step` - Step number (use -1 for auto-generated timestamp)
2606///
2607/// # Returns
2608/// * `1` (ERR_SUCCESS) - Metric logged successfully
2609/// * `0` (ERR_GENERIC) - Generic error
2610/// * `-1` (ERR_DB_NOT_FOUND) - Tracker not found
2611/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2612/// * `-5` (ERR_KEY_NOT_FOUND) - Run not found
2613/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2614///
2615/// _Requirements: 8.1_
2616#[no_mangle]
2617pub extern "C" fn SYNA_exp_log_metric(
2618 path: *const c_char,
2619 run_id: *const c_char,
2620 key: *const c_char,
2621 value: f64,
2622 step: i64,
2623) -> i32 {
2624 std::panic::catch_unwind(|| {
2625 if path.is_null() || run_id.is_null() || key.is_null() {
2626 return ERR_INVALID_PATH;
2627 }
2628
2629 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2630 Ok(s) => s,
2631 Err(_) => return ERR_INVALID_PATH,
2632 };
2633
2634 let run_id_str = match unsafe { CStr::from_ptr(run_id) }.to_str() {
2635 Ok(s) => s,
2636 Err(_) => return ERR_INVALID_PATH,
2637 };
2638
2639 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
2640 Ok(s) => s,
2641 Err(_) => return ERR_INVALID_PATH,
2642 };
2643
2644 let step_opt = if step < 0 { None } else { Some(step as u64) };
2645
2646 let mut reg = EXPERIMENT_REGISTRY.lock();
2647 match reg.get_mut(path_str) {
2648 Some(tracker) => match tracker.log_metric(run_id_str, key_str, value, step_opt) {
2649 Ok(_) => ERR_SUCCESS,
2650 Err(crate::error::SynaError::RunNotFound(_)) => ERR_KEY_NOT_FOUND,
2651 Err(crate::error::SynaError::RunAlreadyEnded(_)) => ERR_GENERIC,
2652 Err(_) => ERR_GENERIC,
2653 },
2654 None => crate::error::ERR_DB_NOT_FOUND,
2655 }
2656 })
2657 .unwrap_or(ERR_INTERNAL_PANIC)
2658}
2659
2660/// Logs an artifact for a run.
2661///
2662/// # Arguments
2663/// * `path` - Null-terminated C string containing the path to the tracker
2664/// * `run_id` - Null-terminated C string containing the run ID
2665/// * `name` - Null-terminated C string containing the artifact name
2666/// * `data` - Pointer to the artifact data
2667/// * `data_len` - Length of the artifact data
2668///
2669/// # Returns
2670/// * `1` (ERR_SUCCESS) - Artifact logged successfully
2671/// * `0` (ERR_GENERIC) - Generic error
2672/// * `-1` (ERR_DB_NOT_FOUND) - Tracker not found
2673/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2674/// * `-5` (ERR_KEY_NOT_FOUND) - Run not found
2675/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2676///
2677/// _Requirements: 8.1_
2678#[no_mangle]
2679pub extern "C" fn SYNA_exp_log_artifact(
2680 path: *const c_char,
2681 run_id: *const c_char,
2682 name: *const c_char,
2683 data: *const u8,
2684 data_len: usize,
2685) -> i32 {
2686 std::panic::catch_unwind(|| {
2687 if path.is_null() || run_id.is_null() || name.is_null() {
2688 return ERR_INVALID_PATH;
2689 }
2690 if data.is_null() && data_len > 0 {
2691 return ERR_INVALID_PATH;
2692 }
2693
2694 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2695 Ok(s) => s,
2696 Err(_) => return ERR_INVALID_PATH,
2697 };
2698
2699 let run_id_str = match unsafe { CStr::from_ptr(run_id) }.to_str() {
2700 Ok(s) => s,
2701 Err(_) => return ERR_INVALID_PATH,
2702 };
2703
2704 let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
2705 Ok(s) => s,
2706 Err(_) => return ERR_INVALID_PATH,
2707 };
2708
2709 let artifact_data = if data_len == 0 {
2710 &[]
2711 } else {
2712 unsafe { std::slice::from_raw_parts(data, data_len) }
2713 };
2714
2715 let mut reg = EXPERIMENT_REGISTRY.lock();
2716 match reg.get_mut(path_str) {
2717 Some(tracker) => match tracker.log_artifact(run_id_str, name_str, artifact_data) {
2718 Ok(_) => ERR_SUCCESS,
2719 Err(crate::error::SynaError::RunNotFound(_)) => ERR_KEY_NOT_FOUND,
2720 Err(crate::error::SynaError::RunAlreadyEnded(_)) => ERR_GENERIC,
2721 Err(_) => ERR_GENERIC,
2722 },
2723 None => crate::error::ERR_DB_NOT_FOUND,
2724 }
2725 })
2726 .unwrap_or(ERR_INTERNAL_PANIC)
2727}
2728
2729/// Ends a run with the given status.
2730///
2731/// # Arguments
2732/// * `path` - Null-terminated C string containing the path to the tracker
2733/// * `run_id` - Null-terminated C string containing the run ID
2734/// * `status` - Status: 0=Running, 1=Completed, 2=Failed, 3=Killed
2735///
2736/// # Returns
2737/// * `1` (ERR_SUCCESS) - Run ended successfully
2738/// * `0` (ERR_GENERIC) - Generic error (e.g., run already ended)
2739/// * `-1` (ERR_DB_NOT_FOUND) - Tracker not found
2740/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2741/// * `-5` (ERR_KEY_NOT_FOUND) - Run not found
2742/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2743///
2744/// _Requirements: 8.1_
2745#[no_mangle]
2746pub extern "C" fn SYNA_exp_end_run(path: *const c_char, run_id: *const c_char, status: i32) -> i32 {
2747 std::panic::catch_unwind(|| {
2748 if path.is_null() || run_id.is_null() {
2749 return ERR_INVALID_PATH;
2750 }
2751
2752 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2753 Ok(s) => s,
2754 Err(_) => return ERR_INVALID_PATH,
2755 };
2756
2757 let run_id_str = match unsafe { CStr::from_ptr(run_id) }.to_str() {
2758 Ok(s) => s,
2759 Err(_) => return ERR_INVALID_PATH,
2760 };
2761
2762 let run_status = match status {
2763 0 => RunStatus::Running,
2764 1 => RunStatus::Completed,
2765 2 => RunStatus::Failed,
2766 3 => RunStatus::Killed,
2767 _ => RunStatus::Completed,
2768 };
2769
2770 let mut reg = EXPERIMENT_REGISTRY.lock();
2771 match reg.get_mut(path_str) {
2772 Some(tracker) => match tracker.end_run(run_id_str, run_status) {
2773 Ok(_) => ERR_SUCCESS,
2774 Err(crate::error::SynaError::RunNotFound(_)) => ERR_KEY_NOT_FOUND,
2775 Err(crate::error::SynaError::RunAlreadyEnded(_)) => ERR_GENERIC,
2776 Err(_) => ERR_GENERIC,
2777 },
2778 None => crate::error::ERR_DB_NOT_FOUND,
2779 }
2780 })
2781 .unwrap_or(ERR_INTERNAL_PANIC)
2782}
2783
2784// =============================================================================
2785// MmapVectorStore FFI Functions (Ultra-High-Throughput)
2786// =============================================================================
2787
2788use crate::mmap_vector::{MmapVectorConfig, MmapVectorStore};
2789
2790/// Thread-safe global registry for managing open MmapVectorStore instances.
2791static MMAP_VECTOR_REGISTRY: Lazy<Mutex<HashMap<String, MmapVectorStore>>> =
2792 Lazy::new(|| Mutex::new(HashMap::new()));
2793
2794/// Creates or opens a memory-mapped vector store at the given path.
2795///
2796/// This is an alternative to `SYNA_vector_store_new()` that uses memory-mapped I/O
2797/// for ultra-high-throughput writes (500K-1M vectors/sec).
2798///
2799/// # Arguments
2800/// * `path` - Null-terminated C string containing the path to the mmap file
2801/// * `dimensions` - Number of dimensions (64-8192)
2802/// * `metric` - Distance metric: 0=Cosine, 1=Euclidean, 2=DotProduct
2803/// * `initial_capacity` - Pre-allocated capacity in number of vectors
2804///
2805/// # Returns
2806/// * `1` (ERR_SUCCESS) - Store opened successfully
2807/// * `0` (ERR_GENERIC) - Generic error during open
2808/// * `-2` (ERR_INVALID_PATH) - Path is null or invalid UTF-8
2809/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2810#[no_mangle]
2811pub extern "C" fn SYNA_mmap_vector_store_new(
2812 path: *const c_char,
2813 dimensions: u16,
2814 metric: i32,
2815 initial_capacity: usize,
2816) -> i32 {
2817 std::panic::catch_unwind(|| {
2818 if path.is_null() {
2819 return ERR_INVALID_PATH;
2820 }
2821
2822 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2823 Ok(s) => s,
2824 Err(_) => return ERR_INVALID_PATH,
2825 };
2826
2827 let distance_metric = match metric {
2828 0 => crate::distance::DistanceMetric::Cosine,
2829 1 => crate::distance::DistanceMetric::Euclidean,
2830 2 => crate::distance::DistanceMetric::DotProduct,
2831 _ => crate::distance::DistanceMetric::Cosine,
2832 };
2833
2834 let config = MmapVectorConfig {
2835 dimensions,
2836 metric: distance_metric,
2837 initial_capacity,
2838 ..Default::default()
2839 };
2840
2841 match MmapVectorStore::new(path_str, config) {
2842 Ok(store) => {
2843 let mut reg = MMAP_VECTOR_REGISTRY.lock();
2844 reg.insert(path_str.to_string(), store);
2845 ERR_SUCCESS
2846 }
2847 Err(_) => ERR_GENERIC,
2848 }
2849 })
2850 .unwrap_or(ERR_INTERNAL_PANIC)
2851}
2852
2853/// Inserts a vector into the mmap vector store.
2854///
2855/// This is an ultra-fast operation (no syscalls, just memcpy).
2856///
2857/// # Arguments
2858/// * `path` - Null-terminated C string containing the path to the store
2859/// * `key` - Null-terminated C string containing the vector key
2860/// * `vector` - Pointer to the vector data (f32 array)
2861/// * `dimensions` - Number of dimensions in the vector
2862///
2863/// # Returns
2864/// * `1` (ERR_SUCCESS) - Vector inserted successfully
2865/// * `0` (ERR_GENERIC) - Generic error
2866/// * `-1` (ERR_DB_NOT_FOUND) - Store not found
2867/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2868/// * `-6` (ERR_TYPE_MISMATCH) - Dimension mismatch
2869/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2870#[no_mangle]
2871pub extern "C" fn SYNA_mmap_vector_store_insert(
2872 path: *const c_char,
2873 key: *const c_char,
2874 vector: *const f32,
2875 dimensions: u16,
2876) -> i32 {
2877 std::panic::catch_unwind(|| {
2878 if path.is_null() || key.is_null() || vector.is_null() {
2879 return ERR_INVALID_PATH;
2880 }
2881
2882 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2883 Ok(s) => s,
2884 Err(_) => return ERR_INVALID_PATH,
2885 };
2886
2887 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
2888 Ok(s) => s,
2889 Err(_) => return ERR_INVALID_PATH,
2890 };
2891
2892 let vec_slice = unsafe { std::slice::from_raw_parts(vector, dimensions as usize) };
2893
2894 let mut reg = MMAP_VECTOR_REGISTRY.lock();
2895 match reg.get_mut(path_str) {
2896 Some(store) => match store.insert(key_str, vec_slice) {
2897 Ok(_) => ERR_SUCCESS,
2898 Err(crate::error::SynaError::DimensionMismatch { .. }) => ERR_TYPE_MISMATCH,
2899 Err(_) => ERR_GENERIC,
2900 },
2901 None => crate::error::ERR_DB_NOT_FOUND,
2902 }
2903 })
2904 .unwrap_or(ERR_INTERNAL_PANIC)
2905}
2906
2907/// Inserts multiple vectors in a batch (maximum throughput).
2908///
2909/// This achieves 500K-1M vectors/sec by writing directly to memory.
2910///
2911/// # Arguments
2912/// * `path` - Null-terminated C string containing the path to the store
2913/// * `keys` - Array of null-terminated C strings (vector keys)
2914/// * `vectors` - Contiguous array of vector data (count * dimensions floats)
2915/// * `dimensions` - Number of dimensions per vector
2916/// * `count` - Number of vectors to insert
2917///
2918/// # Returns
2919/// * Non-negative value - Number of vectors inserted
2920/// * `-1` (ERR_DB_NOT_FOUND) - Store not found
2921/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2922/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2923#[no_mangle]
2924pub extern "C" fn SYNA_mmap_vector_store_insert_batch(
2925 path: *const c_char,
2926 keys: *const *const c_char,
2927 vectors: *const f32,
2928 dimensions: u16,
2929 count: usize,
2930) -> i32 {
2931 std::panic::catch_unwind(|| {
2932 if path.is_null() || keys.is_null() || vectors.is_null() || count == 0 {
2933 return ERR_INVALID_PATH;
2934 }
2935
2936 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
2937 Ok(s) => s,
2938 Err(_) => return ERR_INVALID_PATH,
2939 };
2940
2941 // Convert keys
2942 let key_ptrs = unsafe { std::slice::from_raw_parts(keys, count) };
2943 let mut key_strings: Vec<String> = Vec::with_capacity(count);
2944 for &key_ptr in key_ptrs {
2945 if key_ptr.is_null() {
2946 return ERR_INVALID_PATH;
2947 }
2948 match unsafe { CStr::from_ptr(key_ptr) }.to_str() {
2949 Ok(s) => key_strings.push(s.to_string()),
2950 Err(_) => return ERR_INVALID_PATH,
2951 }
2952 }
2953 let key_refs: Vec<&str> = key_strings.iter().map(|s| s.as_str()).collect();
2954
2955 // Convert vectors
2956 let dims = dimensions as usize;
2957 let all_vectors = unsafe { std::slice::from_raw_parts(vectors, count * dims) };
2958 let vec_refs: Vec<&[f32]> = (0..count)
2959 .map(|i| &all_vectors[i * dims..(i + 1) * dims])
2960 .collect();
2961
2962 let mut reg = MMAP_VECTOR_REGISTRY.lock();
2963 match reg.get_mut(path_str) {
2964 Some(store) => match store.insert_batch(&key_refs, &vec_refs) {
2965 Ok(inserted) => inserted as i32,
2966 Err(_) => ERR_GENERIC,
2967 },
2968 None => crate::error::ERR_DB_NOT_FOUND,
2969 }
2970 })
2971 .unwrap_or(ERR_INTERNAL_PANIC)
2972}
2973
2974/// Searches for the k nearest neighbors in the mmap vector store.
2975///
2976/// # Arguments
2977/// * `path` - Null-terminated C string containing the path to the store
2978/// * `query` - Pointer to the query vector (f32 array)
2979/// * `dimensions` - Number of dimensions in the query
2980/// * `k` - Number of results to return
2981/// * `out_json` - Pointer to write the JSON results string
2982/// * `out_len` - Pointer to write the JSON string length
2983///
2984/// # Returns
2985/// * Non-negative value - Number of results found
2986/// * `-1` (ERR_DB_NOT_FOUND) - Store not found
2987/// * `-2` (ERR_INVALID_PATH) - Invalid arguments
2988/// * `-6` (ERR_TYPE_MISMATCH) - Dimension mismatch
2989/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
2990#[no_mangle]
2991pub extern "C" fn SYNA_mmap_vector_store_search(
2992 path: *const c_char,
2993 query: *const f32,
2994 dimensions: u16,
2995 k: usize,
2996 out_json: *mut *mut c_char,
2997 out_len: *mut usize,
2998) -> i32 {
2999 std::panic::catch_unwind(|| {
3000 if path.is_null() || query.is_null() || out_json.is_null() || out_len.is_null() {
3001 return ERR_INVALID_PATH;
3002 }
3003
3004 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3005 Ok(s) => s,
3006 Err(_) => return ERR_INVALID_PATH,
3007 };
3008
3009 let query_slice = unsafe { std::slice::from_raw_parts(query, dimensions as usize) };
3010
3011 let reg = MMAP_VECTOR_REGISTRY.lock();
3012 match reg.get(path_str) {
3013 Some(store) => match store.search(query_slice, k) {
3014 Ok(results) => {
3015 let count = results.len() as i32;
3016
3017 // Convert results to JSON
3018 let json_results: Vec<serde_json::Value> = results
3019 .iter()
3020 .map(|r| {
3021 serde_json::json!({
3022 "key": r.key,
3023 "score": r.score,
3024 })
3025 })
3026 .collect();
3027
3028 let json =
3029 serde_json::to_string(&json_results).unwrap_or_else(|_| "[]".to_string());
3030
3031 unsafe { *out_len = json.len() };
3032 match CString::new(json) {
3033 Ok(c_string) => {
3034 unsafe { *out_json = c_string.into_raw() };
3035 count
3036 }
3037 Err(_) => ERR_GENERIC,
3038 }
3039 }
3040 Err(crate::error::SynaError::DimensionMismatch { .. }) => ERR_TYPE_MISMATCH,
3041 Err(_) => ERR_GENERIC,
3042 },
3043 None => crate::error::ERR_DB_NOT_FOUND,
3044 }
3045 })
3046 .unwrap_or(ERR_INTERNAL_PANIC)
3047}
3048
3049/// Builds the HNSW index for the mmap vector store.
3050///
3051/// # Arguments
3052/// * `path` - Null-terminated C string containing the path to the store
3053///
3054/// # Returns
3055/// * `1` (ERR_SUCCESS) - Index built successfully
3056/// * `0` (ERR_GENERIC) - Generic error
3057/// * `-1` (ERR_DB_NOT_FOUND) - Store not found
3058/// * `-2` (ERR_INVALID_PATH) - Invalid path
3059/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3060#[no_mangle]
3061pub extern "C" fn SYNA_mmap_vector_store_build_index(path: *const c_char) -> i32 {
3062 std::panic::catch_unwind(|| {
3063 if path.is_null() {
3064 return ERR_INVALID_PATH;
3065 }
3066
3067 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3068 Ok(s) => s,
3069 Err(_) => return ERR_INVALID_PATH,
3070 };
3071
3072 let mut reg = MMAP_VECTOR_REGISTRY.lock();
3073 match reg.get_mut(path_str) {
3074 Some(store) => match store.build_index() {
3075 Ok(_) => ERR_SUCCESS,
3076 Err(_) => ERR_GENERIC,
3077 },
3078 None => crate::error::ERR_DB_NOT_FOUND,
3079 }
3080 })
3081 .unwrap_or(ERR_INTERNAL_PANIC)
3082}
3083
3084/// Flushes the mmap vector store to disk.
3085///
3086/// # Arguments
3087/// * `path` - Null-terminated C string containing the path to the store
3088///
3089/// # Returns
3090/// * `1` (ERR_SUCCESS) - Flushed successfully
3091/// * `0` (ERR_GENERIC) - Generic error
3092/// * `-1` (ERR_DB_NOT_FOUND) - Store not found
3093/// * `-2` (ERR_INVALID_PATH) - Invalid path
3094/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3095#[no_mangle]
3096pub extern "C" fn SYNA_mmap_vector_store_flush(path: *const c_char) -> i32 {
3097 std::panic::catch_unwind(|| {
3098 if path.is_null() {
3099 return ERR_INVALID_PATH;
3100 }
3101
3102 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3103 Ok(s) => s,
3104 Err(_) => return ERR_INVALID_PATH,
3105 };
3106
3107 let mut reg = MMAP_VECTOR_REGISTRY.lock();
3108 match reg.get_mut(path_str) {
3109 Some(store) => match store.flush() {
3110 Ok(_) => ERR_SUCCESS,
3111 Err(_) => ERR_GENERIC,
3112 },
3113 None => crate::error::ERR_DB_NOT_FOUND,
3114 }
3115 })
3116 .unwrap_or(ERR_INTERNAL_PANIC)
3117}
3118
3119/// Closes the mmap vector store and removes it from the registry.
3120///
3121/// # Arguments
3122/// * `path` - Null-terminated C string containing the path to the store
3123///
3124/// # Returns
3125/// * `1` (ERR_SUCCESS) - Closed successfully
3126/// * `-1` (ERR_DB_NOT_FOUND) - Store not found
3127/// * `-2` (ERR_INVALID_PATH) - Invalid path
3128/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3129#[no_mangle]
3130pub extern "C" fn SYNA_mmap_vector_store_close(path: *const c_char) -> i32 {
3131 std::panic::catch_unwind(|| {
3132 if path.is_null() {
3133 return ERR_INVALID_PATH;
3134 }
3135
3136 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3137 Ok(s) => s,
3138 Err(_) => return ERR_INVALID_PATH,
3139 };
3140
3141 let mut reg = MMAP_VECTOR_REGISTRY.lock();
3142 match reg.remove(path_str) {
3143 Some(_store) => {
3144 // Store is dropped here, which triggers checkpoint
3145 ERR_SUCCESS
3146 }
3147 None => crate::error::ERR_DB_NOT_FOUND,
3148 }
3149 })
3150 .unwrap_or(ERR_INTERNAL_PANIC)
3151}
3152
3153/// Returns the number of vectors in the mmap vector store.
3154///
3155/// # Arguments
3156/// * `path` - Null-terminated C string containing the path to the store
3157///
3158/// # Returns
3159/// * Non-negative value - Number of vectors
3160/// * `-1` (ERR_DB_NOT_FOUND) - Store not found
3161/// * `-2` (ERR_INVALID_PATH) - Invalid path
3162/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3163#[no_mangle]
3164pub extern "C" fn SYNA_mmap_vector_store_len(path: *const c_char) -> i64 {
3165 std::panic::catch_unwind(|| {
3166 if path.is_null() {
3167 return ERR_INVALID_PATH as i64;
3168 }
3169
3170 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3171 Ok(s) => s,
3172 Err(_) => return ERR_INVALID_PATH as i64,
3173 };
3174
3175 let reg = MMAP_VECTOR_REGISTRY.lock();
3176 match reg.get(path_str) {
3177 Some(store) => store.len() as i64,
3178 None => crate::error::ERR_DB_NOT_FOUND as i64,
3179 }
3180 })
3181 .unwrap_or(ERR_INTERNAL_PANIC as i64)
3182}
3183
3184// =============================================================================
3185// Gravity Well Index (GWI) FFI Functions
3186// =============================================================================
3187
3188use crate::gwi::{GravityWellIndex, GwiConfig};
3189
3190/// Thread-safe global registry for managing open GravityWellIndex instances.
3191static GWI_REGISTRY: Lazy<Mutex<HashMap<String, GravityWellIndex>>> =
3192 Lazy::new(|| Mutex::new(HashMap::new()));
3193
3194/// Create a new Gravity Well Index
3195///
3196/// # Arguments
3197/// * `path` - Path to the index file
3198/// * `dimensions` - Vector dimensions (64-8192)
3199/// * `branching_factor` - Branching factor at each level (default: 16)
3200/// * `num_levels` - Number of hierarchy levels (default: 3)
3201///
3202/// # Returns
3203/// * `1` (ERR_SUCCESS) - Index created successfully
3204/// * `-2` (ERR_INVALID_PATH) - Invalid path
3205/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3206#[no_mangle]
3207pub extern "C" fn SYNA_gwi_new(
3208 path: *const c_char,
3209 dimensions: u16,
3210 branching_factor: u16,
3211 num_levels: u8,
3212 initial_capacity: usize,
3213) -> i32 {
3214 std::panic::catch_unwind(|| {
3215 if path.is_null() {
3216 return ERR_INVALID_PATH;
3217 }
3218
3219 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3220 Ok(s) => s,
3221 Err(_) => return ERR_INVALID_PATH,
3222 };
3223
3224 let config = GwiConfig {
3225 dimensions,
3226 branching_factor: if branching_factor == 0 {
3227 16
3228 } else {
3229 branching_factor
3230 },
3231 num_levels: if num_levels == 0 { 3 } else { num_levels },
3232 initial_capacity: if initial_capacity == 0 {
3233 10_000
3234 } else {
3235 initial_capacity
3236 },
3237 ..Default::default()
3238 };
3239
3240 match GravityWellIndex::new(path_str, config) {
3241 Ok(index) => {
3242 let mut reg = GWI_REGISTRY.lock();
3243 reg.insert(path_str.to_string(), index);
3244 ERR_SUCCESS
3245 }
3246 Err(_) => ERR_GENERIC,
3247 }
3248 })
3249 .unwrap_or(ERR_INTERNAL_PANIC)
3250}
3251
3252/// Open an existing Gravity Well Index
3253///
3254/// # Arguments
3255/// * `path` - Path to the existing index file
3256///
3257/// # Returns
3258/// * `1` (ERR_SUCCESS) - Index opened successfully
3259/// * `-2` (ERR_INVALID_PATH) - Invalid path or file doesn't exist
3260/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3261#[no_mangle]
3262pub extern "C" fn SYNA_gwi_open(path: *const c_char) -> i32 {
3263 std::panic::catch_unwind(|| {
3264 if path.is_null() {
3265 return ERR_INVALID_PATH;
3266 }
3267
3268 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3269 Ok(s) => s,
3270 Err(_) => return ERR_INVALID_PATH,
3271 };
3272
3273 match GravityWellIndex::open(path_str) {
3274 Ok(index) => {
3275 let mut reg = GWI_REGISTRY.lock();
3276 reg.insert(path_str.to_string(), index);
3277 ERR_SUCCESS
3278 }
3279 Err(_) => ERR_INVALID_PATH,
3280 }
3281 })
3282 .unwrap_or(ERR_INTERNAL_PANIC)
3283}
3284
3285/// Initialize GWI attractors from sample vectors
3286///
3287/// # Arguments
3288/// * `path` - Path to the index file
3289/// * `vectors` - Pointer to contiguous f32 vector data
3290/// * `num_vectors` - Number of sample vectors
3291/// * `dimensions` - Dimensions per vector
3292///
3293/// # Returns
3294/// * `1` (ERR_SUCCESS) - Attractors initialized
3295/// * `-1` (ERR_DB_NOT_FOUND) - Index not found
3296/// * `-2` (ERR_INVALID_PATH) - Invalid path
3297/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3298#[no_mangle]
3299pub extern "C" fn SYNA_gwi_initialize(
3300 path: *const c_char,
3301 vectors: *const f32,
3302 num_vectors: usize,
3303 dimensions: u16,
3304) -> i32 {
3305 std::panic::catch_unwind(|| {
3306 if path.is_null() || vectors.is_null() {
3307 return ERR_INVALID_PATH;
3308 }
3309
3310 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3311 Ok(s) => s,
3312 Err(_) => return ERR_INVALID_PATH,
3313 };
3314
3315 let total_floats = num_vectors * dimensions as usize;
3316 let data_slice = unsafe { std::slice::from_raw_parts(vectors, total_floats) };
3317
3318 let sample_vectors: Vec<&[f32]> = data_slice.chunks(dimensions as usize).collect();
3319
3320 let mut reg = GWI_REGISTRY.lock();
3321 match reg.get_mut(path_str) {
3322 Some(index) => match index.initialize_attractors(&sample_vectors) {
3323 Ok(()) => ERR_SUCCESS,
3324 Err(_) => ERR_GENERIC,
3325 },
3326 None => crate::error::ERR_DB_NOT_FOUND,
3327 }
3328 })
3329 .unwrap_or(ERR_INTERNAL_PANIC)
3330}
3331
3332/// Insert a vector into the GWI
3333///
3334/// # Arguments
3335/// * `path` - Path to the index file
3336/// * `key` - Null-terminated key string
3337/// * `vector` - Pointer to f32 vector data
3338/// * `dimensions` - Vector dimensions
3339///
3340/// # Returns
3341/// * `1` (ERR_SUCCESS) - Vector inserted
3342/// * `-1` (ERR_DB_NOT_FOUND) - Index not found
3343/// * `-2` (ERR_INVALID_PATH) - Invalid path/key
3344/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3345#[no_mangle]
3346pub extern "C" fn SYNA_gwi_insert(
3347 path: *const c_char,
3348 key: *const c_char,
3349 vector: *const f32,
3350 dimensions: u16,
3351) -> i32 {
3352 std::panic::catch_unwind(|| {
3353 if path.is_null() || key.is_null() || vector.is_null() {
3354 return ERR_INVALID_PATH;
3355 }
3356
3357 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3358 Ok(s) => s,
3359 Err(_) => return ERR_INVALID_PATH,
3360 };
3361
3362 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
3363 Ok(s) => s,
3364 Err(_) => return ERR_INVALID_PATH,
3365 };
3366
3367 let vector_slice = unsafe { std::slice::from_raw_parts(vector, dimensions as usize) };
3368
3369 let mut reg = GWI_REGISTRY.lock();
3370 match reg.get_mut(path_str) {
3371 Some(index) => match index.insert(key_str, vector_slice) {
3372 Ok(()) => ERR_SUCCESS,
3373 Err(_) => ERR_GENERIC,
3374 },
3375 None => crate::error::ERR_DB_NOT_FOUND,
3376 }
3377 })
3378 .unwrap_or(ERR_INTERNAL_PANIC)
3379}
3380
3381/// Batch insert vectors into the GWI
3382///
3383/// # Arguments
3384/// * `path` - Path to the index file
3385/// * `keys` - Array of null-terminated key strings
3386/// * `vectors` - Pointer to contiguous f32 vector data
3387/// * `dimensions` - Dimensions per vector
3388/// * `count` - Number of vectors to insert
3389///
3390/// # Returns
3391/// * Non-negative - Number of vectors inserted
3392/// * `-1` (ERR_DB_NOT_FOUND) - Index not found
3393/// * `-2` (ERR_INVALID_PATH) - Invalid path
3394/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3395#[no_mangle]
3396pub extern "C" fn SYNA_gwi_insert_batch(
3397 path: *const c_char,
3398 keys: *const *const c_char,
3399 vectors: *const f32,
3400 dimensions: u16,
3401 count: usize,
3402) -> i32 {
3403 std::panic::catch_unwind(|| {
3404 if path.is_null() || keys.is_null() || (vectors.is_null() && count > 0) {
3405 return ERR_INVALID_PATH;
3406 }
3407
3408 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3409 Ok(s) => s,
3410 Err(_) => return ERR_INVALID_PATH,
3411 };
3412
3413 let keys_slice = unsafe { std::slice::from_raw_parts(keys, count) };
3414 let mut key_strings: Vec<&str> = Vec::with_capacity(count);
3415 for key_ptr in keys_slice {
3416 if key_ptr.is_null() {
3417 return ERR_INVALID_PATH;
3418 }
3419 match unsafe { CStr::from_ptr(*key_ptr) }.to_str() {
3420 Ok(s) => key_strings.push(s),
3421 Err(_) => return ERR_INVALID_PATH,
3422 }
3423 }
3424
3425 let total_floats = count * dimensions as usize;
3426 let data_slice = unsafe { std::slice::from_raw_parts(vectors, total_floats) };
3427 let vector_refs: Vec<&[f32]> = data_slice.chunks(dimensions as usize).collect();
3428
3429 let mut reg = GWI_REGISTRY.lock();
3430 match reg.get_mut(path_str) {
3431 Some(index) => match index.insert_batch(&key_strings, &vector_refs) {
3432 Ok(n) => n as i32,
3433 Err(_) => ERR_GENERIC,
3434 },
3435 None => crate::error::ERR_DB_NOT_FOUND,
3436 }
3437 })
3438 .unwrap_or(ERR_INTERNAL_PANIC)
3439}
3440
3441/// Search for k nearest neighbors in the GWI
3442///
3443/// # Arguments
3444/// * `path` - Path to the index file
3445/// * `query` - Pointer to f32 query vector
3446/// * `dimensions` - Query vector dimensions
3447/// * `k` - Number of neighbors to return
3448/// * `out_json` - Pointer to write JSON result string
3449///
3450/// # Returns
3451/// * Non-negative - Number of results found
3452/// * `-1` (ERR_DB_NOT_FOUND) - Index not found
3453/// * `-2` (ERR_INVALID_PATH) - Invalid path
3454/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3455#[no_mangle]
3456pub extern "C" fn SYNA_gwi_search(
3457 path: *const c_char,
3458 query: *const f32,
3459 dimensions: u16,
3460 k: usize,
3461 out_json: *mut *mut c_char,
3462) -> i32 {
3463 // Default nprobe
3464 SYNA_gwi_search_nprobe(path, query, dimensions, k, 3, out_json)
3465}
3466
3467/// Search for k nearest neighbors with custom nprobe
3468///
3469/// Higher nprobe = better recall but slower search.
3470/// - nprobe=3: Fast, ~5-15% recall
3471/// - nprobe=10: Balanced, ~30-50% recall
3472/// - nprobe=30: High quality, ~70-90% recall
3473/// - nprobe=100: Near-exact, ~95%+ recall
3474///
3475/// # Arguments
3476/// * `path` - Path to the index file
3477/// * `query` - Query vector
3478/// * `dimensions` - Number of dimensions
3479/// * `k` - Number of results to return
3480/// * `nprobe` - Number of clusters to probe
3481/// * `out_json` - Output JSON string with results
3482///
3483/// # Returns
3484/// * Number of results on success
3485/// * `-1` (ERR_DB_NOT_FOUND) - Index not found
3486/// * `-2` (ERR_INVALID_PATH) - Invalid path
3487/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3488#[no_mangle]
3489pub extern "C" fn SYNA_gwi_search_nprobe(
3490 path: *const c_char,
3491 query: *const f32,
3492 dimensions: u16,
3493 k: usize,
3494 nprobe: usize,
3495 out_json: *mut *mut c_char,
3496) -> i32 {
3497 std::panic::catch_unwind(|| {
3498 if path.is_null() || query.is_null() || out_json.is_null() {
3499 return ERR_INVALID_PATH;
3500 }
3501
3502 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3503 Ok(s) => s,
3504 Err(_) => return ERR_INVALID_PATH,
3505 };
3506
3507 let query_slice = unsafe { std::slice::from_raw_parts(query, dimensions as usize) };
3508
3509 let reg = GWI_REGISTRY.lock();
3510 match reg.get(path_str) {
3511 Some(index) => match index.search_with_nprobe(query_slice, k, nprobe) {
3512 Ok(results) => {
3513 let json_results: Vec<serde_json::Value> = results
3514 .iter()
3515 .map(|r| {
3516 serde_json::json!({
3517 "key": r.key,
3518 "score": r.score,
3519 })
3520 })
3521 .collect();
3522
3523 let json_str = serde_json::to_string(&json_results).unwrap_or_default();
3524 let c_str = CString::new(json_str).unwrap_or_default();
3525 unsafe {
3526 *out_json = c_str.into_raw();
3527 }
3528 results.len() as i32
3529 }
3530 Err(_) => ERR_GENERIC,
3531 },
3532 None => crate::error::ERR_DB_NOT_FOUND,
3533 }
3534 })
3535 .unwrap_or(ERR_INTERNAL_PANIC)
3536}
3537
3538/// Flush GWI changes to disk
3539///
3540/// # Arguments
3541/// * `path` - Path to the index file
3542///
3543/// # Returns
3544/// * `1` (ERR_SUCCESS) - Flushed successfully
3545/// * `-1` (ERR_DB_NOT_FOUND) - Index not found
3546/// * `-2` (ERR_INVALID_PATH) - Invalid path
3547/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3548#[no_mangle]
3549pub extern "C" fn SYNA_gwi_flush(path: *const c_char) -> i32 {
3550 std::panic::catch_unwind(|| {
3551 if path.is_null() {
3552 return ERR_INVALID_PATH;
3553 }
3554
3555 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3556 Ok(s) => s,
3557 Err(_) => return ERR_INVALID_PATH,
3558 };
3559
3560 let reg = GWI_REGISTRY.lock();
3561 match reg.get(path_str) {
3562 Some(index) => match index.flush() {
3563 Ok(()) => ERR_SUCCESS,
3564 Err(_) => ERR_GENERIC,
3565 },
3566 None => crate::error::ERR_DB_NOT_FOUND,
3567 }
3568 })
3569 .unwrap_or(ERR_INTERNAL_PANIC)
3570}
3571
3572/// Close a GWI and remove from registry
3573///
3574/// # Arguments
3575/// * `path` - Path to the index file
3576///
3577/// # Returns
3578/// * `1` (ERR_SUCCESS) - Closed successfully
3579/// * `-1` (ERR_DB_NOT_FOUND) - Index not found
3580/// * `-2` (ERR_INVALID_PATH) - Invalid path
3581/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3582#[no_mangle]
3583pub extern "C" fn SYNA_gwi_close(path: *const c_char) -> i32 {
3584 std::panic::catch_unwind(|| {
3585 if path.is_null() {
3586 return ERR_INVALID_PATH;
3587 }
3588
3589 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3590 Ok(s) => s,
3591 Err(_) => return ERR_INVALID_PATH,
3592 };
3593
3594 let mut reg = GWI_REGISTRY.lock();
3595 match reg.remove(path_str) {
3596 Some(mut index) => {
3597 let _ = index.close();
3598 ERR_SUCCESS
3599 }
3600 None => crate::error::ERR_DB_NOT_FOUND,
3601 }
3602 })
3603 .unwrap_or(ERR_INTERNAL_PANIC)
3604}
3605
3606/// Get number of vectors in the GWI
3607///
3608/// # Arguments
3609/// * `path` - Path to the index file
3610///
3611/// # Returns
3612/// * Non-negative - Number of vectors
3613/// * `-1` (ERR_DB_NOT_FOUND) - Index not found
3614/// * `-2` (ERR_INVALID_PATH) - Invalid path
3615/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic occurred
3616#[no_mangle]
3617pub extern "C" fn SYNA_gwi_len(path: *const c_char) -> i64 {
3618 std::panic::catch_unwind(|| {
3619 if path.is_null() {
3620 return ERR_INVALID_PATH as i64;
3621 }
3622
3623 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3624 Ok(s) => s,
3625 Err(_) => return ERR_INVALID_PATH as i64,
3626 };
3627
3628 let reg = GWI_REGISTRY.lock();
3629 match reg.get(path_str) {
3630 Some(index) => index.len() as i64,
3631 None => crate::error::ERR_DB_NOT_FOUND as i64,
3632 }
3633 })
3634 .unwrap_or(ERR_INTERNAL_PANIC as i64)
3635}
3636
3637// =============================================================================
3638// Cascade Index FFI Functions
3639// =============================================================================
3640
3641use crate::cascade::{CascadeConfig, CascadeIndex};
3642
3643/// Global registry for Cascade Index instances
3644static CASCADE_REGISTRY: Lazy<Mutex<HashMap<String, CascadeIndex>>> =
3645 Lazy::new(|| Mutex::new(HashMap::new()));
3646
3647/// Creates a new Cascade Index.
3648///
3649/// # Arguments
3650/// * `path` - Path to the index file
3651/// * `dimensions` - Vector dimensions (64-8192)
3652///
3653/// # Returns
3654/// * `1` (ERR_SUCCESS) - Index created successfully
3655/// * `-2` (ERR_INVALID_PATH) - Invalid path
3656/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic
3657#[no_mangle]
3658pub extern "C" fn SYNA_cascade_new(path: *const c_char, dimensions: u16) -> i32 {
3659 std::panic::catch_unwind(|| {
3660 if path.is_null() {
3661 return ERR_INVALID_PATH;
3662 }
3663
3664 let c_str = unsafe { CStr::from_ptr(path) };
3665 let path_str = match c_str.to_str() {
3666 Ok(s) => s,
3667 Err(_) => return ERR_INVALID_PATH,
3668 };
3669
3670 let config = CascadeConfig {
3671 dimensions,
3672 ..Default::default()
3673 };
3674
3675 match CascadeIndex::new(path_str, config) {
3676 Ok(index) => {
3677 let mut registry = CASCADE_REGISTRY.lock();
3678 registry.insert(path_str.to_string(), index);
3679 ERR_SUCCESS
3680 }
3681 Err(_) => ERR_GENERIC,
3682 }
3683 })
3684 .unwrap_or(ERR_INTERNAL_PANIC)
3685}
3686
3687/// Inserts a vector into the Cascade Index.
3688///
3689/// # Arguments
3690/// * `path` - Path to the index
3691/// * `key` - Key for the vector
3692/// * `vector` - Pointer to vector data
3693/// * `dimensions` - Vector dimensions
3694///
3695/// # Returns
3696/// * `1` (ERR_SUCCESS) - Vector inserted
3697/// * `-2` (ERR_INVALID_PATH) - Invalid path
3698/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic
3699#[no_mangle]
3700pub extern "C" fn SYNA_cascade_insert(
3701 path: *const c_char,
3702 key: *const c_char,
3703 vector: *const f32,
3704 dimensions: u16,
3705) -> i32 {
3706 std::panic::catch_unwind(|| {
3707 if path.is_null() || key.is_null() || vector.is_null() {
3708 return ERR_INVALID_PATH;
3709 }
3710
3711 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3712 Ok(s) => s,
3713 Err(_) => return ERR_INVALID_PATH,
3714 };
3715
3716 let key_str = match unsafe { CStr::from_ptr(key) }.to_str() {
3717 Ok(s) => s,
3718 Err(_) => return ERR_INVALID_PATH,
3719 };
3720
3721 let vec_slice = unsafe { std::slice::from_raw_parts(vector, dimensions as usize) };
3722
3723 let mut registry = CASCADE_REGISTRY.lock();
3724 if let Some(index) = registry.get_mut(path_str) {
3725 match index.insert(key_str, vec_slice) {
3726 Ok(_) => ERR_SUCCESS,
3727 Err(_) => ERR_GENERIC,
3728 }
3729 } else {
3730 ERR_GENERIC
3731 }
3732 })
3733 .unwrap_or(ERR_INTERNAL_PANIC)
3734}
3735
3736/// Inserts multiple vectors into the Cascade Index.
3737///
3738/// # Arguments
3739/// * `path` - Path to the index
3740/// * `keys` - Array of key pointers
3741/// * `vectors` - Pointer to flattened vector data
3742/// * `dimensions` - Vector dimensions
3743/// * `count` - Number of vectors
3744///
3745/// # Returns
3746/// * `1` (ERR_SUCCESS) - Vectors inserted
3747/// * `-2` (ERR_INVALID_PATH) - Invalid path
3748/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic
3749#[no_mangle]
3750pub extern "C" fn SYNA_cascade_insert_batch(
3751 path: *const c_char,
3752 keys: *const *const c_char,
3753 vectors: *const f32,
3754 dimensions: u16,
3755 count: usize,
3756) -> i32 {
3757 std::panic::catch_unwind(|| {
3758 if path.is_null() || keys.is_null() || vectors.is_null() || count == 0 {
3759 return ERR_INVALID_PATH;
3760 }
3761
3762 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3763 Ok(s) => s,
3764 Err(_) => return ERR_INVALID_PATH,
3765 };
3766
3767 let dims = dimensions as usize;
3768 let mut registry = CASCADE_REGISTRY.lock();
3769
3770 if let Some(index) = registry.get_mut(path_str) {
3771 for i in 0..count {
3772 let key_ptr = unsafe { *keys.add(i) };
3773 if key_ptr.is_null() {
3774 continue;
3775 }
3776
3777 let key_str = match unsafe { CStr::from_ptr(key_ptr) }.to_str() {
3778 Ok(s) => s,
3779 Err(_) => continue,
3780 };
3781
3782 let vec_start = i * dims;
3783 let vec_slice = unsafe { std::slice::from_raw_parts(vectors.add(vec_start), dims) };
3784
3785 if index.insert(key_str, vec_slice).is_err() {
3786 return ERR_GENERIC;
3787 }
3788 }
3789 ERR_SUCCESS
3790 } else {
3791 ERR_GENERIC
3792 }
3793 })
3794 .unwrap_or(ERR_INTERNAL_PANIC)
3795}
3796
3797/// Searches the Cascade Index for nearest neighbors.
3798///
3799/// # Arguments
3800/// * `path` - Path to the index
3801/// * `query` - Query vector
3802/// * `dimensions` - Vector dimensions
3803/// * `k` - Number of results
3804/// * `out_json` - Output JSON string pointer
3805///
3806/// # Returns
3807/// * `1` (ERR_SUCCESS) - Search completed
3808/// * `-2` (ERR_INVALID_PATH) - Invalid path
3809/// * `-100` (ERR_INTERNAL_PANIC) - Internal panic
3810#[no_mangle]
3811pub extern "C" fn SYNA_cascade_search(
3812 path: *const c_char,
3813 query: *const f32,
3814 dimensions: u16,
3815 k: usize,
3816 out_json: *mut *mut c_char,
3817) -> i32 {
3818 // Use good defaults matching CascadeConfig::default()
3819 SYNA_cascade_search_params(path, query, dimensions, k, 16, 80, out_json)
3820}
3821
3822/// Searches with custom parameters.
3823#[no_mangle]
3824pub extern "C" fn SYNA_cascade_search_params(
3825 path: *const c_char,
3826 query: *const f32,
3827 dimensions: u16,
3828 k: usize,
3829 num_probes: usize,
3830 ef_search: usize,
3831 out_json: *mut *mut c_char,
3832) -> i32 {
3833 std::panic::catch_unwind(|| {
3834 if path.is_null() || query.is_null() || out_json.is_null() {
3835 return ERR_INVALID_PATH;
3836 }
3837
3838 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3839 Ok(s) => s,
3840 Err(_) => return ERR_INVALID_PATH,
3841 };
3842
3843 let query_slice = unsafe { std::slice::from_raw_parts(query, dimensions as usize) };
3844
3845 let registry = CASCADE_REGISTRY.lock();
3846 if let Some(index) = registry.get(path_str) {
3847 match index.search_with_params(query_slice, k, num_probes, ef_search) {
3848 Ok(results) => {
3849 let json_results: Vec<serde_json::Value> = results
3850 .iter()
3851 .map(|r| {
3852 serde_json::json!({
3853 "key": r.key,
3854 "score": r.score
3855 })
3856 })
3857 .collect();
3858
3859 let json_str = serde_json::to_string(&json_results).unwrap_or_default();
3860 let c_string = std::ffi::CString::new(json_str).unwrap_or_default();
3861 unsafe {
3862 *out_json = c_string.into_raw();
3863 }
3864 ERR_SUCCESS
3865 }
3866 Err(_) => ERR_GENERIC,
3867 }
3868 } else {
3869 ERR_GENERIC
3870 }
3871 })
3872 .unwrap_or(ERR_INTERNAL_PANIC)
3873}
3874
3875/// Flushes the Cascade Index to disk.
3876#[no_mangle]
3877pub extern "C" fn SYNA_cascade_flush(path: *const c_char) -> i32 {
3878 std::panic::catch_unwind(|| {
3879 if path.is_null() {
3880 return ERR_INVALID_PATH;
3881 }
3882
3883 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3884 Ok(s) => s,
3885 Err(_) => return ERR_INVALID_PATH,
3886 };
3887
3888 let registry = CASCADE_REGISTRY.lock();
3889 if let Some(index) = registry.get(path_str) {
3890 match index.flush() {
3891 Ok(_) => ERR_SUCCESS,
3892 Err(_) => ERR_GENERIC,
3893 }
3894 } else {
3895 ERR_GENERIC
3896 }
3897 })
3898 .unwrap_or(ERR_INTERNAL_PANIC)
3899}
3900
3901/// Closes the Cascade Index.
3902#[no_mangle]
3903pub extern "C" fn SYNA_cascade_close(path: *const c_char) -> i32 {
3904 std::panic::catch_unwind(|| {
3905 if path.is_null() {
3906 return ERR_INVALID_PATH;
3907 }
3908
3909 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3910 Ok(s) => s,
3911 Err(_) => return ERR_INVALID_PATH,
3912 };
3913
3914 let mut registry = CASCADE_REGISTRY.lock();
3915 if registry.remove(path_str).is_some() {
3916 ERR_SUCCESS
3917 } else {
3918 ERR_GENERIC
3919 }
3920 })
3921 .unwrap_or(ERR_INTERNAL_PANIC)
3922}
3923
3924/// Returns the number of vectors in the Cascade Index.
3925#[no_mangle]
3926pub extern "C" fn SYNA_cascade_len(path: *const c_char) -> i64 {
3927 std::panic::catch_unwind(|| {
3928 if path.is_null() {
3929 return -1;
3930 }
3931
3932 let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
3933 Ok(s) => s,
3934 Err(_) => return -1,
3935 };
3936
3937 let registry = CASCADE_REGISTRY.lock();
3938 if let Some(index) = registry.get(path_str) {
3939 index.len() as i64
3940 } else {
3941 -1
3942 }
3943 })
3944 .unwrap_or(-1)
3945}