Skip to main content

readcon_db/
ffi.rs

1//! C ABI for readcon-db (always linked into cdylib/staticlib).
2//!
3//! Status codes mirror a small subset of readcon-core style (negative = error).
4
5use std::ffi::{CStr, CString};
6use std::os::raw::{c_char, c_int};
7use std::ptr;
8use std::sync::{Arc, Mutex};
9
10use crate::corpus::ConCorpus;
11use crate::keys::{hash_frame_bytes, ContentHash, FrameKey};
12use crate::select::Select;
13
14pub const RKRDB_OK: c_int = 0;
15pub const RKRDB_ERR: c_int = -1;
16pub const RKRDB_NOT_FOUND: c_int = -2;
17pub const RKRDB_NULL: c_int = -3;
18
19struct Handle {
20    /// Shared so ingest runs **outside** the handle-table mutex (no app-level writer serialize).
21    corpus: Arc<ConCorpus>,
22    last_keys: Vec<FrameKey>,
23    last_error: String,
24}
25
26static HANDLES: Mutex<Vec<Option<Box<Handle>>>> = Mutex::new(Vec::new());
27
28fn push_handle(h: Handle) -> usize {
29    let mut g = HANDLES.lock().unwrap();
30    for (i, slot) in g.iter_mut().enumerate() {
31        if slot.is_none() {
32            *slot = Some(Box::new(h));
33            return i;
34        }
35    }
36    g.push(Some(Box::new(h)));
37    g.len() - 1
38}
39
40/// Brief table lock for bookkeeping only — not held across ingest/select CPU or LMDB work.
41fn with_handle<F, T>(id: usize, f: F) -> Result<T, c_int>
42where
43    F: FnOnce(&mut Handle) -> Result<T, c_int>,
44{
45    let mut g = HANDLES.lock().unwrap();
46    let slot = g.get_mut(id).ok_or(RKRDB_NULL)?;
47    let h = slot.as_mut().ok_or(RKRDB_NULL)?;
48    f(h)
49}
50
51fn corpus_arc(id: usize) -> Result<Arc<ConCorpus>, c_int> {
52    let g = HANDLES.lock().unwrap();
53    let slot = g.get(id).ok_or(RKRDB_NULL)?;
54    let h = slot.as_ref().ok_or(RKRDB_NULL)?;
55    Ok(Arc::clone(&h.corpus))
56}
57
58fn set_err_id(id: usize, e: impl ToString) {
59    let mut g = HANDLES.lock().unwrap();
60    if let Some(Some(h)) = g.get_mut(id) {
61        h.last_error = e.to_string();
62    }
63}
64
65fn set_err(h: &mut Handle, e: impl ToString) {
66    h.last_error = e.to_string();
67}
68
69/// Open corpus directory. On success writes opaque handle id to `out_id` (>=0).
70/// Returns RKRDB_OK or error code.
71#[no_mangle]
72pub unsafe extern "C" fn rkrdb_open(path: *const c_char, out_id: *mut usize) -> c_int {
73    if path.is_null() || out_id.is_null() {
74        return RKRDB_NULL;
75    }
76    let cpath = unsafe { CStr::from_ptr(path) };
77    let path = match cpath.to_str() {
78        Ok(s) => s,
79        Err(_) => return RKRDB_ERR,
80    };
81    match ConCorpus::open(path) {
82        Ok(corpus) => {
83            let id = push_handle(Handle {
84                corpus: Arc::new(corpus),
85                last_keys: Vec::new(),
86                last_error: String::new(),
87            });
88            unsafe { *out_id = id };
89            RKRDB_OK
90        }
91        Err(_) => RKRDB_ERR,
92    }
93}
94
95#[no_mangle]
96pub unsafe extern "C" fn rkrdb_close(id: usize) -> c_int {
97    let mut g = HANDLES.lock().unwrap();
98    if let Some(slot) = g.get_mut(id) {
99        *slot = None;
100        RKRDB_OK
101    } else {
102        RKRDB_NULL
103    }
104}
105
106/// Last error message (thread-safe snapshot into caller buffer). Returns bytes written excluding NUL,
107/// or -1 if truncated / null.
108#[no_mangle]
109pub unsafe extern "C" fn rkrdb_last_error(id: usize, buf: *mut c_char, buflen: usize) -> c_int {
110    if buf.is_null() || buflen == 0 {
111        return RKRDB_NULL;
112    }
113    with_handle(id, |h| {
114        let bytes = h.last_error.as_bytes();
115        let n = (buflen - 1).min(bytes.len());
116        unsafe {
117            ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, n);
118            *buf.add(n) = 0;
119        }
120        Ok(n as c_int)
121    })
122    .unwrap_or(RKRDB_NULL)
123}
124
125#[no_mangle]
126pub unsafe extern "C" fn rkrdb_append_trajectory(
127    id: usize,
128    traj_id: u64,
129    path: *const c_char,
130    out_n_frames: *mut u32,
131) -> c_int {
132    if path.is_null() {
133        return RKRDB_NULL;
134    }
135    let cpath = unsafe { CStr::from_ptr(path) };
136    let path = match cpath.to_str() {
137        Ok(s) => s,
138        Err(_) => return RKRDB_ERR,
139    };
140    // Prepare+commit on Arc corpus **outside** handle mutex (concurrent writers on distinct handles).
141    let corpus = match corpus_arc(id) {
142        Ok(c) => c,
143        Err(c) => return c,
144    };
145    match corpus.append_trajectory_path(traj_id, path) {
146        Ok(n) => {
147            if !out_n_frames.is_null() {
148                unsafe { *out_n_frames = n };
149            }
150            RKRDB_OK
151        }
152        Err(e) => {
153            set_err_id(id, e);
154            RKRDB_ERR
155        }
156    }
157}
158
159/// Select by required symbol (optional) and natoms range (use 0, UINT32_MAX for any).
160/// Results stored internally; use rkrdb_result_count / rkrdb_result_key.
161#[no_mangle]
162pub unsafe extern "C" fn rkrdb_select_basic(
163    id: usize,
164    traj_id: i64,
165    symbol: *const c_char,
166    natoms_min: u32,
167    natoms_max: u32,
168    limit: u32,
169) -> c_int {
170    with_handle(id, |h| {
171        let mut sel = Select::new().natoms_range(natoms_min, natoms_max);
172        if traj_id >= 0 {
173            sel = sel.trajectory(traj_id as u64);
174        }
175        if !symbol.is_null() {
176            let s = unsafe { CStr::from_ptr(symbol) };
177            if let Ok(sym) = s.to_str() {
178                if !sym.is_empty() {
179                    sel = sel.require_symbol(sym);
180                }
181            }
182        }
183        if limit > 0 {
184            sel = sel.limit(limit as usize);
185        }
186        match h.corpus.select(&sel) {
187            Ok(keys) => {
188                h.last_keys = keys;
189                Ok(RKRDB_OK)
190            }
191            Err(e) => {
192                set_err(h, e);
193                Ok(RKRDB_ERR)
194            }
195        }
196    })
197    .unwrap_or(RKRDB_NULL)
198}
199
200/// Select by exact xxHash3-128 (16 bytes LE).
201#[no_mangle]
202pub unsafe extern "C" fn rkrdb_select_hash(id: usize, hash16: *const u8) -> c_int {
203    if hash16.is_null() {
204        return RKRDB_NULL;
205    }
206    let mut hb = [0u8; 16];
207    unsafe { ptr::copy_nonoverlapping(hash16, hb.as_mut_ptr(), 16) };
208    with_handle(id, |h| {
209        let sel = Select::new().exact_hash(hb);
210        match h.corpus.select(&sel) {
211            Ok(keys) => {
212                h.last_keys = keys;
213                Ok(RKRDB_OK)
214            }
215            Err(e) => {
216                set_err(h, e);
217                Ok(RKRDB_ERR)
218            }
219        }
220    })
221    .unwrap_or(RKRDB_NULL)
222}
223
224/// Metadata / section filters. Pass `use_energy_range=0` to ignore energy bounds.
225/// Flags: bit0=require_forces, bit1=require_velocities, bit2=require_energy.
226#[no_mangle]
227pub unsafe extern "C" fn rkrdb_select_meta(
228    id: usize,
229    traj_id: i64,
230    symbol: *const c_char,
231    natoms_min: u32,
232    natoms_max: u32,
233    energy_min: f64,
234    energy_max: f64,
235    use_energy_range: c_int,
236    flags: u32,
237    limit: u32,
238) -> c_int {
239    with_handle(id, |h| {
240        let mut sel = Select::new().natoms_range(natoms_min, natoms_max);
241        if traj_id >= 0 {
242            sel = sel.trajectory(traj_id as u64);
243        }
244        if !symbol.is_null() {
245            let s = unsafe { CStr::from_ptr(symbol) };
246            if let Ok(sym) = s.to_str() {
247                if !sym.is_empty() {
248                    sel = sel.require_symbol(sym);
249                }
250            }
251        }
252        if use_energy_range != 0 {
253            sel = sel.energy_range(energy_min, energy_max);
254        }
255        if flags & 1 != 0 {
256            sel = sel.require_forces();
257        }
258        if flags & 2 != 0 {
259            sel = sel.require_velocities();
260        }
261        if flags & 4 != 0 {
262            sel = sel.require_energy();
263        }
264        if limit > 0 {
265            sel = sel.limit(limit as usize);
266        }
267        match h.corpus.select(&sel) {
268            Ok(keys) => {
269                h.last_keys = keys;
270                Ok(RKRDB_OK)
271            }
272            Err(e) => {
273                set_err(h, e);
274                Ok(RKRDB_ERR)
275            }
276        }
277    })
278    .unwrap_or(RKRDB_NULL)
279}
280
281
282/// Rebuild secondary indexes from authoritative frame blobs.
283#[no_mangle]
284pub unsafe extern "C" fn rkrdb_reindex(id: usize) -> c_int {
285    with_handle(id, |h| match h.corpus.reindex() {
286        Ok(_) => Ok(RKRDB_OK),
287        Err(e) => {
288            set_err(h, e);
289            Ok(RKRDB_ERR)
290        }
291    })
292    .unwrap_or(RKRDB_NULL)
293}
294
295/// Opt-in cook: derive RCSO into `frames_soa` from CON text in `frames` (CON remains authority).
296#[no_mangle]
297pub unsafe extern "C" fn rkrdb_cook_frame(id: usize, traj_id: u64, frame_idx: u32) -> c_int {
298    with_handle(id, |h| {
299        match h.corpus.cook_frame(crate::keys::FrameKey {
300            traj_id,
301            frame_idx,
302        }) {
303            Ok(_) => Ok(RKRDB_OK),
304            Err(e) => {
305                set_err(h, e);
306                Ok(RKRDB_ERR)
307            }
308        }
309    })
310    .unwrap_or(RKRDB_NULL)
311}
312
313/// Drop cooked tier only; CON text and indexes unchanged.
314#[no_mangle]
315pub unsafe extern "C" fn rkrdb_delete_cooked(id: usize, traj_id: u64, frame_idx: u32) -> c_int {
316    with_handle(id, |h| {
317        match h.corpus.delete_cooked_soa(crate::keys::FrameKey {
318            traj_id,
319            frame_idx,
320        }) {
321            Ok(()) => Ok(RKRDB_OK),
322            Err(e) => {
323                set_err(h, e);
324                Ok(RKRDB_ERR)
325            }
326        }
327    })
328    .unwrap_or(RKRDB_NULL)
329}
330
331/// Returns 1 if valid RCSO present, 0 if missing/corrupt, negative on error.
332#[no_mangle]
333pub unsafe extern "C" fn rkrdb_has_valid_cooked(id: usize, traj_id: u64, frame_idx: u32) -> c_int {
334    with_handle(id, |h| {
335        match h.corpus.has_valid_cooked_soa(crate::keys::FrameKey {
336            traj_id,
337            frame_idx,
338        }) {
339            Ok(true) => Ok(1),
340            Ok(false) => Ok(0),
341            Err(e) => {
342                set_err(h, e);
343                Ok(RKRDB_ERR)
344            }
345        }
346    })
347    .unwrap_or(RKRDB_NULL)
348}
349
350/// Prefer cooked positions (no CON parse on hit); else parse CON.
351/// Writes `*out_natoms * 3` doubles into `out_xyz` (row-major N×3). `capacity_atoms` is max N.
352#[no_mangle]
353pub unsafe extern "C" fn rkrdb_get_positions(
354    id: usize,
355    traj_id: u64,
356    frame_idx: u32,
357    out_xyz: *mut f64,
358    capacity_atoms: u32,
359    out_natoms: *mut u32,
360) -> c_int {
361    if out_xyz.is_null() || out_natoms.is_null() {
362        return RKRDB_NULL;
363    }
364    with_handle(id, |h| {
365        match h.corpus.get_positions(crate::keys::FrameKey {
366            traj_id,
367            frame_idx,
368        }) {
369            Ok(pos) => {
370                let n = pos.len() as u32;
371                if n > capacity_atoms {
372                    set_err(
373                        h,
374                        crate::error::Error::Message("positions buffer too small".into()),
375                    );
376                    return Ok(RKRDB_ERR);
377                }
378                unsafe {
379                    *out_natoms = n;
380                    for (i, row) in pos.iter().enumerate() {
381                        *out_xyz.add(i * 3) = row[0];
382                        *out_xyz.add(i * 3 + 1) = row[1];
383                        *out_xyz.add(i * 3 + 2) = row[2];
384                    }
385                }
386                Ok(RKRDB_OK)
387            }
388            Err(e) => {
389                set_err(h, e);
390                Ok(RKRDB_ERR)
391            }
392        }
393    })
394    .unwrap_or(RKRDB_NULL)
395}
396
397/// Prefer cooked forces when present; writes N×3 doubles. Sets *out_has_forces 0/1.
398#[no_mangle]
399pub unsafe extern "C" fn rkrdb_get_forces(
400    id: usize,
401    traj_id: u64,
402    frame_idx: u32,
403    out_xyz: *mut f64,
404    capacity_atoms: u32,
405    out_natoms: *mut u32,
406    out_has_forces: *mut u8,
407) -> c_int {
408    if out_xyz.is_null() || out_natoms.is_null() || out_has_forces.is_null() {
409        return RKRDB_NULL;
410    }
411    with_handle(id, |h| {
412        match h.corpus.get_forces(crate::keys::FrameKey {
413            traj_id,
414            frame_idx,
415        }) {
416            Ok(None) => {
417                unsafe {
418                    *out_has_forces = 0;
419                    *out_natoms = 0;
420                }
421                Ok(RKRDB_OK)
422            }
423            Ok(Some(frc)) => {
424                let n = frc.len() as u32;
425                if n > capacity_atoms {
426                    set_err(
427                        h,
428                        crate::error::Error::Message("forces buffer too small".into()),
429                    );
430                    return Ok(RKRDB_ERR);
431                }
432                unsafe {
433                    *out_has_forces = 1;
434                    *out_natoms = n;
435                    for (i, row) in frc.iter().enumerate() {
436                        *out_xyz.add(i * 3) = row[0];
437                        *out_xyz.add(i * 3 + 1) = row[1];
438                        *out_xyz.add(i * 3 + 2) = row[2];
439                    }
440                }
441                Ok(RKRDB_OK)
442            }
443            Err(e) => {
444                set_err(h, e);
445                Ok(RKRDB_ERR)
446            }
447        }
448    })
449    .unwrap_or(RKRDB_NULL)
450}
451
452/// Cook every frame that has CON text (`recook_all`).
453#[no_mangle]
454pub unsafe extern "C" fn rkrdb_recook_all(id: usize) -> c_int {
455    with_handle(id, |h| match h.corpus.recook_all() {
456        Ok(_) => Ok(RKRDB_OK),
457        Err(e) => {
458            set_err(h, e);
459            Ok(RKRDB_ERR)
460        }
461    })
462    .unwrap_or(RKRDB_NULL)
463}
464
465/// Canonical composition formula for a stored frame (same as core `index_proj`).
466/// Writes into `buf` (NUL-terminated). Returns RKRDB_OK, RKRDB_NOT_FOUND, RKRDB_ERR, or buffer size need as positive? 
467/// On success returns RKRDB_OK; if buflen too small returns RKRDB_ERR and sets last_error.
468#[no_mangle]
469pub unsafe extern "C" fn rkrdb_frame_formula(
470    id: usize,
471    traj_id: u64,
472    frame_idx: u32,
473    buf: *mut c_char,
474    buflen: usize,
475) -> c_int {
476    if buf.is_null() || buflen == 0 {
477        return RKRDB_NULL;
478    }
479    with_handle(id, |h| {
480        match h.corpus.frame_formula(crate::keys::FrameKey {
481            traj_id,
482            frame_idx,
483        }) {
484            Ok(s) => {
485                let bytes = s.as_bytes();
486                if bytes.len() + 1 > buflen {
487                    set_err(h, crate::error::Error::Message("buffer too small".into()));
488                    return Ok(RKRDB_ERR);
489                }
490                unsafe {
491                    std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, bytes.len());
492                    *buf.add(bytes.len()) = 0;
493                }
494                Ok(RKRDB_OK)
495            }
496            Err(e) => {
497                set_err(h, e);
498                Ok(RKRDB_ERR)
499            }
500        }
501    })
502    .unwrap_or(RKRDB_NULL)
503}
504
505/// Campaign select: composition formula (NUL-terminated, may be null), optional fmax window.
506/// `use_fmax_range` non-zero applies fmax_min/max. Flags: bit0 forces, bit1 velocities, bit2 energy.
507/// Element constraints: pass `elem_sym` + `elem_count` + `elem_exact` (1=exact, 0=min) for one pair (null skips).
508#[no_mangle]
509pub unsafe extern "C" fn rkrdb_select_campaign(
510    id: usize,
511    traj_id: i64,
512    symbol: *const c_char,
513    natoms_min: u32,
514    natoms_max: u32,
515    formula: *const c_char,
516    energy_min: f64,
517    energy_max: f64,
518    use_energy_range: c_int,
519    fmax_min: f64,
520    fmax_max: f64,
521    use_fmax_range: c_int,
522    elem_sym: *const c_char,
523    elem_count: u32,
524    elem_exact: c_int,
525    flags: u32,
526    limit: u32,
527) -> c_int {
528    with_handle(id, |h| {
529        let mut sel = Select::new().natoms_range(natoms_min, natoms_max);
530        if traj_id >= 0 {
531            sel = sel.trajectory(traj_id as u64);
532        }
533        if !symbol.is_null() {
534            let s = unsafe { CStr::from_ptr(symbol) };
535            if let Ok(sym) = s.to_str() {
536                if !sym.is_empty() {
537                    sel = sel.require_symbol(sym);
538                }
539            }
540        }
541        if !formula.is_null() {
542            let s = unsafe { CStr::from_ptr(formula) };
543            if let Ok(f) = s.to_str() {
544                if !f.is_empty() {
545                    sel = sel.exact_composition(f);
546                }
547            }
548        }
549        if use_energy_range != 0 {
550            sel = sel.energy_range(energy_min, energy_max);
551        }
552        if use_fmax_range != 0 {
553            sel = sel.fmax_range(fmax_min, fmax_max);
554        }
555        if !elem_sym.is_null() {
556            let s = unsafe { CStr::from_ptr(elem_sym) };
557            if let Ok(sym) = s.to_str() {
558                if !sym.is_empty() {
559                    if elem_exact != 0 {
560                        sel = sel.element_exact(sym, elem_count);
561                    } else {
562                        sel = sel.element_min(sym, elem_count);
563                    }
564                }
565            }
566        }
567        if flags & 1 != 0 {
568            sel = sel.require_forces();
569        }
570        if flags & 2 != 0 {
571            sel = sel.require_velocities();
572        }
573        if flags & 4 != 0 {
574            sel = sel.require_energy();
575        }
576        if limit > 0 {
577            sel = sel.limit(limit as usize);
578        }
579        match h.corpus.select(&sel) {
580            Ok(keys) => {
581                h.last_keys = keys;
582                Ok(RKRDB_OK)
583            }
584            Err(e) => {
585                set_err(h, e);
586                Ok(RKRDB_ERR)
587            }
588        }
589    })
590    .unwrap_or(RKRDB_NULL)
591}
592
593#[no_mangle]
594pub unsafe extern "C" fn rkrdb_result_count(id: usize) -> c_int {
595    with_handle(id, |h| Ok(h.last_keys.len() as c_int)).unwrap_or(RKRDB_NULL)
596}
597
598/// Write traj_id and frame_idx for result index `i` (0-based).
599#[no_mangle]
600pub unsafe extern "C" fn rkrdb_result_key(
601    id: usize,
602    i: usize,
603    out_traj: *mut u64,
604    out_frame: *mut u32,
605) -> c_int {
606    if out_traj.is_null() || out_frame.is_null() {
607        return RKRDB_NULL;
608    }
609    with_handle(id, |h| {
610        let k = match h.last_keys.get(i) {
611            Some(k) => *k,
612            None => return Ok(RKRDB_NOT_FOUND),
613        };
614        unsafe {
615            *out_traj = k.traj_id;
616            *out_frame = k.frame_idx;
617        }
618        Ok(RKRDB_OK)
619    })
620    .unwrap_or(RKRDB_NULL)
621}
622
623/// Hash frame blob at key; writes 16 LE bytes to out_hash16.
624#[no_mangle]
625pub unsafe extern "C" fn rkrdb_frame_hash(
626    id: usize,
627    traj_id: u64,
628    frame_idx: u32,
629    out_hash16: *mut u8,
630) -> c_int {
631    if out_hash16.is_null() {
632        return RKRDB_NULL;
633    }
634    let key = FrameKey {
635        traj_id,
636        frame_idx,
637    };
638    with_handle(id, |h| match h.corpus.frame_hash(key) {
639        Ok(hash) => {
640            let b = hash.to_bytes();
641            unsafe { ptr::copy_nonoverlapping(b.as_ptr(), out_hash16, 16) };
642            Ok(RKRDB_OK)
643        }
644        Err(e) => {
645            set_err(h, e);
646            Ok(RKRDB_ERR)
647        }
648    })
649    .unwrap_or(RKRDB_NULL)
650}
651
652/// Copy frame CON text into buf (NUL-terminated). Returns length excluding NUL, or error code.
653#[no_mangle]
654pub unsafe extern "C" fn rkrdb_get_frame_text(
655    id: usize,
656    traj_id: u64,
657    frame_idx: u32,
658    buf: *mut c_char,
659    buflen: usize,
660) -> c_int {
661    if buf.is_null() || buflen == 0 {
662        return RKRDB_NULL;
663    }
664    let key = FrameKey {
665        traj_id,
666        frame_idx,
667    };
668    with_handle(id, |h| match h.corpus.get_frame_text(key) {
669        Ok(text) => {
670            let bytes = text.as_bytes();
671            if bytes.len() + 1 > buflen {
672                set_err(h, "buffer too small");
673                return Ok(RKRDB_ERR);
674            }
675            unsafe {
676                ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, bytes.len());
677                *buf.add(bytes.len()) = 0;
678            }
679            Ok(bytes.len() as c_int)
680        }
681        Err(e) => {
682            set_err(h, e);
683            Ok(RKRDB_ERR)
684        }
685    })
686    .unwrap_or(RKRDB_NULL)
687}
688
689/// xxHash3-128 of arbitrary bytes (LE 16 bytes) — for clients hashing off-line blobs.
690#[no_mangle]
691pub unsafe extern "C" fn rkrdb_xxh3_128(data: *const u8, len: usize, out_hash16: *mut u8) -> c_int {
692    if data.is_null() || out_hash16.is_null() {
693        return RKRDB_NULL;
694    }
695    let slice = unsafe { std::slice::from_raw_parts(data, len) };
696    let h = hash_frame_bytes(slice);
697    let b = h.to_bytes();
698    unsafe { ptr::copy_nonoverlapping(b.as_ptr(), out_hash16, 16) };
699    RKRDB_OK
700}
701
702// silence unused CString in some builds
703#[allow(dead_code)]
704fn _cs(s: &str) -> Result<CString, c_int> {
705    CString::new(s).map_err(|_| RKRDB_ERR)
706}
707
708// ContentHash used in find path
709#[allow(dead_code)]
710fn _ch(b: [u8; 16]) -> ContentHash {
711    ContentHash(b)
712}