Skip to main content

readcon_core/
ffi.rs

1use crate::helpers::symbol_to_atomic_number;
2use crate::iterators::{self, ConFrameIterator};
3use crate::types::{ConFrame, ConFrameBuilder, meta};
4use crate::writer::ConFrameWriter;
5use std::ffi::{CStr, CString, c_char};
6use std::fs::{self, File};
7use std::path::Path;
8use std::ptr;
9
10//=============================================================================
11// Version & Spec Constants (exported as #define by cbindgen)
12//=============================================================================
13
14/// CON/convel format spec version. Use `#if RKR_CON_SPEC_VERSION >= 2` in C/C++
15/// to gate code that depends on atom_index semantics.
16///
17/// Tracks `crate::CON_SPEC_VERSION` (which the Rust API exposes as
18/// `CON_SPEC_VERSION`). Both macros are emitted into the C header for
19/// the convenience of either naming convention; they always carry the
20/// same value.
21pub const RKR_CON_SPEC_VERSION: u32 = 2;
22
23/// Returns the spec version at runtime (for dynamically linked consumers).
24#[unsafe(no_mangle)]
25pub extern "C" fn rkr_con_spec_version() -> u32 {
26    crate::CON_SPEC_VERSION
27}
28
29/// Returns a pointer to a static, null-terminated library version string.
30/// The returned pointer is valid for the lifetime of the process. Do NOT free it.
31#[unsafe(no_mangle)]
32pub extern "C" fn rkr_library_version() -> *const c_char {
33    // concat! produces a &'static str with a trailing NUL byte
34    const VERSION_NUL: &[u8] = concat!(env!("CARGO_PKG_VERSION"), "\0").as_bytes();
35    VERSION_NUL.as_ptr() as *const c_char
36}
37
38/// Returns the position of an atom inside the frame's `atom_data` array
39/// matching the given `atom_id`. Returns `UINT64_MAX` if no atom with
40/// that id exists or `frame_handle` is NULL.
41///
42/// O(N) per call. C/C++ consumers performing many lookups should cache
43/// a `std::unordered_map<uint64_t, size_t>` from a single sweep over
44/// the frame.
45///
46/// # Safety
47///
48/// `frame_handle` must point to a valid `RKRConFrame` allocation.
49#[unsafe(no_mangle)]
50pub unsafe extern "C" fn rkr_frame_atom_index_by_id(
51    frame_handle: *const RKRConFrame,
52    atom_id: u64,
53) -> u64 {
54    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
55        Some(f) => f,
56        None => return u64::MAX,
57    };
58    match frame.atom_index_by_id(atom_id) {
59        Some(idx) => idx as u64,
60        None => u64::MAX,
61    }
62}
63
64/// Returns the atomic number for a chemical symbol, or 0 if the symbol
65/// is unknown or `symbol` is NULL. Lookup covers H..U (Z = 1..=92) and
66/// is case-sensitive: "Fe" works, "fe" does not.
67///
68/// # Safety
69///
70/// `symbol` must be either NULL or a pointer to a NUL-terminated UTF-8
71/// C string valid for reads up to the terminating NUL byte.
72#[unsafe(no_mangle)]
73pub unsafe extern "C" fn rkr_symbol_to_z(symbol: *const c_char) -> u64 {
74    if symbol.is_null() {
75        return 0;
76    }
77    match unsafe { CStr::from_ptr(symbol) }.to_str() {
78        Ok(s) => symbol_to_atomic_number(s),
79        Err(_) => 0,
80    }
81}
82
83/// Returns a pointer to a static, NUL-terminated chemical symbol for an
84/// atomic number, or "X" for unknown values. Coverage is H..U
85/// (Z = 1..=92). The returned pointer is valid for the lifetime of the
86/// process; do NOT free it.
87#[unsafe(no_mangle)]
88pub extern "C" fn rkr_z_to_symbol(z: u64) -> *const c_char {
89    // The static &str returned by helpers::atomic_number_to_symbol is
90    // not NUL-terminated, so the FFI mirrors the table with literals
91    // that have a trailing NUL. Index 0 holds "X" for unknown Z; indices
92    // 1..=92 hold H..U in order.
93    macro_rules! cstrs {
94        ($($lit:literal),* $(,)?) => {
95            [$(concat!($lit, "\0").as_bytes()),*]
96        };
97    }
98    const TABLE: [&[u8]; 93] = cstrs![
99        "X", "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P",
100        "S", "Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn",
101        "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh",
102        "Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd",
103        "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re",
104        "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th",
105        "Pa", "U",
106    ];
107    let idx = if (1..=92).contains(&z) { z as usize } else { 0 };
108    TABLE[idx].as_ptr() as *const c_char
109}
110
111/// Returns the spec version stored in a parsed frame's header.
112/// Returns 0 on error (null handle).
113#[unsafe(no_mangle)]
114pub extern "C" fn rkr_frame_spec_version(frame_handle: *const RKRConFrame) -> u32 {
115    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
116        Some(f) => f.header.spec_version,
117        None => 0,
118    }
119}
120
121/// Returns the JSON metadata line from a parsed frame as a heap-allocated
122/// null-terminated C string. The caller MUST free with `rkr_free_string`.
123/// Returns NULL on error.
124///
125/// # Safety
126/// frame_handle must be valid. The caller takes ownership of the returned string.
127#[unsafe(no_mangle)]
128pub unsafe extern "C" fn rkr_frame_metadata_json(frame_handle: *const RKRConFrame) -> *mut c_char {
129    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
130        Some(f) => f,
131        None => return ptr::null_mut(),
132    };
133    let mut obj = serde_json::Map::new();
134    obj.insert(
135        meta::CON_SPEC_VERSION.into(),
136        serde_json::Value::from(frame.header.spec_version),
137    );
138    for (k, v) in &frame.header.metadata {
139        obj.insert(k.clone(), v.clone());
140    }
141    let json_str = serde_json::Value::Object(obj).to_string();
142    match CString::new(json_str) {
143        Ok(cs) => cs.into_raw(),
144        Err(_) => ptr::null_mut(),
145    }
146}
147
148/// Returns the per-frame energy from metadata, or NaN if absent.
149#[unsafe(no_mangle)]
150pub extern "C" fn rkr_frame_energy(frame_handle: *const RKRConFrame) -> f64 {
151    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
152        Some(f) => f.header.energy().unwrap_or(f64::NAN),
153        None => f64::NAN,
154    }
155}
156
157/// Returns the potential type string from metadata as a heap-allocated
158/// null-terminated C string. The caller MUST free with `rkr_free_string`.
159/// Returns NULL if absent or on error.
160///
161/// # Safety
162/// frame_handle must be valid. The caller takes ownership of the returned string.
163#[unsafe(no_mangle)]
164pub unsafe extern "C" fn rkr_frame_potential_type(frame_handle: *const RKRConFrame) -> *mut c_char {
165    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
166        Some(f) => f,
167        None => return ptr::null_mut(),
168    };
169    match frame.header.potential_type() {
170        Some(pot_type) => match CString::new(pot_type) {
171            Ok(cs) => cs.into_raw(),
172            Err(_) => ptr::null_mut(),
173        },
174        None => ptr::null_mut(),
175    }
176}
177
178/// Returns the zero-based frame index from metadata, or UINT64_MAX if absent.
179#[unsafe(no_mangle)]
180pub extern "C" fn rkr_frame_frame_index(frame_handle: *const RKRConFrame) -> u64 {
181    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
182        Some(f) => f.header.frame_index().unwrap_or(u64::MAX),
183        None => u64::MAX,
184    }
185}
186
187/// Returns the simulation time from metadata, or NaN if absent.
188#[unsafe(no_mangle)]
189pub extern "C" fn rkr_frame_time(frame_handle: *const RKRConFrame) -> f64 {
190    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
191        Some(f) => f.header.time().unwrap_or(f64::NAN),
192        None => f64::NAN,
193    }
194}
195
196/// Returns the integration timestep from metadata, or NaN if absent.
197#[unsafe(no_mangle)]
198pub extern "C" fn rkr_frame_timestep(frame_handle: *const RKRConFrame) -> f64 {
199    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
200        Some(f) => f.header.timestep().unwrap_or(f64::NAN),
201        None => f64::NAN,
202    }
203}
204
205/// Returns the NEB bead index from metadata, or UINT64_MAX if absent.
206#[unsafe(no_mangle)]
207pub extern "C" fn rkr_frame_neb_bead(frame_handle: *const RKRConFrame) -> u64 {
208    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
209        Some(f) => f.header.neb_bead().unwrap_or(u64::MAX),
210        None => u64::MAX,
211    }
212}
213
214/// Returns the NEB band index from metadata, or UINT64_MAX if absent.
215#[unsafe(no_mangle)]
216pub extern "C" fn rkr_frame_neb_band(frame_handle: *const RKRConFrame) -> u64 {
217    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
218        Some(f) => f.header.neb_band().unwrap_or(u64::MAX),
219        None => u64::MAX,
220    }
221}
222
223//=============================================================================
224// C-Compatible Structs & Handles
225//=============================================================================
226
227/// Error codes for RKR functions.
228#[repr(C)]
229#[allow(non_camel_case_types)]
230#[derive(Debug, PartialEq, Eq)]
231pub enum RKRStatus {
232    /// Function completed successfully.
233    RKR_STATUS_SUCCESS = 0,
234    /// A null pointer was passed for a required argument.
235    RKR_STATUS_NULL_POINTER = -1,
236    /// An input string was not valid UTF-8.
237    RKR_STATUS_INVALID_UTF8 = -2,
238    /// JSON parsing or serialization failed.
239    RKR_STATUS_INVALID_JSON = -3,
240    /// File I/O error.
241    RKR_STATUS_IO_ERROR = -4,
242    /// Index out of bounds.
243    RKR_STATUS_INDEX_OUT_OF_BOUNDS = -5,
244    /// The destination buffer cannot hold a null-terminated string.
245    RKR_STATUS_BUFFER_TOO_SMALL = -6,
246    /// An internal logic error or unhandled state.
247    RKR_STATUS_INTERNAL_ERROR = -7,
248    /// An optional section (velocities, forces, atom_energies) was
249    /// requested but is not declared on the builder.
250    RKR_STATUS_SECTION_ABSENT = -8,
251    /// DLPack export or another validation step failed.
252    RKR_STATUS_VALIDATION_ERROR = -9,
253    /// Chemfiles selection parse/evaluate failed (requires chemfiles-enabled build).
254    RKR_STATUS_SELECTION_ERROR = -10,
255}
256
257/// Number of optional frame topology bonds (`metadata["bonds"]`), or 0 if absent.
258///
259/// # Safety
260/// `frame_handle` must be a valid handle or NULL.
261#[unsafe(no_mangle)]
262pub unsafe extern "C" fn rkr_frame_bond_count(frame_handle: *const RKRConFrame) -> u64 {
263    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
264        Some(f) => f.bonds().len() as u64,
265        None => 0,
266    }
267}
268
269/// Read one bond at `index` (0-based into the `bonds` metadata array).
270///
271/// Writes 0-based `atom_data` indices into `out_i` / `out_j`. When the bond
272/// has an explicit order, sets `out_has_order` to 1 and `out_order` to that
273/// integer; otherwise `out_has_order` is 0.
274///
275/// # Safety
276/// `frame_handle` must be valid. Output pointers must be non-null.
277#[unsafe(no_mangle)]
278pub unsafe extern "C" fn rkr_frame_bond_at(
279    frame_handle: *const RKRConFrame,
280    index: u64,
281    out_i: *mut u32,
282    out_j: *mut u32,
283    out_has_order: *mut u8,
284    out_order: *mut i32,
285) -> RKRStatus {
286    if frame_handle.is_null()
287        || out_i.is_null()
288        || out_j.is_null()
289        || out_has_order.is_null()
290        || out_order.is_null()
291    {
292        return RKRStatus::RKR_STATUS_NULL_POINTER;
293    }
294    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
295        return RKRStatus::RKR_STATUS_NULL_POINTER;
296    };
297    let bonds = frame.bonds();
298    let Some(bond) = bonds.get(index as usize) else {
299        return RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS;
300    };
301    unsafe {
302        *out_i = bond.i;
303        *out_j = bond.j;
304        if let Some(order) = bond.order {
305            *out_has_order = 1;
306            *out_order = order;
307        } else {
308            *out_has_order = 0;
309            *out_order = 0;
310        }
311    }
312    RKRStatus::RKR_STATUS_SUCCESS
313}
314
315/// Returns a stable, static message for a status code.
316/// The returned pointer is valid for the lifetime of the process. Do NOT free it.
317#[unsafe(no_mangle)]
318pub extern "C" fn rkr_status_message(status: RKRStatus) -> *const c_char {
319    match status {
320        RKRStatus::RKR_STATUS_SUCCESS => c"success".as_ptr(),
321        RKRStatus::RKR_STATUS_NULL_POINTER => c"null pointer".as_ptr(),
322        RKRStatus::RKR_STATUS_INVALID_UTF8 => c"invalid UTF-8".as_ptr(),
323        RKRStatus::RKR_STATUS_INVALID_JSON => c"invalid JSON".as_ptr(),
324        RKRStatus::RKR_STATUS_IO_ERROR => c"I/O error".as_ptr(),
325        RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS => c"index out of bounds".as_ptr(),
326        RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL => c"buffer too small".as_ptr(),
327        RKRStatus::RKR_STATUS_INTERNAL_ERROR => c"internal error".as_ptr(),
328        RKRStatus::RKR_STATUS_SECTION_ABSENT => c"section absent".as_ptr(),
329        RKRStatus::RKR_STATUS_VALIDATION_ERROR => c"validation error".as_ptr(),
330        RKRStatus::RKR_STATUS_SELECTION_ERROR => c"selection error".as_ptr(),
331    }
332}
333
334/// An opaque handle to a full, lossless Rust `ConFrame` object.
335/// The C/C++ side needs to treat this as a void pointer
336#[repr(C)]
337pub struct RKRConFrame {
338    _private: [u8; 0],
339}
340
341/// An opaque handle to a Rust `ConFrameWriter` object.
342/// The C/C++ side needs to treat this as a void pointer
343#[repr(C)]
344pub struct RKRConFrameWriter {
345    _private: [u8; 0],
346}
347
348/// A transparent, "lossy" C-struct containing only the core atomic data.
349/// This can be extracted from an `RKRConFrame` handle for direct data access.
350/// The caller is responsible for freeing the `atoms` array using `free_c_frame`.
351#[repr(C)]
352pub struct CFrame {
353    pub atoms: *mut CAtom,
354    pub num_atoms: usize,
355    pub cell: [f64; 3],
356    pub angles: [f64; 3],
357    pub has_velocities: bool,
358    pub has_forces: bool,
359    pub has_energies: bool,
360}
361
362/// Transparent atom record extracted via [`rkr_frame_to_c_frame`].
363///
364/// `is_fixed` is the OR of `fixed_x`, `fixed_y`, `fixed_z`; it is kept
365/// for source compatibility with pre-spec-v2 callers that did not have
366/// per-axis flags. New code should use the per-axis fields.
367///
368/// `vx`/`vy`/`vz`, `fx`/`fy`/`fz`, and `energy` carry meaningful values
369/// only when `has_velocity`, `has_forces`, or `has_energy` is true
370/// respectively; the values are zeroed otherwise.
371#[repr(C)]
372pub struct CAtom {
373    pub atomic_number: u64,
374    pub x: f64,
375    pub y: f64,
376    pub z: f64,
377    pub atom_id: u64,
378    pub mass: f64,
379    /// True when any of `fixed_x`, `fixed_y`, `fixed_z` is true.
380    /// Kept for source compatibility; prefer the per-axis fields.
381    pub is_fixed: bool,
382    pub fixed_x: bool,
383    pub fixed_y: bool,
384    pub fixed_z: bool,
385    pub vx: f64,
386    pub vy: f64,
387    pub vz: f64,
388    pub has_velocity: bool,
389    pub fx: f64,
390    pub fy: f64,
391    pub fz: f64,
392    pub has_forces: bool,
393    /// Per-atom energy contribution; meaningful only when
394    /// `has_energy` is true. See [`crate::types::SECTION_ENERGIES`].
395    pub energy: f64,
396    pub has_energy: bool,
397}
398
399#[repr(C)]
400pub struct CConFrameIterator {
401    iterator: *mut ConFrameIterator<'static>,
402    file_contents: *mut String,
403}
404
405//=============================================================================
406// Iterator and Memory Management
407//=============================================================================
408
409/// Creates a new iterator for a .con or .convel file.
410///
411/// Returns NULL if the file cannot be read (missing, unreadable, or
412/// not valid UTF-8). A successfully-opened file with zero frames
413/// returns a non-NULL iterator that yields NULL on the first call to
414/// [`con_frame_iterator_next`]. The caller OWNS the returned pointer
415/// and MUST call [`free_con_frame_iterator`].
416///
417/// # Safety
418/// filename_c must be a valid null-terminated string. The caller takes
419/// ownership of the returned iterator.
420#[unsafe(no_mangle)]
421pub unsafe extern "C" fn read_con_file_iterator(
422    filename_c: *const c_char,
423) -> *mut CConFrameIterator {
424    if filename_c.is_null() {
425        return ptr::null_mut();
426    }
427    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
428        Ok(s) => s,
429        Err(_) => return ptr::null_mut(),
430    };
431    let file_contents_box = match fs::read_to_string(filename) {
432        Ok(contents) => Box::new(contents),
433        Err(_) => return ptr::null_mut(),
434    };
435    let file_contents_ptr = Box::into_raw(file_contents_box);
436    let static_file_contents: &'static str = unsafe { &*file_contents_ptr };
437    let iterator = Box::new(ConFrameIterator::new(static_file_contents));
438    let c_iterator = Box::new(CConFrameIterator {
439        iterator: Box::into_raw(iterator),
440        file_contents: file_contents_ptr,
441    });
442    Box::into_raw(c_iterator)
443}
444
445/// Reads the next frame from the iterator, returning an opaque handle.
446/// The caller OWNS the returned handle and must free it with `free_rkr_frame`.
447///
448/// # Safety
449/// iterator must be valid. The caller takes ownership of the returned frame.
450#[unsafe(no_mangle)]
451pub unsafe extern "C" fn con_frame_iterator_next(
452    iterator: *mut CConFrameIterator,
453) -> *mut RKRConFrame {
454    if iterator.is_null() {
455        return ptr::null_mut();
456    }
457    let iter = unsafe { &mut *(*iterator).iterator };
458    match iter.next() {
459        Some(Ok(frame)) => Box::into_raw(Box::new(frame)) as *mut RKRConFrame,
460        _ => ptr::null_mut(),
461    }
462}
463
464/// Frees the memory for an opaque `RKRConFrame` handle.
465///
466/// # Safety
467/// frame_handle must be valid or null.
468#[unsafe(no_mangle)]
469pub unsafe extern "C" fn free_rkr_frame(frame_handle: *mut RKRConFrame) {
470    if !frame_handle.is_null() {
471        let _ = unsafe { Box::from_raw(frame_handle as *mut ConFrame) };
472    }
473}
474
475/// Frees the memory for a `CConFrameIterator`.
476///
477/// # Safety
478/// iterator must be valid or null.
479#[unsafe(no_mangle)]
480pub unsafe extern "C" fn free_con_frame_iterator(iterator: *mut CConFrameIterator) {
481    if iterator.is_null() {
482        return;
483    }
484    unsafe {
485        let c_iterator_box = Box::from_raw(iterator);
486        let _ = Box::from_raw(c_iterator_box.iterator);
487        let _ = Box::from_raw(c_iterator_box.file_contents);
488    }
489}
490
491//=============================================================================
492// Data Accessors (The "Getter" API)
493//=============================================================================
494
495/// Extracts the core atomic data into a transparent `CFrame` struct.
496/// The caller OWNS the returned pointer and MUST call `free_c_frame` on it.
497///
498/// # Safety
499/// frame_handle must be valid. The caller takes ownership of the returned CFrame.
500#[unsafe(no_mangle)]
501pub unsafe extern "C" fn rkr_frame_to_c_frame(frame_handle: *const RKRConFrame) -> *mut CFrame {
502    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
503        Some(f) => f,
504        None => return ptr::null_mut(),
505    };
506
507    let masses_iter = frame
508        .header
509        .natms_per_type
510        .iter()
511        .zip(frame.header.masses_per_type.iter())
512        .flat_map(|(num_atoms, mass)| std::iter::repeat_n(*mass, *num_atoms));
513
514    let has_velocities = frame.has_velocities();
515
516    let mut c_atoms: Vec<CAtom> = frame
517        .atom_data
518        .iter()
519        .zip(masses_iter)
520        .map(|(atom_datum, mass)| {
521            let [vx, vy, vz] = atom_datum.velocity.unwrap_or([0.0; 3]);
522            let [fx, fy, fz] = atom_datum.force.unwrap_or([0.0; 3]);
523            CAtom {
524                atomic_number: symbol_to_atomic_number(&atom_datum.symbol),
525                x: atom_datum.x,
526                y: atom_datum.y,
527                z: atom_datum.z,
528                is_fixed: atom_datum.is_fixed(),
529                fixed_x: atom_datum.fixed[0],
530                fixed_y: atom_datum.fixed[1],
531                fixed_z: atom_datum.fixed[2],
532                atom_id: atom_datum.atom_id,
533                mass,
534                vx,
535                vy,
536                vz,
537                has_velocity: atom_datum.has_velocity(),
538                fx,
539                fy,
540                fz,
541                has_forces: atom_datum.has_forces(),
542                energy: atom_datum.energy.unwrap_or(0.0),
543                has_energy: atom_datum.has_energy(),
544            }
545        })
546        .collect();
547
548    let atoms_ptr = c_atoms.as_mut_ptr();
549    let num_atoms = c_atoms.len();
550    std::mem::forget(c_atoms);
551
552    let has_forces = frame.has_forces();
553    let has_energies = frame.has_energies();
554
555    let c_frame = Box::new(CFrame {
556        atoms: atoms_ptr,
557        num_atoms,
558        cell: frame.header.boxl,
559        angles: frame.header.angles,
560        has_velocities,
561        has_forces,
562        has_energies,
563    });
564
565    Box::into_raw(c_frame)
566}
567
568/// Frees the memory of a `CFrame` struct, including its internal atoms array.
569///
570/// # Safety
571/// frame must be valid or null.
572#[unsafe(no_mangle)]
573pub unsafe extern "C" fn free_c_frame(frame: *mut CFrame) {
574    if frame.is_null() {
575        return;
576    }
577    unsafe {
578        let frame_box = Box::from_raw(frame);
579        let _ = Vec::from_raw_parts(frame_box.atoms, frame_box.num_atoms, frame_box.num_atoms);
580    }
581}
582
583/// Copies a header string line into a caller-provided buffer.
584///
585/// `is_prebox=true` selects from the two prebox lines (line 0 = user
586/// text, line 1 = JSON metadata); `false` selects from the two postbox
587/// lines. Strings longer than `buffer_len - 1` bytes are truncated; the
588/// final byte is always set to NUL.
589///
590/// Returns `RKR_STATUS_SUCCESS` on success,
591/// `RKR_STATUS_INDEX_OUT_OF_BOUNDS` if `line_index >= 2`,
592/// `RKR_STATUS_NULL_POINTER` if `frame_handle` or `buffer` is NULL,
593/// `RKR_STATUS_BUFFER_TOO_SMALL` if `buffer_len == 0`.
594///
595/// Pair with [`rkr_frame_get_header_line_cpp`] when the caller prefers
596/// an allocated string with no fixed length cap; that variant returns
597/// NULL for the same out-of-bounds condition.
598///
599/// # Safety
600/// frame_handle must be valid. buffer must be at least buffer_len bytes.
601#[unsafe(no_mangle)]
602pub unsafe extern "C" fn rkr_frame_get_header_line(
603    frame_handle: *const RKRConFrame,
604    is_prebox: bool,
605    line_index: usize,
606    buffer: *mut c_char,
607    buffer_len: usize,
608) -> RKRStatus {
609    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
610        Some(f) => f,
611        None => return RKRStatus::RKR_STATUS_NULL_POINTER,
612    };
613    if buffer.is_null() {
614        return RKRStatus::RKR_STATUS_NULL_POINTER;
615    }
616    if buffer_len == 0 {
617        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
618    }
619    let line_to_copy: Option<&str> = if is_prebox {
620        match line_index {
621            0 => Some(frame.header.prebox_header.user.as_str()),
622            1 => Some(frame.header.prebox_header.metadata_line()),
623            _ => None,
624        }
625    } else {
626        frame.header.postbox_header.get(line_index).map(String::as_str)
627    };
628    if let Some(line) = line_to_copy {
629        let bytes = line.as_bytes();
630        let len_to_copy = std::cmp::min(bytes.len(), buffer_len - 1);
631        unsafe {
632            ptr::copy_nonoverlapping(bytes.as_ptr(), buffer as *mut u8, len_to_copy);
633            *buffer.add(len_to_copy) = 0;
634        }
635        RKRStatus::RKR_STATUS_SUCCESS
636    } else {
637        RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS
638    }
639}
640
641/// Gets a header string line as a newly allocated, null-terminated C string.
642///
643/// The caller OWNS the returned pointer and MUST call `rkr_free_string`
644/// on it to prevent a memory leak. Returns NULL on error or if the
645/// index is invalid (use [`rkr_frame_get_header_line`] when a status
646/// code is preferred to NULL-vs-success disambiguation).
647///
648/// The `_cpp` suffix is historical; the function is callable from both
649/// C and C++.
650///
651/// # Safety
652/// frame_handle must be valid. The caller takes ownership of the returned string.
653#[unsafe(no_mangle)]
654pub unsafe extern "C" fn rkr_frame_get_header_line_cpp(
655    frame_handle: *const RKRConFrame,
656    is_prebox: bool,
657    line_index: usize,
658) -> *mut c_char {
659    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
660        Some(f) => f,
661        None => return ptr::null_mut(),
662    };
663
664    let line_to_copy: Option<&str> = if is_prebox {
665        match line_index {
666            0 => Some(frame.header.prebox_header.user.as_str()),
667            1 => Some(frame.header.prebox_header.metadata_line()),
668            _ => None,
669        }
670    } else {
671        frame.header.postbox_header.get(line_index).map(String::as_str)
672    };
673
674    if let Some(line) = line_to_copy {
675        // Convert the Rust string slice to a C-compatible, heap-allocated string.
676        match CString::new(line) {
677            Ok(c_string) => c_string.into_raw(), // Give ownership to the C caller
678            Err(_) => ptr::null_mut(),           // In case the string contains a null byte
679        }
680    } else {
681        ptr::null_mut() // Index out of bounds
682    }
683}
684
685/// Frees a C string that was allocated by Rust (e.g., from
686/// `rkr_frame_metadata_json`, `rkr_frame_potential_type`, or
687/// `rkr_frame_get_header_line_cpp`). Safe to call with NULL (no-op).
688///
689/// # Safety
690/// s must be either NULL or a pointer previously returned by an
691/// allocating Rust FFI function in this crate.
692#[unsafe(no_mangle)]
693pub unsafe extern "C" fn rkr_free_string(s: *mut c_char) {
694    if !s.is_null() {
695        // Retake ownership of the CString to deallocate it properly.
696        let _ = unsafe { CString::from_raw(s) };
697    }
698}
699
700//=============================================================================
701// FFI Writer Functions (Writer Object Model)
702//=============================================================================
703
704/// Type-erased writer that backs every `RKRConFrameWriter` handle.
705///
706/// `ConFrameWriter<W>` is generic over its sink, so a plain `File`, a
707/// gzip `GzEncoder<File>`, and a zstd encoder all monomorphise to
708/// distinct, layout-incompatible types. Boxing the sink as
709/// `Box<dyn Write>` collapses them to a single concrete handle type, so
710/// `free_rkr_writer` and `rkr_writer_extend` can cast the opaque pointer
711/// to exactly one type regardless of the compression chosen at
712/// construction. Dropping the box flushes the `BufWriter` and then runs
713/// the sink's own `Drop` (gzip/zstd finalize their streams there).
714type RkrWriter = ConFrameWriter<Box<dyn std::io::Write>>;
715
716/// Boxes a sink into an `RKRConFrameWriter` handle at the requested
717/// precision. `precision == None` selects the writer's built-in default.
718#[inline]
719fn into_rkr_writer(
720    sink: Box<dyn std::io::Write>,
721    precision: Option<u8>,
722) -> *mut RKRConFrameWriter {
723    let writer: RkrWriter = match precision {
724        Some(p) => ConFrameWriter::with_precision(sink, p as usize),
725        None => ConFrameWriter::new(sink),
726    };
727    Box::into_raw(Box::new(writer)) as *mut RKRConFrameWriter
728}
729
730/// Parses a borrowed C string, returning `None` for null or non-UTF-8.
731#[inline]
732unsafe fn cstr_path<'a>(filename_c: *const c_char) -> Option<&'a str> {
733    if filename_c.is_null() {
734        return None;
735    }
736    unsafe { CStr::from_ptr(filename_c).to_str().ok() }
737}
738
739/// Creates a new frame writer for the specified file.
740/// The caller OWNS the returned pointer and MUST call `free_rkr_writer`.
741///
742/// # Safety
743/// filename_c must be valid. The caller takes ownership of the returned writer.
744#[unsafe(no_mangle)]
745pub unsafe extern "C" fn create_writer_from_path_c(
746    filename_c: *const c_char,
747) -> *mut RKRConFrameWriter {
748    let filename = match unsafe { cstr_path(filename_c) } {
749        Some(s) => s,
750        None => return ptr::null_mut(),
751    };
752    match File::create(filename) {
753        Ok(file) => into_rkr_writer(Box::new(file), None),
754        Err(_) => ptr::null_mut(),
755    }
756}
757
758/// Frees the memory for an `RKRConFrameWriter`, closing the associated file.
759///
760/// # Safety
761/// writer_handle must be valid or null.
762#[unsafe(no_mangle)]
763pub unsafe extern "C" fn free_rkr_writer(writer_handle: *mut RKRConFrameWriter) {
764    if !writer_handle.is_null() {
765        let _ = unsafe { Box::from_raw(writer_handle as *mut RkrWriter) };
766    }
767}
768
769/// Writes multiple frames from an array of handles to the file managed by the writer.
770/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
771///
772/// # Safety
773/// writer_handle and frame_handles must be valid.
774#[unsafe(no_mangle)]
775pub unsafe extern "C" fn rkr_writer_extend(
776    writer_handle: *mut RKRConFrameWriter,
777    frame_handles: *const *const RKRConFrame,
778    num_frames: usize,
779) -> RKRStatus {
780    let writer = match unsafe { (writer_handle as *mut RkrWriter).as_mut() } {
781        Some(w) => w,
782        None => return RKRStatus::RKR_STATUS_NULL_POINTER,
783    };
784    if frame_handles.is_null() {
785        return RKRStatus::RKR_STATUS_NULL_POINTER;
786    }
787
788    let handles_slice = unsafe { std::slice::from_raw_parts(frame_handles, num_frames) };
789    let mut rust_frames: Vec<&ConFrame> = Vec::with_capacity(num_frames);
790    if handles_slice.iter().any(|&handle| handle.is_null()) {
791        // Fail fast if any handle is null, as this indicates a bug on the
792        // caller's side.
793        return RKRStatus::RKR_STATUS_NULL_POINTER;
794    }
795    for &handle in handles_slice.iter() {
796        // Assume the handle is valid.
797        match unsafe { (handle as *const ConFrame).as_ref() } {
798            Some(frame) => rust_frames.push(frame),
799            // This case should be unreachable if the handle is not null, but we handle it for safety.
800            None => return RKRStatus::RKR_STATUS_NULL_POINTER,
801        }
802    }
803
804    match writer.extend(rust_frames.into_iter()) {
805        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
806        Err(_) => RKRStatus::RKR_STATUS_IO_ERROR,
807    }
808}
809
810//=============================================================================
811// Writer with Precision
812//=============================================================================
813
814/// Creates a new frame writer with custom floating-point precision.
815/// The caller OWNS the returned pointer and MUST call `free_rkr_writer`.
816///
817/// # Safety
818/// filename_c must be valid. The caller takes ownership of the returned writer.
819#[unsafe(no_mangle)]
820pub unsafe extern "C" fn create_writer_from_path_with_precision_c(
821    filename_c: *const c_char,
822    precision: u8,
823) -> *mut RKRConFrameWriter {
824    let filename = match unsafe { cstr_path(filename_c) } {
825        Some(s) => s,
826        None => return ptr::null_mut(),
827    };
828    match File::create(filename) {
829        Ok(file) => into_rkr_writer(Box::new(file), Some(precision)),
830        Err(_) => ptr::null_mut(),
831    }
832}
833
834//=============================================================================
835// Frame Builder FFI (construct ConFrame from C data)
836//=============================================================================
837
838/// An opaque handle to a Rust `ConFrameBuilder` object.
839#[repr(C)]
840pub struct RKRConFrameBuilder {
841    _private: [u8; 0],
842}
843
844#[allow(clippy::too_many_arguments)]
845unsafe fn add_builder_atom(
846    builder_handle: *mut RKRConFrameBuilder,
847    symbol: *const c_char,
848    x: f64,
849    y: f64,
850    z: f64,
851    fixed: [bool; 3],
852    atom_id: u64,
853    mass: f64,
854    velocity: Option<[f64; 3]>,
855    forces: Option<[f64; 3]>,
856) -> RKRStatus {
857    if builder_handle.is_null() || symbol.is_null() {
858        return RKRStatus::RKR_STATUS_NULL_POINTER;
859    }
860    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
861    let sym = match unsafe { CStr::from_ptr(symbol).to_str() } {
862        Ok(s) => s,
863        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
864    };
865
866    builder.add_atom(sym, x, y, z, fixed, atom_id, mass);
867    if let Some(v) = velocity {
868        builder.with_velocity(v);
869    }
870    if let Some(f) = forces {
871        builder.with_force(f);
872    }
873
874    RKRStatus::RKR_STATUS_SUCCESS
875}
876
877/// Attaches a velocity vector to the most recently added atom on a builder.
878/// No-op if no atom has been added yet.
879///
880/// # Safety
881/// builder_handle must be valid. velocity must point to 3 contiguous f64 values.
882#[unsafe(no_mangle)]
883pub unsafe extern "C" fn rkr_frame_builder_set_last_velocity(
884    builder_handle: *mut RKRConFrameBuilder,
885    velocity: *const f64,
886) -> RKRStatus {
887    if builder_handle.is_null() || velocity.is_null() {
888        return RKRStatus::RKR_STATUS_NULL_POINTER;
889    }
890    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
891    let v = unsafe { [*velocity, *velocity.add(1), *velocity.add(2)] };
892    builder.with_velocity(v);
893    RKRStatus::RKR_STATUS_SUCCESS
894}
895
896/// Attaches a force vector to the most recently added atom on a builder.
897/// No-op if no atom has been added yet.
898///
899/// # Safety
900/// builder_handle must be valid. force must point to 3 contiguous f64 values.
901#[unsafe(no_mangle)]
902pub unsafe extern "C" fn rkr_frame_builder_set_last_force(
903    builder_handle: *mut RKRConFrameBuilder,
904    force: *const f64,
905) -> RKRStatus {
906    if builder_handle.is_null() || force.is_null() {
907        return RKRStatus::RKR_STATUS_NULL_POINTER;
908    }
909    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
910    let f = unsafe { [*force, *force.add(1), *force.add(2)] };
911    builder.with_force(f);
912    RKRStatus::RKR_STATUS_SUCCESS
913}
914
915/// Attaches a per-atom energy to the most recently added atom on a
916/// builder. No-op if no atom has been added yet.
917///
918/// Use this together with the per-frame `energy` metadata key when a
919/// caller wants to round-trip an "Energies of Component" decomposition
920/// alongside the total.
921///
922/// # Safety
923/// builder_handle must be valid.
924#[unsafe(no_mangle)]
925pub unsafe extern "C" fn rkr_frame_builder_set_last_energy(
926    builder_handle: *mut RKRConFrameBuilder,
927    energy: f64,
928) -> RKRStatus {
929    if builder_handle.is_null() {
930        return RKRStatus::RKR_STATUS_NULL_POINTER;
931    }
932    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
933    builder.with_energy(energy);
934    RKRStatus::RKR_STATUS_SUCCESS
935}
936
937// ----- v0.11.0 in-place mutation FFI ---------------------------------------
938//
939// Mirrors `ConFrameBuilder::set_atom_* / clear_atom_* / *_from_flat /
940// get_atom_* / atom_count` for C / C++ / Python / Julia consumers. All
941// mutators return RKRStatus; getters return raw values via out-parameters
942// (so a caller can distinguish "atom has no force" from "successful read of
943// f={0,0,0}" via the `has_*` boolean out-parameter).
944//
945// IndexOutOfBounds errors from the Rust side surface as
946// RKR_STATUS_INDEX_OUT_OF_BOUNDS; all NULL-handle / NULL-out-pointer paths
947// return RKR_STATUS_NULL_POINTER. Bulk setters with the wrong length return
948// RKR_STATUS_INDEX_OUT_OF_BOUNDS as well (the caller sized the buffer
949// wrong).
950
951fn map_builder_err(e: crate::error::ParseError) -> RKRStatus {
952    use crate::error::ParseError;
953    match e {
954        ParseError::IndexOutOfBounds { .. } | ParseError::InvalidVectorLength { .. } => {
955            RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS
956        }
957        _ => RKRStatus::RKR_STATUS_INTERNAL_ERROR,
958    }
959}
960
961/// Returns the number of atoms currently held in the builder.
962///
963/// # Safety
964/// builder_handle must be a valid pointer returned by rkr_frame_new and
965/// not yet consumed by rkr_frame_builder_build / freed.
966/// Returns 0 on NULL handle.
967#[unsafe(no_mangle)]
968pub unsafe extern "C" fn rkr_frame_builder_atom_count(
969    builder_handle: *const RKRConFrameBuilder,
970) -> usize {
971    if builder_handle.is_null() {
972        return 0;
973    }
974    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
975    builder.atom_count()
976}
977
978/// Updates the Cartesian position of an existing atom.
979/// # Safety
980/// builder_handle must be valid.
981#[unsafe(no_mangle)]
982pub unsafe extern "C" fn rkr_frame_builder_set_atom_position(
983    builder_handle: *mut RKRConFrameBuilder,
984    index: usize,
985    x: f64,
986    y: f64,
987    z: f64,
988) -> RKRStatus {
989    if builder_handle.is_null() {
990        return RKRStatus::RKR_STATUS_NULL_POINTER;
991    }
992    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
993    match builder.set_atom_position(index, x, y, z) {
994        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
995        Err(e) => map_builder_err(e),
996    }
997}
998
999/// Sets the velocity vector of an existing atom from 3 contiguous f64 values.
1000/// # Safety
1001/// builder_handle must be valid; velocity must point to 3 contiguous f64.
1002#[unsafe(no_mangle)]
1003pub unsafe extern "C" fn rkr_frame_builder_set_atom_velocity(
1004    builder_handle: *mut RKRConFrameBuilder,
1005    index: usize,
1006    velocity: *const f64,
1007) -> RKRStatus {
1008    if builder_handle.is_null() || velocity.is_null() {
1009        return RKRStatus::RKR_STATUS_NULL_POINTER;
1010    }
1011    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1012    let v = unsafe { [*velocity, *velocity.add(1), *velocity.add(2)] };
1013    match builder.set_atom_velocity(index, v) {
1014        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1015        Err(e) => map_builder_err(e),
1016    }
1017}
1018
1019/// Sets the force vector of an existing atom from 3 contiguous f64 values.
1020/// # Safety
1021/// builder_handle must be valid; force must point to 3 contiguous f64.
1022#[unsafe(no_mangle)]
1023pub unsafe extern "C" fn rkr_frame_builder_set_atom_force(
1024    builder_handle: *mut RKRConFrameBuilder,
1025    index: usize,
1026    force: *const f64,
1027) -> RKRStatus {
1028    if builder_handle.is_null() || force.is_null() {
1029        return RKRStatus::RKR_STATUS_NULL_POINTER;
1030    }
1031    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1032    let f = unsafe { [*force, *force.add(1), *force.add(2)] };
1033    match builder.set_atom_force(index, f) {
1034        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1035        Err(e) => map_builder_err(e),
1036    }
1037}
1038
1039/// Sets the per-atom energy contribution of an existing atom.
1040/// # Safety
1041/// builder_handle must be valid.
1042#[unsafe(no_mangle)]
1043pub unsafe extern "C" fn rkr_frame_builder_set_atom_energy(
1044    builder_handle: *mut RKRConFrameBuilder,
1045    index: usize,
1046    energy: f64,
1047) -> RKRStatus {
1048    if builder_handle.is_null() {
1049        return RKRStatus::RKR_STATUS_NULL_POINTER;
1050    }
1051    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1052    match builder.set_atom_energy(index, energy) {
1053        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1054        Err(e) => map_builder_err(e),
1055    }
1056}
1057
1058/// Updates per-direction fixed flags `[fixed_x, fixed_y, fixed_z]`.
1059/// # Safety
1060/// builder_handle must be valid.
1061#[unsafe(no_mangle)]
1062pub unsafe extern "C" fn rkr_frame_builder_set_atom_fixed(
1063    builder_handle: *mut RKRConFrameBuilder,
1064    index: usize,
1065    fixed_x: bool,
1066    fixed_y: bool,
1067    fixed_z: bool,
1068) -> RKRStatus {
1069    if builder_handle.is_null() {
1070        return RKRStatus::RKR_STATUS_NULL_POINTER;
1071    }
1072    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1073    match builder.set_atom_fixed(index, [fixed_x, fixed_y, fixed_z]) {
1074        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1075        Err(e) => map_builder_err(e),
1076    }
1077}
1078
1079/// Updates the mass of an existing atom.
1080/// # Safety
1081/// builder_handle must be valid.
1082#[unsafe(no_mangle)]
1083pub unsafe extern "C" fn rkr_frame_builder_set_atom_mass(
1084    builder_handle: *mut RKRConFrameBuilder,
1085    index: usize,
1086    mass: f64,
1087) -> RKRStatus {
1088    if builder_handle.is_null() {
1089        return RKRStatus::RKR_STATUS_NULL_POINTER;
1090    }
1091    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1092    match builder.set_atom_mass(index, mass) {
1093        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1094        Err(e) => map_builder_err(e),
1095    }
1096}
1097
1098/// Updates the atom_id (pre-grouping index from .con column 5) of an
1099/// existing atom. The underlying `Array1<u64>` buffer pointer stays
1100/// stable; callers that hold a raw `*const u64` via
1101/// `rkr_frame_builder_atom_ids_data` do not need to refresh after this.
1102/// # Safety
1103/// builder_handle must be valid.
1104#[unsafe(no_mangle)]
1105pub unsafe extern "C" fn rkr_frame_builder_set_atom_id(
1106    builder_handle: *mut RKRConFrameBuilder,
1107    index: usize,
1108    atom_id: u64,
1109) -> RKRStatus {
1110    if builder_handle.is_null() {
1111        return RKRStatus::RKR_STATUS_NULL_POINTER;
1112    }
1113    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1114    match builder.set_atom_id(index, atom_id) {
1115        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1116        Err(e) => map_builder_err(e),
1117    }
1118}
1119
1120/// Removes velocity / force / energy data from an existing atom.
1121/// # Safety
1122/// builder_handle must be valid.
1123#[unsafe(no_mangle)]
1124pub unsafe extern "C" fn rkr_frame_builder_clear_atom_velocity(
1125    builder_handle: *mut RKRConFrameBuilder,
1126    index: usize,
1127) -> RKRStatus {
1128    if builder_handle.is_null() {
1129        return RKRStatus::RKR_STATUS_NULL_POINTER;
1130    }
1131    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1132    match builder.clear_atom_velocity(index) {
1133        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1134        Err(e) => map_builder_err(e),
1135    }
1136}
1137
1138/// # Safety
1139/// builder_handle must be valid.
1140#[unsafe(no_mangle)]
1141pub unsafe extern "C" fn rkr_frame_builder_clear_atom_force(
1142    builder_handle: *mut RKRConFrameBuilder,
1143    index: usize,
1144) -> RKRStatus {
1145    if builder_handle.is_null() {
1146        return RKRStatus::RKR_STATUS_NULL_POINTER;
1147    }
1148    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1149    match builder.clear_atom_force(index) {
1150        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1151        Err(e) => map_builder_err(e),
1152    }
1153}
1154
1155/// # Safety
1156/// builder_handle must be valid.
1157#[unsafe(no_mangle)]
1158pub unsafe extern "C" fn rkr_frame_builder_clear_atom_energy(
1159    builder_handle: *mut RKRConFrameBuilder,
1160    index: usize,
1161) -> RKRStatus {
1162    if builder_handle.is_null() {
1163        return RKRStatus::RKR_STATUS_NULL_POINTER;
1164    }
1165    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1166    match builder.clear_atom_energy(index) {
1167        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1168        Err(e) => map_builder_err(e),
1169    }
1170}
1171
1172/// Bulk-update positions for every atom from a flat row-major
1173/// `[x0,y0,z0,x1,y1,z1,...]` buffer of length `3 * atom_count()`.
1174/// # Safety
1175/// builder_handle must be valid; positions must point to `3 * len` f64.
1176#[unsafe(no_mangle)]
1177pub unsafe extern "C" fn rkr_frame_builder_set_positions_from_flat(
1178    builder_handle: *mut RKRConFrameBuilder,
1179    positions: *const f64,
1180    len: usize,
1181) -> RKRStatus {
1182    if builder_handle.is_null() || positions.is_null() {
1183        return RKRStatus::RKR_STATUS_NULL_POINTER;
1184    }
1185    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1186    let slice = unsafe { std::slice::from_raw_parts(positions, len) };
1187    match builder.set_positions_from_flat(slice) {
1188        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1189        Err(e) => map_builder_err(e),
1190    }
1191}
1192
1193/// Bulk-update forces for every atom.
1194/// # Safety
1195/// builder_handle must be valid; forces must point to `3 * len` f64.
1196#[unsafe(no_mangle)]
1197pub unsafe extern "C" fn rkr_frame_builder_set_forces_from_flat(
1198    builder_handle: *mut RKRConFrameBuilder,
1199    forces: *const f64,
1200    len: usize,
1201) -> RKRStatus {
1202    if builder_handle.is_null() || forces.is_null() {
1203        return RKRStatus::RKR_STATUS_NULL_POINTER;
1204    }
1205    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1206    let slice = unsafe { std::slice::from_raw_parts(forces, len) };
1207    match builder.set_forces_from_flat(slice) {
1208        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1209        Err(e) => map_builder_err(e),
1210    }
1211}
1212
1213/// Bulk-update per-atom energies (one f64 per atom).
1214/// # Safety
1215/// builder_handle must be valid; energies must point to `len` f64.
1216#[unsafe(no_mangle)]
1217pub unsafe extern "C" fn rkr_frame_builder_set_atom_energies_from_flat(
1218    builder_handle: *mut RKRConFrameBuilder,
1219    energies: *const f64,
1220    len: usize,
1221) -> RKRStatus {
1222    if builder_handle.is_null() || energies.is_null() {
1223        return RKRStatus::RKR_STATUS_NULL_POINTER;
1224    }
1225    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1226    let slice = unsafe { std::slice::from_raw_parts(energies, len) };
1227    match builder.set_atom_energies_from_flat(slice) {
1228        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1229        Err(e) => map_builder_err(e),
1230    }
1231}
1232
1233/// Reads the position of an existing atom into 3 contiguous f64 out values.
1234/// # Safety
1235/// builder_handle must be valid; out_xyz must point to 3 writable f64.
1236#[unsafe(no_mangle)]
1237pub unsafe extern "C" fn rkr_frame_builder_get_atom_position(
1238    builder_handle: *const RKRConFrameBuilder,
1239    index: usize,
1240    out_xyz: *mut f64,
1241) -> RKRStatus {
1242    if builder_handle.is_null() || out_xyz.is_null() {
1243        return RKRStatus::RKR_STATUS_NULL_POINTER;
1244    }
1245    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
1246    match builder.get_atom_position(index) {
1247        Ok((x, y, z)) => unsafe {
1248            *out_xyz = x;
1249            *out_xyz.add(1) = y;
1250            *out_xyz.add(2) = z;
1251            RKRStatus::RKR_STATUS_SUCCESS
1252        },
1253        Err(e) => map_builder_err(e),
1254    }
1255}
1256
1257/// Reads the velocity / force vector of an atom (if any) into 3 contiguous
1258/// f64. `*has_value` is set to `true` if the atom carries that vector,
1259/// `false` if it does not (in which case `out_xyz` is left untouched).
1260///
1261/// # Safety
1262/// builder_handle, out_xyz, has_value must all be valid pointers.
1263#[unsafe(no_mangle)]
1264pub unsafe extern "C" fn rkr_frame_builder_get_atom_velocity(
1265    builder_handle: *const RKRConFrameBuilder,
1266    index: usize,
1267    out_xyz: *mut f64,
1268    has_value: *mut bool,
1269) -> RKRStatus {
1270    if builder_handle.is_null() || out_xyz.is_null() || has_value.is_null() {
1271        return RKRStatus::RKR_STATUS_NULL_POINTER;
1272    }
1273    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
1274    match builder.get_atom_velocity(index) {
1275        Ok(Some(v)) => unsafe {
1276            *out_xyz = v[0];
1277            *out_xyz.add(1) = v[1];
1278            *out_xyz.add(2) = v[2];
1279            *has_value = true;
1280            RKRStatus::RKR_STATUS_SUCCESS
1281        },
1282        Ok(None) => unsafe {
1283            *has_value = false;
1284            RKRStatus::RKR_STATUS_SUCCESS
1285        },
1286        Err(e) => map_builder_err(e),
1287    }
1288}
1289
1290/// # Safety
1291/// builder_handle, out_xyz, has_value must all be valid pointers.
1292#[unsafe(no_mangle)]
1293pub unsafe extern "C" fn rkr_frame_builder_get_atom_force(
1294    builder_handle: *const RKRConFrameBuilder,
1295    index: usize,
1296    out_xyz: *mut f64,
1297    has_value: *mut bool,
1298) -> RKRStatus {
1299    if builder_handle.is_null() || out_xyz.is_null() || has_value.is_null() {
1300        return RKRStatus::RKR_STATUS_NULL_POINTER;
1301    }
1302    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
1303    match builder.get_atom_force(index) {
1304        Ok(Some(f)) => unsafe {
1305            *out_xyz = f[0];
1306            *out_xyz.add(1) = f[1];
1307            *out_xyz.add(2) = f[2];
1308            *has_value = true;
1309            RKRStatus::RKR_STATUS_SUCCESS
1310        },
1311        Ok(None) => unsafe {
1312            *has_value = false;
1313            RKRStatus::RKR_STATUS_SUCCESS
1314        },
1315        Err(e) => map_builder_err(e),
1316    }
1317}
1318
1319/// Reads the per-atom energy of an atom (if any). `*has_value` is set to
1320/// `true` if the atom carries an energy contribution, else `false` and
1321/// `*out_value` is left untouched.
1322/// # Safety
1323/// builder_handle, out_value, has_value must all be valid pointers.
1324#[unsafe(no_mangle)]
1325pub unsafe extern "C" fn rkr_frame_builder_get_atom_energy(
1326    builder_handle: *const RKRConFrameBuilder,
1327    index: usize,
1328    out_value: *mut f64,
1329    has_value: *mut bool,
1330) -> RKRStatus {
1331    if builder_handle.is_null() || out_value.is_null() || has_value.is_null() {
1332        return RKRStatus::RKR_STATUS_NULL_POINTER;
1333    }
1334    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
1335    match builder.get_atom_energy(index) {
1336        Ok(Some(e)) => unsafe {
1337            *out_value = e;
1338            *has_value = true;
1339            RKRStatus::RKR_STATUS_SUCCESS
1340        },
1341        Ok(None) => unsafe {
1342            *has_value = false;
1343            RKRStatus::RKR_STATUS_SUCCESS
1344        },
1345        Err(e) => map_builder_err(e),
1346    }
1347}
1348
1349/// Reads the mass of an existing atom.
1350/// # Safety
1351/// builder_handle and out_mass must be valid pointers.
1352#[unsafe(no_mangle)]
1353pub unsafe extern "C" fn rkr_frame_builder_get_atom_mass(
1354    builder_handle: *const RKRConFrameBuilder,
1355    index: usize,
1356    out_mass: *mut f64,
1357) -> RKRStatus {
1358    if builder_handle.is_null() || out_mass.is_null() {
1359        return RKRStatus::RKR_STATUS_NULL_POINTER;
1360    }
1361    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
1362    match builder.get_atom_mass(index) {
1363        Ok(m) => unsafe {
1364            *out_mass = m;
1365            RKRStatus::RKR_STATUS_SUCCESS
1366        },
1367        Err(e) => map_builder_err(e),
1368    }
1369}
1370
1371// ----- v0.11.0 DLPack tier-3 export FFI -------------------------------------
1372//
1373// Cross-language zero-copy via the DLPack 1.0 ABI. Each per-atom field of
1374// the builder is exported as an owning `DLManagedTensorVersioned*`. The
1375// caller is responsible for invoking the tensor's deleter callback to
1376// release the backing storage when finished. v0.11 ships the OWNING /
1377// CLONED variant: the tensor carries its own copy of the field data so it
1378// remains valid past the builder's lifetime. This is the conservative
1379// choice for cross-process / language-runtime consumers (Python GC,
1380// Julia GC, ...) where the consumer may outlive the Rust-side
1381// ConFrameBuilder. A future v0.12 will add a `*_dlpack_borrowed` variant
1382// that hands out a non-owning view backed by `Arc<ndarray::Array<...>>`
1383// storage (matches metatensor v2's `Arc<RwLock<ArrayD<T>>>` pattern) for
1384// in-process zero-copy.
1385//
1386// Optional sections (velocities, forces, atom_energies) return
1387// RKR_STATUS_SECTION_ABSENT when the section is not declared on the
1388// builder; the out parameter is left untouched. Always-present fields
1389// (positions, masses, atom_ids) always return a tensor on success.
1390
1391/// Re-export of dlpk's `DLManagedTensorVersioned` for the C ABI surface.
1392/// Defined here so cbindgen emits a forward declaration without pulling
1393/// in the full dlpk header; consumers include `<dlpack/dlpack.h>` (or
1394/// equivalent) and cast through the standard DLPack ABI.
1395pub use dlpk::sys::DLManagedTensorVersioned as RKRDLManagedTensorVersioned;
1396
1397fn map_dlpack_err(e: crate::error::ParseError) -> RKRStatus {
1398    use crate::error::ParseError;
1399    match e {
1400        ParseError::ValidationError(_) => RKRStatus::RKR_STATUS_VALIDATION_ERROR,
1401        _ => RKRStatus::RKR_STATUS_INTERNAL_ERROR,
1402    }
1403}
1404
1405fn export_owned_array2_dlpack(
1406    arr: &ndarray::ArcArray2<f64>,
1407    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
1408) -> RKRStatus {
1409    // ArcArray clone is a cheap Arc bump; the resulting tensor stays
1410    // safely owned even if the source builder later CoW-mutates.
1411    let shared = arr.clone();
1412    match dlpk::DLPackTensor::try_from(shared) {
1413        Ok(tensor) => {
1414            let raw = tensor.into_raw();
1415            unsafe {
1416                *out_tensor = raw.as_ptr();
1417            }
1418            RKRStatus::RKR_STATUS_SUCCESS
1419        }
1420        Err(e) => map_dlpack_err(crate::error::ParseError::ValidationError(format!(
1421            "DLPack export failed: {e}"
1422        ))),
1423    }
1424}
1425
1426fn export_owned_array1_f64_dlpack(
1427    arr: &ndarray::ArcArray1<f64>,
1428    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
1429) -> RKRStatus {
1430    let shared = arr.clone();
1431    match dlpk::DLPackTensor::try_from(shared) {
1432        Ok(tensor) => {
1433            let raw = tensor.into_raw();
1434            unsafe {
1435                *out_tensor = raw.as_ptr();
1436            }
1437            RKRStatus::RKR_STATUS_SUCCESS
1438        }
1439        Err(e) => map_dlpack_err(crate::error::ParseError::ValidationError(format!(
1440            "DLPack export failed: {e}"
1441        ))),
1442    }
1443}
1444
1445fn export_owned_array1_u64_dlpack(
1446    arr: &ndarray::ArcArray1<u64>,
1447    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
1448) -> RKRStatus {
1449    let shared = arr.clone();
1450    match dlpk::DLPackTensor::try_from(shared) {
1451        Ok(tensor) => {
1452            let raw = tensor.into_raw();
1453            unsafe {
1454                *out_tensor = raw.as_ptr();
1455            }
1456            RKRStatus::RKR_STATUS_SUCCESS
1457        }
1458        Err(e) => map_dlpack_err(crate::error::ParseError::ValidationError(format!(
1459            "DLPack export failed: {e}"
1460        ))),
1461    }
1462}
1463
1464/// Export builder positions as a DLPack-managed tensor.
1465///
1466/// On success the caller-supplied `*out_tensor` is set to a newly-
1467/// allocated `DLManagedTensorVersioned*` that owns a clone of the
1468/// builder's `(N, 3) f64` row-major positions buffer. The caller MUST
1469/// invoke `(*out_tensor)->deleter(*out_tensor)` to release it.
1470///
1471/// # Safety
1472/// `builder_handle` must be a valid builder handle; `out_tensor` must
1473/// be a valid pointer to a writable `*mut DLManagedTensorVersioned`.
1474#[unsafe(no_mangle)]
1475pub unsafe extern "C" fn rkr_frame_builder_positions_dlpack(
1476    builder_handle: *const RKRConFrameBuilder,
1477    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
1478) -> RKRStatus {
1479    if builder_handle.is_null() || out_tensor.is_null() {
1480        return RKRStatus::RKR_STATUS_NULL_POINTER;
1481    }
1482    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
1483    export_owned_array2_dlpack(builder.positions_2d_ref(), out_tensor)
1484}
1485
1486/// Export builder velocities as a DLPack-managed tensor.
1487///
1488/// Returns `RKR_STATUS_SECTION_ABSENT` if the velocities section is not
1489/// declared; otherwise `(N, 3) f64`. See positions_dlpack for ownership
1490/// semantics.
1491///
1492/// # Safety
1493/// `builder_handle` must be a valid builder handle; `out_tensor` must
1494/// be a valid pointer to a writable `*mut DLManagedTensorVersioned`.
1495#[unsafe(no_mangle)]
1496pub unsafe extern "C" fn rkr_frame_builder_velocities_dlpack(
1497    builder_handle: *const RKRConFrameBuilder,
1498    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
1499) -> RKRStatus {
1500    if builder_handle.is_null() || out_tensor.is_null() {
1501        return RKRStatus::RKR_STATUS_NULL_POINTER;
1502    }
1503    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
1504    if !builder.has_velocities_section() {
1505        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
1506    }
1507    export_owned_array2_dlpack(builder.velocities_2d_ref(), out_tensor)
1508}
1509
1510/// Export builder forces as a DLPack-managed tensor.
1511///
1512/// Returns `RKR_STATUS_SECTION_ABSENT` if the forces section is not
1513/// declared.
1514///
1515/// # Safety
1516/// `builder_handle` must be a valid builder handle; `out_tensor` must
1517/// be a valid pointer to a writable `*mut DLManagedTensorVersioned`.
1518#[unsafe(no_mangle)]
1519pub unsafe extern "C" fn rkr_frame_builder_forces_dlpack(
1520    builder_handle: *const RKRConFrameBuilder,
1521    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
1522) -> RKRStatus {
1523    if builder_handle.is_null() || out_tensor.is_null() {
1524        return RKRStatus::RKR_STATUS_NULL_POINTER;
1525    }
1526    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
1527    if !builder.has_forces_section() {
1528        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
1529    }
1530    export_owned_array2_dlpack(builder.forces_2d_ref(), out_tensor)
1531}
1532
1533/// Export builder per-atom energies as a DLPack-managed tensor.
1534///
1535/// Returns `RKR_STATUS_SECTION_ABSENT` if the energies section is not
1536/// declared; otherwise `(N,) f64`.
1537///
1538/// # Safety
1539/// `builder_handle` must be a valid builder handle; `out_tensor` must
1540/// be a valid pointer to a writable `*mut DLManagedTensorVersioned`.
1541#[unsafe(no_mangle)]
1542pub unsafe extern "C" fn rkr_frame_builder_atom_energies_dlpack(
1543    builder_handle: *const RKRConFrameBuilder,
1544    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
1545) -> RKRStatus {
1546    if builder_handle.is_null() || out_tensor.is_null() {
1547        return RKRStatus::RKR_STATUS_NULL_POINTER;
1548    }
1549    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
1550    if !builder.has_energies_section() {
1551        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
1552    }
1553    export_owned_array1_f64_dlpack(builder.atom_energies_1d_ref(), out_tensor)
1554}
1555
1556/// Export builder per-atom masses as a DLPack-managed tensor `(N,) f64`.
1557///
1558/// # Safety
1559/// `builder_handle` must be a valid builder handle; `out_tensor` must
1560/// be a valid pointer to a writable `*mut DLManagedTensorVersioned`.
1561#[unsafe(no_mangle)]
1562pub unsafe extern "C" fn rkr_frame_builder_masses_dlpack(
1563    builder_handle: *const RKRConFrameBuilder,
1564    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
1565) -> RKRStatus {
1566    if builder_handle.is_null() || out_tensor.is_null() {
1567        return RKRStatus::RKR_STATUS_NULL_POINTER;
1568    }
1569    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
1570    export_owned_array1_f64_dlpack(builder.masses_1d_ref(), out_tensor)
1571}
1572
1573/// Export builder per-atom ids as a DLPack-managed tensor `(N,) u64`.
1574///
1575/// # Safety
1576/// `builder_handle` must be a valid builder handle; `out_tensor` must
1577/// be a valid pointer to a writable `*mut DLManagedTensorVersioned`.
1578#[unsafe(no_mangle)]
1579pub unsafe extern "C" fn rkr_frame_builder_atom_ids_dlpack(
1580    builder_handle: *const RKRConFrameBuilder,
1581    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
1582) -> RKRStatus {
1583    if builder_handle.is_null() || out_tensor.is_null() {
1584        return RKRStatus::RKR_STATUS_NULL_POINTER;
1585    }
1586    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
1587    export_owned_array1_u64_dlpack(builder.atom_ids_1d_ref(), out_tensor)
1588}
1589
1590// ----- v0.11.1 in-process zero-copy raw-pointer FFI -------------------------
1591//
1592// The DLPack tier-3 export above clones field data into an owning tensor so
1593// the consumer can outlive the builder; this is the right contract for
1594// language-runtime / cross-process consumers (Python GC, Julia GC,
1595// inter-process exchange). For *in-process* zero-copy on the hot path
1596// (LAMMPS-style `lmp->atom->x` direct pointer access used by integrators,
1597// dynamics drivers, eOn's Matter Eigen::Map<RowMajor> views), we expose
1598// raw pointers into the builder's storage. The lifetime contract is
1599// purely caller-managed: the pointer is valid while the builder is alive
1600// and no add_atom call has grown the underlying ndarray. This mirrors
1601// the LAMMPS / OpenMM / GROMACS C-side hot path and is what makes a
1602// thin Matter wrapper over ConFrameBuilder fast.
1603//
1604// Cross-language ML consumers should use the DLPack tier above; raw
1605// pointer access is for in-process hot paths only.
1606
1607/// Borrow the positions buffer as a raw `(N, 3) f64` row-major pointer.
1608/// Returns NULL on invalid handle. Pointer is valid until the builder
1609/// is dropped or `add_atom` reallocates.
1610///
1611/// # Safety
1612/// builder_handle must be valid; the returned pointer must not be
1613/// dereferenced after a call to add_atom on the same builder.
1614#[unsafe(no_mangle)]
1615pub unsafe extern "C" fn rkr_frame_builder_positions_data(
1616    builder_handle: *mut RKRConFrameBuilder,
1617) -> *mut f64 {
1618    if builder_handle.is_null() {
1619        return std::ptr::null_mut();
1620    }
1621    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1622    builder
1623        .positions_view_mut()
1624        .as_slice_memory_order_mut()
1625        .map(|s| s.as_mut_ptr())
1626        .unwrap_or(std::ptr::null_mut())
1627}
1628
1629/// Borrow the velocities buffer as a raw `(N, 3) f64` row-major pointer.
1630/// Returns NULL if the velocities section is absent or the handle is
1631/// invalid.
1632///
1633/// # Safety
1634/// Same contract as rkr_frame_builder_positions_data.
1635#[unsafe(no_mangle)]
1636pub unsafe extern "C" fn rkr_frame_builder_velocities_data(
1637    builder_handle: *mut RKRConFrameBuilder,
1638) -> *mut f64 {
1639    if builder_handle.is_null() {
1640        return std::ptr::null_mut();
1641    }
1642    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1643    if !builder.has_velocities_section() {
1644        return std::ptr::null_mut();
1645    }
1646    let slice = builder.velocities_mut();
1647    if slice.is_empty() {
1648        std::ptr::null_mut()
1649    } else {
1650        slice.as_mut_ptr()
1651    }
1652}
1653
1654/// Borrow the forces buffer as a raw `(N, 3) f64` row-major pointer.
1655/// Returns NULL if the forces section is absent.
1656///
1657/// # Safety
1658/// Same contract as rkr_frame_builder_positions_data.
1659#[unsafe(no_mangle)]
1660pub unsafe extern "C" fn rkr_frame_builder_forces_data(
1661    builder_handle: *mut RKRConFrameBuilder,
1662) -> *mut f64 {
1663    if builder_handle.is_null() {
1664        return std::ptr::null_mut();
1665    }
1666    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1667    if !builder.has_forces_section() {
1668        return std::ptr::null_mut();
1669    }
1670    let slice = builder.forces_mut();
1671    if slice.is_empty() {
1672        std::ptr::null_mut()
1673    } else {
1674        slice.as_mut_ptr()
1675    }
1676}
1677
1678/// Borrow the per-atom energies buffer as a raw `(N,) f64` pointer.
1679/// Returns NULL if the energies section is absent.
1680///
1681/// # Safety
1682/// Same contract as rkr_frame_builder_positions_data.
1683#[unsafe(no_mangle)]
1684pub unsafe extern "C" fn rkr_frame_builder_atom_energies_data(
1685    builder_handle: *mut RKRConFrameBuilder,
1686) -> *mut f64 {
1687    if builder_handle.is_null() {
1688        return std::ptr::null_mut();
1689    }
1690    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1691    if !builder.has_energies_section() {
1692        return std::ptr::null_mut();
1693    }
1694    let slice = builder.atom_energies_mut();
1695    if slice.is_empty() {
1696        std::ptr::null_mut()
1697    } else {
1698        slice.as_mut_ptr()
1699    }
1700}
1701
1702/// Borrow the per-atom masses buffer as a raw `(N,) f64` pointer.
1703///
1704/// # Safety
1705/// Same contract as rkr_frame_builder_positions_data.
1706#[unsafe(no_mangle)]
1707pub unsafe extern "C" fn rkr_frame_builder_masses_data(
1708    builder_handle: *mut RKRConFrameBuilder,
1709) -> *mut f64 {
1710    if builder_handle.is_null() {
1711        return std::ptr::null_mut();
1712    }
1713    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1714    let slice = builder.masses_mut();
1715    if slice.is_empty() {
1716        std::ptr::null_mut()
1717    } else {
1718        slice.as_mut_ptr()
1719    }
1720}
1721
1722/// Borrow the per-atom atom_ids buffer as a raw `(N,) u64` pointer.
1723///
1724/// # Safety
1725/// Same contract as rkr_frame_builder_positions_data.
1726#[unsafe(no_mangle)]
1727pub unsafe extern "C" fn rkr_frame_builder_atom_ids_data(
1728    builder_handle: *const RKRConFrameBuilder,
1729) -> *const u64 {
1730    if builder_handle.is_null() {
1731        return std::ptr::null();
1732    }
1733    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
1734    let slice = builder.atom_ids();
1735    if slice.is_empty() {
1736        std::ptr::null()
1737    } else {
1738        slice.as_ptr()
1739    }
1740}
1741
1742// ----- end v0.11.0 in-place mutation FFI ------------------------------------
1743
1744/// Adds an atom with optional per-axis fixed mask, velocity, and force vectors.
1745///
1746/// `velocity` and `force` are pointers to 3 contiguous f64 values, or NULL if
1747/// absent. This is the unified entry point that replaces the eight
1748/// `rkr_frame_add_atom_*` convenience functions; callers may continue using
1749/// those for source compatibility.
1750///
1751/// # Safety
1752/// builder_handle and symbol must be valid. velocity (if non-null) must point
1753/// to 3 contiguous f64 values, and force (if non-null) likewise.
1754#[unsafe(no_mangle)]
1755#[allow(clippy::too_many_arguments)]
1756pub unsafe extern "C" fn rkr_frame_add_atom_full(
1757    builder_handle: *mut RKRConFrameBuilder,
1758    symbol: *const c_char,
1759    x: f64,
1760    y: f64,
1761    z: f64,
1762    fixed_x: bool,
1763    fixed_y: bool,
1764    fixed_z: bool,
1765    atom_id: u64,
1766    mass: f64,
1767    velocity: *const f64,
1768    force: *const f64,
1769) -> RKRStatus {
1770    let velocity = if velocity.is_null() {
1771        None
1772    } else {
1773        Some(unsafe { [*velocity, *velocity.add(1), *velocity.add(2)] })
1774    };
1775    let force = if force.is_null() {
1776        None
1777    } else {
1778        Some(unsafe { [*force, *force.add(1), *force.add(2)] })
1779    };
1780    unsafe {
1781        add_builder_atom(
1782            builder_handle,
1783            symbol,
1784            x,
1785            y,
1786            z,
1787            [fixed_x, fixed_y, fixed_z],
1788            atom_id,
1789            mass,
1790            velocity,
1791            force,
1792        )
1793    }
1794}
1795
1796/// Creates a new frame builder with the given cell dimensions, angles,
1797/// and header lines.
1798///
1799/// `prebox1` is accepted for source compatibility but ignored: the
1800/// JSON metadata line is regenerated by the writer from the builder's
1801/// `spec_version`, `metadata`, and `sections`. Pass NULL or any string.
1802/// The caller OWNS the returned pointer and MUST call
1803/// `free_rkr_frame_builder` or consume it via `rkr_frame_builder_build`.
1804/// Returns NULL on error.
1805///
1806/// # Safety
1807/// cell and angles must point to 3 doubles. prebox0, postbox0, and
1808/// postbox1 must be NULL or valid null-terminated strings; prebox1 is
1809/// not dereferenced. The caller takes ownership of the returned
1810/// builder.
1811#[unsafe(no_mangle)]
1812pub unsafe extern "C" fn rkr_frame_new(
1813    cell: *const f64,
1814    angles: *const f64,
1815    prebox0: *const c_char,
1816    prebox1: *const c_char,
1817    postbox0: *const c_char,
1818    postbox1: *const c_char,
1819) -> *mut RKRConFrameBuilder {
1820    if cell.is_null() || angles.is_null() {
1821        return ptr::null_mut();
1822    }
1823    let cell_arr = unsafe { [*cell, *cell.add(1), *cell.add(2)] };
1824    let angles_arr = unsafe { [*angles, *angles.add(1), *angles.add(2)] };
1825
1826    let get_str = |p: *const c_char| -> String {
1827        if p.is_null() {
1828            String::new()
1829        } else {
1830            unsafe { CStr::from_ptr(p) }
1831                .to_str()
1832                .unwrap_or("")
1833                .to_string()
1834        }
1835    };
1836
1837    // prebox1 is the JSON metadata slot, regenerated on write from
1838    // metadata + sections; it is accepted for ABI continuity but ignored.
1839    let _ = get_str(prebox1);
1840    let mut builder = ConFrameBuilder::new(cell_arr, angles_arr);
1841    builder
1842        .prebox_header(get_str(prebox0))
1843        .postbox_header([get_str(postbox0), get_str(postbox1)]);
1844
1845    Box::into_raw(Box::new(builder)) as *mut RKRConFrameBuilder
1846}
1847
1848/// Parses and sets JSON metadata on an existing frame builder.
1849/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1850///
1851/// # Safety
1852/// builder_handle and metadata_json must be valid.
1853#[unsafe(no_mangle)]
1854pub unsafe extern "C" fn rkr_frame_builder_set_metadata_json(
1855    builder_handle: *mut RKRConFrameBuilder,
1856    metadata_json: *const c_char,
1857) -> RKRStatus {
1858    if builder_handle.is_null() || metadata_json.is_null() {
1859        return RKRStatus::RKR_STATUS_NULL_POINTER;
1860    }
1861    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1862    let metadata_json = match unsafe { CStr::from_ptr(metadata_json).to_str() } {
1863        Ok(s) => s,
1864        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
1865    };
1866    match builder.set_metadata_json(metadata_json) {
1867        Ok(()) => RKRStatus::RKR_STATUS_SUCCESS,
1868        Err(_) => RKRStatus::RKR_STATUS_INVALID_JSON,
1869    }
1870}
1871
1872/// Sets a numeric metadata key on an existing frame builder.
1873/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1874///
1875/// # Safety
1876/// builder_handle and key must be valid.
1877#[unsafe(no_mangle)]
1878pub unsafe extern "C" fn rkr_frame_builder_set_scalar_metadata(
1879    builder_handle: *mut RKRConFrameBuilder,
1880    key: *const c_char,
1881    value: f64,
1882) -> RKRStatus {
1883    if builder_handle.is_null() || key.is_null() {
1884        return RKRStatus::RKR_STATUS_NULL_POINTER;
1885    }
1886    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1887    let key = match unsafe { CStr::from_ptr(key).to_str() } {
1888        Ok(s) => s,
1889        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
1890    };
1891    builder.set_scalar_metadata(key, value);
1892    RKRStatus::RKR_STATUS_SUCCESS
1893}
1894
1895/// Sets a string metadata key on an existing frame builder.
1896/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1897///
1898/// # Safety
1899/// builder_handle, key, and value must be valid.
1900#[unsafe(no_mangle)]
1901pub unsafe extern "C" fn rkr_frame_builder_set_string_metadata(
1902    builder_handle: *mut RKRConFrameBuilder,
1903    key: *const c_char,
1904    value: *const c_char,
1905) -> RKRStatus {
1906    if builder_handle.is_null() || key.is_null() || value.is_null() {
1907        return RKRStatus::RKR_STATUS_NULL_POINTER;
1908    }
1909    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1910    let key = match unsafe { CStr::from_ptr(key).to_str() } {
1911        Ok(s) => s,
1912        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
1913    };
1914    let value = match unsafe { CStr::from_ptr(value).to_str() } {
1915        Ok(s) => s,
1916        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
1917    };
1918    builder.set_string_metadata(key, value);
1919    RKRStatus::RKR_STATUS_SUCCESS
1920}
1921
1922/// Sets the per-frame total energy metadata on an existing frame builder.
1923/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1924///
1925/// # Safety
1926/// builder_handle must be valid.
1927#[unsafe(no_mangle)]
1928pub unsafe extern "C" fn rkr_frame_builder_set_energy(
1929    builder_handle: *mut RKRConFrameBuilder,
1930    energy: f64,
1931) -> RKRStatus {
1932    if builder_handle.is_null() {
1933        return RKRStatus::RKR_STATUS_NULL_POINTER;
1934    }
1935    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1936    builder.set_energy(energy);
1937    RKRStatus::RKR_STATUS_SUCCESS
1938}
1939
1940/// Sets the zero-based frame index metadata on an existing frame builder.
1941/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1942///
1943/// # Safety
1944/// builder_handle must be valid.
1945#[unsafe(no_mangle)]
1946pub unsafe extern "C" fn rkr_frame_builder_set_frame_index(
1947    builder_handle: *mut RKRConFrameBuilder,
1948    idx: u64,
1949) -> RKRStatus {
1950    if builder_handle.is_null() {
1951        return RKRStatus::RKR_STATUS_NULL_POINTER;
1952    }
1953    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1954    builder.set_frame_index(idx);
1955    RKRStatus::RKR_STATUS_SUCCESS
1956}
1957
1958/// Sets the simulation time metadata on an existing frame builder.
1959/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1960///
1961/// # Safety
1962/// builder_handle must be valid.
1963#[unsafe(no_mangle)]
1964pub unsafe extern "C" fn rkr_frame_builder_set_time(
1965    builder_handle: *mut RKRConFrameBuilder,
1966    time: f64,
1967) -> RKRStatus {
1968    if builder_handle.is_null() {
1969        return RKRStatus::RKR_STATUS_NULL_POINTER;
1970    }
1971    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1972    builder.set_time(time);
1973    RKRStatus::RKR_STATUS_SUCCESS
1974}
1975
1976/// Sets the timestep metadata on an existing frame builder.
1977/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1978///
1979/// # Safety
1980/// builder_handle must be valid.
1981#[unsafe(no_mangle)]
1982pub unsafe extern "C" fn rkr_frame_builder_set_timestep(
1983    builder_handle: *mut RKRConFrameBuilder,
1984    dt: f64,
1985) -> RKRStatus {
1986    if builder_handle.is_null() {
1987        return RKRStatus::RKR_STATUS_NULL_POINTER;
1988    }
1989    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1990    builder.set_timestep(dt);
1991    RKRStatus::RKR_STATUS_SUCCESS
1992}
1993
1994/// Sets the NEB bead index metadata on an existing frame builder.
1995/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1996///
1997/// # Safety
1998/// builder_handle must be valid.
1999#[unsafe(no_mangle)]
2000pub unsafe extern "C" fn rkr_frame_builder_set_neb_bead(
2001    builder_handle: *mut RKRConFrameBuilder,
2002    bead: u64,
2003) -> RKRStatus {
2004    if builder_handle.is_null() {
2005        return RKRStatus::RKR_STATUS_NULL_POINTER;
2006    }
2007    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
2008    builder.set_neb_bead(bead);
2009    RKRStatus::RKR_STATUS_SUCCESS
2010}
2011
2012/// Sets the NEB band index metadata on an existing frame builder.
2013/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
2014///
2015/// # Safety
2016/// builder_handle must be valid.
2017#[unsafe(no_mangle)]
2018pub unsafe extern "C" fn rkr_frame_builder_set_neb_band(
2019    builder_handle: *mut RKRConFrameBuilder,
2020    band: u64,
2021) -> RKRStatus {
2022    if builder_handle.is_null() {
2023        return RKRStatus::RKR_STATUS_NULL_POINTER;
2024    }
2025    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
2026    builder.set_neb_band(band);
2027    RKRStatus::RKR_STATUS_SUCCESS
2028}
2029
2030// -----------------------------------------------------------------------------
2031// Legacy add_atom variants (kept for source compatibility)
2032//
2033// The unified entry point is `rkr_frame_add_atom_full`, which accepts
2034// optional velocity and force pointers. The eight functions below
2035// pre-date the unified call and remain in the API for code that was
2036// written against earlier 0.x releases. New callers should prefer
2037// `rkr_frame_add_atom_full`.
2038// -----------------------------------------------------------------------------
2039
2040/// **Deprecated**: prefer `rkr_frame_add_atom_full` with NULL velocity
2041/// and force pointers. Adds an atom (no velocity, no forces) to the
2042/// builder using a single uniform fixed flag.
2043/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
2044///
2045/// # Safety
2046/// builder_handle and symbol must be valid.
2047#[unsafe(no_mangle)]
2048pub unsafe extern "C" fn rkr_frame_add_atom(
2049    builder_handle: *mut RKRConFrameBuilder,
2050    symbol: *const c_char,
2051    x: f64,
2052    y: f64,
2053    z: f64,
2054    is_fixed: bool,
2055    atom_id: u64,
2056    mass: f64,
2057) -> RKRStatus {
2058    unsafe {
2059        add_builder_atom(
2060            builder_handle,
2061            symbol,
2062            x,
2063            y,
2064            z,
2065            [is_fixed; 3],
2066            atom_id,
2067            mass,
2068            None,
2069            None,
2070        )
2071    }
2072}
2073
2074/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom (no
2075/// velocity, no forces) using per-axis fixed flags.
2076/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
2077///
2078/// # Safety
2079/// builder_handle and symbol must be valid.
2080#[unsafe(no_mangle)]
2081pub unsafe extern "C" fn rkr_frame_add_atom_with_fixed_mask(
2082    builder_handle: *mut RKRConFrameBuilder,
2083    symbol: *const c_char,
2084    x: f64,
2085    y: f64,
2086    z: f64,
2087    fixed_x: bool,
2088    fixed_y: bool,
2089    fixed_z: bool,
2090    atom_id: u64,
2091    mass: f64,
2092) -> RKRStatus {
2093    unsafe {
2094        add_builder_atom(
2095            builder_handle,
2096            symbol,
2097            x,
2098            y,
2099            z,
2100            [fixed_x, fixed_y, fixed_z],
2101            atom_id,
2102            mass,
2103            None,
2104            None,
2105        )
2106    }
2107}
2108
2109/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
2110/// a velocity vector and a single uniform fixed flag.
2111/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
2112///
2113/// # Safety
2114/// builder_handle and symbol must be valid.
2115#[unsafe(no_mangle)]
2116pub unsafe extern "C" fn rkr_frame_add_atom_with_velocity(
2117    builder_handle: *mut RKRConFrameBuilder,
2118    symbol: *const c_char,
2119    x: f64,
2120    y: f64,
2121    z: f64,
2122    is_fixed: bool,
2123    atom_id: u64,
2124    mass: f64,
2125    vx: f64,
2126    vy: f64,
2127    vz: f64,
2128) -> RKRStatus {
2129    unsafe {
2130        add_builder_atom(
2131            builder_handle,
2132            symbol,
2133            x,
2134            y,
2135            z,
2136            [is_fixed; 3],
2137            atom_id,
2138            mass,
2139            Some([vx, vy, vz]),
2140            None,
2141        )
2142    }
2143}
2144
2145/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
2146/// a velocity vector and per-axis fixed flags.
2147/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
2148///
2149/// # Safety
2150/// builder_handle and symbol must be valid.
2151#[unsafe(no_mangle)]
2152pub unsafe extern "C" fn rkr_frame_add_atom_with_velocity_fixed_mask(
2153    builder_handle: *mut RKRConFrameBuilder,
2154    symbol: *const c_char,
2155    x: f64,
2156    y: f64,
2157    z: f64,
2158    fixed_x: bool,
2159    fixed_y: bool,
2160    fixed_z: bool,
2161    atom_id: u64,
2162    mass: f64,
2163    vx: f64,
2164    vy: f64,
2165    vz: f64,
2166) -> RKRStatus {
2167    unsafe {
2168        add_builder_atom(
2169            builder_handle,
2170            symbol,
2171            x,
2172            y,
2173            z,
2174            [fixed_x, fixed_y, fixed_z],
2175            atom_id,
2176            mass,
2177            Some([vx, vy, vz]),
2178            None,
2179        )
2180    }
2181}
2182
2183/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
2184/// a force vector and a single uniform fixed flag.
2185/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
2186///
2187/// # Safety
2188/// builder_handle and symbol must be valid.
2189#[unsafe(no_mangle)]
2190pub unsafe extern "C" fn rkr_frame_add_atom_with_forces(
2191    builder_handle: *mut RKRConFrameBuilder,
2192    symbol: *const c_char,
2193    x: f64,
2194    y: f64,
2195    z: f64,
2196    is_fixed: bool,
2197    atom_id: u64,
2198    mass: f64,
2199    fx: f64,
2200    fy: f64,
2201    fz: f64,
2202) -> RKRStatus {
2203    unsafe {
2204        add_builder_atom(
2205            builder_handle,
2206            symbol,
2207            x,
2208            y,
2209            z,
2210            [is_fixed; 3],
2211            atom_id,
2212            mass,
2213            None,
2214            Some([fx, fy, fz]),
2215        )
2216    }
2217}
2218
2219/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
2220/// a force vector and per-axis fixed flags.
2221/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
2222///
2223/// # Safety
2224/// builder_handle and symbol must be valid.
2225#[unsafe(no_mangle)]
2226pub unsafe extern "C" fn rkr_frame_add_atom_with_forces_fixed_mask(
2227    builder_handle: *mut RKRConFrameBuilder,
2228    symbol: *const c_char,
2229    x: f64,
2230    y: f64,
2231    z: f64,
2232    fixed_x: bool,
2233    fixed_y: bool,
2234    fixed_z: bool,
2235    atom_id: u64,
2236    mass: f64,
2237    fx: f64,
2238    fy: f64,
2239    fz: f64,
2240) -> RKRStatus {
2241    unsafe {
2242        add_builder_atom(
2243            builder_handle,
2244            symbol,
2245            x,
2246            y,
2247            z,
2248            [fixed_x, fixed_y, fixed_z],
2249            atom_id,
2250            mass,
2251            None,
2252            Some([fx, fy, fz]),
2253        )
2254    }
2255}
2256
2257/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
2258/// both velocity and force vectors and a single uniform fixed flag.
2259/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
2260///
2261/// # Safety
2262/// builder_handle and symbol must be valid.
2263#[unsafe(no_mangle)]
2264pub unsafe extern "C" fn rkr_frame_add_atom_with_velocity_and_forces(
2265    builder_handle: *mut RKRConFrameBuilder,
2266    symbol: *const c_char,
2267    x: f64,
2268    y: f64,
2269    z: f64,
2270    is_fixed: bool,
2271    atom_id: u64,
2272    mass: f64,
2273    vx: f64,
2274    vy: f64,
2275    vz: f64,
2276    fx: f64,
2277    fy: f64,
2278    fz: f64,
2279) -> RKRStatus {
2280    unsafe {
2281        add_builder_atom(
2282            builder_handle,
2283            symbol,
2284            x,
2285            y,
2286            z,
2287            [is_fixed; 3],
2288            atom_id,
2289            mass,
2290            Some([vx, vy, vz]),
2291            Some([fx, fy, fz]),
2292        )
2293    }
2294}
2295
2296/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
2297/// both velocity and force vectors and per-axis fixed flags.
2298/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
2299///
2300/// # Safety
2301/// builder_handle and symbol must be valid.
2302#[unsafe(no_mangle)]
2303pub unsafe extern "C" fn rkr_frame_add_atom_with_velocity_and_forces_fixed_mask(
2304    builder_handle: *mut RKRConFrameBuilder,
2305    symbol: *const c_char,
2306    x: f64,
2307    y: f64,
2308    z: f64,
2309    fixed_x: bool,
2310    fixed_y: bool,
2311    fixed_z: bool,
2312    atom_id: u64,
2313    mass: f64,
2314    vx: f64,
2315    vy: f64,
2316    vz: f64,
2317    fx: f64,
2318    fy: f64,
2319    fz: f64,
2320) -> RKRStatus {
2321    unsafe {
2322        add_builder_atom(
2323            builder_handle,
2324            symbol,
2325            x,
2326            y,
2327            z,
2328            [fixed_x, fixed_y, fixed_z],
2329            atom_id,
2330            mass,
2331            Some([vx, vy, vz]),
2332            Some([fx, fy, fz]),
2333        )
2334    }
2335}
2336
2337/// Consumes the builder and returns a finalized RKRConFrame handle.
2338/// The builder handle is invalidated after this call.
2339/// The caller OWNS the returned frame and MUST call `free_rkr_frame`.
2340/// Returns NULL on error.
2341///
2342/// # Safety
2343/// builder_handle must be valid. The caller takes ownership of the returned frame.
2344#[unsafe(no_mangle)]
2345pub unsafe extern "C" fn rkr_frame_builder_build(
2346    builder_handle: *mut RKRConFrameBuilder,
2347) -> *mut RKRConFrame {
2348    if builder_handle.is_null() {
2349        return ptr::null_mut();
2350    }
2351    let builder = unsafe { *Box::from_raw(builder_handle as *mut ConFrameBuilder) };
2352    let frame = builder.build();
2353    Box::into_raw(Box::new(frame)) as *mut RKRConFrame
2354}
2355
2356/// Frees a frame builder without building.
2357///
2358/// # Safety
2359/// builder_handle must be valid or null.
2360#[unsafe(no_mangle)]
2361pub unsafe extern "C" fn free_rkr_frame_builder(builder_handle: *mut RKRConFrameBuilder) {
2362    if !builder_handle.is_null() {
2363        let _ = unsafe { Box::from_raw(builder_handle as *mut ConFrameBuilder) };
2364    }
2365}
2366
2367/// Cheap, copy-on-write clone of a frame builder. Returned handle owns
2368/// a new `ConFrameBuilder` whose per-atom buffers share storage with
2369/// the source via ArcArray; any subsequent mutation triggers a
2370/// per-buffer copy-on-write so writes do not leak across clones.
2371///
2372/// Intended for downstream consumers (NEB image bulk allocation,
2373/// trajectory snapshots) that need many builders carrying the same
2374/// per-atom data without paying N copies up-front. Returns NULL on
2375/// NULL input.
2376///
2377/// The caller OWNS the returned handle and MUST call
2378/// `free_rkr_frame_builder` (or consume via `rkr_frame_builder_build`).
2379///
2380/// # Safety
2381/// `builder_handle` must be a valid pointer returned by `rkr_frame_new`
2382/// (or by an earlier `rkr_frame_builder_clone`) and not yet freed.
2383#[unsafe(no_mangle)]
2384pub unsafe extern "C" fn rkr_frame_builder_clone(
2385    builder_handle: *const RKRConFrameBuilder,
2386) -> *mut RKRConFrameBuilder {
2387    if builder_handle.is_null() {
2388        return std::ptr::null_mut();
2389    }
2390    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2391    let cloned = builder.clone();
2392    Box::into_raw(Box::new(cloned)) as *mut RKRConFrameBuilder
2393}
2394
2395/// Creates a new gzip-compressed frame writer for the specified file.
2396/// The caller OWNS the returned pointer and MUST call `free_rkr_writer`.
2397///
2398/// # Safety
2399/// filename_c must be valid. The caller takes ownership of the returned writer.
2400#[unsafe(no_mangle)]
2401pub unsafe extern "C" fn create_writer_gzip_c(filename_c: *const c_char) -> *mut RKRConFrameWriter {
2402    let filename = match unsafe { cstr_path(filename_c) } {
2403        Some(s) => s,
2404        None => return ptr::null_mut(),
2405    };
2406    match crate::compression::gzip_writer(Path::new(filename)) {
2407        Ok(encoder) => into_rkr_writer(Box::new(encoder), None),
2408        Err(_) => ptr::null_mut(),
2409    }
2410}
2411
2412/// Creates a gzip-compressed frame writer with a custom floating-point
2413/// precision. The caller OWNS the returned pointer and MUST call
2414/// `free_rkr_writer`.
2415///
2416/// # Safety
2417/// filename_c must be valid. The caller takes ownership of the returned writer.
2418#[unsafe(no_mangle)]
2419pub unsafe extern "C" fn create_writer_gzip_with_precision_c(
2420    filename_c: *const c_char,
2421    precision: u8,
2422) -> *mut RKRConFrameWriter {
2423    let filename = match unsafe { cstr_path(filename_c) } {
2424        Some(s) => s,
2425        None => return ptr::null_mut(),
2426    };
2427    match crate::compression::gzip_writer(Path::new(filename)) {
2428        Ok(encoder) => into_rkr_writer(Box::new(encoder), Some(precision)),
2429        Err(_) => ptr::null_mut(),
2430    }
2431}
2432
2433/// Creates a new zstd-compressed frame writer for the specified file.
2434/// The caller OWNS the returned pointer and MUST call `free_rkr_writer`.
2435///
2436/// Only present when readcon-core is built with the `zstd` Cargo
2437/// feature; the C header guards the declaration with
2438/// `READCON_CORE_HAS_ZSTD`.
2439///
2440/// # Safety
2441/// filename_c must be valid. The caller takes ownership of the returned writer.
2442#[cfg(feature = "zstd")]
2443#[unsafe(no_mangle)]
2444pub unsafe extern "C" fn create_writer_zstd_c(filename_c: *const c_char) -> *mut RKRConFrameWriter {
2445    let filename = match unsafe { cstr_path(filename_c) } {
2446        Some(s) => s,
2447        None => return ptr::null_mut(),
2448    };
2449    match crate::compression::zstd_writer(Path::new(filename)) {
2450        Ok(encoder) => into_rkr_writer(Box::new(encoder), None),
2451        Err(_) => ptr::null_mut(),
2452    }
2453}
2454
2455/// Creates a zstd-compressed frame writer with a custom floating-point
2456/// precision. The caller OWNS the returned pointer and MUST call
2457/// `free_rkr_writer`.
2458///
2459/// Only present when readcon-core is built with the `zstd` Cargo
2460/// feature; the C header guards the declaration with
2461/// `READCON_CORE_HAS_ZSTD`.
2462///
2463/// # Safety
2464/// filename_c must be valid. The caller takes ownership of the returned writer.
2465#[cfg(feature = "zstd")]
2466#[unsafe(no_mangle)]
2467pub unsafe extern "C" fn create_writer_zstd_with_precision_c(
2468    filename_c: *const c_char,
2469    precision: u8,
2470) -> *mut RKRConFrameWriter {
2471    let filename = match unsafe { cstr_path(filename_c) } {
2472        Some(s) => s,
2473        None => return ptr::null_mut(),
2474    };
2475    match crate::compression::zstd_writer(Path::new(filename)) {
2476        Ok(encoder) => into_rkr_writer(Box::new(encoder), Some(precision)),
2477        Err(_) => ptr::null_mut(),
2478    }
2479}
2480
2481//=============================================================================
2482// Direct mmap-based Reader FFI
2483//=============================================================================
2484
2485/// Reads the first frame from a .con file.
2486/// Uses `read_to_string` for small files (< 64 KiB) and mmap for larger ones.
2487/// Stops after the first frame rather than parsing the entire file.
2488/// The caller OWNS the returned handle and MUST call `free_rkr_frame`.
2489/// Returns NULL on error.
2490///
2491/// # Safety
2492/// filename_c must be valid. The caller takes ownership of the returned frame.
2493#[unsafe(no_mangle)]
2494pub unsafe extern "C" fn rkr_read_first_frame(filename_c: *const c_char) -> *mut RKRConFrame {
2495    if filename_c.is_null() {
2496        return ptr::null_mut();
2497    }
2498    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
2499        Ok(s) => s,
2500        Err(_) => return ptr::null_mut(),
2501    };
2502    match iterators::read_first_frame(Path::new(filename)) {
2503        Ok(frame) => Box::into_raw(Box::new(frame)) as *mut RKRConFrame,
2504        Err(_) => ptr::null_mut(),
2505    }
2506}
2507
2508/// Reads all frames from a .con file using mmap.
2509/// Returns an array of frame handles and sets `num_frames` to the count.
2510/// The caller OWNS both the array and each frame handle.
2511/// Free frames with `free_rkr_frame` and the array with `free_rkr_frame_array`.
2512/// Returns NULL on error.
2513///
2514/// # Safety
2515/// filename_c and num_frames must be valid. The caller takes ownership of the returned handles and array.
2516#[unsafe(no_mangle)]
2517pub unsafe extern "C" fn rkr_read_all_frames(
2518    filename_c: *const c_char,
2519    num_frames: *mut usize,
2520) -> *mut *mut RKRConFrame {
2521    if filename_c.is_null() || num_frames.is_null() {
2522        return ptr::null_mut();
2523    }
2524    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
2525        Ok(s) => s,
2526        Err(_) => return ptr::null_mut(),
2527    };
2528    match iterators::read_all_frames(Path::new(filename)) {
2529        Ok(frames) => {
2530            let count = frames.len();
2531            // shrink_to_fit ensures len == capacity so the matching
2532            // free_rkr_frame_array can soundly call Vec::from_raw_parts
2533            // with len == cap.
2534            let mut handles: Vec<*mut RKRConFrame> = frames
2535                .into_iter()
2536                .map(|f| Box::into_raw(Box::new(f)) as *mut RKRConFrame)
2537                .collect();
2538            handles.shrink_to_fit();
2539            debug_assert_eq!(handles.len(), handles.capacity());
2540            let ptr = handles.as_mut_ptr();
2541            std::mem::forget(handles);
2542            unsafe { *num_frames = count };
2543            ptr
2544        }
2545        Err(_) => ptr::null_mut(),
2546    }
2547}
2548
2549/// Frees an array of frame handles returned by `rkr_read_all_frames`.
2550/// Each frame is freed individually, then the array itself.
2551///
2552/// # Safety
2553/// frames must be valid or null.
2554#[unsafe(no_mangle)]
2555pub unsafe extern "C" fn free_rkr_frame_array(frames: *mut *mut RKRConFrame, num_frames: usize) {
2556    if frames.is_null() {
2557        return;
2558    }
2559    unsafe {
2560        let handles = Vec::from_raw_parts(frames, num_frames, num_frames);
2561        for handle in handles {
2562            if !handle.is_null() {
2563                let _ = Box::from_raw(handle as *mut ConFrame);
2564            }
2565        }
2566    }
2567}
2568
2569//=============================================================================
2570// Chemfiles selection (always linked; real impl needs --features chemfiles)
2571//=============================================================================
2572
2573/// Opaque handle for a cached selection evaluation result.
2574pub struct RKRSelectionResult;
2575
2576/// Evaluate a chemfiles selection-language string on an `RKRConFrame`.
2577///
2578/// On success writes a heap-allocated result handle to `*out_result` (caller
2579/// frees with [`rkr_selection_result_free`]). Returns
2580/// `RKR_STATUS_SELECTION_ERROR` for invalid grammar, evaluation failure, or
2581/// when this build was compiled without the `chemfiles` feature.
2582///
2583/// # Safety
2584/// `frame_handle`, `selection`, and `out_result` must be non-null; `selection`
2585/// must point to a valid UTF-8 C string.
2586#[unsafe(no_mangle)]
2587pub unsafe extern "C" fn rkr_frame_select(
2588    frame_handle: *const RKRConFrame,
2589    selection: *const c_char,
2590    out_result: *mut *mut RKRSelectionResult,
2591) -> RKRStatus {
2592    if frame_handle.is_null() || selection.is_null() || out_result.is_null() {
2593        return RKRStatus::RKR_STATUS_NULL_POINTER;
2594    }
2595    let frame = unsafe { &*(frame_handle as *const ConFrame) };
2596    let sel_str = match unsafe { CStr::from_ptr(selection) }.to_str() {
2597        Ok(s) => s,
2598        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
2599    };
2600    match crate::chemfiles_selection::evaluate_selection_on_con_frame(sel_str, frame) {
2601        Ok(result) => {
2602            let boxed = Box::new(result);
2603            unsafe {
2604                *out_result = Box::into_raw(boxed) as *mut RKRSelectionResult;
2605            }
2606            RKRStatus::RKR_STATUS_SUCCESS
2607        }
2608        Err(_) => RKRStatus::RKR_STATUS_SELECTION_ERROR,
2609    }
2610}
2611
2612/// Number of matches in a selection result.
2613///
2614/// # Safety
2615/// `result_handle` must be a valid handle from [`rkr_frame_select`] or NULL.
2616#[unsafe(no_mangle)]
2617pub unsafe extern "C" fn rkr_selection_result_match_count(
2618    result_handle: *const RKRSelectionResult,
2619) -> u64 {
2620    if result_handle.is_null() {
2621        return 0;
2622    }
2623    let result =
2624        unsafe { &*(result_handle as *const crate::chemfiles_selection::SelectionResult) };
2625    result.matches.len() as u64
2626}
2627
2628/// Selection context size (1=atom, 2=pair, 3=angle, 4=dihedral).
2629///
2630/// # Safety
2631/// `result_handle` must be valid or NULL (returns 0).
2632#[unsafe(no_mangle)]
2633pub unsafe extern "C" fn rkr_selection_result_context_size(
2634    result_handle: *const RKRSelectionResult,
2635) -> u32 {
2636    if result_handle.is_null() {
2637        return 0;
2638    }
2639    let result =
2640        unsafe { &*(result_handle as *const crate::chemfiles_selection::SelectionResult) };
2641    result.context_size as u32
2642}
2643
2644/// Copy match `match_index` atom indices into `out_atoms` (up to 4 slots).
2645/// Writes actual arity to `*out_size` when non-null.
2646///
2647/// # Safety
2648/// Handles and `out_atoms` must be valid; `out_atoms` needs space for 4 `uint64_t`.
2649#[unsafe(no_mangle)]
2650pub unsafe extern "C" fn rkr_selection_result_match_at(
2651    result_handle: *const RKRSelectionResult,
2652    match_index: u64,
2653    out_atoms: *mut u64,
2654    out_size: *mut u32,
2655) -> RKRStatus {
2656    if result_handle.is_null() || out_atoms.is_null() {
2657        return RKRStatus::RKR_STATUS_NULL_POINTER;
2658    }
2659    let result =
2660        unsafe { &*(result_handle as *const crate::chemfiles_selection::SelectionResult) };
2661    let idx = match_index as usize;
2662    if idx >= result.matches.len() {
2663        return RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS;
2664    }
2665    let m = &result.matches[idx];
2666    unsafe {
2667        for i in 0..4 {
2668            *out_atoms.add(i) = if i < m.size {
2669                m.atoms[i] as u64
2670            } else {
2671                u64::MAX
2672            };
2673        }
2674        if !out_size.is_null() {
2675            *out_size = m.size as u32;
2676        }
2677    }
2678    RKRStatus::RKR_STATUS_SUCCESS
2679}
2680
2681/// Fill `out_indices` with primary atom indices for each match (length =
2682/// match count). Returns `RKR_STATUS_BUFFER_TOO_SMALL` if `capacity` is too small.
2683///
2684/// # Safety
2685/// `result_handle` and `out_indices` must be valid when capacity > 0.
2686#[unsafe(no_mangle)]
2687pub unsafe extern "C" fn rkr_selection_result_primary_indices(
2688    result_handle: *const RKRSelectionResult,
2689    out_indices: *mut u64,
2690    capacity: u64,
2691    out_written: *mut u64,
2692) -> RKRStatus {
2693    if result_handle.is_null() {
2694        return RKRStatus::RKR_STATUS_NULL_POINTER;
2695    }
2696    let result =
2697        unsafe { &*(result_handle as *const crate::chemfiles_selection::SelectionResult) };
2698    let n = result.matches.len() as u64;
2699    if !out_written.is_null() {
2700        unsafe {
2701            *out_written = n;
2702        }
2703    }
2704    if n == 0 {
2705        return RKRStatus::RKR_STATUS_SUCCESS;
2706    }
2707    if out_indices.is_null() {
2708        return RKRStatus::RKR_STATUS_NULL_POINTER;
2709    }
2710    if capacity < n {
2711        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
2712    }
2713    unsafe {
2714        for (i, m) in result.matches.iter().enumerate() {
2715            *out_indices.add(i) = m.atoms[0] as u64;
2716        }
2717    }
2718    RKRStatus::RKR_STATUS_SUCCESS
2719}
2720
2721/// Free a selection result from [`rkr_frame_select`]. Safe with NULL.
2722///
2723/// # Safety
2724/// `result_handle` must be from `rkr_frame_select` or NULL.
2725#[unsafe(no_mangle)]
2726pub unsafe extern "C" fn rkr_selection_result_free(result_handle: *mut RKRSelectionResult) {
2727    if result_handle.is_null() {
2728        return;
2729    }
2730    unsafe {
2731        drop(Box::from_raw(
2732            result_handle as *mut crate::chemfiles_selection::SelectionResult,
2733        ));
2734    }
2735}
2736
2737/// Returns 1 when this library build includes chemfiles selection support.
2738#[unsafe(no_mangle)]
2739pub extern "C" fn rkr_has_chemfiles_support() -> u8 {
2740    #[cfg(feature = "chemfiles")]
2741    {
2742        1
2743    }
2744    #[cfg(not(feature = "chemfiles"))]
2745    {
2746        0
2747    }
2748}
2749
2750#[cfg(test)]
2751mod tests {
2752    use super::*;
2753    use std::ffi::{CStr, CString};
2754
2755    fn test_frame_handle() -> *mut RKRConFrame {
2756        let mut builder = ConFrameBuilder::new([10.0, 10.0, 10.0], [90.0, 90.0, 90.0]);
2757        builder
2758            .prebox_header("Generated by test")
2759            .postbox_header(["0 0".to_string(), "0 0 0".to_string()]);
2760        builder.add_atom("Cu", 0.0, 0.0, 0.0, [false, false, false], 0, 63.546);
2761        Box::into_raw(Box::new(builder.build())) as *mut RKRConFrame
2762    }
2763
2764    #[test]
2765    fn header_line_rejects_null_buffer() {
2766        let frame = test_frame_handle();
2767        let status = unsafe { rkr_frame_get_header_line(frame, true, 0, std::ptr::null_mut(), 16) };
2768        unsafe { free_rkr_frame(frame) };
2769
2770        assert_eq!(status, RKRStatus::RKR_STATUS_NULL_POINTER);
2771    }
2772
2773    #[test]
2774    fn header_line_rejects_empty_buffer() {
2775        let frame = test_frame_handle();
2776        let mut buffer = [0 as c_char; 1];
2777        let status = unsafe { rkr_frame_get_header_line(frame, true, 0, buffer.as_mut_ptr(), 0) };
2778        unsafe { free_rkr_frame(frame) };
2779
2780        assert_eq!(status, RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL);
2781    }
2782
2783    #[test]
2784    fn header_line_truncates_and_terminates_buffer() {
2785        let frame = test_frame_handle();
2786        let mut buffer = [0 as c_char; 10];
2787        let status =
2788            unsafe { rkr_frame_get_header_line(frame, true, 0, buffer.as_mut_ptr(), buffer.len()) };
2789        unsafe { free_rkr_frame(frame) };
2790
2791        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
2792        let copied = unsafe { CStr::from_ptr(buffer.as_ptr()) };
2793        assert_eq!(copied.to_str().unwrap(), "Generated");
2794    }
2795
2796    fn test_builder_handle() -> *mut RKRConFrameBuilder {
2797        let cell = [10.0, 11.0, 12.0];
2798        let angles = [90.0, 91.0, 92.0];
2799        unsafe {
2800            rkr_frame_new(
2801                cell.as_ptr(),
2802                angles.as_ptr(),
2803                ptr::null(),
2804                ptr::null(),
2805                ptr::null(),
2806                ptr::null(),
2807            )
2808        }
2809    }
2810
2811    fn c_string(s: &str) -> CString {
2812        CString::new(s).unwrap()
2813    }
2814
2815    unsafe fn assert_single_atom(
2816        frame: *mut RKRConFrame,
2817        fixed: [bool; 3],
2818        velocity: Option<[f64; 3]>,
2819        forces: Option<[f64; 3]>,
2820    ) {
2821        let c_frame = unsafe { rkr_frame_to_c_frame(frame) };
2822        assert!(!c_frame.is_null());
2823        let c_frame_ref = unsafe { &*c_frame };
2824        assert_eq!(c_frame_ref.num_atoms, 1);
2825        assert_eq!(c_frame_ref.has_velocities, velocity.is_some());
2826        assert_eq!(c_frame_ref.has_forces, forces.is_some());
2827
2828        let atom = unsafe { &*c_frame_ref.atoms };
2829        assert_eq!(atom.fixed_x, fixed[0]);
2830        assert_eq!(atom.fixed_y, fixed[1]);
2831        assert_eq!(atom.fixed_z, fixed[2]);
2832        assert_eq!(atom.is_fixed, fixed.iter().any(|&value| value));
2833        assert_eq!(atom.has_velocity, velocity.is_some());
2834        assert_eq!(atom.has_forces, forces.is_some());
2835        if let Some([vx, vy, vz]) = velocity {
2836            assert_eq!([atom.vx, atom.vy, atom.vz], [vx, vy, vz]);
2837        }
2838        if let Some([fx, fy, fz]) = forces {
2839            assert_eq!([atom.fx, atom.fy, atom.fz], [fx, fy, fz]);
2840        }
2841
2842        unsafe { free_c_frame(c_frame) };
2843        unsafe { free_rkr_frame(frame) };
2844    }
2845
2846    #[test]
2847    fn builder_preserves_fixed_mask_for_atom_without_velocity_or_forces() {
2848        let builder = test_builder_handle();
2849        let symbol = c_string("Cu");
2850
2851        let status = unsafe {
2852            rkr_frame_add_atom_with_fixed_mask(
2853                builder,
2854                symbol.as_ptr(),
2855                1.0,
2856                2.0,
2857                3.0,
2858                true,
2859                false,
2860                true,
2861                7,
2862                63.546,
2863            )
2864        };
2865        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
2866
2867        let frame = unsafe { rkr_frame_builder_build(builder) };
2868        unsafe { assert_single_atom(frame, [true, false, true], None, None) };
2869    }
2870
2871    #[test]
2872    fn builder_preserves_fixed_mask_for_atom_with_velocity() {
2873        let builder = test_builder_handle();
2874        let symbol = c_string("H");
2875
2876        let status = unsafe {
2877            rkr_frame_add_atom_with_velocity_fixed_mask(
2878                builder,
2879                symbol.as_ptr(),
2880                1.0,
2881                2.0,
2882                3.0,
2883                false,
2884                true,
2885                false,
2886                9,
2887                1.008,
2888                0.1,
2889                0.2,
2890                0.3,
2891            )
2892        };
2893        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
2894
2895        let frame = unsafe { rkr_frame_builder_build(builder) };
2896        unsafe { assert_single_atom(frame, [false, true, false], Some([0.1, 0.2, 0.3]), None) };
2897    }
2898
2899    #[test]
2900    fn builder_preserves_fixed_mask_for_atom_with_forces() {
2901        let builder = test_builder_handle();
2902        let symbol = c_string("O");
2903
2904        let status = unsafe {
2905            rkr_frame_add_atom_with_forces_fixed_mask(
2906                builder,
2907                symbol.as_ptr(),
2908                1.0,
2909                2.0,
2910                3.0,
2911                true,
2912                true,
2913                false,
2914                11,
2915                15.999,
2916                -0.1,
2917                -0.2,
2918                -0.3,
2919            )
2920        };
2921        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
2922
2923        let frame = unsafe { rkr_frame_builder_build(builder) };
2924        unsafe { assert_single_atom(frame, [true, true, false], None, Some([-0.1, -0.2, -0.3])) };
2925    }
2926
2927    #[test]
2928    fn builder_preserves_fixed_mask_for_atom_with_velocity_and_forces() {
2929        let builder = test_builder_handle();
2930        let symbol = c_string("N");
2931
2932        let status = unsafe {
2933            rkr_frame_add_atom_with_velocity_and_forces_fixed_mask(
2934                builder,
2935                symbol.as_ptr(),
2936                1.0,
2937                2.0,
2938                3.0,
2939                false,
2940                true,
2941                true,
2942                13,
2943                14.007,
2944                0.4,
2945                0.5,
2946                0.6,
2947                -0.4,
2948                -0.5,
2949                -0.6,
2950            )
2951        };
2952        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
2953
2954        let frame = unsafe { rkr_frame_builder_build(builder) };
2955        unsafe {
2956            assert_single_atom(
2957                frame,
2958                [false, true, true],
2959                Some([0.4, 0.5, 0.6]),
2960                Some([-0.4, -0.5, -0.6]),
2961            )
2962        };
2963    }
2964
2965    #[test]
2966    fn builder_bool_fixed_functions_set_all_axes_together() {
2967        let builder = test_builder_handle();
2968        let cu = c_string("Cu");
2969        let h = c_string("H");
2970
2971        let atom_status =
2972            unsafe { rkr_frame_add_atom(builder, cu.as_ptr(), 1.0, 2.0, 3.0, true, 1, 63.546) };
2973        assert_eq!(atom_status, RKRStatus::RKR_STATUS_SUCCESS);
2974
2975        let velocity_status = unsafe {
2976            rkr_frame_add_atom_with_velocity(
2977                builder,
2978                h.as_ptr(),
2979                4.0,
2980                5.0,
2981                6.0,
2982                false,
2983                2,
2984                1.008,
2985                0.7,
2986                0.8,
2987                0.9,
2988            )
2989        };
2990        assert_eq!(velocity_status, RKRStatus::RKR_STATUS_SUCCESS);
2991
2992        let frame = unsafe { rkr_frame_builder_build(builder) };
2993        let c_frame = unsafe { rkr_frame_to_c_frame(frame) };
2994        assert!(!c_frame.is_null());
2995        let c_frame_ref = unsafe { &*c_frame };
2996        assert_eq!(c_frame_ref.num_atoms, 2);
2997
2998        let atoms = unsafe { std::slice::from_raw_parts(c_frame_ref.atoms, c_frame_ref.num_atoms) };
2999        assert_eq!(
3000            [atoms[0].fixed_x, atoms[0].fixed_y, atoms[0].fixed_z],
3001            [true, true, true]
3002        );
3003        assert_eq!(
3004            [atoms[1].fixed_x, atoms[1].fixed_y, atoms[1].fixed_z],
3005            [false, false, false]
3006        );
3007
3008        unsafe { free_c_frame(c_frame) };
3009        unsafe { free_rkr_frame(frame) };
3010    }
3011
3012    #[test]
3013    fn status_message_returns_static_strings_for_all_status_values() {
3014        let cases = [
3015            (RKRStatus::RKR_STATUS_SUCCESS, "success"),
3016            (RKRStatus::RKR_STATUS_NULL_POINTER, "null pointer"),
3017            (RKRStatus::RKR_STATUS_INVALID_UTF8, "invalid UTF-8"),
3018            (RKRStatus::RKR_STATUS_INVALID_JSON, "invalid JSON"),
3019            (RKRStatus::RKR_STATUS_IO_ERROR, "I/O error"),
3020            (
3021                RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS,
3022                "index out of bounds",
3023            ),
3024            (RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL, "buffer too small"),
3025            (RKRStatus::RKR_STATUS_INTERNAL_ERROR, "internal error"),
3026            (RKRStatus::RKR_STATUS_SECTION_ABSENT, "section absent"),
3027            (RKRStatus::RKR_STATUS_VALIDATION_ERROR, "validation error"),
3028            (RKRStatus::RKR_STATUS_SELECTION_ERROR, "selection error"),
3029        ];
3030
3031        for (status, expected) in cases {
3032            let message = unsafe { CStr::from_ptr(rkr_status_message(status)) };
3033            assert_eq!(message.to_str().unwrap(), expected);
3034        }
3035    }
3036
3037    // ----- DLPack FFI smoke tests ----------------------------------------------
3038
3039    #[test]
3040    fn ffi_positions_dlpack_round_trip() {
3041        let handle = test_builder_handle();
3042        let sym = c_string("Cu");
3043        unsafe {
3044            rkr_frame_add_atom_full(
3045                handle,
3046                sym.as_ptr(),
3047                1.0,
3048                2.0,
3049                3.0,
3050                false,
3051                false,
3052                false,
3053                7,
3054                63.5,
3055                ptr::null(),
3056                ptr::null(),
3057            )
3058        };
3059
3060        let mut t: *mut RKRDLManagedTensorVersioned = ptr::null_mut();
3061        let status = unsafe { rkr_frame_builder_positions_dlpack(handle, &mut t) };
3062        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
3063        assert!(!t.is_null());
3064
3065        // Inspect the DLPack tensor: shape (1, 3), dtype kDLFloat / 64, CPU.
3066        let dl = unsafe { &(*t).dl_tensor };
3067        assert_eq!(dl.ndim, 2);
3068        let shape = unsafe { std::slice::from_raw_parts(dl.shape, 2) };
3069        assert_eq!(shape, &[1, 3]);
3070        assert_eq!(dl.dtype.code, dlpk::sys::DLDataTypeCode::kDLFloat);
3071        assert_eq!(dl.dtype.bits, 64);
3072        assert_eq!(dl.dtype.lanes, 1);
3073        assert_eq!(dl.device, dlpk::sys::DLDevice::cpu());
3074        let data = unsafe { std::slice::from_raw_parts(dl.data as *const f64, 3) };
3075        assert_eq!(data, &[1.0, 2.0, 3.0]);
3076
3077        // Invoke the deleter the same way a C consumer would.
3078        let deleter = unsafe { (*t).deleter };
3079        if let Some(del) = deleter {
3080            unsafe { del(t) };
3081        }
3082
3083        unsafe { free_rkr_frame_builder(handle) };
3084    }
3085
3086    #[test]
3087    fn ffi_velocities_dlpack_section_absent() {
3088        let handle = test_builder_handle();
3089        let sym = c_string("Cu");
3090        unsafe {
3091            rkr_frame_add_atom_full(
3092                handle,
3093                sym.as_ptr(),
3094                0.0,
3095                0.0,
3096                0.0,
3097                false,
3098                false,
3099                false,
3100                0,
3101                63.5,
3102                ptr::null(),
3103                ptr::null(),
3104            )
3105        };
3106        let mut t: *mut RKRDLManagedTensorVersioned = ptr::null_mut();
3107        let status = unsafe { rkr_frame_builder_velocities_dlpack(handle, &mut t) };
3108        assert_eq!(status, RKRStatus::RKR_STATUS_SECTION_ABSENT);
3109        assert!(t.is_null());
3110        unsafe { free_rkr_frame_builder(handle) };
3111    }
3112
3113    #[test]
3114    fn ffi_dlpack_null_handle_rejects() {
3115        let mut t: *mut RKRDLManagedTensorVersioned = ptr::null_mut();
3116        let status = unsafe { rkr_frame_builder_positions_dlpack(ptr::null(), &mut t) };
3117        assert_eq!(status, RKRStatus::RKR_STATUS_NULL_POINTER);
3118        assert!(t.is_null());
3119    }
3120
3121    #[cfg(feature = "chemfiles")]
3122    #[test]
3123    fn rkr_frame_select_finds_oxygen() {
3124        use crate::types::ConFrameBuilder;
3125        let mut b = ConFrameBuilder::new([10.0; 3], [90.0; 3]);
3126        b.add_atom("O", 0.0, 0.0, 0.0, [false; 3], 0, 16.0);
3127        b.add_atom("H", 1.0, 0.0, 0.0, [false; 3], 1, 1.0);
3128        let frame = b.build();
3129        let frame_ptr = Box::into_raw(Box::new(frame)) as *mut RKRConFrame;
3130        let sel = CString::new("name O").unwrap();
3131        let mut out: *mut RKRSelectionResult = ptr::null_mut();
3132        let st = unsafe { rkr_frame_select(frame_ptr, sel.as_ptr(), &mut out) };
3133        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
3134        assert!(!out.is_null());
3135        let n = unsafe { rkr_selection_result_match_count(out) };
3136        assert_eq!(n, 1);
3137        let mut atoms = [u64::MAX; 4];
3138        let mut size = 0u32;
3139        let st2 = unsafe { rkr_selection_result_match_at(out, 0, atoms.as_mut_ptr(), &mut size) };
3140        assert_eq!(st2, RKRStatus::RKR_STATUS_SUCCESS);
3141        assert_eq!(size, 1);
3142        assert_eq!(atoms[0], 0);
3143        unsafe {
3144            rkr_selection_result_free(out);
3145            free_rkr_frame(frame_ptr);
3146        }
3147    }
3148
3149    /// C surface: chemfiles selection.cpp chain topology via `rkr_frame_select`.
3150    #[cfg(feature = "chemfiles")]
3151    #[test]
3152    fn rkr_frame_select_cpp_topology_bonds_angles_dihedrals() {
3153        use crate::types::{Bond, ConFrameBuilder};
3154        // H-O-O-H chain (chemfiles testing_frame topology), bonds in atom_data order.
3155        let mut b = ConFrameBuilder::new([10.0; 3], [90.0; 3]);
3156        b.add_atom("H", 0.0, 1.0, 2.0, [false; 3], 0, 1.0);
3157        b.add_atom("O", 1.0, 2.0, 3.0, [false; 3], 1, 16.0);
3158        b.add_atom("O", 2.0, 3.0, 4.0, [false; 3], 2, 16.0);
3159        b.add_atom("H", 3.0, 4.0, 5.0, [false; 3], 3, 1.0);
3160        let mut frame = b.build();
3161        let id_to = |id: u64| {
3162            frame
3163                .atom_data
3164                .iter()
3165                .position(|a| a.atom_id == id)
3166                .unwrap() as u32
3167        };
3168        frame.header.set_bonds(&[
3169            Bond::new(id_to(0), id_to(1)),
3170            Bond::new(id_to(1), id_to(2)),
3171            Bond::new(id_to(2), id_to(3)),
3172        ]);
3173        let frame_ptr = Box::into_raw(Box::new(frame)) as *mut RKRConFrame;
3174
3175        let run = |sel: &str| -> (u64, u32) {
3176            let csel = CString::new(sel).unwrap();
3177            let mut out: *mut RKRSelectionResult = ptr::null_mut();
3178            let st = unsafe { rkr_frame_select(frame_ptr, csel.as_ptr(), &mut out) };
3179            assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS, "select failed: {sel}");
3180            let n = unsafe { rkr_selection_result_match_count(out) };
3181            let ctx = unsafe { rkr_selection_result_context_size(out) };
3182            unsafe { rkr_selection_result_free(out) };
3183            (n, ctx)
3184        };
3185
3186        assert_eq!(run("bonds: all"), (3, 2));
3187        assert_eq!(run("angles: all"), (2, 3));
3188        assert_eq!(run("dihedrals: all"), (1, 4));
3189        assert_eq!(run("bonds: name(#1) O and type(#2) H").0, 2);
3190        assert_eq!(
3191            run("two: type(#1) H and name(#2) O and is_bonded(#1, #2)").0,
3192            run("bonds: type(#1) H and name(#2) O").0
3193        );
3194
3195        unsafe { free_rkr_frame(frame_ptr) };
3196    }
3197}