Skip to main content

overdrive/
ffi.rs

1//! C FFI Layer for OverDrive InCode SDK
2//! 
3//! Provides C-compatible functions for cross-language bindings.
4//! All functions use `extern "C"` with C-compatible types.
5//! 
6//! ## Memory Rules
7//! - All `*mut c_char` returned must be freed with `overdrive_free_string()`
8//! - `*mut ODB` must be closed with `overdrive_close()`
9//! - Errors are stored thread-locally, retrieve with `overdrive_last_error()`
10
11use std::ffi::{CStr, CString};
12use std::os::raw::c_char;
13use std::ptr;
14use std::cell::RefCell;
15
16use crate::OverDriveDB;
17
18/// Opaque database handle for C API
19pub struct ODB {
20    inner: OverDriveDB,
21}
22
23thread_local! {
24    static LAST_ERROR: RefCell<Option<String>> = RefCell::new(None);
25}
26
27fn set_error(msg: String) {
28    LAST_ERROR.with(|e| *e.borrow_mut() = Some(msg));
29}
30
31fn clear_error() {
32    LAST_ERROR.with(|e| *e.borrow_mut() = None);
33}
34
35fn to_c_string(s: &str) -> *mut c_char {
36    CString::new(s).unwrap_or_default().into_raw()
37}
38
39unsafe fn from_c_str(ptr: *const c_char) -> Option<String> {
40    if ptr.is_null() {
41        return None;
42    }
43    Some(CStr::from_ptr(ptr).to_string_lossy().into_owned())
44}
45
46// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
47// LIFECYCLE
48// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
49
50/// Open (or create) a database. Returns NULL on error.
51#[no_mangle]
52pub unsafe extern "C" fn overdrive_open(path: *const c_char) -> *mut ODB {
53    clear_error();
54    let path = match from_c_str(path) {
55        Some(p) => p,
56        None => { set_error("Invalid path".into()); return ptr::null_mut(); }
57    };
58    match OverDriveDB::open(&path) {
59        Ok(db) => Box::into_raw(Box::new(ODB { inner: db })),
60        Err(e) => { set_error(e.to_string()); ptr::null_mut() }
61    }
62}
63
64/// Close a database and free resources.
65#[no_mangle]
66pub unsafe extern "C" fn overdrive_close(db: *mut ODB) {
67    if !db.is_null() {
68        let odb = Box::from_raw(db);
69        let _ = odb.inner.close();
70    }
71}
72
73/// Sync data to disk.
74#[no_mangle]
75pub unsafe extern "C" fn overdrive_sync(db: *mut ODB) {
76    if db.is_null() { return; }
77    let _ = (*db).inner.sync();
78}
79
80// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
81// TABLE MANAGEMENT
82// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
83
84/// Create a table. Returns 0 on success, -1 on error.
85#[no_mangle]
86pub unsafe extern "C" fn overdrive_create_table(db: *mut ODB, name: *const c_char) -> i32 {
87    clear_error();
88    if db.is_null() { set_error("Null db handle".into()); return -1; }
89    let name = match from_c_str(name) {
90        Some(n) => n,
91        None => { set_error("Invalid table name".into()); return -1; }
92    };
93    match (*db).inner.create_table(&name) {
94        Ok(()) => 0,
95        Err(e) => { set_error(e.to_string()); -1 }
96    }
97}
98
99/// Drop a table. Returns 0 on success, -1 on error.
100#[no_mangle]
101pub unsafe extern "C" fn overdrive_drop_table(db: *mut ODB, name: *const c_char) -> i32 {
102    clear_error();
103    if db.is_null() { set_error("Null db handle".into()); return -1; }
104    let name = match from_c_str(name) {
105        Some(n) => n,
106        None => { set_error("Invalid table name".into()); return -1; }
107    };
108    match (*db).inner.drop_table(&name) {
109        Ok(()) => 0,
110        Err(e) => { set_error(e.to_string()); -1 }
111    }
112}
113
114/// List all tables. Returns JSON array string. Must be freed with overdrive_free_string().
115#[no_mangle]
116pub unsafe extern "C" fn overdrive_list_tables(db: *mut ODB) -> *mut c_char {
117    clear_error();
118    if db.is_null() { set_error("Null db handle".into()); return ptr::null_mut(); }
119    match (*db).inner.list_tables() {
120        Ok(tables) => {
121            let json = serde_json::to_string(&tables).unwrap_or_else(|_| "[]".into());
122            to_c_string(&json)
123        }
124        Err(e) => { set_error(e.to_string()); ptr::null_mut() }
125    }
126}
127
128/// Check if table exists. Returns 1 if exists, 0 if not, -1 on error.
129#[no_mangle]
130pub unsafe extern "C" fn overdrive_table_exists(db: *mut ODB, name: *const c_char) -> i32 {
131    clear_error();
132    if db.is_null() { return -1; }
133    let name = match from_c_str(name) {
134        Some(n) => n,
135        None => { return -1; }
136    };
137    match (*db).inner.table_exists(&name) {
138        Ok(true) => 1,
139        Ok(false) => 0,
140        Err(e) => { set_error(e.to_string()); -1 }
141    }
142}
143
144// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
145// CRUD OPERATIONS
146// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
147
148/// Insert a JSON document. Returns the _id string. Must be freed.
149#[no_mangle]
150pub unsafe extern "C" fn overdrive_insert(db: *mut ODB, table: *const c_char, json: *const c_char) -> *mut c_char {
151    clear_error();
152    if db.is_null() { set_error("Null db handle".into()); return ptr::null_mut(); }
153    let table = match from_c_str(table) { Some(t) => t, None => { set_error("Invalid table".into()); return ptr::null_mut(); } };
154    let json_str = match from_c_str(json) { Some(j) => j, None => { set_error("Invalid JSON".into()); return ptr::null_mut(); } };
155    
156    let value: serde_json::Value = match serde_json::from_str(&json_str) {
157        Ok(v) => v,
158        Err(e) => { set_error(format!("JSON parse error: {}", e)); return ptr::null_mut(); }
159    };
160    
161    match (*db).inner.insert(&table, &value) {
162        Ok(id) => to_c_string(&id),
163        Err(e) => { set_error(e.to_string()); ptr::null_mut() }
164    }
165}
166
167/// Get a document by ID. Returns JSON string. Must be freed.
168#[no_mangle]
169pub unsafe extern "C" fn overdrive_get(db: *mut ODB, table: *const c_char, id: *const c_char) -> *mut c_char {
170    clear_error();
171    if db.is_null() { return ptr::null_mut(); }
172    let table = match from_c_str(table) { Some(t) => t, None => return ptr::null_mut() };
173    let id = match from_c_str(id) { Some(i) => i, None => return ptr::null_mut() };
174    
175    match (*db).inner.get(&table, &id) {
176        Ok(Some(val)) => to_c_string(&val.to_string()),
177        Ok(None) => ptr::null_mut(),
178        Err(e) => { set_error(e.to_string()); ptr::null_mut() }
179    }
180}
181
182/// Update a document. Returns 1 if updated, 0 if not found, -1 on error.
183#[no_mangle]
184pub unsafe extern "C" fn overdrive_update(db: *mut ODB, table: *const c_char, id: *const c_char, json: *const c_char) -> i32 {
185    clear_error();
186    if db.is_null() { return -1; }
187    let table = match from_c_str(table) { Some(t) => t, None => return -1 };
188    let id = match from_c_str(id) { Some(i) => i, None => return -1 };
189    let json_str = match from_c_str(json) { Some(j) => j, None => return -1 };
190    
191    let value: serde_json::Value = match serde_json::from_str(&json_str) {
192        Ok(v) => v,
193        Err(e) => { set_error(format!("JSON parse error: {}", e)); return -1; }
194    };
195    
196    match (*db).inner.update(&table, &id, &value) {
197        Ok(true) => 1,
198        Ok(false) => 0,
199        Err(e) => { set_error(e.to_string()); -1 }
200    }
201}
202
203/// Delete a document. Returns 1 if deleted, 0 if not found, -1 on error.
204#[no_mangle]
205pub unsafe extern "C" fn overdrive_delete(db: *mut ODB, table: *const c_char, id: *const c_char) -> i32 {
206    clear_error();
207    if db.is_null() { return -1; }
208    let table = match from_c_str(table) { Some(t) => t, None => return -1 };
209    let id = match from_c_str(id) { Some(i) => i, None => return -1 };
210    
211    match (*db).inner.delete(&table, &id) {
212        Ok(true) => 1,
213        Ok(false) => 0,
214        Err(e) => { set_error(e.to_string()); -1 }
215    }
216}
217
218/// Count documents in a table. Returns count or -1 on error.
219#[no_mangle]
220pub unsafe extern "C" fn overdrive_count(db: *mut ODB, table: *const c_char) -> i32 {
221    clear_error();
222    if db.is_null() { return -1; }
223    let table = match from_c_str(table) { Some(t) => t, None => return -1 };
224    
225    match (*db).inner.count(&table) {
226        Ok(n) => n as i32,
227        Err(e) => { set_error(e.to_string()); -1 }
228    }
229}
230
231// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
232// QUERY
233// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
234
235/// Execute SQL query. Returns JSON result string. Must be freed.
236#[no_mangle]
237pub unsafe extern "C" fn overdrive_query(db: *mut ODB, sql: *const c_char) -> *mut c_char {
238    clear_error();
239    if db.is_null() { set_error("Null db handle".into()); return ptr::null_mut(); }
240    let sql = match from_c_str(sql) { Some(s) => s, None => return ptr::null_mut() };
241    
242    match (*db).inner.query(&sql) {
243        Ok(result) => {
244            let json = serde_json::json!({
245                "rows": result.rows,
246                "columns": result.columns,
247                "rows_affected": result.rows_affected,
248                "execution_time_ms": result.execution_time_ms,
249            });
250            to_c_string(&json.to_string())
251        }
252        Err(e) => { set_error(e.to_string()); ptr::null_mut() }
253    }
254}
255
256/// Full-text search. Returns JSON array. Must be freed.
257#[no_mangle]
258pub unsafe extern "C" fn overdrive_search(db: *mut ODB, table: *const c_char, text: *const c_char) -> *mut c_char {
259    clear_error();
260    if db.is_null() { return ptr::null_mut(); }
261    let table = match from_c_str(table) { Some(t) => t, None => return ptr::null_mut() };
262    let text = match from_c_str(text) { Some(t) => t, None => return ptr::null_mut() };
263    
264    match (*db).inner.search(&table, &text) {
265        Ok(results) => {
266            let json = serde_json::to_string(&results).unwrap_or_else(|_| "[]".into());
267            to_c_string(&json)
268        }
269        Err(e) => { set_error(e.to_string()); ptr::null_mut() }
270    }
271}
272
273// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
274// ERROR & UTILITY
275// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
276
277/// Get the last error message. Returns NULL if no error.
278/// The returned string is valid until the next API call.
279#[no_mangle]
280pub unsafe extern "C" fn overdrive_last_error() -> *const c_char {
281    LAST_ERROR.with(|e| {
282        match &*e.borrow() {
283            Some(msg) => {
284                // Leak a CString — caller should not free this
285                let cs = CString::new(msg.as_str()).unwrap_or_default();
286                cs.into_raw() as *const c_char
287            }
288            None => ptr::null(),
289        }
290    })
291}
292
293/// Free a string returned by the SDK.
294#[no_mangle]
295pub unsafe extern "C" fn overdrive_free_string(ptr: *mut c_char) {
296    if !ptr.is_null() {
297        let _ = CString::from_raw(ptr);
298    }
299}
300
301/// Get SDK version string.
302#[no_mangle]
303pub extern "C" fn overdrive_version() -> *const c_char {
304    // Static string, no need to free
305    b"1.4.0\0".as_ptr() as *const c_char
306}
307
308// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
309// MVCC TRANSACTIONS (Phase 5)
310// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
311
312/// Begin a transaction. isolation_level: 0=ReadUncommitted, 1=ReadCommitted, 2=RepeatableRead, 3=Serializable.
313/// Returns a transaction ID (>0) on success, 0 on error.
314#[no_mangle]
315pub unsafe extern "C" fn overdrive_begin_transaction(db: *mut ODB, isolation_level: i32) -> u64 {
316    clear_error();
317    if db.is_null() { set_error("Null db handle".into()); return 0; }
318    let isolation = crate::IsolationLevel::from_i32(isolation_level);
319    match (*db).inner.begin_transaction(isolation) {
320        Ok(txn) => txn.txn_id,
321        Err(e) => { set_error(e.to_string()); 0 }
322    }
323}
324
325/// Commit a transaction. Returns 0 on success, -1 on error.
326#[no_mangle]
327pub unsafe extern "C" fn overdrive_commit_transaction(db: *mut ODB, txn_id: u64) -> i32 {
328    clear_error();
329    if db.is_null() { set_error("Null db handle".into()); return -1; }
330    let txn = crate::TransactionHandle {
331        txn_id,
332        isolation: crate::IsolationLevel::ReadCommitted,
333        active: true,
334    };
335    match (*db).inner.commit_transaction(&txn) {
336        Ok(()) => 0,
337        Err(e) => { set_error(e.to_string()); -1 }
338    }
339}
340
341/// Abort (rollback) a transaction. Returns 0 on success, -1 on error.
342#[no_mangle]
343pub unsafe extern "C" fn overdrive_abort_transaction(db: *mut ODB, txn_id: u64) -> i32 {
344    clear_error();
345    if db.is_null() { set_error("Null db handle".into()); return -1; }
346    let txn = crate::TransactionHandle {
347        txn_id,
348        isolation: crate::IsolationLevel::ReadCommitted,
349        active: true,
350    };
351    match (*db).inner.abort_transaction(&txn) {
352        Ok(()) => 0,
353        Err(e) => { set_error(e.to_string()); -1 }
354    }
355}
356
357// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
358// INTEGRITY VERIFICATION (Phase 5)
359// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
360
361/// Verify database integrity. Returns JSON report string. Must be freed.
362#[no_mangle]
363pub unsafe extern "C" fn overdrive_verify_integrity(db: *mut ODB) -> *mut c_char {
364    clear_error();
365    if db.is_null() { set_error("Null db handle".into()); return ptr::null_mut(); }
366    match (*db).inner.verify_integrity() {
367        Ok(report) => {
368            let json = serde_json::json!({
369                "valid": report.is_valid,
370                "pages_checked": report.pages_checked,
371                "tables_verified": report.tables_verified,
372                "issues": report.issues,
373            });
374            to_c_string(&json.to_string())
375        }
376        Err(e) => { set_error(e.to_string()); ptr::null_mut() }
377    }
378}
379
380/// Get extended database stats. Returns JSON string. Must be freed.
381#[no_mangle]
382pub unsafe extern "C" fn overdrive_stats(db: *mut ODB) -> *mut c_char {
383    clear_error();
384    if db.is_null() { set_error("Null db handle".into()); return ptr::null_mut(); }
385    match (*db).inner.stats() {
386        Ok(stats) => {
387            let json = serde_json::json!({
388                "tables": stats.tables,
389                "total_records": stats.total_records,
390                "file_size_bytes": stats.file_size_bytes,
391                "path": stats.path,
392                "mvcc_active_versions": stats.mvcc_active_versions,
393                "page_size": stats.page_size,
394                "sdk_version": stats.sdk_version,
395            });
396            to_c_string(&json.to_string())
397        }
398        Err(e) => { set_error(e.to_string()); ptr::null_mut() }
399    }
400}
401