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;
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, 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
19/// Breaking-change major for the public C ABI.
20pub const RKRDB_ABI_VERSION_MAJOR: u32 = 1;
21/// Additive-change minor for the public C ABI.
22pub const RKRDB_ABI_VERSION_MINOR: u32 = 1;
23/// Layout revision for exported records and the handle contract.
24pub const RKRDB_ABI_LAYOUT_REVISION: u32 = 1;
25
26/// Returns the breaking-change major of the loaded library's C ABI.
27#[no_mangle]
28pub extern "C" fn rkrdb_abi_version_major() -> u32 {
29    RKRDB_ABI_VERSION_MAJOR
30}
31
32/// Returns the additive-change minor of the loaded library's C ABI.
33#[no_mangle]
34pub extern "C" fn rkrdb_abi_version_minor() -> u32 {
35    RKRDB_ABI_VERSION_MINOR
36}
37
38/// Returns the layout revision of the loaded library's C ABI.
39#[no_mangle]
40pub extern "C" fn rkrdb_abi_layout_revision() -> u32 {
41    RKRDB_ABI_LAYOUT_REVISION
42}
43
44/// Returns the stable human-readable ABI negotiation stamp.
45///
46/// The pointer refers to static storage and must not be freed.
47#[no_mangle]
48pub extern "C" fn rkrdb_abi_stamp() -> *const c_char {
49    const STAMP: &[u8] = b"readcon-db/abi-1.1/layout-1\0";
50    STAMP.as_ptr() as *const c_char
51}
52
53/// Whether a library with stamp `library` serves a caller built against
54/// `caller`: equal major, equal layout revision, library minor at least the
55/// caller's. Each triple is `(major, minor, layout_revision)`.
56const fn abi_accepts(library: (u32, u32, u32), caller: (u32, u32, u32)) -> bool {
57    caller.0 == library.0 && caller.2 == library.2 && caller.1 <= library.1
58}
59
60/// Returns 1 when the loaded library can serve a caller built against the
61/// given `RKRDB_ABI_VERSION_MAJOR`, `RKRDB_ABI_VERSION_MINOR` and
62/// `RKRDB_ABI_LAYOUT_REVISION`, and 0 otherwise.
63#[no_mangle]
64pub extern "C" fn rkrdb_abi_compatible(major: u32, minor: u32, layout_revision: u32) -> c_int {
65    c_int::from(abi_accepts(
66        (
67            RKRDB_ABI_VERSION_MAJOR,
68            RKRDB_ABI_VERSION_MINOR,
69            RKRDB_ABI_LAYOUT_REVISION,
70        ),
71        (major, minor, layout_revision),
72    ))
73}
74
75struct Handle {
76    /// Shared so ingest runs **outside** the handle-table mutex (no app-level writer serialize).
77    corpus: Arc<ConCorpus>,
78    last_keys: Vec<FrameKey>,
79    last_error: String,
80    /// D10 override for H5MD velocity conversion. Empty means the frame rule.
81    velocity_time_unit: Option<String>,
82}
83
84static HANDLES: Mutex<Vec<Option<Box<Handle>>>> = Mutex::new(Vec::new());
85
86fn push_handle(h: Handle) -> usize {
87    let mut g = HANDLES.lock().unwrap();
88    for (i, slot) in g.iter_mut().enumerate() {
89        if slot.is_none() {
90            *slot = Some(Box::new(h));
91            return i;
92        }
93    }
94    g.push(Some(Box::new(h)));
95    g.len() - 1
96}
97
98/// Brief table lock for bookkeeping only — not held across ingest/select CPU or LMDB work.
99fn with_handle<F, T>(id: usize, f: F) -> Result<T, c_int>
100where
101    F: FnOnce(&mut Handle) -> Result<T, c_int>,
102{
103    let mut g = HANDLES.lock().unwrap();
104    let slot = g.get_mut(id).ok_or(RKRDB_NULL)?;
105    let h = slot.as_mut().ok_or(RKRDB_NULL)?;
106    f(h)
107}
108
109fn corpus_arc(id: usize) -> Result<Arc<ConCorpus>, c_int> {
110    let g = HANDLES.lock().unwrap();
111    let slot = g.get(id).ok_or(RKRDB_NULL)?;
112    let h = slot.as_ref().ok_or(RKRDB_NULL)?;
113    Ok(Arc::clone(&h.corpus))
114}
115
116fn set_err_id(id: usize, e: impl ToString) {
117    let mut g = HANDLES.lock().unwrap();
118    if let Some(Some(h)) = g.get_mut(id) {
119        h.last_error = e.to_string();
120    }
121}
122
123fn set_err(h: &mut Handle, e: impl ToString) {
124    h.last_error = e.to_string();
125}
126
127/// Open corpus directory. On success writes opaque handle id to `out_id` (>=0).
128/// Returns RKRDB_OK or error code.
129#[no_mangle]
130pub unsafe extern "C" fn rkrdb_open(path: *const c_char, out_id: *mut usize) -> c_int {
131    if path.is_null() || out_id.is_null() {
132        return RKRDB_NULL;
133    }
134    let cpath = unsafe { CStr::from_ptr(path) };
135    let path = match cpath.to_str() {
136        Ok(s) => s,
137        Err(_) => return RKRDB_ERR,
138    };
139    match ConCorpus::open(path) {
140        Ok(corpus) => {
141            let id = push_handle(Handle {
142                corpus: Arc::new(corpus),
143                last_keys: Vec::new(),
144                last_error: String::new(),
145                velocity_time_unit: None,
146            });
147            unsafe { *out_id = id };
148            RKRDB_OK
149        }
150        Err(_) => RKRDB_ERR,
151    }
152}
153
154/// Open an existing corpus `MDB_RDONLY`. No mkdir, no write txn.
155#[no_mangle]
156pub unsafe extern "C" fn rkrdb_open_readonly(path: *const c_char, out_id: *mut usize) -> c_int {
157    if path.is_null() || out_id.is_null() {
158        return RKRDB_NULL;
159    }
160    let cpath = unsafe { CStr::from_ptr(path) };
161    let path = match cpath.to_str() {
162        Ok(s) => s,
163        Err(_) => return RKRDB_ERR,
164    };
165    match ConCorpus::open_readonly(path) {
166        Ok(corpus) => {
167            let id = push_handle(Handle {
168                corpus: Arc::new(corpus),
169                last_keys: Vec::new(),
170                last_error: String::new(),
171                velocity_time_unit: None,
172            });
173            unsafe { *out_id = id };
174            RKRDB_OK
175        }
176        Err(_) => RKRDB_ERR,
177    }
178}
179
180/// Pack one frame as RCSO bytes for `MPI_Bcast` on the *caller* communicator.
181/// `buf == NULL` returns the required size. Rank 0 of that comm calls this;
182/// other ranks never open the env.
183#[no_mangle]
184pub unsafe extern "C" fn rkrdb_pack_frame(
185    id: usize,
186    traj_id: u64,
187    frame_idx: u32,
188    buf: *mut u8,
189    buflen: usize,
190) -> c_int {
191    let key = FrameKey { traj_id, frame_idx };
192    with_handle(id, |h| match h.corpus.pack_frame(key) {
193        Ok(bytes) => {
194            if buf.is_null() {
195                return Ok(bytes.len() as c_int);
196            }
197            if bytes.len() > buflen {
198                set_err(h, "buffer too small");
199                return Ok(RKRDB_ERR);
200            }
201            unsafe {
202                ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
203            }
204            Ok(bytes.len() as c_int)
205        }
206        Err(e) => {
207            set_err(h, e);
208            Ok(RKRDB_ERR)
209        }
210    })
211    .unwrap_or(RKRDB_NULL)
212}
213
214/// Pack many frames as one RCSB envelope. `buf == NULL` returns the required size.
215/// Rank 0 of the caller comm packs; others never open the env.
216#[no_mangle]
217pub unsafe extern "C" fn rkrdb_pack_frames(
218    id: usize,
219    traj_ids: *const u64,
220    frame_idxs: *const u32,
221    nkeys: u32,
222    buf: *mut u8,
223    buflen: usize,
224) -> c_int {
225    if traj_ids.is_null() || frame_idxs.is_null() || nkeys == 0 {
226        return RKRDB_NULL;
227    }
228    let n = nkeys as usize;
229    let trajs = unsafe { std::slice::from_raw_parts(traj_ids, n) };
230    let frames = unsafe { std::slice::from_raw_parts(frame_idxs, n) };
231    let keys: Vec<FrameKey> = trajs
232        .iter()
233        .zip(frames.iter())
234        .map(|(&traj_id, &frame_idx)| FrameKey { traj_id, frame_idx })
235        .collect();
236    with_handle(id, |h| match h.corpus.pack_frames(&keys) {
237        Ok(bytes) => {
238            if buf.is_null() {
239                return Ok(bytes.len() as c_int);
240            }
241            if bytes.len() > buflen {
242                set_err(h, "buffer too small");
243                return Ok(RKRDB_ERR);
244            }
245            unsafe {
246                ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
247            }
248            Ok(bytes.len() as c_int)
249        }
250        Err(e) => {
251            set_err(h, e);
252            Ok(RKRDB_ERR)
253        }
254    })
255    .unwrap_or(RKRDB_NULL)
256}
257
258/// Number of RCSO blobs in an RCSB envelope. No corpus handle.
259#[no_mangle]
260pub unsafe extern "C" fn rkrdb_unpack_batch_nframes(
261    buf: *const u8,
262    buflen: usize,
263    out_n: *mut u32,
264) -> c_int {
265    if buf.is_null() || out_n.is_null() {
266        return RKRDB_NULL;
267    }
268    let slice = unsafe { std::slice::from_raw_parts(buf, buflen) };
269    match crate::cooked_soa::decode_batch(slice) {
270        Ok(parts) => {
271            unsafe { *out_n = parts.len() as u32 };
272            RKRDB_OK
273        }
274        Err(_) => RKRDB_ERR,
275    }
276}
277
278/// Unpack one RCSO item from an RCSB envelope into xyz. No corpus handle.
279#[no_mangle]
280pub unsafe extern "C" fn rkrdb_unpack_batch_item(
281    buf: *const u8,
282    buflen: usize,
283    index: u32,
284    out_xyz: *mut f64,
285    capacity_atoms: u32,
286    out_natoms: *mut u32,
287) -> c_int {
288    if buf.is_null() || out_xyz.is_null() || out_natoms.is_null() {
289        return RKRDB_NULL;
290    }
291    let slice = unsafe { std::slice::from_raw_parts(buf, buflen) };
292    let parts = match crate::cooked_soa::decode_batch(slice) {
293        Ok(p) => p,
294        Err(_) => return RKRDB_ERR,
295    };
296    let item = match parts.get(index as usize) {
297        Some(b) => b,
298        None => return RKRDB_NOT_FOUND,
299    };
300    rkrdb_unpack_positions(
301        item.as_ptr(),
302        item.len(),
303        out_xyz,
304        capacity_atoms,
305        out_natoms,
306    )
307}
308
309/// Unpack RCSO positions (no corpus handle — MPI worker side).
310#[no_mangle]
311pub unsafe extern "C" fn rkrdb_unpack_positions(
312    buf: *const u8,
313    buflen: usize,
314    out_xyz: *mut f64,
315    capacity_atoms: u32,
316    out_natoms: *mut u32,
317) -> c_int {
318    if buf.is_null() || out_xyz.is_null() || out_natoms.is_null() {
319        return RKRDB_NULL;
320    }
321    let slice = unsafe { std::slice::from_raw_parts(buf, buflen) };
322    let cooked = match crate::cooked_soa::CookedSoa::decode(slice) {
323        Ok(c) => c,
324        Err(_) => return RKRDB_ERR,
325    };
326    if cooked.natoms > capacity_atoms {
327        return RKRDB_ERR;
328    }
329    unsafe { *out_natoms = cooked.natoms };
330    let n = cooked.natoms as usize;
331    let dest = unsafe { std::slice::from_raw_parts_mut(out_xyz, n.saturating_mul(3)) };
332    for (i, p) in cooked.positions.iter().enumerate() {
333        dest[i * 3] = p[0];
334        dest[i * 3 + 1] = p[1];
335        dest[i * 3 + 2] = p[2];
336    }
337    RKRDB_OK
338}
339
340#[no_mangle]
341pub unsafe extern "C" fn rkrdb_close(id: usize) -> c_int {
342    let mut g = HANDLES.lock().unwrap();
343    if let Some(slot) = g.get_mut(id) {
344        *slot = None;
345        RKRDB_OK
346    } else {
347        RKRDB_NULL
348    }
349}
350
351/// Last error message (thread-safe snapshot into caller buffer). Returns bytes written excluding NUL,
352/// or -1 if truncated / null.
353#[no_mangle]
354pub unsafe extern "C" fn rkrdb_last_error(id: usize, buf: *mut c_char, buflen: usize) -> c_int {
355    if buf.is_null() || buflen == 0 {
356        return RKRDB_NULL;
357    }
358    with_handle(id, |h| {
359        let bytes = h.last_error.as_bytes();
360        let n = (buflen - 1).min(bytes.len());
361        unsafe {
362            ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, n);
363            *buf.add(n) = 0;
364        }
365        Ok(n as c_int)
366    })
367    .unwrap_or(RKRDB_NULL)
368}
369
370fn parse_units_json(p: *const c_char) -> Result<Option<serde_json::Value>, c_int> {
371    if p.is_null() {
372        return Ok(None);
373    }
374    let s = unsafe { CStr::from_ptr(p) }
375        .to_str()
376        .map_err(|_| RKRDB_ERR)?;
377    if s.is_empty() {
378        return Ok(None);
379    }
380    serde_json::from_str(s).map(Some).map_err(|_| RKRDB_ERR)
381}
382
383/// `units_json` is optional JSON (`{"length":"A","energy":"ev"}`). NULL stamps nothing.
384#[no_mangle]
385pub unsafe extern "C" fn rkrdb_append_trajectory_units(
386    id: usize,
387    traj_id: u64,
388    path: *const c_char,
389    units_json: *const c_char,
390    out_n_frames: *mut u32,
391) -> c_int {
392    if path.is_null() {
393        return RKRDB_NULL;
394    }
395    let cpath = unsafe { CStr::from_ptr(path) };
396    let path = match cpath.to_str() {
397        Ok(s) => s,
398        Err(_) => return RKRDB_ERR,
399    };
400    let units = match parse_units_json(units_json) {
401        Ok(u) => u,
402        Err(c) => return c,
403    };
404    let corpus = match corpus_arc(id) {
405        Ok(c) => c,
406        Err(c) => return c,
407    };
408    match corpus.append_trajectory_path_units(traj_id, path, units) {
409        Ok(n) => {
410            if !out_n_frames.is_null() {
411                unsafe { *out_n_frames = n };
412            }
413            RKRDB_OK
414        }
415        Err(e) => {
416            set_err_id(id, e);
417            RKRDB_ERR
418        }
419    }
420}
421
422#[no_mangle]
423pub unsafe extern "C" fn rkrdb_append_trajectory(
424    id: usize,
425    traj_id: u64,
426    path: *const c_char,
427    out_n_frames: *mut u32,
428) -> c_int {
429    rkrdb_append_trajectory_units(id, traj_id, path, ptr::null(), out_n_frames)
430}
431
432/// Ingest CON text. `source` may be NULL (`"memory"`).
433#[no_mangle]
434pub unsafe extern "C" fn rkrdb_append_trajectory_str(
435    id: usize,
436    traj_id: u64,
437    text: *const c_char,
438    source: *const c_char,
439    out_n_frames: *mut u32,
440) -> c_int {
441    if text.is_null() {
442        return RKRDB_NULL;
443    }
444    let text = match unsafe { CStr::from_ptr(text) }.to_str() {
445        Ok(s) => s,
446        Err(_) => return RKRDB_ERR,
447    };
448    let source = if source.is_null() {
449        "memory"
450    } else {
451        match unsafe { CStr::from_ptr(source) }.to_str() {
452            Ok(s) if !s.is_empty() => s,
453            Ok(_) => "memory",
454            Err(_) => return RKRDB_ERR,
455        }
456    };
457    let corpus = match corpus_arc(id) {
458        Ok(c) => c,
459        Err(c) => return c,
460    };
461    match corpus.append_trajectory_str(traj_id, text, source) {
462        Ok(n) => {
463            if !out_n_frames.is_null() {
464                unsafe { *out_n_frames = n };
465            }
466            RKRDB_OK
467        }
468        Err(e) => {
469            set_err_id(id, e);
470            RKRDB_ERR
471        }
472    }
473}
474
475/// Ingest one parsed `RKRConFrame*` from libreadcon_core. Caller keeps the handle.
476#[no_mangle]
477pub unsafe extern "C" fn rkrdb_append_trajectory_frame(
478    id: usize,
479    traj_id: u64,
480    frame: *const std::ffi::c_void,
481    source: *const c_char,
482    out_n_frames: *mut u32,
483) -> c_int {
484    ingest_trajectory_frame(id, traj_id, frame, source, out_n_frames, false)
485}
486
487/// Extend an existing trajectory with one parsed frame, or create it.
488/// Returns the total frame count. Caller retains ownership of the frame.
489#[no_mangle]
490pub unsafe extern "C" fn rkrdb_extend_trajectory_frame(
491    id: usize,
492    traj_id: u64,
493    frame: *const std::ffi::c_void,
494    source: *const c_char,
495    out_n_frames: *mut u32,
496) -> c_int {
497    ingest_trajectory_frame(id, traj_id, frame, source, out_n_frames, true)
498}
499
500unsafe fn ingest_trajectory_frame(
501    id: usize,
502    traj_id: u64,
503    frame: *const std::ffi::c_void,
504    source: *const c_char,
505    out_n_frames: *mut u32,
506    extend: bool,
507) -> c_int {
508    if frame.is_null() {
509        return RKRDB_NULL;
510    }
511    let source = if source.is_null() {
512        "memory"
513    } else {
514        match unsafe { CStr::from_ptr(source) }.to_str() {
515            Ok(s) if !s.is_empty() => s,
516            Ok(_) => "memory",
517            Err(_) => return RKRDB_ERR,
518        }
519    };
520    let corpus = match corpus_arc(id) {
521        Ok(c) => c,
522        Err(c) => return c,
523    };
524    let parsed = unsafe { &*(frame as *const readcon_core::types::ConFrame) };
525    let frames = std::slice::from_ref(parsed);
526    let result = if extend {
527        corpus.extend_trajectory_frames(traj_id, frames, source)
528    } else {
529        corpus.append_trajectory_frames(traj_id, frames, source)
530    };
531    match result {
532        Ok(n) => {
533            if !out_n_frames.is_null() {
534                unsafe { *out_n_frames = n };
535            }
536            RKRDB_OK
537        }
538        Err(e) => {
539            set_err_id(id, e);
540            RKRDB_ERR
541        }
542    }
543}
544
545/// Create the trajectory or append frames after the live count.
546/// `units_json` is optional; NULL stamps nothing on the new frames.
547#[no_mangle]
548pub unsafe extern "C" fn rkrdb_extend_trajectory_units(
549    id: usize,
550    traj_id: u64,
551    path: *const c_char,
552    units_json: *const c_char,
553    out_n_frames: *mut u32,
554) -> c_int {
555    if path.is_null() {
556        return RKRDB_NULL;
557    }
558    let cpath = unsafe { CStr::from_ptr(path) };
559    let path = match cpath.to_str() {
560        Ok(s) => s,
561        Err(_) => return RKRDB_ERR,
562    };
563    let units = match parse_units_json(units_json) {
564        Ok(u) => u,
565        Err(c) => return c,
566    };
567    let corpus = match corpus_arc(id) {
568        Ok(c) => c,
569        Err(c) => return c,
570    };
571    match corpus.extend_trajectory_path_units(traj_id, path, units) {
572        Ok(n) => {
573            if !out_n_frames.is_null() {
574                unsafe { *out_n_frames = n };
575            }
576            RKRDB_OK
577        }
578        Err(e) => {
579            set_err_id(id, e);
580            RKRDB_ERR
581        }
582    }
583}
584
585#[no_mangle]
586pub unsafe extern "C" fn rkrdb_extend_trajectory(
587    id: usize,
588    traj_id: u64,
589    path: *const c_char,
590    out_n_frames: *mut u32,
591) -> c_int {
592    rkrdb_extend_trajectory_units(id, traj_id, path, ptr::null(), out_n_frames)
593}
594
595/// Convert stored numbers and rewrite CON `units` for every frame of `traj_id`.
596#[no_mangle]
597pub unsafe extern "C" fn rkrdb_set_units(
598    id: usize,
599    traj_id: u64,
600    units_json: *const c_char,
601    out_n_frames: *mut u32,
602) -> c_int {
603    if units_json.is_null() {
604        return RKRDB_NULL;
605    }
606    let units = match parse_units_json(units_json) {
607        Ok(Some(u)) => u,
608        Ok(None) => return RKRDB_ERR,
609        Err(c) => return c,
610    };
611    let corpus = match corpus_arc(id) {
612        Ok(c) => c,
613        Err(c) => return c,
614    };
615    match corpus.set_trajectory_units(traj_id, units) {
616        Ok(n) => {
617            if !out_n_frames.is_null() {
618                unsafe { *out_n_frames = n };
619            }
620            RKRDB_OK
621        }
622        Err(e) => {
623            set_err_id(id, e);
624            RKRDB_ERR
625        }
626    }
627}
628
629/// Write the frame `units` object as JSON into `buf` (NUL-terminated).
630#[no_mangle]
631pub unsafe extern "C" fn rkrdb_frame_units(
632    id: usize,
633    traj_id: u64,
634    frame_idx: u32,
635    buf: *mut c_char,
636    buflen: usize,
637) -> c_int {
638    if buf.is_null() || buflen == 0 {
639        return RKRDB_NULL;
640    }
641    let corpus = match corpus_arc(id) {
642        Ok(c) => c,
643        Err(c) => return c,
644    };
645    match corpus.frame_units(FrameKey { traj_id, frame_idx }) {
646        Ok(Some(v)) => {
647            let s = v.to_string();
648            if s.len() + 1 > buflen {
649                set_err_id(id, "frame_units buffer too small");
650                return RKRDB_ERR;
651            }
652            unsafe {
653                ptr::copy_nonoverlapping(s.as_ptr(), buf as *mut u8, s.len());
654                *buf.add(s.len()) = 0;
655            }
656            RKRDB_OK
657        }
658        Ok(None) => RKRDB_NOT_FOUND,
659        Err(e) => {
660            set_err_id(id, e);
661            RKRDB_ERR
662        }
663    }
664}
665
666/// Select by required symbol (optional) and natoms range (use 0, UINT32_MAX for any).
667/// Results stored internally; use rkrdb_result_count / rkrdb_result_key.
668#[no_mangle]
669pub unsafe extern "C" fn rkrdb_select_basic(
670    id: usize,
671    traj_id: i64,
672    symbol: *const c_char,
673    natoms_min: u32,
674    natoms_max: u32,
675    limit: u32,
676) -> c_int {
677    with_handle(id, |h| {
678        let mut sel = Select::new().natoms_range(natoms_min, natoms_max);
679        if traj_id >= 0 {
680            sel = sel.trajectory(traj_id as u64);
681        }
682        if !symbol.is_null() {
683            let s = unsafe { CStr::from_ptr(symbol) };
684            if let Ok(sym) = s.to_str() {
685                if !sym.is_empty() {
686                    sel = sel.require_symbol(sym);
687                }
688            }
689        }
690        if limit > 0 {
691            sel = sel.limit(limit as usize);
692        }
693        match h.corpus.select(&sel) {
694            Ok(keys) => {
695                h.last_keys = keys;
696                Ok(RKRDB_OK)
697            }
698            Err(e) => {
699                set_err(h, e);
700                Ok(RKRDB_ERR)
701            }
702        }
703    })
704    .unwrap_or(RKRDB_NULL)
705}
706
707/// Select by exact xxHash3-128 (16 bytes LE).
708#[no_mangle]
709pub unsafe extern "C" fn rkrdb_select_hash(id: usize, hash16: *const u8) -> c_int {
710    if hash16.is_null() {
711        return RKRDB_NULL;
712    }
713    let mut hb = [0u8; 16];
714    unsafe { ptr::copy_nonoverlapping(hash16, hb.as_mut_ptr(), 16) };
715    with_handle(id, |h| {
716        let sel = Select::new().exact_hash(hb);
717        match h.corpus.select(&sel) {
718            Ok(keys) => {
719                h.last_keys = keys;
720                Ok(RKRDB_OK)
721            }
722            Err(e) => {
723                set_err(h, e);
724                Ok(RKRDB_ERR)
725            }
726        }
727    })
728    .unwrap_or(RKRDB_NULL)
729}
730
731/// Metadata / section filters. Pass `use_energy_range=0` to ignore energy bounds.
732/// Flags: bit0=require_forces, bit1=require_velocities, bit2=require_energy.
733#[no_mangle]
734pub unsafe extern "C" fn rkrdb_select_meta(
735    id: usize,
736    traj_id: i64,
737    symbol: *const c_char,
738    natoms_min: u32,
739    natoms_max: u32,
740    energy_min: f64,
741    energy_max: f64,
742    use_energy_range: c_int,
743    flags: u32,
744    limit: u32,
745) -> c_int {
746    with_handle(id, |h| {
747        let mut sel = Select::new().natoms_range(natoms_min, natoms_max);
748        if traj_id >= 0 {
749            sel = sel.trajectory(traj_id as u64);
750        }
751        if !symbol.is_null() {
752            let s = unsafe { CStr::from_ptr(symbol) };
753            if let Ok(sym) = s.to_str() {
754                if !sym.is_empty() {
755                    sel = sel.require_symbol(sym);
756                }
757            }
758        }
759        if use_energy_range != 0 {
760            sel = sel.energy_range(energy_min, energy_max);
761        }
762        if flags & 1 != 0 {
763            sel = sel.require_forces();
764        }
765        if flags & 2 != 0 {
766            sel = sel.require_velocities();
767        }
768        if flags & 4 != 0 {
769            sel = sel.require_energy();
770        }
771        if limit > 0 {
772            sel = sel.limit(limit as usize);
773        }
774        match h.corpus.select(&sel) {
775            Ok(keys) => {
776                h.last_keys = keys;
777                Ok(RKRDB_OK)
778            }
779            Err(e) => {
780                set_err(h, e);
781                Ok(RKRDB_ERR)
782            }
783        }
784    })
785    .unwrap_or(RKRDB_NULL)
786}
787
788/// Rebuild secondary indexes from authoritative frame blobs.
789#[no_mangle]
790pub unsafe extern "C" fn rkrdb_reindex(id: usize) -> c_int {
791    with_handle(id, |h| match h.corpus.reindex() {
792        Ok(_) => Ok(RKRDB_OK),
793        Err(e) => {
794            set_err(h, e);
795            Ok(RKRDB_ERR)
796        }
797    })
798    .unwrap_or(RKRDB_NULL)
799}
800
801/// Opt-in cook: derive RCSO into `frames_soa` from CON text in `frames` (CON remains authority).
802#[no_mangle]
803pub unsafe extern "C" fn rkrdb_cook_frame(id: usize, traj_id: u64, frame_idx: u32) -> c_int {
804    with_handle(id, |h| {
805        match h
806            .corpus
807            .cook_frame(crate::keys::FrameKey { traj_id, frame_idx })
808        {
809            Ok(_) => Ok(RKRDB_OK),
810            Err(e) => {
811                set_err(h, e);
812                Ok(RKRDB_ERR)
813            }
814        }
815    })
816    .unwrap_or(RKRDB_NULL)
817}
818
819/// Drop cooked tier only; CON text and indexes unchanged.
820#[no_mangle]
821pub unsafe extern "C" fn rkrdb_delete_cooked(id: usize, traj_id: u64, frame_idx: u32) -> c_int {
822    with_handle(id, |h| {
823        match h
824            .corpus
825            .delete_cooked_soa(crate::keys::FrameKey { traj_id, frame_idx })
826        {
827            Ok(()) => Ok(RKRDB_OK),
828            Err(e) => {
829                set_err(h, e);
830                Ok(RKRDB_ERR)
831            }
832        }
833    })
834    .unwrap_or(RKRDB_NULL)
835}
836
837/// Returns 1 if valid RCSO present, 0 if missing/corrupt, negative on error.
838#[no_mangle]
839pub unsafe extern "C" fn rkrdb_has_valid_cooked(id: usize, traj_id: u64, frame_idx: u32) -> c_int {
840    with_handle(id, |h| {
841        match h
842            .corpus
843            .has_valid_cooked_soa(crate::keys::FrameKey { traj_id, frame_idx })
844        {
845            Ok(true) => Ok(1),
846            Ok(false) => Ok(0),
847            Err(e) => {
848                set_err(h, e);
849                Ok(RKRDB_ERR)
850            }
851        }
852    })
853    .unwrap_or(RKRDB_NULL)
854}
855
856/// Prefer cooked positions (no CON parse on hit); else parse CON.
857/// Writes `*out_natoms * 3` doubles into `out_xyz` (row-major N×3). `capacity_atoms` is max N.
858#[no_mangle]
859pub unsafe extern "C" fn rkrdb_get_positions(
860    id: usize,
861    traj_id: u64,
862    frame_idx: u32,
863    out_xyz: *mut f64,
864    capacity_atoms: u32,
865    out_natoms: *mut u32,
866) -> c_int {
867    if out_xyz.is_null() || out_natoms.is_null() {
868        return RKRDB_NULL;
869    }
870    with_handle(id, |h| {
871        match h
872            .corpus
873            .get_positions(crate::keys::FrameKey { traj_id, frame_idx })
874        {
875            Ok(pos) => {
876                let n = pos.len() as u32;
877                if n > capacity_atoms {
878                    set_err(
879                        h,
880                        crate::error::Error::Message("positions buffer too small".into()),
881                    );
882                    return Ok(RKRDB_ERR);
883                }
884                unsafe {
885                    *out_natoms = n;
886                    for (i, row) in pos.iter().enumerate() {
887                        *out_xyz.add(i * 3) = row[0];
888                        *out_xyz.add(i * 3 + 1) = row[1];
889                        *out_xyz.add(i * 3 + 2) = row[2];
890                    }
891                }
892                Ok(RKRDB_OK)
893            }
894            Err(e) => {
895                set_err(h, e);
896                Ok(RKRDB_ERR)
897            }
898        }
899    })
900    .unwrap_or(RKRDB_NULL)
901}
902
903/// Prefer cooked forces when present; writes N×3 doubles. Sets *out_has_forces 0/1.
904#[no_mangle]
905pub unsafe extern "C" fn rkrdb_get_forces(
906    id: usize,
907    traj_id: u64,
908    frame_idx: u32,
909    out_xyz: *mut f64,
910    capacity_atoms: u32,
911    out_natoms: *mut u32,
912    out_has_forces: *mut u8,
913) -> c_int {
914    if out_xyz.is_null() || out_natoms.is_null() || out_has_forces.is_null() {
915        return RKRDB_NULL;
916    }
917    with_handle(id, |h| {
918        match h
919            .corpus
920            .get_forces(crate::keys::FrameKey { traj_id, frame_idx })
921        {
922            Ok(None) => {
923                unsafe {
924                    *out_has_forces = 0;
925                    *out_natoms = 0;
926                }
927                Ok(RKRDB_OK)
928            }
929            Ok(Some(frc)) => {
930                let n = frc.len() as u32;
931                if n > capacity_atoms {
932                    set_err(
933                        h,
934                        crate::error::Error::Message("forces buffer too small".into()),
935                    );
936                    return Ok(RKRDB_ERR);
937                }
938                unsafe {
939                    *out_has_forces = 1;
940                    *out_natoms = n;
941                    for (i, row) in frc.iter().enumerate() {
942                        *out_xyz.add(i * 3) = row[0];
943                        *out_xyz.add(i * 3 + 1) = row[1];
944                        *out_xyz.add(i * 3 + 2) = row[2];
945                    }
946                }
947                Ok(RKRDB_OK)
948            }
949            Err(e) => {
950                set_err(h, e);
951                Ok(RKRDB_ERR)
952            }
953        }
954    })
955    .unwrap_or(RKRDB_NULL)
956}
957
958/// Cook every frame that has CON text (`recook_all`).
959#[no_mangle]
960pub unsafe extern "C" fn rkrdb_recook_all(id: usize) -> c_int {
961    with_handle(id, |h| match h.corpus.recook_all() {
962        Ok(_) => Ok(RKRDB_OK),
963        Err(e) => {
964            set_err(h, e);
965            Ok(RKRDB_ERR)
966        }
967    })
968    .unwrap_or(RKRDB_NULL)
969}
970
971/// Canonical composition formula for a stored frame (same as core `index_proj`).
972/// Writes into `buf` (NUL-terminated). Returns RKRDB_OK, RKRDB_NOT_FOUND, RKRDB_ERR, or buffer size need as positive?
973/// On success returns RKRDB_OK; if buflen too small returns RKRDB_ERR and sets last_error.
974#[no_mangle]
975pub unsafe extern "C" fn rkrdb_frame_formula(
976    id: usize,
977    traj_id: u64,
978    frame_idx: u32,
979    buf: *mut c_char,
980    buflen: usize,
981) -> c_int {
982    if buf.is_null() || buflen == 0 {
983        return RKRDB_NULL;
984    }
985    with_handle(id, |h| {
986        match h
987            .corpus
988            .frame_formula(crate::keys::FrameKey { traj_id, frame_idx })
989        {
990            Ok(s) => {
991                let bytes = s.as_bytes();
992                if bytes.len() + 1 > buflen {
993                    set_err(h, crate::error::Error::Message("buffer too small".into()));
994                    return Ok(RKRDB_ERR);
995                }
996                unsafe {
997                    std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, bytes.len());
998                    *buf.add(bytes.len()) = 0;
999                }
1000                Ok(RKRDB_OK)
1001            }
1002            Err(e) => {
1003                set_err(h, e);
1004                Ok(RKRDB_ERR)
1005            }
1006        }
1007    })
1008    .unwrap_or(RKRDB_NULL)
1009}
1010
1011/// Campaign select: composition formula (NUL-terminated, may be null), optional fmax window.
1012/// `use_fmax_range` non-zero applies fmax_min/max. Flags: bit0 forces, bit1 velocities, bit2 energy.
1013/// Element constraints: pass `elem_sym` + `elem_count` + `elem_exact` (1=exact, 0=min) for one pair (null skips).
1014#[no_mangle]
1015pub unsafe extern "C" fn rkrdb_select_campaign(
1016    id: usize,
1017    traj_id: i64,
1018    symbol: *const c_char,
1019    natoms_min: u32,
1020    natoms_max: u32,
1021    formula: *const c_char,
1022    energy_min: f64,
1023    energy_max: f64,
1024    use_energy_range: c_int,
1025    fmax_min: f64,
1026    fmax_max: f64,
1027    use_fmax_range: c_int,
1028    elem_sym: *const c_char,
1029    elem_count: u32,
1030    elem_exact: c_int,
1031    flags: u32,
1032    limit: u32,
1033) -> c_int {
1034    with_handle(id, |h| {
1035        let mut sel = Select::new().natoms_range(natoms_min, natoms_max);
1036        if traj_id >= 0 {
1037            sel = sel.trajectory(traj_id as u64);
1038        }
1039        if !symbol.is_null() {
1040            let s = unsafe { CStr::from_ptr(symbol) };
1041            if let Ok(sym) = s.to_str() {
1042                if !sym.is_empty() {
1043                    sel = sel.require_symbol(sym);
1044                }
1045            }
1046        }
1047        if !formula.is_null() {
1048            let s = unsafe { CStr::from_ptr(formula) };
1049            if let Ok(f) = s.to_str() {
1050                if !f.is_empty() {
1051                    sel = sel.exact_composition(f);
1052                }
1053            }
1054        }
1055        if use_energy_range != 0 {
1056            sel = sel.energy_range(energy_min, energy_max);
1057        }
1058        if use_fmax_range != 0 {
1059            sel = sel.fmax_range(fmax_min, fmax_max);
1060        }
1061        if !elem_sym.is_null() {
1062            let s = unsafe { CStr::from_ptr(elem_sym) };
1063            if let Ok(sym) = s.to_str() {
1064                if !sym.is_empty() {
1065                    if elem_exact != 0 {
1066                        sel = sel.element_exact(sym, elem_count);
1067                    } else {
1068                        sel = sel.element_min(sym, elem_count);
1069                    }
1070                }
1071            }
1072        }
1073        if flags & 1 != 0 {
1074            sel = sel.require_forces();
1075        }
1076        if flags & 2 != 0 {
1077            sel = sel.require_velocities();
1078        }
1079        if flags & 4 != 0 {
1080            sel = sel.require_energy();
1081        }
1082        if limit > 0 {
1083            sel = sel.limit(limit as usize);
1084        }
1085        match h.corpus.select(&sel) {
1086            Ok(keys) => {
1087                h.last_keys = keys;
1088                Ok(RKRDB_OK)
1089            }
1090            Err(e) => {
1091                set_err(h, e);
1092                Ok(RKRDB_ERR)
1093            }
1094        }
1095    })
1096    .unwrap_or(RKRDB_NULL)
1097}
1098
1099#[no_mangle]
1100pub unsafe extern "C" fn rkrdb_result_count(id: usize) -> c_int {
1101    with_handle(id, |h| Ok(h.last_keys.len() as c_int)).unwrap_or(RKRDB_NULL)
1102}
1103
1104/// Write traj_id and frame_idx for result index `i` (0-based).
1105#[no_mangle]
1106pub unsafe extern "C" fn rkrdb_result_key(
1107    id: usize,
1108    i: usize,
1109    out_traj: *mut u64,
1110    out_frame: *mut u32,
1111) -> c_int {
1112    if out_traj.is_null() || out_frame.is_null() {
1113        return RKRDB_NULL;
1114    }
1115    with_handle(id, |h| {
1116        let k = match h.last_keys.get(i) {
1117            Some(k) => *k,
1118            None => return Ok(RKRDB_NOT_FOUND),
1119        };
1120        unsafe {
1121            *out_traj = k.traj_id;
1122            *out_frame = k.frame_idx;
1123        }
1124        Ok(RKRDB_OK)
1125    })
1126    .unwrap_or(RKRDB_NULL)
1127}
1128
1129/// Hash frame blob at key; writes 16 LE bytes to out_hash16.
1130#[no_mangle]
1131pub unsafe extern "C" fn rkrdb_frame_hash(
1132    id: usize,
1133    traj_id: u64,
1134    frame_idx: u32,
1135    out_hash16: *mut u8,
1136) -> c_int {
1137    if out_hash16.is_null() {
1138        return RKRDB_NULL;
1139    }
1140    let key = FrameKey { traj_id, frame_idx };
1141    with_handle(id, |h| match h.corpus.frame_hash(key) {
1142        Ok(hash) => {
1143            let b = hash.to_bytes();
1144            unsafe { ptr::copy_nonoverlapping(b.as_ptr(), out_hash16, 16) };
1145            Ok(RKRDB_OK)
1146        }
1147        Err(e) => {
1148            set_err(h, e);
1149            Ok(RKRDB_ERR)
1150        }
1151    })
1152    .unwrap_or(RKRDB_NULL)
1153}
1154
1155/// Copy frame CON text into buf (NUL-terminated). Returns length excluding NUL, or error code.
1156#[no_mangle]
1157pub unsafe extern "C" fn rkrdb_get_frame_text(
1158    id: usize,
1159    traj_id: u64,
1160    frame_idx: u32,
1161    buf: *mut c_char,
1162    buflen: usize,
1163) -> c_int {
1164    if buf.is_null() || buflen == 0 {
1165        return RKRDB_NULL;
1166    }
1167    let key = FrameKey { traj_id, frame_idx };
1168    with_handle(id, |h| match h.corpus.get_frame_text(key) {
1169        Ok(text) => {
1170            let bytes = text.as_bytes();
1171            if bytes.len() + 1 > buflen {
1172                set_err(h, "buffer too small");
1173                return Ok(RKRDB_ERR);
1174            }
1175            unsafe {
1176                ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, bytes.len());
1177                *buf.add(bytes.len()) = 0;
1178            }
1179            Ok(bytes.len() as c_int)
1180        }
1181        Err(e) => {
1182            set_err(h, e);
1183            Ok(RKRDB_ERR)
1184        }
1185    })
1186    .unwrap_or(RKRDB_NULL)
1187}
1188
1189/// Parse stored CON text into a readcon-core `RKRConFrame`.
1190/// Caller frees with `free_rkr_frame` from libreadcon_core. NULL on error.
1191#[no_mangle]
1192pub unsafe extern "C" fn rkrdb_get_frame(
1193    id: usize,
1194    traj_id: u64,
1195    frame_idx: u32,
1196) -> *mut std::ffi::c_void {
1197    let key = FrameKey { traj_id, frame_idx };
1198    with_handle(id, |h| match h.corpus.get_frame(key) {
1199        Ok(frame) => Ok(Box::into_raw(Box::new(frame)) as *mut std::ffi::c_void),
1200        Err(e) => {
1201            set_err(h, e);
1202            Ok(std::ptr::null_mut())
1203        }
1204    })
1205    .unwrap_or(std::ptr::null_mut())
1206}
1207
1208fn collect_h5md_handle(
1209    id: usize,
1210    traj_id: u64,
1211) -> std::result::Result<crate::export_h5md::H5mdArrays, c_int> {
1212    let (corpus, unit) = {
1213        let g = HANDLES.lock().unwrap();
1214        let slot = g.get(id).ok_or(RKRDB_NULL)?;
1215        let h = slot.as_ref().ok_or(RKRDB_NULL)?;
1216        (Arc::clone(&h.corpus), h.velocity_time_unit.clone())
1217    };
1218    corpus
1219        .collect_h5md_with(
1220            traj_id,
1221            &crate::export_h5md::H5mdCollectOpts {
1222                velocity_time_unit: unit,
1223            },
1224        )
1225        .map_err(|e| {
1226            set_err_id(id, e);
1227            RKRDB_ERR
1228        })
1229}
1230
1231/// Override D10 for later `rkrdb_h5md_*` calls.
1232/// The same unit is the clock when a v3.1 frame has no `units.time`.
1233/// NULL clears the override.
1234///
1235/// # Safety
1236/// `unit` is null or points at a NUL-terminated string that stays valid for this call.
1237#[no_mangle]
1238pub unsafe extern "C" fn rkrdb_set_velocity_time_unit(id: usize, unit: *const c_char) -> c_int {
1239    let parsed = if unit.is_null() {
1240        None
1241    } else {
1242        let c = unsafe { CStr::from_ptr(unit) };
1243        let s = match c.to_str() {
1244            Ok(s) => s,
1245            Err(_) => return RKRDB_ERR,
1246        };
1247        if readcon_core::units::unit_conversion_factor(s, "ps").is_err() {
1248            set_err_id(id, "velocity_time_unit is not a time unit");
1249            return RKRDB_ERR;
1250        }
1251        Some(s.to_string())
1252    };
1253    with_handle(id, |h| {
1254        h.velocity_time_unit = parsed;
1255        Ok(RKRDB_OK)
1256    })
1257    .unwrap_or(RKRDB_NULL)
1258}
1259
1260/// `nframes` and `natoms` for one trajectory's H5MD arrays.
1261#[no_mangle]
1262pub unsafe extern "C" fn rkrdb_h5md_shape(
1263    id: usize,
1264    traj_id: u64,
1265    out_nframes: *mut u32,
1266    out_natoms: *mut u32,
1267) -> c_int {
1268    if out_nframes.is_null() || out_natoms.is_null() {
1269        return RKRDB_NULL;
1270    }
1271    match collect_h5md_handle(id, traj_id) {
1272        Ok(a) => {
1273            unsafe {
1274                *out_nframes = a.n_frames as u32;
1275                *out_natoms = a.natoms as u32;
1276            }
1277            RKRDB_OK
1278        }
1279        Err(e) => {
1280            set_err_id(id, e);
1281            RKRDB_ERR
1282        }
1283    }
1284}
1285
1286/// Row-major `[T][N][3]` dest-Å positions from `collect_h5md`.
1287#[no_mangle]
1288pub unsafe extern "C" fn rkrdb_h5md_positions(
1289    id: usize,
1290    traj_id: u64,
1291    out: *mut f64,
1292    cap: usize,
1293    out_nframes: *mut u32,
1294    out_natoms: *mut u32,
1295) -> c_int {
1296    if out.is_null() || out_nframes.is_null() || out_natoms.is_null() {
1297        return RKRDB_NULL;
1298    }
1299    match collect_h5md_handle(id, traj_id) {
1300        Ok(a) => {
1301            if a.positions.len() > cap {
1302                set_err_id(id, "h5md_positions buffer too small");
1303                return RKRDB_ERR;
1304            }
1305            unsafe {
1306                ptr::copy_nonoverlapping(a.positions.as_ptr(), out, a.positions.len());
1307                *out_nframes = a.n_frames as u32;
1308                *out_natoms = a.natoms as u32;
1309            }
1310            RKRDB_OK
1311        }
1312        Err(e) => {
1313            set_err_id(id, e);
1314            RKRDB_ERR
1315        }
1316    }
1317}
1318
1319unsafe fn h5md_copy_slice(
1320    id: usize,
1321    traj_id: u64,
1322    out: *mut f64,
1323    cap: usize,
1324    pick: fn(&crate::export_h5md::H5mdArrays) -> Option<&[f64]>,
1325    missing: &str,
1326) -> c_int {
1327    if out.is_null() {
1328        return RKRDB_NULL;
1329    }
1330    match collect_h5md_handle(id, traj_id) {
1331        Ok(a) => {
1332            let Some(sl) = pick(&a) else {
1333                set_err_id(id, missing);
1334                return RKRDB_NOT_FOUND;
1335            };
1336            if sl.len() > cap {
1337                set_err_id(id, "h5md buffer too small");
1338                return RKRDB_ERR;
1339            }
1340            unsafe {
1341                ptr::copy_nonoverlapping(sl.as_ptr(), out, sl.len());
1342            }
1343            RKRDB_OK
1344        }
1345        Err(e) => {
1346            set_err_id(id, e);
1347            RKRDB_ERR
1348        }
1349    }
1350}
1351
1352#[no_mangle]
1353pub unsafe extern "C" fn rkrdb_h5md_edges(
1354    id: usize,
1355    traj_id: u64,
1356    out: *mut f64,
1357    cap: usize,
1358) -> c_int {
1359    h5md_copy_slice(id, traj_id, out, cap, |a| Some(a.edges.as_slice()), "")
1360}
1361
1362#[no_mangle]
1363pub unsafe extern "C" fn rkrdb_h5md_forces(
1364    id: usize,
1365    traj_id: u64,
1366    out: *mut f64,
1367    cap: usize,
1368) -> c_int {
1369    h5md_copy_slice(id, traj_id, out, cap, |a| a.forces.as_deref(), "no forces")
1370}
1371
1372#[no_mangle]
1373pub unsafe extern "C" fn rkrdb_h5md_velocities(
1374    id: usize,
1375    traj_id: u64,
1376    out: *mut f64,
1377    cap: usize,
1378) -> c_int {
1379    h5md_copy_slice(
1380        id,
1381        traj_id,
1382        out,
1383        cap,
1384        |a| a.velocities.as_deref(),
1385        "no velocities",
1386    )
1387}
1388
1389/// Integer Z, length `N` (`collect_h5md` species_z).
1390#[no_mangle]
1391pub unsafe extern "C" fn rkrdb_h5md_species(
1392    id: usize,
1393    traj_id: u64,
1394    out: *mut i32,
1395    cap: usize,
1396    out_natoms: *mut u32,
1397) -> c_int {
1398    if out.is_null() || out_natoms.is_null() {
1399        return RKRDB_NULL;
1400    }
1401    match collect_h5md_handle(id, traj_id) {
1402        Ok(a) => {
1403            if a.species_z.len() > cap {
1404                set_err_id(id, "h5md_species buffer too small");
1405                return RKRDB_ERR;
1406            }
1407            unsafe {
1408                ptr::copy_nonoverlapping(a.species_z.as_ptr(), out, a.species_z.len());
1409                *out_natoms = a.species_z.len() as u32;
1410            }
1411            RKRDB_OK
1412        }
1413        Err(e) => {
1414            set_err_id(id, e);
1415            RKRDB_ERR
1416        }
1417    }
1418}
1419
1420#[no_mangle]
1421pub unsafe extern "C" fn rkrdb_get_velocities(
1422    id: usize,
1423    traj_id: u64,
1424    frame_idx: u32,
1425    out_xyz: *mut f64,
1426    capacity_atoms: u32,
1427    out_natoms: *mut u32,
1428    out_has_velocities: *mut u8,
1429) -> c_int {
1430    if out_xyz.is_null() || out_natoms.is_null() || out_has_velocities.is_null() {
1431        return RKRDB_NULL;
1432    }
1433    let corpus = match corpus_arc(id) {
1434        Ok(c) => c,
1435        Err(c) => return c,
1436    };
1437    match corpus.get_velocities(FrameKey { traj_id, frame_idx }) {
1438        Ok(Some(rows)) => {
1439            if rows.len() > capacity_atoms as usize {
1440                set_err_id(id, "velocities buffer too small");
1441                return RKRDB_ERR;
1442            }
1443            unsafe {
1444                *out_has_velocities = 1;
1445                *out_natoms = rows.len() as u32;
1446                for (i, r) in rows.iter().enumerate() {
1447                    *out_xyz.add(i * 3) = r[0];
1448                    *out_xyz.add(i * 3 + 1) = r[1];
1449                    *out_xyz.add(i * 3 + 2) = r[2];
1450                }
1451            }
1452            RKRDB_OK
1453        }
1454        Ok(None) => {
1455            unsafe {
1456                *out_has_velocities = 0;
1457                *out_natoms = 0;
1458            }
1459            RKRDB_OK
1460        }
1461        Err(e) => {
1462            set_err_id(id, e);
1463            RKRDB_ERR
1464        }
1465    }
1466}
1467
1468/// Dest-`ps` times from `collect_h5md` (CON time or `i * timestep`, else index).
1469#[no_mangle]
1470pub unsafe extern "C" fn rkrdb_h5md_times(
1471    id: usize,
1472    traj_id: u64,
1473    out: *mut f64,
1474    cap: usize,
1475    out_n: *mut u32,
1476) -> c_int {
1477    if out.is_null() || out_n.is_null() {
1478        return RKRDB_NULL;
1479    }
1480    match collect_h5md_handle(id, traj_id) {
1481        Ok(a) => {
1482            if a.times.len() > cap {
1483                set_err_id(id, "h5md_times buffer too small");
1484                return RKRDB_ERR;
1485            }
1486            unsafe {
1487                ptr::copy_nonoverlapping(a.times.as_ptr(), out, a.times.len());
1488                *out_n = a.times.len() as u32;
1489            }
1490            RKRDB_OK
1491        }
1492        Err(e) => {
1493            set_err_id(id, e);
1494            RKRDB_ERR
1495        }
1496    }
1497}
1498
1499/// xxHash3-128 of arbitrary bytes (LE 16 bytes) — for clients hashing off-line blobs.
1500#[no_mangle]
1501pub unsafe extern "C" fn rkrdb_xxh3_128(data: *const u8, len: usize, out_hash16: *mut u8) -> c_int {
1502    if data.is_null() || out_hash16.is_null() {
1503        return RKRDB_NULL;
1504    }
1505    let slice = unsafe { std::slice::from_raw_parts(data, len) };
1506    let h = hash_frame_bytes(slice);
1507    let b = h.to_bytes();
1508    unsafe { ptr::copy_nonoverlapping(b.as_ptr(), out_hash16, 16) };
1509    RKRDB_OK
1510}
1511
1512#[cfg(test)]
1513mod tests {
1514    use super::*;
1515    use std::ffi::CString;
1516    use std::path::PathBuf;
1517
1518    #[test]
1519    fn abi_stamp_equals_the_constants() {
1520        assert_eq!(rkrdb_abi_version_major(), RKRDB_ABI_VERSION_MAJOR);
1521        assert_eq!(rkrdb_abi_version_minor(), RKRDB_ABI_VERSION_MINOR);
1522        assert_eq!(rkrdb_abi_layout_revision(), RKRDB_ABI_LAYOUT_REVISION);
1523        let stamp = unsafe { CStr::from_ptr(rkrdb_abi_stamp()) }
1524            .to_str()
1525            .unwrap();
1526        assert_eq!(
1527            stamp,
1528            format!(
1529                "readcon-db/abi-{}.{}/layout-{}",
1530                RKRDB_ABI_VERSION_MAJOR, RKRDB_ABI_VERSION_MINOR, RKRDB_ABI_LAYOUT_REVISION
1531            )
1532        );
1533        assert_eq!(
1534            rkrdb_abi_compatible(
1535                RKRDB_ABI_VERSION_MAJOR,
1536                RKRDB_ABI_VERSION_MINOR,
1537                RKRDB_ABI_LAYOUT_REVISION
1538            ),
1539            1
1540        );
1541    }
1542
1543    #[test]
1544    fn abi_header_carries_the_stamp() {
1545        let header =
1546            std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/include/readcon-db.h"))
1547                .unwrap();
1548        for (name, value) in [
1549            ("RKRDB_ABI_VERSION_MAJOR", RKRDB_ABI_VERSION_MAJOR),
1550            ("RKRDB_ABI_VERSION_MINOR", RKRDB_ABI_VERSION_MINOR),
1551            ("RKRDB_ABI_LAYOUT_REVISION", RKRDB_ABI_LAYOUT_REVISION),
1552        ] {
1553            let want = format!("#define {name} {value}");
1554            assert!(header.contains(&want), "readcon-db.h lacks {want}");
1555        }
1556        for f in [
1557            "rkrdb_abi_version_major(void)",
1558            "rkrdb_abi_version_minor(void)",
1559            "rkrdb_abi_layout_revision(void)",
1560            "rkrdb_abi_stamp(void)",
1561            "rkrdb_abi_compatible(uint32_t major",
1562        ] {
1563            assert!(header.contains(f), "readcon-db.h lacks {f}");
1564        }
1565    }
1566
1567    #[test]
1568    fn a_lower_caller_minor_is_accepted() {
1569        let library = (2, 3, 5);
1570        assert!(abi_accepts(library, (2, 2, 5)));
1571        assert!(abi_accepts(library, (2, 0, 5)));
1572        assert!(abi_accepts(library, (2, 3, 5)));
1573    }
1574
1575    #[test]
1576    fn a_higher_minor_other_major_or_other_layout_is_refused() {
1577        let library = (2, 3, 5);
1578        assert!(!abi_accepts(library, (2, 4, 5)));
1579        assert!(!abi_accepts(library, (1, 3, 5)));
1580        assert!(!abi_accepts(library, (3, 3, 5)));
1581        assert!(!abi_accepts(library, (2, 3, 4)));
1582        assert!(!abi_accepts(library, (2, 3, 6)));
1583    }
1584
1585    #[test]
1586    fn the_exported_check_refuses_a_foreign_stamp() {
1587        assert_eq!(
1588            rkrdb_abi_compatible(
1589                RKRDB_ABI_VERSION_MAJOR + 1,
1590                RKRDB_ABI_VERSION_MINOR,
1591                RKRDB_ABI_LAYOUT_REVISION
1592            ),
1593            0
1594        );
1595        assert_eq!(
1596            rkrdb_abi_compatible(
1597                RKRDB_ABI_VERSION_MAJOR,
1598                RKRDB_ABI_VERSION_MINOR + 1,
1599                RKRDB_ABI_LAYOUT_REVISION
1600            ),
1601            0
1602        );
1603        assert_eq!(
1604            rkrdb_abi_compatible(
1605                RKRDB_ABI_VERSION_MAJOR,
1606                RKRDB_ABI_VERSION_MINOR,
1607                RKRDB_ABI_LAYOUT_REVISION + 1
1608            ),
1609            0
1610        );
1611    }
1612
1613    fn natural_dest_velocity(native: f64) -> f64 {
1614        let t_ps = readcon_core::units::unit_conversion_factor(
1615            crate::units_canon::NATURAL_TIME_UNIT,
1616            "ps",
1617        )
1618        .unwrap();
1619        native / t_ps
1620    }
1621
1622    fn fixture(name: &str) -> PathBuf {
1623        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1624            .join("resources/test")
1625            .join(name)
1626    }
1627
1628    #[test]
1629    fn c_abi_append_str_and_frame() {
1630        let dir = tempfile::tempdir().unwrap();
1631        let path = CString::new(dir.path().to_str().unwrap()).unwrap();
1632        let text = std::fs::read_to_string(fixture("tiny_cuh2.con")).unwrap();
1633        let ctext = CString::new(text).unwrap();
1634        let src = CString::new("mem").unwrap();
1635        let mut id = 0usize;
1636        let mut n = 0u32;
1637        unsafe {
1638            assert_eq!(rkrdb_open(path.as_ptr(), &mut id), RKRDB_OK);
1639            assert_eq!(
1640                rkrdb_append_trajectory_str(id, 1, ctext.as_ptr(), src.as_ptr(), &mut n),
1641                RKRDB_OK
1642            );
1643            assert!(n >= 1);
1644            let handle = rkrdb_get_frame(id, 1, 0);
1645            assert!(!handle.is_null());
1646            let mut n2 = 0u32;
1647            assert_eq!(
1648                rkrdb_append_trajectory_frame(id, 2, handle, src.as_ptr(), &mut n2),
1649                RKRDB_OK
1650            );
1651            assert!(n2 >= 1);
1652            let again = rkrdb_get_frame(id, 2, 0);
1653            assert!(!again.is_null());
1654            rkrdb_close(id);
1655        }
1656    }
1657
1658    #[test]
1659    fn c_abi_extend_frame_preserves_sequential_indices() {
1660        let dir = tempfile::tempdir().unwrap();
1661        let path = CString::new(dir.path().to_str().unwrap()).unwrap();
1662        let text =
1663            CString::new(std::fs::read_to_string(fixture("tiny_cuh2.con")).unwrap()).unwrap();
1664        let mut id = 0usize;
1665        let mut n = 0u32;
1666        unsafe {
1667            assert_eq!(rkrdb_open(path.as_ptr(), &mut id), RKRDB_OK);
1668            assert_eq!(
1669                rkrdb_append_trajectory_str(id, 1, text.as_ptr(), ptr::null(), &mut n),
1670                RKRDB_OK
1671            );
1672            let handle = rkrdb_get_frame(id, 1, 0);
1673            assert!(!handle.is_null());
1674            let frame = Box::from_raw(handle as *mut readcon_core::types::ConFrame);
1675            let frame_ptr = (&*frame as *const readcon_core::types::ConFrame).cast();
1676            for expected in 1..=3 {
1677                assert_eq!(
1678                    rkrdb_extend_trajectory_frame(id, 2, frame_ptr, ptr::null(), &mut n),
1679                    RKRDB_OK
1680                );
1681                assert_eq!(n, expected);
1682            }
1683            for index in 0..3 {
1684                let stored = rkrdb_get_frame(id, 2, index);
1685                assert!(!stored.is_null());
1686                drop(Box::from_raw(stored as *mut readcon_core::types::ConFrame));
1687            }
1688            assert_eq!(
1689                rkrdb_extend_trajectory_frame(id, 2, ptr::null(), ptr::null(), &mut n),
1690                RKRDB_NULL
1691            );
1692            assert_eq!(n, 3);
1693            rkrdb_close(id);
1694        }
1695    }
1696
1697    #[test]
1698    fn c_abi_append_units_canonical() {
1699        let dir = tempfile::tempdir().unwrap();
1700        let path = CString::new(dir.path().to_str().unwrap()).unwrap();
1701        let con = CString::new(fixture("tiny_cuh2.con").to_str().unwrap()).unwrap();
1702        let units = CString::new(r#"{"length":"A","energy":"ev","time":"femtosecond"}"#).unwrap();
1703        let mut id = 0usize;
1704        let mut n = 0u32;
1705        unsafe {
1706            assert_eq!(rkrdb_open(path.as_ptr(), &mut id), RKRDB_OK);
1707            assert_eq!(
1708                rkrdb_append_trajectory_units(id, 1, con.as_ptr(), units.as_ptr(), &mut n),
1709                RKRDB_OK
1710            );
1711            assert!(n >= 1);
1712            let mut buf = vec![0i8; 256];
1713            assert_eq!(
1714                rkrdb_frame_units(id, 1, 0, buf.as_mut_ptr(), buf.len()),
1715                RKRDB_OK
1716            );
1717            let s = CStr::from_ptr(buf.as_ptr()).to_str().unwrap();
1718            assert!(s.contains("angstrom"), "{s}");
1719            assert!(s.contains("eV"), "{s}");
1720            assert!(s.contains("fs"), "{s}");
1721            assert!(!s.contains("\"A\""), "{s}");
1722            let mut times = [0.0f64; 8];
1723            let mut nt = 0u32;
1724            assert_eq!(
1725                rkrdb_h5md_times(id, 1, times.as_mut_ptr(), times.len(), &mut nt),
1726                RKRDB_OK
1727            );
1728            assert!(nt >= 1);
1729            assert!(
1730                (times[0] - 0.0).abs() < 1e-12,
1731                "undeclared header.time dest ps times[0]={}",
1732                times[0]
1733            );
1734            assert_eq!(rkrdb_recook_all(id), RKRDB_OK);
1735            let need1 = rkrdb_pack_frame(id, 1, 0, std::ptr::null_mut(), 0);
1736            assert!(need1 > 0, "pack_frame size={need1}");
1737            let mut one = vec![0u8; need1 as usize];
1738            let n1 = rkrdb_pack_frame(id, 1, 0, one.as_mut_ptr(), one.len());
1739            assert_eq!(n1, need1);
1740            let mut xyz1 = vec![0.0f64; 32];
1741            let mut nxyz1 = 0u32;
1742            assert_eq!(
1743                rkrdb_unpack_positions(one.as_ptr(), n1 as usize, xyz1.as_mut_ptr(), 8, &mut nxyz1),
1744                RKRDB_OK
1745            );
1746            assert!(nxyz1 >= 1);
1747            assert!(
1748                (xyz1[0] - 0.6394).abs() < 1e-3,
1749                "pack_frame dest x0={}",
1750                xyz1[0]
1751            );
1752            let multi = CString::new(fixture("tiny_multi_cuh2.con").to_str().unwrap()).unwrap();
1753            let mut n2 = 0u32;
1754            assert_eq!(
1755                rkrdb_append_trajectory(id, 3, multi.as_ptr(), &mut n2),
1756                RKRDB_OK
1757            );
1758            let tids = [3u64, 3];
1759            let fids = [0u32, 1];
1760            let need =
1761                rkrdb_pack_frames(id, tids.as_ptr(), fids.as_ptr(), 2, std::ptr::null_mut(), 0);
1762            assert!(need > 0, "pack_frames size={need}");
1763            let mut pbuf = vec![0u8; need as usize];
1764            let npack = rkrdb_pack_frames(
1765                id,
1766                tids.as_ptr(),
1767                fids.as_ptr(),
1768                2,
1769                pbuf.as_mut_ptr(),
1770                pbuf.len(),
1771            );
1772            assert_eq!(npack, need);
1773            let mut nfr = 0u32;
1774            assert_eq!(
1775                rkrdb_unpack_batch_nframes(pbuf.as_ptr(), npack as usize, &mut nfr),
1776                RKRDB_OK
1777            );
1778            assert_eq!(nfr, 2);
1779            let mut item = vec![0.0f64; 32];
1780            let mut nitem = 0u32;
1781            assert_eq!(
1782                rkrdb_unpack_batch_item(
1783                    pbuf.as_ptr(),
1784                    npack as usize,
1785                    0,
1786                    item.as_mut_ptr(),
1787                    8,
1788                    &mut nitem
1789                ),
1790                RKRDB_OK
1791            );
1792            assert!(nitem >= 1);
1793            assert!((item[0] - 0.6394).abs() < 1e-3, "dest x0={}", item[0]);
1794            let mut item1 = vec![0.0f64; 32];
1795            let mut nitem1 = 0u32;
1796            assert_eq!(
1797                rkrdb_unpack_batch_item(
1798                    pbuf.as_ptr(),
1799                    npack as usize,
1800                    1,
1801                    item1.as_mut_ptr(),
1802                    8,
1803                    &mut nitem1
1804                ),
1805                RKRDB_OK
1806            );
1807            assert!(nitem1 >= 1);
1808            let mut native1 = vec![0.0f64; 32];
1809            let mut nn1 = 0u32;
1810            assert_eq!(
1811                rkrdb_get_positions(id, 3, 1, native1.as_mut_ptr(), 8, &mut nn1),
1812                RKRDB_OK
1813            );
1814            assert_eq!(nitem1, nn1);
1815            assert!(
1816                (item1[0] - native1[0]).abs() < 1e-9,
1817                "item1 x0={} native={}",
1818                item1[0],
1819                native1[0]
1820            );
1821            assert!((item1[6] - 8.8549).abs() < 1e-4, "frame1 H x={}", item1[6]);
1822            let mut times3 = [0.0f64; 8];
1823            let mut nt3 = 0u32;
1824            assert_eq!(
1825                rkrdb_h5md_times(id, 3, times3.as_mut_ptr(), times3.len(), &mut nt3),
1826                RKRDB_OK
1827            );
1828            assert!(nt3 >= 2);
1829            assert!((times3[0] - 0.0).abs() < 1e-12);
1830            assert!((times3[1] - 1.0).abs() < 1e-12);
1831            let mut nf3 = 0u32;
1832            let mut na3 = 0u32;
1833            assert_eq!(rkrdb_h5md_shape(id, 3, &mut nf3, &mut na3), RKRDB_OK);
1834            assert!(nf3 >= 2 && na3 >= 3);
1835            let mut pos3 = vec![0.0f64; (nf3 as usize) * (na3 as usize) * 3];
1836            assert_eq!(
1837                rkrdb_h5md_positions(id, 3, pos3.as_mut_ptr(), pos3.len(), &mut nf3, &mut na3),
1838                RKRDB_OK
1839            );
1840            assert!((pos3[0] - 0.6394).abs() < 1e-4);
1841            let i_h1 = (na3 as usize) * 3 + 2 * 3;
1842            assert!(
1843                (pos3[i_h1] - 8.8549).abs() < 1e-4,
1844                "c_abi T>1 dest H x={}",
1845                pos3[i_h1]
1846            );
1847            let mut nf = 0u32;
1848            let mut na = 0u32;
1849            assert_eq!(rkrdb_h5md_shape(id, 1, &mut nf, &mut na), RKRDB_OK);
1850            assert!(nf >= 1 && na >= 1);
1851            let mut pos = vec![0.0f64; (nf as usize) * (na as usize) * 3];
1852            assert_eq!(
1853                rkrdb_h5md_positions(id, 1, pos.as_mut_ptr(), pos.len(), &mut nf, &mut na),
1854                RKRDB_OK
1855            );
1856            assert_eq!(pos.len(), (nf as usize) * (na as usize) * 3);
1857            // stamped A → dest Å; first Cu x on tiny_cuh2.con
1858            assert!((pos[0] - 0.6394).abs() < 1e-4, "dest A x0={}", pos[0]);
1859            assert!(pos.iter().any(|&x| x != 0.0));
1860            let mut xyz = vec![0.0f64; 32];
1861            let mut npos = 0u32;
1862            assert_eq!(
1863                rkrdb_get_positions(id, 1, 0, xyz.as_mut_ptr(), 8, &mut npos),
1864                RKRDB_OK
1865            );
1866            assert!(npos >= 1);
1867            assert!((xyz[0] - 0.6394).abs() < 1e-4);
1868            let mut fr0 = vec![0.0f64; 32];
1869            let mut nf0 = 0u32;
1870            let mut hf0 = 1u8;
1871            assert_eq!(
1872                rkrdb_get_forces(id, 1, 0, fr0.as_mut_ptr(), 8, &mut nf0, &mut hf0),
1873                RKRDB_OK
1874            );
1875            assert_eq!(hf0, 0);
1876            let mut no_f = [0.0f64; 8];
1877            assert_eq!(
1878                rkrdb_h5md_forces(id, 1, no_f.as_mut_ptr(), no_f.len()),
1879                RKRDB_NOT_FOUND
1880            );
1881            let mut no_v = [0.0f64; 8];
1882            assert_eq!(
1883                rkrdb_h5md_velocities(id, 1, no_v.as_mut_ptr(), no_v.len()),
1884                RKRDB_NOT_FOUND
1885            );
1886            let mut edges = vec![0.0f64; (nf as usize) * 9];
1887            assert_eq!(
1888                rkrdb_h5md_edges(id, 1, edges.as_mut_ptr(), edges.len()),
1889                RKRDB_OK
1890            );
1891            assert!((edges[0] - 15.3456).abs() < 1e-4, "edge a_x={}", edges[0]);
1892            let mut z = vec![0i32; na as usize];
1893            let mut nz = 0u32;
1894            assert_eq!(
1895                rkrdb_h5md_species(id, 1, z.as_mut_ptr(), z.len(), &mut nz),
1896                RKRDB_OK
1897            );
1898            assert_eq!(nz, na);
1899            assert!(z.contains(&29), "{z:?}");
1900            assert!(z.contains(&1), "{z:?}");
1901            let forces = CString::new(fixture("tiny_cuh2_forces.con").to_str().unwrap()).unwrap();
1902            let ext_u = CString::new(r#"{"length":"A","energy":"ev"}"#).unwrap();
1903            let mut n2 = 0u32;
1904            assert_eq!(
1905                rkrdb_extend_trajectory_units(id, 1, forces.as_ptr(), ext_u.as_ptr(), &mut n2),
1906                RKRDB_OK
1907            );
1908            assert!(n2 >= 1);
1909            let mut buf2 = vec![0i8; 256];
1910            assert_eq!(
1911                rkrdb_frame_units(id, 1, 1, buf2.as_mut_ptr(), buf2.len()),
1912                RKRDB_OK
1913            );
1914            let s2 = CStr::from_ptr(buf2.as_ptr()).to_str().unwrap();
1915            assert!(s2.contains("angstrom"), "{s2}");
1916            let mut frc = vec![0.0f64; 256];
1917            assert_eq!(
1918                rkrdb_h5md_forces(id, 1, frc.as_mut_ptr(), frc.len()),
1919                RKRDB_OK
1920            );
1921            let dest_f0 = -1.234567 * 96.485_332;
1922            assert!(
1923                frc.iter().any(|&x| (x - dest_f0).abs() < 1e-3),
1924                "missing dest force ~{dest_f0}, got {:?}",
1925                &frc[..12]
1926            );
1927            let units2 = CString::new(r#"{"length":"nm","energy":"eV"}"#).unwrap();
1928            let mut nset = 0u32;
1929            assert_eq!(rkrdb_set_units(id, 1, units2.as_ptr(), &mut nset), RKRDB_OK);
1930            let mut nf2 = 0u32;
1931            let mut na2 = 0u32;
1932            assert_eq!(rkrdb_h5md_shape(id, 1, &mut nf2, &mut na2), RKRDB_OK);
1933            let mut pos2 = vec![0.0f64; (nf2 as usize) * (na2 as usize) * 3];
1934            assert_eq!(
1935                rkrdb_h5md_positions(id, 1, pos2.as_mut_ptr(), pos2.len(), &mut nf2, &mut na2),
1936                RKRDB_OK
1937            );
1938            assert!(
1939                (pos2[0] - 0.6394).abs() < 1e-4,
1940                "dest A after set_units x0={}",
1941                pos2[0]
1942            );
1943            let mut frc2 = vec![0.0f64; 256];
1944            assert_eq!(
1945                rkrdb_h5md_forces(id, 1, frc2.as_mut_ptr(), frc2.len()),
1946                RKRDB_OK
1947            );
1948            let i_hfx = (na2 as usize) * 3 + 2 * 3;
1949            assert!(
1950                (frc2[i_hfx] - dest_f0).abs() < 1e-3,
1951                "dest force of force-bearing frame H Fx after set_units: {} vs {dest_f0}",
1952                frc2[i_hfx]
1953            );
1954            assert!(
1955                frc2[..na2 as usize * 3].iter().all(|&x| x.abs() < 1e-12),
1956                "frame 0 dest force must stay zero-pad, got {:?}",
1957                &frc2[..na2 as usize * 3]
1958            );
1959            rkrdb_close(id);
1960            let mut idro = 0usize;
1961            assert_eq!(rkrdb_open_readonly(path.as_ptr(), &mut idro), RKRDB_OK);
1962            let mut nfr = 0u32;
1963            let mut nar = 0u32;
1964            let mut posr = vec![0.0f64; 64];
1965            assert_eq!(
1966                rkrdb_h5md_positions(idro, 1, posr.as_mut_ptr(), posr.len(), &mut nfr, &mut nar),
1967                RKRDB_OK
1968            );
1969            assert!(
1970                (posr[0] - 0.6394).abs() < 1e-4,
1971                "readonly dest x0={}",
1972                posr[0]
1973            );
1974            assert_eq!(rkrdb_close(idro), RKRDB_OK);
1975            let velcon = CString::new(fixture("tiny_cuh2.convel").to_str().unwrap()).unwrap();
1976            let mut id2 = 0usize;
1977            let mut n3 = 0u32;
1978            assert_eq!(rkrdb_open(path.as_ptr(), &mut id2), RKRDB_OK);
1979            assert_eq!(
1980                rkrdb_append_trajectory(id2, 2, velcon.as_ptr(), &mut n3),
1981                RKRDB_OK
1982            );
1983            let mut vel = vec![0.0f64; 64];
1984            assert_eq!(
1985                rkrdb_h5md_velocities(id2, 2, vel.as_mut_ptr(), vel.len()),
1986                RKRDB_OK
1987            );
1988            let expect = natural_dest_velocity(0.001234);
1989            assert!((vel[0] - expect).abs() < 1e-9, "got {}", vel[0]);
1990            let mut native = vec![0.0f64; 32];
1991            let mut nv = 0u32;
1992            let mut has_v = 0u8;
1993            assert_eq!(
1994                rkrdb_get_velocities(id2, 2, 0, native.as_mut_ptr(), 8, &mut nv, &mut has_v),
1995                RKRDB_OK
1996            );
1997            assert_eq!(has_v, 1);
1998            assert!(nv >= 1);
1999            assert!(
2000                (native[0] - 0.001234).abs() < 1e-9,
2001                "native vx0={}",
2002                native[0]
2003            );
2004            let mut tiny = [0.0f64; 1];
2005            let mut ntiny = 0u32;
2006            let mut htiny = 0u8;
2007            assert_eq!(
2008                rkrdb_get_velocities(id2, 2, 0, tiny.as_mut_ptr(), 0, &mut ntiny, &mut htiny),
2009                RKRDB_ERR
2010            );
2011            let mut ebuf = vec![0i8; 128];
2012            assert!(rkrdb_last_error(id2, ebuf.as_mut_ptr(), ebuf.len()) >= 0);
2013            let es = CStr::from_ptr(ebuf.as_ptr()).to_str().unwrap();
2014            assert!(es.contains("too small"), "{es}");
2015            let mut none = vec![0.0f64; 8];
2016            let mut nn = 99u32;
2017            let mut has_none = 1u8;
2018            assert_eq!(
2019                rkrdb_get_velocities(id2, 1, 0, none.as_mut_ptr(), 2, &mut nn, &mut has_none),
2020                RKRDB_OK
2021            );
2022            assert_eq!(has_none, 0);
2023            assert_eq!(nn, 0);
2024            assert_eq!(rkrdb_close(id2), RKRDB_OK);
2025        }
2026    }
2027
2028    #[test]
2029    fn c_abi_set_units_keeps_dest_vel_time_edges() {
2030        let dir = tempfile::tempdir().unwrap();
2031        let path = CString::new(dir.path().to_str().unwrap()).unwrap();
2032        let velcon = CString::new(fixture("tiny_cuh2.convel").to_str().unwrap()).unwrap();
2033        let mut id = 0usize;
2034        let mut n = 0u32;
2035        unsafe {
2036            assert_eq!(rkrdb_open(path.as_ptr(), &mut id), RKRDB_OK);
2037            assert_eq!(
2038                rkrdb_append_trajectory(id, 1, velcon.as_ptr(), &mut n),
2039                RKRDB_OK
2040            );
2041            let mut vel0 = vec![0.0f64; 64];
2042            assert_eq!(
2043                rkrdb_h5md_velocities(id, 1, vel0.as_mut_ptr(), vel0.len()),
2044                RKRDB_OK
2045            );
2046            let mut edges0 = vec![0.0f64; 32];
2047            assert_eq!(
2048                rkrdb_h5md_edges(id, 1, edges0.as_mut_ptr(), edges0.len()),
2049                RKRDB_OK
2050            );
2051            let mut times0 = [0.0f64; 8];
2052            let mut nt = 0u32;
2053            assert_eq!(
2054                rkrdb_h5md_times(id, 1, times0.as_mut_ptr(), times0.len(), &mut nt),
2055                RKRDB_OK
2056            );
2057            let units = CString::new(r#"{"length":"nm","energy":"eV","time":"ps"}"#).unwrap();
2058            let mut nset = 0u32;
2059            assert_eq!(rkrdb_set_units(id, 1, units.as_ptr(), &mut nset), RKRDB_OK);
2060            let mut vel1 = vec![0.0f64; 64];
2061            assert_eq!(
2062                rkrdb_h5md_velocities(id, 1, vel1.as_mut_ptr(), vel1.len()),
2063                RKRDB_OK
2064            );
2065            let expect = natural_dest_velocity(0.001234);
2066            assert!((vel0[0] - expect).abs() < 1e-9, "got {}", vel0[0]);
2067            assert!(
2068                (vel0[0] - vel1[0]).abs() < 1e-12,
2069                "dest vel after set_units {} vs {}",
2070                vel0[0],
2071                vel1[0]
2072            );
2073            let mut edges1 = vec![0.0f64; 32];
2074            assert_eq!(
2075                rkrdb_h5md_edges(id, 1, edges1.as_mut_ptr(), edges1.len()),
2076                RKRDB_OK
2077            );
2078            assert!((edges0[0] - 15.3456).abs() < 1e-4, "edge a_x={}", edges0[0]);
2079            assert!(
2080                (edges0[0] - edges1[0]).abs() < 1e-12,
2081                "dest edges after set_units {} vs {}",
2082                edges0[0],
2083                edges1[0]
2084            );
2085            let mut times1 = [0.0f64; 8];
2086            let mut nt1 = 0u32;
2087            assert_eq!(
2088                rkrdb_h5md_times(id, 1, times1.as_mut_ptr(), times1.len(), &mut nt1),
2089                RKRDB_OK
2090            );
2091            assert_eq!(nt, nt1);
2092            assert!(
2093                (times0[0] - times1[0]).abs() < 1e-12,
2094                "dest time after set_units {} vs {}",
2095                times0[0],
2096                times1[0]
2097            );
2098            let mut timed = std::fs::read_to_string(fixture("tiny_cuh2.con")).unwrap();
2099            let mut tlines: Vec<String> = timed.lines().map(str::to_string).collect();
2100            tlines[1] = concat!(
2101                r#"{"con_spec_version":3,"time":12.5,"#,
2102                r#""units":{"length":"angstrom","energy":"eV","time":"fs"}}"#
2103            )
2104            .to_string();
2105            timed = tlines.join("\n");
2106            timed.push('\n');
2107            let ttext = CString::new(timed).unwrap();
2108            let tsrc = CString::new("memory").unwrap();
2109            let mut ntimed = 0u32;
2110            assert_eq!(
2111                rkrdb_append_trajectory_str(id, 2, ttext.as_ptr(), tsrc.as_ptr(), &mut ntimed),
2112                RKRDB_OK
2113            );
2114            let mut t0 = [0.0f64; 8];
2115            let mut nt0 = 0u32;
2116            assert_eq!(
2117                rkrdb_h5md_times(id, 2, t0.as_mut_ptr(), t0.len(), &mut nt0),
2118                RKRDB_OK
2119            );
2120            assert!((t0[0] - 0.0125).abs() < 1e-12, "dest time fs->ps {}", t0[0]);
2121            let tunits =
2122                CString::new(r#"{"length":"angstrom","energy":"eV","time":"ps"}"#).unwrap();
2123            let mut nset2 = 0u32;
2124            assert_eq!(
2125                rkrdb_set_units(id, 2, tunits.as_ptr(), &mut nset2),
2126                RKRDB_OK
2127            );
2128            let mut t1 = [0.0f64; 8];
2129            let mut nt1t = 0u32;
2130            assert_eq!(
2131                rkrdb_h5md_times(id, 2, t1.as_mut_ptr(), t1.len(), &mut nt1t),
2132                RKRDB_OK
2133            );
2134            assert!(
2135                (t1[0] - 0.0125).abs() < 1e-12,
2136                "dest time after set_units time {}",
2137                t1[0]
2138            );
2139            assert_eq!(rkrdb_close(id), RKRDB_OK);
2140        }
2141    }
2142}