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, binding_rewrite_frame};
5use std::cell::RefCell;
6use std::ffi::{CStr, CString, c_char};
7use std::fs::File;
8use std::path::Path;
9use std::ptr;
10//=============================================================================
11// Version & Spec Constants (exported as #define by cbindgen)
12//=============================================================================
13/// Breaking-change major for the public C ABI.
14pub const RKR_ABI_VERSION_MAJOR: u32 = 1;
15/// Additive-change minor for the public C ABI.
16pub const RKR_ABI_VERSION_MINOR: u32 = 1;
17/// Layout revision for opaque handles and exported records.
18pub const RKR_ABI_LAYOUT_REVISION: u32 = 1;
19
20/// Returns the breaking-change major for the public C ABI.
21#[unsafe(no_mangle)]
22pub extern "C" fn rkr_abi_version_major() -> u32 {
23    RKR_ABI_VERSION_MAJOR
24}
25
26/// Returns the additive-change minor for the public C ABI.
27#[unsafe(no_mangle)]
28pub extern "C" fn rkr_abi_version_minor() -> u32 {
29    RKR_ABI_VERSION_MINOR
30}
31
32/// Returns the opaque-handle and exported-record layout revision.
33#[unsafe(no_mangle)]
34pub extern "C" fn rkr_abi_layout_revision() -> u32 {
35    RKR_ABI_LAYOUT_REVISION
36}
37
38/// Returns the stable human-readable ABI negotiation stamp.
39#[unsafe(no_mangle)]
40pub extern "C" fn rkr_abi_stamp() -> *const c_char {
41    const STAMP: &[u8] = b"readcon-core/abi-1.1/layout-1\0";
42    STAMP.as_ptr() as *const c_char
43}
44
45/// CON/convel format spec version. Use `#if RKR_CON_SPEC_VERSION >= 2` in C/C++
46/// to gate code that depends on atom_index semantics.
47///
48/// Tracks `crate::CON_SPEC_VERSION` (which the Rust API exposes as
49/// `CON_SPEC_VERSION`). Both macros are emitted into the C header for
50/// the convenience of either naming convention; they always carry the
51/// same value.
52pub const RKR_CON_SPEC_VERSION: u32 = 3;
53/// Returns the spec version at runtime (for dynamically linked consumers).
54#[unsafe(no_mangle)]
55pub extern "C" fn rkr_con_spec_version() -> u32 {
56    crate::CON_SPEC_VERSION
57}
58/// Returns a pointer to a static, null-terminated library version string.
59/// The returned pointer is valid for the lifetime of the process. Do NOT free it.
60#[unsafe(no_mangle)]
61pub extern "C" fn rkr_library_version() -> *const c_char {
62    // concat! produces a &'static str with a trailing NUL byte
63    const VERSION_NUL: &[u8] = concat!(env!("CARGO_PKG_VERSION"), "\0").as_bytes();
64    VERSION_NUL.as_ptr() as *const c_char
65}
66/// Returns the position of an atom inside the frame's `atom_data` array
67/// matching the given `atom_id`. Returns `UINT64_MAX` if no atom with
68/// that id exists or `frame_handle` is NULL.
69///
70/// O(N) per call. C/C++ consumers performing many lookups should cache
71/// a `std::unordered_map<uint64_t, size_t>` from a single sweep over
72/// the frame.
73///
74/// # Safety
75///
76/// `frame_handle` must point to a valid `RKRConFrame` allocation.
77#[unsafe(no_mangle)]
78pub unsafe extern "C" fn rkr_frame_atom_index_by_id(
79    frame_handle: *const RKRConFrame,
80    atom_id: u64,
81) -> u64 {
82    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
83        Some(f) => f,
84        None => return u64::MAX,
85    };
86    match frame.atom_index_by_id(atom_id) {
87        Some(idx) => idx as u64,
88        None => u64::MAX,
89    }
90}
91/// Returns the atomic number for a chemical symbol, or 0 if the symbol
92/// is unknown or `symbol` is NULL. Lookup covers H..U (Z = 1..=92) and
93/// is case-sensitive: "Fe" works, "fe" does not.
94///
95/// # Safety
96///
97/// `symbol` must be either NULL or a pointer to a NUL-terminated UTF-8
98/// C string valid for reads up to the terminating NUL byte.
99#[unsafe(no_mangle)]
100pub unsafe extern "C" fn rkr_symbol_to_z(symbol: *const c_char) -> u64 {
101    if symbol.is_null() {
102        return 0;
103    }
104    match unsafe { CStr::from_ptr(symbol) }.to_str() {
105        Ok(s) => symbol_to_atomic_number(s),
106        Err(_) => 0,
107    }
108}
109/// Returns a pointer to a static, NUL-terminated chemical symbol for an
110/// atomic number, or "X" for unknown values. Coverage is H..U
111/// (Z = 1..=92). The returned pointer is valid for the lifetime of the
112/// process; do NOT free it.
113#[unsafe(no_mangle)]
114pub extern "C" fn rkr_z_to_symbol(z: u64) -> *const c_char {
115    // The static &str returned by helpers::atomic_number_to_symbol is
116    // not NUL-terminated, so the FFI mirrors the table with literals
117    // that have a trailing NUL. Index 0 holds "X" for unknown Z; indices
118    // 1..=92 hold H..U in order.
119    macro_rules! cstrs {
120        ($($lit:literal),* $(,)?) => {
121            [$(concat!($lit, "\0").as_bytes()),*]
122        };
123    }
124    const TABLE: [&[u8]; 93] = cstrs![
125        "X", "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P",
126        "S", "Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn",
127        "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh",
128        "Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd",
129        "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re",
130        "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th",
131        "Pa", "U",
132    ];
133    let idx = if (1..=92).contains(&z) { z as usize } else { 0 };
134    TABLE[idx].as_ptr() as *const c_char
135}
136/// Returns the spec version stored in a parsed frame's header.
137/// Returns 0 on error (null handle).
138#[unsafe(no_mangle)]
139pub extern "C" fn rkr_frame_spec_version(frame_handle: *const RKRConFrame) -> u32 {
140    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
141        Some(f) => f.header.spec_version,
142        None => 0,
143    }
144}
145/// Returns the JSON metadata line from a parsed frame as a heap-allocated
146/// null-terminated C string. The caller MUST free with `rkr_free_string`.
147/// Returns NULL on error.
148///
149/// # Safety
150/// frame_handle must be valid. The caller takes ownership of the returned string.
151#[unsafe(no_mangle)]
152pub unsafe extern "C" fn rkr_frame_metadata_json(frame_handle: *const RKRConFrame) -> *mut c_char {
153    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
154        Some(f) => f,
155        None => return ptr::null_mut(),
156    };
157    let mut obj = serde_json::Map::new();
158    obj.insert(
159        meta::CON_SPEC_VERSION.into(),
160        serde_json::Value::from(frame.header.spec_version),
161    );
162    for (k, v) in &frame.header.metadata {
163        obj.insert(k.clone(), v.clone());
164    }
165    let json_str = serde_json::Value::Object(obj).to_string();
166    match CString::new(json_str) {
167        Ok(cs) => cs.into_raw(),
168        Err(_) => ptr::null_mut(),
169    }
170}
171/// Returns the per-frame energy from metadata, or NaN if absent.
172#[unsafe(no_mangle)]
173pub extern "C" fn rkr_frame_energy(frame_handle: *const RKRConFrame) -> f64 {
174    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
175        Some(f) => f.header.energy().unwrap_or(f64::NAN),
176        None => f64::NAN,
177    }
178}
179
180/// Campaign **finite** energy ([`crate::index_proj::finite_energy`]): NaN if missing or non-finite.
181/// Prefer this over [`rkr_frame_energy`] when mirroring `readcon-db` indexes.
182#[unsafe(no_mangle)]
183pub extern "C" fn rkr_frame_index_energy(frame_handle: *const RKRConFrame) -> f64 {
184    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
185        Some(f) => crate::index_proj::finite_energy(f).unwrap_or(f64::NAN),
186        None => f64::NAN,
187    }
188}
189
190/// Canonical multiset formula (`Cu:2|H:2`) for campaign `idx_formula`. Free with `rkr_free_string`.
191#[unsafe(no_mangle)]
192pub unsafe extern "C" fn rkr_frame_composition_formula(
193    frame_handle: *const RKRConFrame,
194) -> *mut c_char {
195    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
196        Some(f) => f,
197        None => return ptr::null_mut(),
198    };
199    let s = crate::index_proj::frame_composition_formula(frame);
200    match CString::new(s) {
201        Ok(cs) => cs.into_raw(),
202        Err(_) => ptr::null_mut(),
203    }
204}
205
206/// Total mass from type masses × counts; NaN if not all finite.
207#[unsafe(no_mangle)]
208pub extern "C" fn rkr_frame_total_mass(frame_handle: *const RKRConFrame) -> f64 {
209    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
210        Some(f) => crate::index_proj::frame_total_mass(f).unwrap_or(f64::NAN),
211        None => f64::NAN,
212    }
213}
214
215/// Cell volume (lattice det or triclinic); NaN if unavailable.
216#[unsafe(no_mangle)]
217pub extern "C" fn rkr_frame_cell_volume(frame_handle: *const RKRConFrame) -> f64 {
218    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
219        Some(f) => crate::index_proj::frame_cell_volume(f).unwrap_or(f64::NAN),
220        None => f64::NAN,
221    }
222}
223
224/// Max \(\|F_i\|\); NaN if no finite forces.
225#[unsafe(no_mangle)]
226pub extern "C" fn rkr_frame_fmax(frame_handle: *const RKRConFrame) -> f64 {
227    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
228        Some(f) => crate::index_proj::frame_fmax(f).unwrap_or(f64::NAN),
229        None => f64::NAN,
230    }
231}
232
233/// Sections presence bitmask: bit0 forces, bit1 velocities, bit2 energies (see `index_proj`).
234#[unsafe(no_mangle)]
235pub extern "C" fn rkr_frame_sections_mask(frame_handle: *const RKRConFrame) -> u8 {
236    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
237        Some(f) => crate::index_proj::sections_present_mask(f),
238        None => 0,
239    }
240}
241
242/// Atom count used for campaign `idx_natoms` (same as `atom_data.len()`).
243#[unsafe(no_mangle)]
244pub extern "C" fn rkr_frame_index_natoms(frame_handle: *const RKRConFrame) -> u32 {
245    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
246        Some(f) => f.atom_data.len() as u32,
247        None => 0,
248    }
249}
250
251/// Compact JSON of [`crate::index_proj::FrameIndexProjection`] (campaign screening fields).
252/// Free with `rkr_free_string`. NULL on error.
253#[unsafe(no_mangle)]
254pub unsafe extern "C" fn rkr_frame_index_projection_json(
255    frame_handle: *const RKRConFrame,
256) -> *mut c_char {
257    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
258        Some(f) => f,
259        None => return ptr::null_mut(),
260    };
261    let p = crate::index_proj::FrameIndexProjection::from_frame(frame);
262    let v = serde_json::json!({
263        "n_atoms": p.n_atoms,
264        "formula": p.formula,
265        "energy": p.energy,
266        "fmax": p.fmax,
267        "total_mass": p.total_mass,
268        "cell_volume": p.cell_volume,
269        "sections_mask": p.sections_mask,
270        "has_forces": p.has_forces,
271        "has_velocities": p.has_velocities,
272        "has_energy": p.has_energy,
273        "symbols": p.symbols,
274        "species_counts": p.species_counts.iter().map(|(s,c)| serde_json::json!([s, c])).collect::<Vec<_>>(),
275        "time": p.time,
276        "timestep": p.timestep,
277        "frame_index": p.frame_index,
278        "neb_bead": p.neb_bead,
279        "neb_band": p.neb_band,
280        "charge": p.charge,
281        "magmom": p.magmom,
282    });
283    match CString::new(v.to_string()) {
284        Ok(cs) => cs.into_raw(),
285        Err(_) => ptr::null_mut(),
286    }
287}
288/// Returns the potential type string from metadata as a heap-allocated
289/// null-terminated C string. The caller MUST free with `rkr_free_string`.
290/// Returns NULL if absent or on error.
291///
292/// # Safety
293/// frame_handle must be valid. The caller takes ownership of the returned string.
294#[unsafe(no_mangle)]
295pub unsafe extern "C" fn rkr_frame_potential_type(frame_handle: *const RKRConFrame) -> *mut c_char {
296    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
297        Some(f) => f,
298        None => return ptr::null_mut(),
299    };
300    match frame.header.potential_type() {
301        Some(pot_type) => match CString::new(pot_type) {
302            Ok(cs) => cs.into_raw(),
303            Err(_) => ptr::null_mut(),
304        },
305        None => ptr::null_mut(),
306    }
307}
308/// Returns the zero-based frame index from metadata, or UINT64_MAX if absent.
309#[unsafe(no_mangle)]
310pub extern "C" fn rkr_frame_frame_index(frame_handle: *const RKRConFrame) -> u64 {
311    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
312        Some(f) => f.header.frame_index().unwrap_or(u64::MAX),
313        None => u64::MAX,
314    }
315}
316/// Returns the simulation time from metadata, or NaN if absent.
317#[unsafe(no_mangle)]
318pub extern "C" fn rkr_frame_time(frame_handle: *const RKRConFrame) -> f64 {
319    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
320        Some(f) => f.header.time().unwrap_or(f64::NAN),
321        None => f64::NAN,
322    }
323}
324/// Returns the integration timestep from metadata, or NaN if absent.
325#[unsafe(no_mangle)]
326pub extern "C" fn rkr_frame_timestep(frame_handle: *const RKRConFrame) -> f64 {
327    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
328        Some(f) => f.header.timestep().unwrap_or(f64::NAN),
329        None => f64::NAN,
330    }
331}
332/// Returns the NEB bead index from metadata, or UINT64_MAX if absent.
333#[unsafe(no_mangle)]
334pub extern "C" fn rkr_frame_neb_bead(frame_handle: *const RKRConFrame) -> u64 {
335    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
336        Some(f) => f.header.neb_bead().unwrap_or(u64::MAX),
337        None => u64::MAX,
338    }
339}
340/// Returns the NEB band index from metadata, or UINT64_MAX if absent.
341#[unsafe(no_mangle)]
342pub extern "C" fn rkr_frame_neb_band(frame_handle: *const RKRConFrame) -> u64 {
343    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
344        Some(f) => f.header.neb_band().unwrap_or(u64::MAX),
345        None => u64::MAX,
346    }
347}
348//=============================================================================
349// C-Compatible Structs & Handles
350//=============================================================================
351/// Error codes for RKR functions.
352#[repr(C)]
353#[allow(non_camel_case_types)]
354#[derive(Debug, PartialEq, Eq)]
355pub enum RKRStatus {
356    /// Function completed successfully.
357    RKR_STATUS_SUCCESS = 0,
358    /// A null pointer was passed for a required argument.
359    RKR_STATUS_NULL_POINTER = -1,
360    /// An input string was not valid UTF-8.
361    RKR_STATUS_INVALID_UTF8 = -2,
362    /// JSON parsing or serialization failed.
363    RKR_STATUS_INVALID_JSON = -3,
364    /// File I/O error.
365    RKR_STATUS_IO_ERROR = -4,
366    /// Index out of bounds.
367    RKR_STATUS_INDEX_OUT_OF_BOUNDS = -5,
368    /// The destination buffer cannot hold a null-terminated string.
369    RKR_STATUS_BUFFER_TOO_SMALL = -6,
370    /// An internal logic error or unhandled state.
371    RKR_STATUS_INTERNAL_ERROR = -7,
372    /// An optional section (velocities, forces, atom_energies) was
373    /// requested but is not declared on the builder.
374    RKR_STATUS_SECTION_ABSENT = -8,
375    /// DLPack export or another validation step failed.
376    RKR_STATUS_VALIDATION_ERROR = -9,
377    /// Chemfiles selection parse/evaluate failed (requires chemfiles-enabled build).
378    RKR_STATUS_SELECTION_ERROR = -10,
379    /// Requested API is not in this build (Cargo feature off / symbols omitted).
380    /// Never use `-7` for this — that is [`RKR_STATUS_INTERNAL_ERROR`].
381    RKR_STATUS_FEATURE_DISABLED = -11,
382    /// Requested DLPack device does not match the array's residency.
383    RKR_STATUS_DEVICE_MISMATCH = -12,
384    /// Build cannot allocate on the requested non-CPU device (use caller-supplied buffers).
385    RKR_STATUS_DEVICE_ALLOC_UNSUPPORTED = -13,
386}
387/// Number of optional frame topology bonds (`metadata["bonds"]`), or 0 if absent.
388///
389/// # Safety
390/// `frame_handle` must be a valid handle or NULL.
391#[unsafe(no_mangle)]
392pub unsafe extern "C" fn rkr_frame_bond_count(frame_handle: *const RKRConFrame) -> u64 {
393    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
394        Some(f) => f.bonds().len() as u64,
395        None => 0,
396    }
397}
398/// Read one bond at `index` (0-based into the `bonds` metadata array).
399///
400/// Writes 0-based `atom_data` indices into `out_i` / `out_j`. When the bond
401/// has an explicit order, sets `out_has_order` to 1 and `out_order` to that
402/// integer; otherwise `out_has_order` is 0.
403///
404/// # Safety
405/// `frame_handle` must be valid. Output pointers must be non-null.
406#[unsafe(no_mangle)]
407pub unsafe extern "C" fn rkr_frame_bond_at(
408    frame_handle: *const RKRConFrame,
409    index: u64,
410    out_i: *mut u32,
411    out_j: *mut u32,
412    out_has_order: *mut u8,
413    out_order: *mut i32,
414) -> RKRStatus {
415    if frame_handle.is_null()
416        || out_i.is_null()
417        || out_j.is_null()
418        || out_has_order.is_null()
419        || out_order.is_null()
420    {
421        return RKRStatus::RKR_STATUS_NULL_POINTER;
422    }
423    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
424        return RKRStatus::RKR_STATUS_NULL_POINTER;
425    };
426    let bonds = frame.bonds();
427    let Some(bond) = bonds.get(index as usize) else {
428        return RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS;
429    };
430    unsafe {
431        *out_i = bond.i;
432        *out_j = bond.j;
433        if let Some(order) = bond.order {
434            *out_has_order = 1;
435            *out_order = order;
436        } else {
437            *out_has_order = 0;
438            *out_order = 0;
439        }
440    }
441    RKRStatus::RKR_STATUS_SUCCESS
442}
443thread_local! {
444    static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
445}
446
447fn set_last_error(message: &str) {
448    let stored = CString::new(message.replace('\0', ""))
449        .unwrap_or_else(|_| c"unrepresentable error".to_owned());
450    LAST_ERROR.with(|slot| *slot.borrow_mut() = Some(stored));
451}
452
453fn clear_last_error() {
454    LAST_ERROR.with(|slot| *slot.borrow_mut() = None);
455}
456
457/// Returns a stable, static message for a status code.
458/// The returned pointer is valid for the lifetime of the process. Do NOT free it.
459#[unsafe(no_mangle)]
460pub extern "C" fn rkr_status_message(status: RKRStatus) -> *const c_char {
461    match status {
462        RKRStatus::RKR_STATUS_SUCCESS => c"success".as_ptr(),
463        RKRStatus::RKR_STATUS_NULL_POINTER => c"null pointer".as_ptr(),
464        RKRStatus::RKR_STATUS_INVALID_UTF8 => c"invalid UTF-8".as_ptr(),
465        RKRStatus::RKR_STATUS_INVALID_JSON => c"invalid JSON".as_ptr(),
466        RKRStatus::RKR_STATUS_IO_ERROR => c"I/O error".as_ptr(),
467        RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS => c"index out of bounds".as_ptr(),
468        RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL => c"buffer too small".as_ptr(),
469        RKRStatus::RKR_STATUS_INTERNAL_ERROR => c"internal error".as_ptr(),
470        RKRStatus::RKR_STATUS_SECTION_ABSENT => c"section absent".as_ptr(),
471        RKRStatus::RKR_STATUS_VALIDATION_ERROR => c"validation error".as_ptr(),
472        RKRStatus::RKR_STATUS_SELECTION_ERROR => c"selection error".as_ptr(),
473        RKRStatus::RKR_STATUS_FEATURE_DISABLED => c"feature disabled in this build".as_ptr(),
474        RKRStatus::RKR_STATUS_DEVICE_MISMATCH => c"DLPack device mismatch".as_ptr(),
475        RKRStatus::RKR_STATUS_DEVICE_ALLOC_UNSUPPORTED => {
476            c"device allocation unsupported in this build".as_ptr()
477        }
478    }
479}
480
481/// Detailed message from the most recent failing `rkr_writer_extend`,
482/// `rkr_writer_flush`, `rkr_trajectory_open` or `rkr_trajectory_read_frame`
483/// on this thread, or from `rkr_trajectory_len` on a NULL handle.
484///
485/// `rkr_status_message` stays a fixed string per status code. This pointer
486/// carries the underlying I/O or format text, for example
487/// `CON validation failed: atom id 1 is repeated in the frame`.
488/// Returns NULL when that call succeeded or no such failure has been
489/// recorded. The pointer is valid until the next call on this thread that
490/// records or clears the message. Do NOT free it.
491#[unsafe(no_mangle)]
492pub extern "C" fn rkr_last_error_message() -> *const c_char {
493    LAST_ERROR.with(|slot| match slot.borrow().as_ref() {
494        Some(message) => message.as_ptr(),
495        None => ptr::null(),
496    })
497}
498/// An opaque handle to a full, lossless Rust `ConFrame` object.
499/// The C/C++ side needs to treat this as a void pointer
500// No #[repr(C)]: cbindgen then emits an incomplete struct, which C and C++
501// accept under -pedantic. The zero-length field keeps it unconstructible.
502pub struct RKRConFrame {
503    _private: [u8; 0],
504}
505/// An opaque handle to a Rust `ConFrameWriter` object.
506/// The C/C++ side needs to treat this as a void pointer
507// No #[repr(C)]: cbindgen then emits an incomplete struct, which C and C++
508// accept under -pedantic. The zero-length field keeps it unconstructible.
509pub struct RKRConFrameWriter {
510    _private: [u8; 0],
511}
512/// A transparent, "lossy" C-struct containing only the core atomic data.
513/// This can be extracted from an `RKRConFrame` handle for direct data access.
514/// The caller is responsible for freeing the `atoms` array using `free_c_frame`.
515/// Borrowed SoA column. `data` is valid until the parent frame is freed
516/// or a mutating call reallocates storage. No copy.
517#[repr(C)]
518pub struct RKRArrayView {
519    pub data: *const std::ffi::c_void,
520    pub n: usize,
521    pub cols: u32,
522    pub dtype_code: u8,
523    pub dtype_bits: u8,
524}
525
526impl RKRArrayView {
527    fn empty() -> Self {
528        Self {
529            data: std::ptr::null(),
530            n: 0,
531            cols: 0,
532            dtype_code: 0,
533            dtype_bits: 0,
534        }
535    }
536
537    fn from_array2(arr: &crate::storage_dtype::FloatArray2) -> Self {
538        let kind = arr.kind();
539        Self {
540            data: arr.data_ptr() as *const std::ffi::c_void,
541            n: arr.nrows(),
542            cols: arr.ncols() as u32,
543            dtype_code: kind.dlpack_code(),
544            dtype_bits: kind.dlpack_bits(),
545        }
546    }
547
548    fn from_array1(arr: &crate::storage_dtype::FloatArray1) -> Self {
549        let kind = arr.kind();
550        Self {
551            data: arr.data_ptr() as *const std::ffi::c_void,
552            n: arr.len(),
553            cols: 1,
554            dtype_code: kind.dlpack_code(),
555            dtype_bits: kind.dlpack_bits(),
556        }
557    }
558}
559
560#[repr(C)]
561pub struct CFrame {
562    pub atoms: *mut CAtom,
563    pub num_atoms: usize,
564    pub cell: [f64; 3],
565    pub angles: [f64; 3],
566    pub has_velocities: bool,
567    pub has_forces: bool,
568    pub has_energies: bool,
569}
570/// Transparent atom record extracted via [`rkr_frame_to_c_frame`].
571///
572/// `is_fixed` is the OR of `fixed_x`, `fixed_y`, `fixed_z`; it is kept
573/// for source compatibility with pre-spec-v2 callers that did not have
574/// per-axis flags. New code should use the per-axis fields.
575///
576/// `vx`/`vy`/`vz`, `fx`/`fy`/`fz`, and `energy` carry meaningful values
577/// only when `has_velocity`, `has_forces`, or `has_energy` is true
578/// respectively; the values are zeroed otherwise.
579#[repr(C)]
580pub struct CAtom {
581    pub atomic_number: u64,
582    pub x: f64,
583    pub y: f64,
584    pub z: f64,
585    pub atom_id: u64,
586    pub mass: f64,
587    /// True when any of `fixed_x`, `fixed_y`, `fixed_z` is true.
588    /// Kept for source compatibility; prefer the per-axis fields.
589    pub is_fixed: bool,
590    pub fixed_x: bool,
591    pub fixed_y: bool,
592    pub fixed_z: bool,
593    pub vx: f64,
594    pub vy: f64,
595    pub vz: f64,
596    pub has_velocity: bool,
597    pub fx: f64,
598    pub fy: f64,
599    pub fz: f64,
600    pub has_forces: bool,
601    /// Per-atom energy contribution; meaningful only when
602    /// `has_energy` is true. See [`crate::types::SECTION_ENERGIES`].
603    pub energy: f64,
604    pub has_energy: bool,
605}
606#[repr(C)]
607pub struct CConFrameIterator {
608    iterator: *mut ConFrameIterator<'static>,
609    file_contents: *mut String,
610}
611
612/// Build a path/buffer-backed C iterator from an owned CON text buffer.
613fn c_iterator_from_owned_string(contents: String) -> *mut CConFrameIterator {
614    let file_contents_box = Box::new(contents);
615    let file_contents_ptr = Box::into_raw(file_contents_box);
616    let static_file_contents: &'static str = unsafe { &*file_contents_ptr };
617    let iterator = Box::new(ConFrameIterator::new(static_file_contents));
618    let c_iterator = Box::new(CConFrameIterator {
619        iterator: Box::into_raw(iterator),
620        file_contents: file_contents_ptr,
621    });
622    Box::into_raw(c_iterator)
623}
624
625//=============================================================================
626// Iterator and Memory Management
627//=============================================================================
628/// Creates a new iterator for a .con / .convel path, including transparent
629/// gzip (`.con.gz`) and zstd (`.con.zst`, requires `zstd` feature) inputs via
630/// [`crate::compression::read_file_contents`].
631///
632/// Returns NULL if the file cannot be read, decompressed, or is not valid
633/// UTF-8. A successfully-opened file with zero frames returns a non-NULL
634/// iterator that yields NULL on the first call to [`con_frame_iterator_next`].
635/// The caller OWNS the returned pointer and MUST call [`free_con_frame_iterator`].
636///
637/// # Safety
638/// filename_c must be a valid null-terminated string. The caller takes
639/// ownership of the returned iterator.
640#[unsafe(no_mangle)]
641pub unsafe extern "C" fn read_con_file_iterator(
642    filename_c: *const c_char,
643) -> *mut CConFrameIterator {
644    if filename_c.is_null() {
645        return ptr::null_mut();
646    }
647    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
648        Ok(s) => s,
649        Err(_) => return ptr::null_mut(),
650    };
651    let owned = match crate::compression::read_file_contents(Path::new(filename)) {
652        Ok(fc) => match fc.as_str() {
653            Ok(s) => s.to_owned(),
654            Err(_) => return ptr::null_mut(),
655        },
656        Err(_) => return ptr::null_mut(),
657    };
658    c_iterator_from_owned_string(owned)
659}
660
661/// Iterate frames from an in-memory CON text buffer (null-terminated C string).
662///
663/// Use when the caller already decompressed (chemfiles, custom I/O) and wants
664/// to avoid a temp file. Same ownership rules as [`read_con_file_iterator`].
665///
666/// # Safety
667/// `contents_c` must be a valid null-terminated UTF-8 string, or NULL (returns NULL).
668#[unsafe(no_mangle)]
669pub unsafe extern "C" fn read_con_string_iterator(
670    contents_c: *const c_char,
671) -> *mut CConFrameIterator {
672    if contents_c.is_null() {
673        return ptr::null_mut();
674    }
675    let contents = match unsafe { CStr::from_ptr(contents_c).to_str() } {
676        Ok(s) => s.to_owned(),
677        Err(_) => return ptr::null_mut(),
678    };
679    c_iterator_from_owned_string(contents)
680}
681
682/// Iterate frames from a byte buffer (not necessarily null-terminated).
683///
684/// `len` is the number of bytes at `data`. Bytes must be valid UTF-8 CON text.
685///
686/// # Safety
687/// `data` must be valid for `len` bytes if non-null and `len > 0`.
688#[unsafe(no_mangle)]
689pub unsafe extern "C" fn read_con_buffer_iterator(
690    data: *const u8,
691    len: usize,
692) -> *mut CConFrameIterator {
693    if data.is_null() && len > 0 {
694        return ptr::null_mut();
695    }
696    if len == 0 {
697        return c_iterator_from_owned_string(String::new());
698    }
699    let slice = unsafe { std::slice::from_raw_parts(data, len) };
700    let contents = match std::str::from_utf8(slice) {
701        Ok(s) => s.to_owned(),
702        Err(_) => return ptr::null_mut(),
703    };
704    c_iterator_from_owned_string(contents)
705}
706/// Reads the next frame from the iterator, returning an opaque handle.
707/// The caller OWNS the returned handle and must free it with `free_rkr_frame`.
708///
709/// # Safety
710/// iterator must be valid. The caller takes ownership of the returned frame.
711#[unsafe(no_mangle)]
712pub unsafe extern "C" fn con_frame_iterator_next(
713    iterator: *mut CConFrameIterator,
714) -> *mut RKRConFrame {
715    if iterator.is_null() {
716        return ptr::null_mut();
717    }
718    let iter = unsafe { &mut *(*iterator).iterator };
719    match iter.next() {
720        Some(Ok(frame)) => Box::into_raw(Box::new(frame)) as *mut RKRConFrame,
721        _ => ptr::null_mut(),
722    }
723}
724
725/// Skip one frame without parsing atoms. `RKR_STATUS_SUCCESS` on skip,
726/// `RKR_STATUS_INDEX_OUT_OF_BOUNDS` at EOF, other codes on parse error.
727#[unsafe(no_mangle)]
728pub unsafe extern "C" fn con_frame_iterator_forward(iterator: *mut CConFrameIterator) -> RKRStatus {
729    if iterator.is_null() {
730        return RKRStatus::RKR_STATUS_NULL_POINTER;
731    }
732    let iter = unsafe { &mut *(*iterator).iterator };
733    match iter.forward() {
734        Some(Ok(())) => RKRStatus::RKR_STATUS_SUCCESS,
735        Some(Err(_)) => RKRStatus::RKR_STATUS_IO_ERROR,
736        None => RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS,
737    }
738}
739
740/// Skip `n` frames without parsing atoms. Returns the number skipped, or
741/// `usize::MAX` on a null iterator.
742#[unsafe(no_mangle)]
743pub unsafe extern "C" fn con_frame_iterator_skip(
744    iterator: *mut CConFrameIterator,
745    n: usize,
746) -> usize {
747    if iterator.is_null() {
748        return usize::MAX;
749    }
750    let iter = unsafe { &mut *(*iterator).iterator };
751    iter.skip_frames(n).unwrap_or(usize::MAX)
752}
753
754/// Skip `index` frames, then parse the next. NULL at EOF or on error.
755#[unsafe(no_mangle)]
756pub unsafe extern "C" fn con_frame_iterator_nth(
757    iterator: *mut CConFrameIterator,
758    index: usize,
759) -> *mut RKRConFrame {
760    if iterator.is_null() {
761        return ptr::null_mut();
762    }
763    let iter = unsafe { &mut *(*iterator).iterator };
764    match iter.skip_frames(index) {
765        Ok(skipped) if skipped == index => {}
766        _ => return ptr::null_mut(),
767    }
768    match iter.next() {
769        Some(Ok(frame)) => Box::into_raw(Box::new(frame)) as *mut RKRConFrame,
770        _ => ptr::null_mut(),
771    }
772}
773
774/// Count frames on a path (skip walk, no atom parse). `usize::MAX` on error.
775#[unsafe(no_mangle)]
776pub unsafe extern "C" fn rkr_count_frames(filename_c: *const c_char) -> usize {
777    if filename_c.is_null() {
778        return usize::MAX;
779    }
780    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
781        Ok(s) => s,
782        Err(_) => return usize::MAX,
783    };
784    crate::iterators::count_frames(Path::new(filename)).unwrap_or(usize::MAX)
785}
786
787/// Skip `index` frames on a path, then parse one. Caller frees with `free_rkr_frame`.
788#[unsafe(no_mangle)]
789pub unsafe extern "C" fn rkr_read_nth_frame(
790    filename_c: *const c_char,
791    index: usize,
792) -> *mut RKRConFrame {
793    if filename_c.is_null() {
794        return ptr::null_mut();
795    }
796    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
797        Ok(s) => s,
798        Err(_) => return ptr::null_mut(),
799    };
800    match crate::iterators::read_nth_frame(Path::new(filename), index) {
801        Ok(frame) => Box::into_raw(Box::new(frame)) as *mut RKRConFrame,
802        Err(_) => ptr::null_mut(),
803    }
804}
805
806/// An opaque random-access handle over one file. Free with `rkr_trajectory_free`.
807// No #[repr(C)]: cbindgen then emits an incomplete struct, which C and C++
808// accept under -pedantic. The zero-length field keeps it unconstructible.
809pub struct RKRTrajectory {
810    _private: [u8; 0],
811}
812
813/// Open a file for reads by frame number.
814///
815/// The file is read once and its frame index is kept. An uncompressed file
816/// reuses a valid `<path>.rcidx`; a nonzero `write_index` writes a fresh scan
817/// there. gzip and zstd input is indexed in memory only. Returns NULL on
818/// error, and `rkr_last_error_message` gives the reason.
819///
820/// # Safety
821/// `filename_c` must be NULL or a valid null-terminated string.
822#[unsafe(no_mangle)]
823pub unsafe extern "C" fn rkr_trajectory_open(
824    filename_c: *const c_char,
825    write_index: i32,
826) -> *mut RKRTrajectory {
827    if filename_c.is_null() {
828        set_last_error("filename is NULL");
829        return ptr::null_mut();
830    }
831    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
832        Ok(s) => s,
833        Err(_) => {
834            set_last_error("filename is not valid UTF-8");
835            return ptr::null_mut();
836        }
837    };
838    let opened = std::panic::catch_unwind(|| {
839        crate::trajectory::ConTrajectory::open(Path::new(filename), write_index != 0)
840            .map_err(|e| e.to_string())
841    })
842    .unwrap_or_else(|_| Err("internal error while opening the file".to_owned()));
843    match opened {
844        Ok(traj) => {
845            clear_last_error();
846            Box::into_raw(Box::new(traj)) as *mut RKRTrajectory
847        }
848        Err(msg) => {
849            set_last_error(&msg);
850            ptr::null_mut()
851        }
852    }
853}
854
855/// Number of frames in the handle.
856///
857/// A NULL handle returns `usize::MAX` and records `null argument`. A live
858/// handle leaves `rkr_last_error_message` as it was.
859///
860/// # Safety
861/// `handle` must be NULL or a live handle from `rkr_trajectory_open`.
862#[unsafe(no_mangle)]
863pub unsafe extern "C" fn rkr_trajectory_len(handle: *const RKRTrajectory) -> usize {
864    match unsafe { (handle as *const crate::trajectory::ConTrajectory).as_ref() } {
865        Some(traj) => traj.len(),
866        None => {
867            set_last_error("null argument");
868            usize::MAX
869        }
870    }
871}
872
873/// Parse frame `index` into `*out_frame`, which the caller frees with
874/// `free_rkr_frame`.
875///
876/// Returns `RKR_STATUS_INDEX_OUT_OF_BOUNDS` when `index` is not below
877/// `rkr_trajectory_len`, and `RKR_STATUS_IO_ERROR` when the frame does not
878/// parse. A NULL argument returns `RKR_STATUS_NULL_POINTER` and records
879/// `null argument`. `*out_frame` is NULL unless the status is success, and
880/// on failure `rkr_last_error_message` gives the reason.
881///
882/// # Safety
883/// `handle` must be NULL or a live handle; `out_frame` must be NULL or
884/// valid for one pointer write.
885#[unsafe(no_mangle)]
886pub unsafe extern "C" fn rkr_trajectory_read_frame(
887    handle: *const RKRTrajectory,
888    index: usize,
889    out_frame: *mut *mut RKRConFrame,
890) -> RKRStatus {
891    if !out_frame.is_null() {
892        unsafe { *out_frame = ptr::null_mut() };
893    }
894    if handle.is_null() || out_frame.is_null() {
895        set_last_error("null argument");
896        return RKRStatus::RKR_STATUS_NULL_POINTER;
897    }
898    let traj = unsafe { &*(handle as *const crate::trajectory::ConTrajectory) };
899    let parsed = std::panic::catch_unwind(|| traj.frame(index));
900    let (status, msg) = match parsed {
901        Ok(Ok(frame)) => {
902            clear_last_error();
903            unsafe { *out_frame = Box::into_raw(Box::new(frame)) as *mut RKRConFrame };
904            return RKRStatus::RKR_STATUS_SUCCESS;
905        }
906        Ok(Err(crate::error::ParseError::IndexOutOfBounds { index, len })) => (
907            RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS,
908            format!("frame index {index} is out of range for a trajectory of {len} frames"),
909        ),
910        Ok(Err(e)) => (
911            RKRStatus::RKR_STATUS_IO_ERROR,
912            format!("frame {index}: {e}"),
913        ),
914        Err(_) => (
915            RKRStatus::RKR_STATUS_INTERNAL_ERROR,
916            format!("internal error while reading frame {index}"),
917        ),
918    };
919    set_last_error(&msg);
920    status
921}
922
923/// Free a handle from `rkr_trajectory_open`. NULL is a no-op.
924///
925/// # Safety
926/// `handle` must be NULL or a live handle that is not used afterwards.
927#[unsafe(no_mangle)]
928pub unsafe extern "C" fn rkr_trajectory_free(handle: *mut RKRTrajectory) {
929    if !handle.is_null() {
930        let _ = unsafe { Box::from_raw(handle as *mut crate::trajectory::ConTrajectory) };
931    }
932}
933/// Frees the memory for an opaque `RKRConFrame` handle.
934///
935/// # Safety
936/// frame_handle must be valid or null.
937#[unsafe(no_mangle)]
938pub unsafe extern "C" fn free_rkr_frame(frame_handle: *mut RKRConFrame) {
939    if !frame_handle.is_null() {
940        let _ = unsafe { Box::from_raw(frame_handle as *mut ConFrame) };
941    }
942}
943/// Frees the memory for a `CConFrameIterator`.
944///
945/// # Safety
946/// iterator must be valid or null.
947#[unsafe(no_mangle)]
948pub unsafe extern "C" fn free_con_frame_iterator(iterator: *mut CConFrameIterator) {
949    if iterator.is_null() {
950        return;
951    }
952    unsafe {
953        let c_iterator_box = Box::from_raw(iterator);
954        let _ = Box::from_raw(c_iterator_box.iterator);
955        let _ = Box::from_raw(c_iterator_box.file_contents);
956    }
957}
958//=============================================================================
959// Data Accessors (The "Getter" API)
960//=============================================================================
961/// Extracts the core atomic data into a transparent `CFrame` struct.
962/// The caller OWNS the returned pointer and MUST call `free_c_frame` on it.
963///
964/// # Safety
965/// frame_handle must be valid. The caller takes ownership of the returned CFrame.
966#[unsafe(no_mangle)]
967pub unsafe extern "C" fn rkr_frame_to_c_frame(frame_handle: *const RKRConFrame) -> *mut CFrame {
968    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
969        Some(f) => f,
970        None => return ptr::null_mut(),
971    };
972    let masses_iter = frame
973        .header
974        .natms_per_type
975        .iter()
976        .zip(frame.header.masses_per_type.iter())
977        .flat_map(|(num_atoms, mass)| std::iter::repeat_n(*mass, *num_atoms));
978    let has_velocities = frame.has_velocities();
979    let mut c_atoms: Vec<CAtom> = frame
980        .atom_data
981        .iter()
982        .zip(masses_iter)
983        .map(|(atom_datum, mass)| {
984            let [vx, vy, vz] = atom_datum.velocity().unwrap_or([0.0; 3]);
985            let [fx, fy, fz] = atom_datum.force().unwrap_or([0.0; 3]);
986            CAtom {
987                atomic_number: symbol_to_atomic_number(&atom_datum.symbol),
988                x: atom_datum.x,
989                y: atom_datum.y,
990                z: atom_datum.z,
991                is_fixed: atom_datum.is_fixed(),
992                fixed_x: atom_datum.fixed[0],
993                fixed_y: atom_datum.fixed[1],
994                fixed_z: atom_datum.fixed[2],
995                atom_id: atom_datum.atom_id,
996                mass,
997                vx,
998                vy,
999                vz,
1000                has_velocity: atom_datum.has_velocity(),
1001                fx,
1002                fy,
1003                fz,
1004                has_forces: atom_datum.has_forces(),
1005                energy: atom_datum.energy().unwrap_or(0.0),
1006                has_energy: atom_datum.has_energy(),
1007            }
1008        })
1009        .collect();
1010    let atoms_ptr = c_atoms.as_mut_ptr();
1011    let num_atoms = c_atoms.len();
1012    std::mem::forget(c_atoms);
1013    let has_forces = frame.has_forces();
1014    let has_energies = frame.has_energies();
1015    let c_frame = Box::new(CFrame {
1016        atoms: atoms_ptr,
1017        num_atoms,
1018        cell: frame.header.boxl,
1019        angles: frame.header.angles,
1020        has_velocities,
1021        has_forces,
1022        has_energies,
1023    });
1024    Box::into_raw(c_frame)
1025}
1026/// Frees the memory of a `CFrame` struct, including its internal atoms array.
1027///
1028/// # Safety
1029/// frame must be valid or null.
1030#[unsafe(no_mangle)]
1031pub unsafe extern "C" fn free_c_frame(frame: *mut CFrame) {
1032    if frame.is_null() {
1033        return;
1034    }
1035    unsafe {
1036        let frame_box = Box::from_raw(frame);
1037        let _ = Vec::from_raw_parts(frame_box.atoms, frame_box.num_atoms, frame_box.num_atoms);
1038    }
1039}
1040/// Copies a header string line into a caller-provided buffer.
1041///
1042/// `is_prebox=true` selects from the two prebox lines (line 0 = user
1043/// text, line 1 = JSON metadata); `false` selects from the two postbox
1044/// lines. Strings longer than `buffer_len - 1` bytes are truncated; the
1045/// final byte is always set to NUL.
1046///
1047/// Returns `RKR_STATUS_SUCCESS` on success,
1048/// `RKR_STATUS_INDEX_OUT_OF_BOUNDS` if `line_index >= 2`,
1049/// `RKR_STATUS_NULL_POINTER` if `frame_handle` or `buffer` is NULL,
1050/// `RKR_STATUS_BUFFER_TOO_SMALL` if `buffer_len == 0`.
1051///
1052/// Pair with [`rkr_frame_get_header_line_cpp`] when the caller prefers
1053/// an allocated string with no fixed length cap; that variant returns
1054/// NULL for the same out-of-bounds condition.
1055///
1056/// # Safety
1057/// frame_handle must be valid. buffer must be at least buffer_len bytes.
1058#[unsafe(no_mangle)]
1059pub unsafe extern "C" fn rkr_frame_get_header_line(
1060    frame_handle: *const RKRConFrame,
1061    is_prebox: bool,
1062    line_index: usize,
1063    buffer: *mut c_char,
1064    buffer_len: usize,
1065) -> RKRStatus {
1066    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
1067        Some(f) => f,
1068        None => return RKRStatus::RKR_STATUS_NULL_POINTER,
1069    };
1070    if buffer.is_null() {
1071        return RKRStatus::RKR_STATUS_NULL_POINTER;
1072    }
1073    if buffer_len == 0 {
1074        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
1075    }
1076    let line_to_copy: Option<&str> = if is_prebox {
1077        match line_index {
1078            0 => Some(frame.header.prebox_header.user.as_str()),
1079            1 => Some(frame.header.prebox_header.metadata_line()),
1080            _ => None,
1081        }
1082    } else {
1083        frame
1084            .header
1085            .postbox_header
1086            .get(line_index)
1087            .map(String::as_str)
1088    };
1089    if let Some(line) = line_to_copy {
1090        let bytes = line.as_bytes();
1091        let len_to_copy = std::cmp::min(bytes.len(), buffer_len - 1);
1092        unsafe {
1093            ptr::copy_nonoverlapping(bytes.as_ptr(), buffer as *mut u8, len_to_copy);
1094            *buffer.add(len_to_copy) = 0;
1095        }
1096        RKRStatus::RKR_STATUS_SUCCESS
1097    } else {
1098        RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS
1099    }
1100}
1101/// Gets a header string line as a newly allocated, null-terminated C string.
1102///
1103/// The caller OWNS the returned pointer and MUST call `rkr_free_string`
1104/// on it to prevent a memory leak. Returns NULL on error or if the
1105/// index is invalid (use [`rkr_frame_get_header_line`] when a status
1106/// code is preferred to NULL-vs-success disambiguation).
1107///
1108/// The `_cpp` suffix is historical; the function is callable from both
1109/// C and C++.
1110///
1111/// # Safety
1112/// frame_handle must be valid. The caller takes ownership of the returned string.
1113#[unsafe(no_mangle)]
1114pub unsafe extern "C" fn rkr_frame_get_header_line_cpp(
1115    frame_handle: *const RKRConFrame,
1116    is_prebox: bool,
1117    line_index: usize,
1118) -> *mut c_char {
1119    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
1120        Some(f) => f,
1121        None => return ptr::null_mut(),
1122    };
1123    let line_to_copy: Option<&str> = if is_prebox {
1124        match line_index {
1125            0 => Some(frame.header.prebox_header.user.as_str()),
1126            1 => Some(frame.header.prebox_header.metadata_line()),
1127            _ => None,
1128        }
1129    } else {
1130        frame
1131            .header
1132            .postbox_header
1133            .get(line_index)
1134            .map(String::as_str)
1135    };
1136    if let Some(line) = line_to_copy {
1137        // Convert the Rust string slice to a C-compatible, heap-allocated string.
1138        match CString::new(line) {
1139            Ok(c_string) => c_string.into_raw(), // Give ownership to the C caller
1140            Err(_) => ptr::null_mut(),           // In case the string contains a null byte
1141        }
1142    } else {
1143        ptr::null_mut() // Index out of bounds
1144    }
1145}
1146/// Frees a C string that was allocated by Rust (e.g., from
1147/// `rkr_frame_metadata_json`, `rkr_frame_potential_type`, or
1148/// `rkr_frame_get_header_line_cpp`). Safe to call with NULL (no-op).
1149///
1150/// # Safety
1151/// s must be either NULL or a pointer previously returned by an
1152/// allocating Rust FFI function in this crate.
1153#[unsafe(no_mangle)]
1154pub unsafe extern "C" fn rkr_free_string(s: *mut c_char) {
1155    if !s.is_null() {
1156        // Retake ownership of the CString to deallocate it properly.
1157        let _ = unsafe { CString::from_raw(s) };
1158    }
1159}
1160//=============================================================================
1161// FFI Writer Functions (Writer Object Model)
1162//=============================================================================
1163/// Type-erased writer that backs every `RKRConFrameWriter` handle.
1164///
1165/// `ConFrameWriter<W>` is generic over its sink, so a plain `File`, a
1166/// gzip `GzEncoder<File>`, and a zstd encoder all monomorphise to
1167/// distinct, layout-incompatible types. Boxing the sink as
1168/// `Box<dyn Write>` collapses them to a single concrete handle type, so
1169/// `free_rkr_writer` and `rkr_writer_extend` can cast the opaque pointer
1170/// to exactly one type regardless of the compression chosen at
1171/// construction. Dropping the box flushes the `BufWriter` and then runs
1172/// the sink's own `Drop` (gzip/zstd finalize their streams there).
1173type RkrWriter = ConFrameWriter<Box<dyn std::io::Write>>;
1174/// Boxes a sink into an `RKRConFrameWriter` handle at the requested
1175/// precision. `precision == None` selects the writer's built-in default.
1176#[inline]
1177fn into_rkr_writer(sink: Box<dyn std::io::Write>, precision: Option<u8>) -> *mut RKRConFrameWriter {
1178    let writer: RkrWriter = match precision {
1179        Some(p) => ConFrameWriter::with_precision(sink, p as usize),
1180        None => ConFrameWriter::new(sink),
1181    };
1182    Box::into_raw(Box::new(writer)) as *mut RKRConFrameWriter
1183}
1184/// Parses a borrowed C string, returning `None` for null or non-UTF-8.
1185#[inline]
1186unsafe fn cstr_path<'a>(filename_c: *const c_char) -> Option<&'a str> {
1187    if filename_c.is_null() {
1188        return None;
1189    }
1190    unsafe { CStr::from_ptr(filename_c).to_str().ok() }
1191}
1192/// Creates a new frame writer for the specified file.
1193/// The caller OWNS the returned pointer and MUST call `free_rkr_writer`.
1194///
1195/// # Safety
1196/// filename_c must be valid. The caller takes ownership of the returned writer.
1197#[unsafe(no_mangle)]
1198pub unsafe extern "C" fn create_writer_from_path_c(
1199    filename_c: *const c_char,
1200) -> *mut RKRConFrameWriter {
1201    let filename = match unsafe { cstr_path(filename_c) } {
1202        Some(s) => s,
1203        None => return ptr::null_mut(),
1204    };
1205    match File::create(filename) {
1206        Ok(file) => into_rkr_writer(Box::new(file), None),
1207        Err(_) => ptr::null_mut(),
1208    }
1209}
1210/// Creates a CON writer that preserves every finite binary64 value.
1211/// The caller owns the handle and must call `free_rkr_writer`.
1212///
1213/// # Safety
1214/// filename_c must be a valid NUL-terminated path or null.
1215#[unsafe(no_mangle)]
1216pub unsafe extern "C" fn create_writer_from_path_round_trip_c(
1217    filename_c: *const c_char,
1218) -> *mut RKRConFrameWriter {
1219    let filename = match unsafe { cstr_path(filename_c) } {
1220        Some(s) => s,
1221        None => return ptr::null_mut(),
1222    };
1223    match File::create(filename) {
1224        Ok(file) => {
1225            let writer: RkrWriter = ConFrameWriter::with_float_format(
1226                Box::new(file),
1227                crate::writer::FloatFormat::RoundTrip,
1228            );
1229            Box::into_raw(Box::new(writer)) as *mut RKRConFrameWriter
1230        }
1231        Err(_) => ptr::null_mut(),
1232    }
1233}
1234
1235/// Flushes buffered CON data and reports write errors.
1236///
1237/// On failure the status is `RKR_STATUS_IO_ERROR` and
1238/// [`rkr_last_error_message`] returns the underlying I/O text.
1239///
1240/// # Safety
1241/// writer_handle must be a live writer handle or null.
1242#[unsafe(no_mangle)]
1243pub unsafe extern "C" fn rkr_writer_flush(writer_handle: *mut RKRConFrameWriter) -> RKRStatus {
1244    let writer = match unsafe { (writer_handle as *mut RkrWriter).as_mut() } {
1245        Some(w) => w,
1246        None => return RKRStatus::RKR_STATUS_NULL_POINTER,
1247    };
1248    match writer.flush() {
1249        Ok(()) => {
1250            clear_last_error();
1251            RKRStatus::RKR_STATUS_SUCCESS
1252        }
1253        Err(err) => {
1254            set_last_error(&err.to_string());
1255            RKRStatus::RKR_STATUS_IO_ERROR
1256        }
1257    }
1258}
1259
1260/// Frees the memory for an `RKRConFrameWriter`, closing the associated file.
1261///
1262/// # Safety
1263/// writer_handle must be valid or null.
1264#[unsafe(no_mangle)]
1265pub unsafe extern "C" fn free_rkr_writer(writer_handle: *mut RKRConFrameWriter) {
1266    if !writer_handle.is_null() {
1267        let _ = unsafe { Box::from_raw(writer_handle as *mut RkrWriter) };
1268    }
1269}
1270/// Rebuild a parsed frame the way the Python, Julia, and Fortran writers do.
1271///
1272/// The builder stamps `con_spec_minor` when the file omitted it. The caller
1273/// owns the returned frame and must free it with `free_rkr_frame`. Returns
1274/// NULL on a null argument or when the rebuild fails.
1275///
1276/// # Safety
1277/// `frame_handle` must be a valid frame or null.
1278#[unsafe(no_mangle)]
1279pub unsafe extern "C" fn rkr_frame_binding_rewrite(
1280    frame_handle: *const RKRConFrame,
1281) -> *mut RKRConFrame {
1282    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
1283        return ptr::null_mut();
1284    };
1285    match binding_rewrite_frame(frame) {
1286        Ok(rewritten) => Box::into_raw(Box::new(rewritten)) as *mut RKRConFrame,
1287        Err(_) => ptr::null_mut(),
1288    }
1289}
1290
1291/// Writes multiple frames from an array of handles to the file managed by the writer.
1292/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1293///
1294/// Format failures such as a repeated atom id stay `RKR_STATUS_IO_ERROR`.
1295/// [`rkr_last_error_message`] returns the underlying text, which
1296/// [`rkr_status_message`] does not carry.
1297///
1298/// # Safety
1299/// writer_handle and frame_handles must be valid.
1300#[unsafe(no_mangle)]
1301pub unsafe extern "C" fn rkr_writer_extend(
1302    writer_handle: *mut RKRConFrameWriter,
1303    frame_handles: *const *const RKRConFrame,
1304    num_frames: usize,
1305) -> RKRStatus {
1306    let writer = match unsafe { (writer_handle as *mut RkrWriter).as_mut() } {
1307        Some(w) => w,
1308        None => return RKRStatus::RKR_STATUS_NULL_POINTER,
1309    };
1310    if frame_handles.is_null() {
1311        return RKRStatus::RKR_STATUS_NULL_POINTER;
1312    }
1313    let handles_slice = unsafe { std::slice::from_raw_parts(frame_handles, num_frames) };
1314    let mut rust_frames: Vec<&ConFrame> = Vec::with_capacity(num_frames);
1315    if handles_slice.iter().any(|&handle| handle.is_null()) {
1316        // Fail fast if any handle is null, as this indicates a bug on the
1317        // caller's side.
1318        return RKRStatus::RKR_STATUS_NULL_POINTER;
1319    }
1320    for &handle in handles_slice.iter() {
1321        // Assume the handle is valid.
1322        match unsafe { (handle as *const ConFrame).as_ref() } {
1323            Some(frame) => rust_frames.push(frame),
1324            // This case should be unreachable if the handle is not null, but we handle it for safety.
1325            None => return RKRStatus::RKR_STATUS_NULL_POINTER,
1326        }
1327    }
1328    match writer.extend(rust_frames.into_iter()) {
1329        Ok(_) => {
1330            clear_last_error();
1331            RKRStatus::RKR_STATUS_SUCCESS
1332        }
1333        Err(err) => {
1334            set_last_error(&err.to_string());
1335            RKRStatus::RKR_STATUS_IO_ERROR
1336        }
1337    }
1338}
1339
1340/// Enable (`canonical != 0`) or disable campaign-stable CON serialization on an open writer.
1341/// Matches Rust `ConFrameWriter::canonical(true)` (deterministic metadata key order).
1342///
1343/// # Safety
1344/// `writer_handle` must be valid or null (null → `RKR_STATUS_NULL_POINTER`).
1345#[unsafe(no_mangle)]
1346pub unsafe extern "C" fn rkr_writer_set_canonical(
1347    writer_handle: *mut RKRConFrameWriter,
1348    canonical: u8,
1349) -> RKRStatus {
1350    let writer = match unsafe { (writer_handle as *mut RkrWriter).as_mut() } {
1351        Some(w) => w,
1352        None => return RKRStatus::RKR_STATUS_NULL_POINTER,
1353    };
1354    writer.set_canonical(canonical != 0);
1355    RKRStatus::RKR_STATUS_SUCCESS
1356}
1357
1358/// Returns 1 if the writer is in canonical mode, 0 otherwise (or on null handle).
1359#[unsafe(no_mangle)]
1360pub unsafe extern "C" fn rkr_writer_is_canonical(writer_handle: *const RKRConFrameWriter) -> u8 {
1361    match unsafe { (writer_handle as *const RkrWriter).as_ref() } {
1362        Some(w) => u8::from(w.is_canonical()),
1363        None => 0,
1364    }
1365}
1366
1367#[cfg(test)]
1368mod index_proj_ffi_tests {
1369    use super::*;
1370    use std::ffi::CStr;
1371    use std::fs;
1372    use std::path::PathBuf;
1373
1374    fn fixture_path() -> PathBuf {
1375        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2.con")
1376    }
1377
1378    #[test]
1379    fn ffi_projection_matches_index_proj() {
1380        let frames = crate::iterators::read_all_frames(&fixture_path()).unwrap();
1381        let fr = &frames[0];
1382        let handle = fr as *const ConFrame as *const RKRConFrame;
1383        let proj = crate::index_proj::FrameIndexProjection::from_frame(fr);
1384        // Full eight-surface campaign contract (plus JSON) via shipped C symbols.
1385        assert_eq!(rkr_frame_index_natoms(handle), proj.n_atoms);
1386        assert_eq!(rkr_frame_sections_mask(handle), proj.sections_mask);
1387        let formula_c = unsafe { rkr_frame_composition_formula(handle) };
1388        assert!(!formula_c.is_null());
1389        let formula = unsafe { CStr::from_ptr(formula_c) }.to_str().unwrap();
1390        assert_eq!(formula, proj.formula);
1391        unsafe { rkr_free_string(formula_c) };
1392        let ie = rkr_frame_index_energy(handle);
1393        match proj.energy {
1394            Some(e) => assert!((ie - e).abs() < 1e-12 || (ie.is_nan() && e.is_nan())),
1395            None => assert!(ie.is_nan()),
1396        }
1397        let tm = rkr_frame_total_mass(handle);
1398        match proj.total_mass {
1399            Some(m) => assert!((tm - m).abs() < 1e-9, "total_mass C={tm} proj={m}"),
1400            None => assert!(tm.is_nan()),
1401        }
1402        let cv = rkr_frame_cell_volume(handle);
1403        match proj.cell_volume {
1404            Some(v) => assert!(
1405                (cv - v).abs() < 1e-6 * v.max(1.0),
1406                "cell_volume C={cv} proj={v}"
1407            ),
1408            None => assert!(cv.is_nan()),
1409        }
1410        let fm = rkr_frame_fmax(handle);
1411        match proj.fmax {
1412            Some(f) => assert!((fm - f).abs() < 1e-12 || (fm.is_nan() && f.is_nan())),
1413            None => assert!(fm.is_nan()),
1414        }
1415        let json_c = unsafe { rkr_frame_index_projection_json(handle) };
1416        assert!(!json_c.is_null());
1417        let json = unsafe { CStr::from_ptr(json_c) }.to_str().unwrap();
1418        assert!(json.contains("\"formula\""));
1419        assert!(json.contains(&proj.formula) || proj.formula.is_empty());
1420        assert!(json.contains("\"n_atoms\""));
1421        assert!(json.contains("\"total_mass\"") || json.contains("\"cell_volume\""));
1422        unsafe { rkr_free_string(json_c) };
1423    }
1424
1425    #[test]
1426    fn ffi_canonical_writer_byte_identical() {
1427        let frames = crate::iterators::read_all_frames(&fixture_path()).unwrap();
1428        let fr = &frames[0];
1429        let dir = tempfile::tempdir().unwrap();
1430        let p1 = dir.path().join("a.con");
1431        let p2 = dir.path().join("b.con");
1432        for p in [&p1, &p2] {
1433            let path_c = std::ffi::CString::new(p.to_str().unwrap()).unwrap();
1434            let w = unsafe { create_writer_from_path_c(path_c.as_ptr()) };
1435            assert!(!w.is_null());
1436            assert_eq!(
1437                unsafe { rkr_writer_set_canonical(w, 1) },
1438                RKRStatus::RKR_STATUS_SUCCESS
1439            );
1440            assert_eq!(unsafe { rkr_writer_is_canonical(w) }, 1);
1441            let handles = [fr as *const ConFrame as *const RKRConFrame];
1442            assert_eq!(
1443                unsafe { rkr_writer_extend(w, handles.as_ptr(), 1) },
1444                RKRStatus::RKR_STATUS_SUCCESS
1445            );
1446            unsafe { free_rkr_writer(w) };
1447        }
1448        let b1 = fs::read(&p1).unwrap();
1449        let b2 = fs::read(&p2).unwrap();
1450        assert_eq!(b1, b2);
1451        assert!(!b1.is_empty());
1452    }
1453}
1454//=============================================================================
1455// Writer with Precision
1456//=============================================================================
1457/// Creates a new frame writer with custom floating-point precision.
1458/// The caller OWNS the returned pointer and MUST call `free_rkr_writer`.
1459///
1460/// # Safety
1461/// filename_c must be valid. The caller takes ownership of the returned writer.
1462#[unsafe(no_mangle)]
1463pub unsafe extern "C" fn create_writer_from_path_with_precision_c(
1464    filename_c: *const c_char,
1465    precision: u8,
1466) -> *mut RKRConFrameWriter {
1467    let filename = match unsafe { cstr_path(filename_c) } {
1468        Some(s) => s,
1469        None => return ptr::null_mut(),
1470    };
1471    match File::create(filename) {
1472        Ok(file) => into_rkr_writer(Box::new(file), Some(precision)),
1473        Err(_) => ptr::null_mut(),
1474    }
1475}
1476//=============================================================================
1477// Frame Builder FFI (construct ConFrame from C data)
1478//=============================================================================
1479/// An opaque handle to a Rust `ConFrameBuilder` object.
1480// No #[repr(C)]: cbindgen then emits an incomplete struct, which C and C++
1481// accept under -pedantic. The zero-length field keeps it unconstructible.
1482pub struct RKRConFrameBuilder {
1483    _private: [u8; 0],
1484}
1485#[allow(clippy::too_many_arguments)]
1486unsafe fn add_builder_atom(
1487    builder_handle: *mut RKRConFrameBuilder,
1488    symbol: *const c_char,
1489    x: f64,
1490    y: f64,
1491    z: f64,
1492    fixed: [bool; 3],
1493    atom_id: u64,
1494    mass: f64,
1495    velocity: Option<[f64; 3]>,
1496    forces: Option<[f64; 3]>,
1497) -> RKRStatus {
1498    if builder_handle.is_null() || symbol.is_null() {
1499        return RKRStatus::RKR_STATUS_NULL_POINTER;
1500    }
1501    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1502    let sym = match unsafe { CStr::from_ptr(symbol).to_str() } {
1503        Ok(s) => s,
1504        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
1505    };
1506    builder.add_atom(sym, x, y, z, fixed, atom_id, mass);
1507    if let Some(v) = velocity {
1508        builder.with_velocity(v);
1509    }
1510    if let Some(f) = forces {
1511        builder.with_force(f);
1512    }
1513    RKRStatus::RKR_STATUS_SUCCESS
1514}
1515/// Attaches a velocity vector to the most recently added atom on a builder.
1516/// No-op if no atom has been added yet.
1517///
1518/// # Safety
1519/// builder_handle must be valid. velocity must point to 3 contiguous f64 values.
1520#[unsafe(no_mangle)]
1521pub unsafe extern "C" fn rkr_frame_builder_set_last_velocity(
1522    builder_handle: *mut RKRConFrameBuilder,
1523    velocity: *const f64,
1524) -> RKRStatus {
1525    if builder_handle.is_null() || velocity.is_null() {
1526        return RKRStatus::RKR_STATUS_NULL_POINTER;
1527    }
1528    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1529    let v = unsafe { [*velocity, *velocity.add(1), *velocity.add(2)] };
1530    builder.with_velocity(v);
1531    RKRStatus::RKR_STATUS_SUCCESS
1532}
1533/// Attaches a force vector to the most recently added atom on a builder.
1534/// No-op if no atom has been added yet.
1535///
1536/// # Safety
1537/// builder_handle must be valid. force must point to 3 contiguous f64 values.
1538#[unsafe(no_mangle)]
1539pub unsafe extern "C" fn rkr_frame_builder_set_last_force(
1540    builder_handle: *mut RKRConFrameBuilder,
1541    force: *const f64,
1542) -> RKRStatus {
1543    if builder_handle.is_null() || force.is_null() {
1544        return RKRStatus::RKR_STATUS_NULL_POINTER;
1545    }
1546    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1547    let f = unsafe { [*force, *force.add(1), *force.add(2)] };
1548    builder.with_force(f);
1549    RKRStatus::RKR_STATUS_SUCCESS
1550}
1551/// Attaches a displacement vector (Angstrom) to the most recently added
1552/// atom on a builder. No-op if no atom has been added yet.
1553///
1554/// # Safety
1555/// builder_handle must be valid. displacement must point to 3 contiguous
1556/// f64 values.
1557#[unsafe(no_mangle)]
1558pub unsafe extern "C" fn rkr_frame_builder_set_last_displacement(
1559    builder_handle: *mut RKRConFrameBuilder,
1560    displacement: *const f64,
1561) -> RKRStatus {
1562    if builder_handle.is_null() || displacement.is_null() {
1563        return RKRStatus::RKR_STATUS_NULL_POINTER;
1564    }
1565    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1566    let d = unsafe { [*displacement, *displacement.add(1), *displacement.add(2)] };
1567    builder.with_displacement(d);
1568    RKRStatus::RKR_STATUS_SUCCESS
1569}
1570/// Attaches a spread vector (Angstrom) to the most recently added
1571/// atom on a builder. No-op if no atom has been added yet.
1572///
1573/// # Safety
1574/// builder_handle must be valid. spread must point to 3 contiguous
1575/// f64 values.
1576#[unsafe(no_mangle)]
1577pub unsafe extern "C" fn rkr_frame_builder_set_last_spread(
1578    builder_handle: *mut RKRConFrameBuilder,
1579    spread: *const f64,
1580) -> RKRStatus {
1581    if builder_handle.is_null() || spread.is_null() {
1582        return RKRStatus::RKR_STATUS_NULL_POINTER;
1583    }
1584    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1585    let d = unsafe { [*spread, *spread.add(1), *spread.add(2)] };
1586    builder.with_spread(d);
1587    RKRStatus::RKR_STATUS_SUCCESS
1588}
1589/// Attaches a per-atom energy to the most recently added atom on a
1590/// builder. No-op if no atom has been added yet.
1591///
1592/// Use this together with the per-frame `energy` metadata key when a
1593/// caller wants to round-trip an "Energies of Component" decomposition
1594/// alongside the total.
1595///
1596/// # Safety
1597/// builder_handle must be valid.
1598#[unsafe(no_mangle)]
1599pub unsafe extern "C" fn rkr_frame_builder_set_last_energy(
1600    builder_handle: *mut RKRConFrameBuilder,
1601    energy: f64,
1602) -> RKRStatus {
1603    if builder_handle.is_null() {
1604        return RKRStatus::RKR_STATUS_NULL_POINTER;
1605    }
1606    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1607    builder.with_energy(energy);
1608    RKRStatus::RKR_STATUS_SUCCESS
1609}
1610// ----- v0.11.0 in-place mutation FFI ---------------------------------------
1611//
1612// Mirrors `ConFrameBuilder::set_atom_* / clear_atom_* / *_from_flat /
1613// get_atom_* / atom_count` for C / C++ / Python / Julia consumers. All
1614// mutators return RKRStatus; getters return raw values via out-parameters
1615// (so a caller can distinguish "atom has no force" from "successful read of
1616// f={0,0,0}" via the `has_*` boolean out-parameter).
1617//
1618// IndexOutOfBounds errors from the Rust side surface as
1619// RKR_STATUS_INDEX_OUT_OF_BOUNDS; all NULL-handle / NULL-out-pointer paths
1620// return RKR_STATUS_NULL_POINTER. Bulk setters with the wrong length return
1621// RKR_STATUS_INDEX_OUT_OF_BOUNDS as well (the caller sized the buffer
1622// wrong).
1623fn map_builder_err(e: crate::error::ParseError) -> RKRStatus {
1624    use crate::error::ParseError;
1625    match e {
1626        ParseError::IndexOutOfBounds { .. } | ParseError::InvalidVectorLength { .. } => {
1627            RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS
1628        }
1629        ParseError::MassMismatch { .. } | ParseError::ValidationError(_) => {
1630            RKRStatus::RKR_STATUS_VALIDATION_ERROR
1631        }
1632        _ => RKRStatus::RKR_STATUS_INTERNAL_ERROR,
1633    }
1634}
1635/// Returns the number of atoms currently held in the builder.
1636///
1637/// # Safety
1638/// builder_handle must be a valid pointer returned by rkr_frame_new and
1639/// not yet consumed by rkr_frame_builder_build / freed.
1640/// Returns 0 on NULL handle.
1641#[unsafe(no_mangle)]
1642pub unsafe extern "C" fn rkr_frame_builder_atom_count(
1643    builder_handle: *const RKRConFrameBuilder,
1644) -> usize {
1645    if builder_handle.is_null() {
1646        return 0;
1647    }
1648    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
1649    builder.atom_count()
1650}
1651/// Updates the Cartesian position of an existing atom.
1652/// # Safety
1653/// builder_handle must be valid.
1654#[unsafe(no_mangle)]
1655pub unsafe extern "C" fn rkr_frame_builder_set_atom_position(
1656    builder_handle: *mut RKRConFrameBuilder,
1657    index: usize,
1658    x: f64,
1659    y: f64,
1660    z: f64,
1661) -> RKRStatus {
1662    if builder_handle.is_null() {
1663        return RKRStatus::RKR_STATUS_NULL_POINTER;
1664    }
1665    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1666    match builder.set_atom_position(index, x, y, z) {
1667        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1668        Err(e) => map_builder_err(e),
1669    }
1670}
1671/// Sets the velocity vector of an existing atom from 3 contiguous f64 values.
1672/// # Safety
1673/// builder_handle must be valid; velocity must point to 3 contiguous f64.
1674#[unsafe(no_mangle)]
1675pub unsafe extern "C" fn rkr_frame_builder_set_atom_velocity(
1676    builder_handle: *mut RKRConFrameBuilder,
1677    index: usize,
1678    velocity: *const f64,
1679) -> RKRStatus {
1680    if builder_handle.is_null() || velocity.is_null() {
1681        return RKRStatus::RKR_STATUS_NULL_POINTER;
1682    }
1683    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1684    let v = unsafe { [*velocity, *velocity.add(1), *velocity.add(2)] };
1685    match builder.set_atom_velocity(index, v) {
1686        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1687        Err(e) => map_builder_err(e),
1688    }
1689}
1690/// Sets the force vector of an existing atom from 3 contiguous f64 values.
1691/// # Safety
1692/// builder_handle must be valid; force must point to 3 contiguous f64.
1693#[unsafe(no_mangle)]
1694pub unsafe extern "C" fn rkr_frame_builder_set_atom_force(
1695    builder_handle: *mut RKRConFrameBuilder,
1696    index: usize,
1697    force: *const f64,
1698) -> RKRStatus {
1699    if builder_handle.is_null() || force.is_null() {
1700        return RKRStatus::RKR_STATUS_NULL_POINTER;
1701    }
1702    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1703    let f = unsafe { [*force, *force.add(1), *force.add(2)] };
1704    match builder.set_atom_force(index, f) {
1705        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1706        Err(e) => map_builder_err(e),
1707    }
1708}
1709/// Sets the displacement vector (Angstrom) of an existing atom from 3
1710/// contiguous f64 values.
1711/// # Safety
1712/// builder_handle must be valid; displacement must point to 3 contiguous f64.
1713#[unsafe(no_mangle)]
1714pub unsafe extern "C" fn rkr_frame_builder_set_atom_displacement(
1715    builder_handle: *mut RKRConFrameBuilder,
1716    index: usize,
1717    displacement: *const f64,
1718) -> RKRStatus {
1719    if builder_handle.is_null() || displacement.is_null() {
1720        return RKRStatus::RKR_STATUS_NULL_POINTER;
1721    }
1722    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1723    let d = unsafe { [*displacement, *displacement.add(1), *displacement.add(2)] };
1724    match builder.set_atom_displacement(index, d) {
1725        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1726        Err(e) => map_builder_err(e),
1727    }
1728}
1729/// Sets the spread vector (Angstrom) of an existing atom from 3
1730/// contiguous f64 values.
1731/// # Safety
1732/// builder_handle must be valid; spread must point to 3 contiguous f64.
1733#[unsafe(no_mangle)]
1734pub unsafe extern "C" fn rkr_frame_builder_set_atom_spread(
1735    builder_handle: *mut RKRConFrameBuilder,
1736    index: usize,
1737    spread: *const f64,
1738) -> RKRStatus {
1739    if builder_handle.is_null() || spread.is_null() {
1740        return RKRStatus::RKR_STATUS_NULL_POINTER;
1741    }
1742    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1743    let d = unsafe { [*spread, *spread.add(1), *spread.add(2)] };
1744    match builder.set_atom_spread(index, d) {
1745        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1746        Err(e) => map_builder_err(e),
1747    }
1748}
1749/// Sets the per-atom energy contribution of an existing atom.
1750/// # Safety
1751/// builder_handle must be valid.
1752#[unsafe(no_mangle)]
1753pub unsafe extern "C" fn rkr_frame_builder_set_atom_energy(
1754    builder_handle: *mut RKRConFrameBuilder,
1755    index: usize,
1756    energy: f64,
1757) -> RKRStatus {
1758    if builder_handle.is_null() {
1759        return RKRStatus::RKR_STATUS_NULL_POINTER;
1760    }
1761    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1762    match builder.set_atom_energy(index, energy) {
1763        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1764        Err(e) => map_builder_err(e),
1765    }
1766}
1767/// Updates per-direction fixed flags `[fixed_x, fixed_y, fixed_z]`.
1768/// # Safety
1769/// builder_handle must be valid.
1770#[unsafe(no_mangle)]
1771pub unsafe extern "C" fn rkr_frame_builder_set_atom_fixed(
1772    builder_handle: *mut RKRConFrameBuilder,
1773    index: usize,
1774    fixed_x: bool,
1775    fixed_y: bool,
1776    fixed_z: bool,
1777) -> RKRStatus {
1778    if builder_handle.is_null() {
1779        return RKRStatus::RKR_STATUS_NULL_POINTER;
1780    }
1781    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1782    match builder.set_atom_fixed(index, [fixed_x, fixed_y, fixed_z]) {
1783        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1784        Err(e) => map_builder_err(e),
1785    }
1786}
1787/// Updates the mass of an existing atom.
1788/// # Safety
1789/// builder_handle must be valid.
1790#[unsafe(no_mangle)]
1791pub unsafe extern "C" fn rkr_frame_builder_set_atom_mass(
1792    builder_handle: *mut RKRConFrameBuilder,
1793    index: usize,
1794    mass: f64,
1795) -> RKRStatus {
1796    if builder_handle.is_null() {
1797        return RKRStatus::RKR_STATUS_NULL_POINTER;
1798    }
1799    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1800    match builder.set_atom_mass(index, mass) {
1801        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1802        Err(e) => map_builder_err(e),
1803    }
1804}
1805/// Updates the atom_id (pre-grouping index from .con column 5) of an
1806/// existing atom. The underlying `Array1<u64>` buffer pointer stays
1807/// stable; callers that hold a raw `*const u64` via
1808/// `rkr_frame_builder_atom_ids_data` do not need to refresh after this.
1809/// # Safety
1810/// builder_handle must be valid.
1811#[unsafe(no_mangle)]
1812pub unsafe extern "C" fn rkr_frame_builder_set_atom_id(
1813    builder_handle: *mut RKRConFrameBuilder,
1814    index: usize,
1815    atom_id: u64,
1816) -> RKRStatus {
1817    if builder_handle.is_null() {
1818        return RKRStatus::RKR_STATUS_NULL_POINTER;
1819    }
1820    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1821    match builder.set_atom_id(index, atom_id) {
1822        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1823        Err(e) => map_builder_err(e),
1824    }
1825}
1826/// Removes velocity / force / energy data from an existing atom.
1827/// # Safety
1828/// builder_handle must be valid.
1829#[unsafe(no_mangle)]
1830pub unsafe extern "C" fn rkr_frame_builder_clear_atom_velocity(
1831    builder_handle: *mut RKRConFrameBuilder,
1832    index: usize,
1833) -> RKRStatus {
1834    if builder_handle.is_null() {
1835        return RKRStatus::RKR_STATUS_NULL_POINTER;
1836    }
1837    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1838    match builder.clear_atom_velocity(index) {
1839        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1840        Err(e) => map_builder_err(e),
1841    }
1842}
1843/// # Safety
1844/// builder_handle must be valid.
1845#[unsafe(no_mangle)]
1846pub unsafe extern "C" fn rkr_frame_builder_clear_atom_force(
1847    builder_handle: *mut RKRConFrameBuilder,
1848    index: usize,
1849) -> RKRStatus {
1850    if builder_handle.is_null() {
1851        return RKRStatus::RKR_STATUS_NULL_POINTER;
1852    }
1853    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1854    match builder.clear_atom_force(index) {
1855        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1856        Err(e) => map_builder_err(e),
1857    }
1858}
1859/// # Safety
1860/// builder_handle must be valid.
1861#[unsafe(no_mangle)]
1862pub unsafe extern "C" fn rkr_frame_builder_clear_atom_displacement(
1863    builder_handle: *mut RKRConFrameBuilder,
1864    index: usize,
1865) -> RKRStatus {
1866    if builder_handle.is_null() {
1867        return RKRStatus::RKR_STATUS_NULL_POINTER;
1868    }
1869    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1870    match builder.clear_atom_displacement(index) {
1871        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1872        Err(e) => map_builder_err(e),
1873    }
1874}
1875/// # Safety
1876/// builder_handle must be valid.
1877#[unsafe(no_mangle)]
1878pub unsafe extern "C" fn rkr_frame_builder_clear_atom_spread(
1879    builder_handle: *mut RKRConFrameBuilder,
1880    index: usize,
1881) -> RKRStatus {
1882    if builder_handle.is_null() {
1883        return RKRStatus::RKR_STATUS_NULL_POINTER;
1884    }
1885    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1886    match builder.clear_atom_spread(index) {
1887        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1888        Err(e) => map_builder_err(e),
1889    }
1890}
1891/// # Safety
1892/// builder_handle must be valid.
1893#[unsafe(no_mangle)]
1894pub unsafe extern "C" fn rkr_frame_builder_clear_atom_energy(
1895    builder_handle: *mut RKRConFrameBuilder,
1896    index: usize,
1897) -> RKRStatus {
1898    if builder_handle.is_null() {
1899        return RKRStatus::RKR_STATUS_NULL_POINTER;
1900    }
1901    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1902    match builder.clear_atom_energy(index) {
1903        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1904        Err(e) => map_builder_err(e),
1905    }
1906}
1907/// Bulk-update positions for every atom from a flat row-major
1908/// `[x0,y0,z0,x1,y1,z1,...]` buffer of length `3 * atom_count()`.
1909/// # Safety
1910/// builder_handle must be valid; positions must point to `3 * len` f64.
1911#[unsafe(no_mangle)]
1912pub unsafe extern "C" fn rkr_frame_builder_set_positions_from_flat(
1913    builder_handle: *mut RKRConFrameBuilder,
1914    positions: *const f64,
1915    len: usize,
1916) -> RKRStatus {
1917    if builder_handle.is_null() || positions.is_null() {
1918        return RKRStatus::RKR_STATUS_NULL_POINTER;
1919    }
1920    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1921    let slice = unsafe { std::slice::from_raw_parts(positions, len) };
1922    match builder.set_positions_from_flat(slice) {
1923        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1924        Err(e) => map_builder_err(e),
1925    }
1926}
1927/// Bulk-update forces for every atom.
1928/// # Safety
1929/// builder_handle must be valid; forces must point to `3 * len` f64.
1930#[unsafe(no_mangle)]
1931pub unsafe extern "C" fn rkr_frame_builder_set_forces_from_flat(
1932    builder_handle: *mut RKRConFrameBuilder,
1933    forces: *const f64,
1934    len: usize,
1935) -> RKRStatus {
1936    if builder_handle.is_null() || forces.is_null() {
1937        return RKRStatus::RKR_STATUS_NULL_POINTER;
1938    }
1939    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1940    let slice = unsafe { std::slice::from_raw_parts(forces, len) };
1941    match builder.set_forces_from_flat(slice) {
1942        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1943        Err(e) => map_builder_err(e),
1944    }
1945}
1946/// Bulk-update displacements (Angstrom) for every atom.
1947/// # Safety
1948/// builder_handle must be valid; displacements must point to `len` f64
1949/// (`len == 3 * atom_count`).
1950#[unsafe(no_mangle)]
1951pub unsafe extern "C" fn rkr_frame_builder_set_displacements_from_flat(
1952    builder_handle: *mut RKRConFrameBuilder,
1953    displacements: *const f64,
1954    len: usize,
1955) -> RKRStatus {
1956    if builder_handle.is_null() || displacements.is_null() {
1957        return RKRStatus::RKR_STATUS_NULL_POINTER;
1958    }
1959    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1960    let slice = unsafe { std::slice::from_raw_parts(displacements, len) };
1961    match builder.set_displacements_from_flat(slice) {
1962        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1963        Err(e) => map_builder_err(e),
1964    }
1965}
1966/// Bulk-update spreads (Angstrom) for every atom.
1967/// # Safety
1968/// builder_handle must be valid; spreads must point to `len` f64
1969/// (`len == 3 * atom_count`).
1970#[unsafe(no_mangle)]
1971pub unsafe extern "C" fn rkr_frame_builder_set_spreads_from_flat(
1972    builder_handle: *mut RKRConFrameBuilder,
1973    spreads: *const f64,
1974    len: usize,
1975) -> RKRStatus {
1976    if builder_handle.is_null() || spreads.is_null() {
1977        return RKRStatus::RKR_STATUS_NULL_POINTER;
1978    }
1979    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1980    let slice = unsafe { std::slice::from_raw_parts(spreads, len) };
1981    match builder.set_spreads_from_flat(slice) {
1982        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
1983        Err(e) => map_builder_err(e),
1984    }
1985}
1986/// Bulk-update per-atom energies (one f64 per atom).
1987/// # Safety
1988/// builder_handle must be valid; energies must point to `len` f64.
1989#[unsafe(no_mangle)]
1990pub unsafe extern "C" fn rkr_frame_builder_set_atom_energies_from_flat(
1991    builder_handle: *mut RKRConFrameBuilder,
1992    energies: *const f64,
1993    len: usize,
1994) -> RKRStatus {
1995    if builder_handle.is_null() || energies.is_null() {
1996        return RKRStatus::RKR_STATUS_NULL_POINTER;
1997    }
1998    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1999    let slice = unsafe { std::slice::from_raw_parts(energies, len) };
2000    match builder.set_atom_energies_from_flat(slice) {
2001        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
2002        Err(e) => map_builder_err(e),
2003    }
2004}
2005/// Reads the position of an existing atom into 3 contiguous f64 out values.
2006/// # Safety
2007/// builder_handle must be valid; out_xyz must point to 3 writable f64.
2008#[unsafe(no_mangle)]
2009pub unsafe extern "C" fn rkr_frame_builder_get_atom_position(
2010    builder_handle: *const RKRConFrameBuilder,
2011    index: usize,
2012    out_xyz: *mut f64,
2013) -> RKRStatus {
2014    if builder_handle.is_null() || out_xyz.is_null() {
2015        return RKRStatus::RKR_STATUS_NULL_POINTER;
2016    }
2017    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2018    match builder.get_atom_position(index) {
2019        Ok((x, y, z)) => unsafe {
2020            *out_xyz = x;
2021            *out_xyz.add(1) = y;
2022            *out_xyz.add(2) = z;
2023            RKRStatus::RKR_STATUS_SUCCESS
2024        },
2025        Err(e) => map_builder_err(e),
2026    }
2027}
2028/// Reads the velocity / force vector of an atom (if any) into 3 contiguous
2029/// f64. `*has_value` is set to `true` if the atom carries that vector,
2030/// `false` if it does not (in which case `out_xyz` is left untouched).
2031///
2032/// # Safety
2033/// builder_handle, out_xyz, has_value must all be valid pointers.
2034#[unsafe(no_mangle)]
2035pub unsafe extern "C" fn rkr_frame_builder_get_atom_velocity(
2036    builder_handle: *const RKRConFrameBuilder,
2037    index: usize,
2038    out_xyz: *mut f64,
2039    has_value: *mut bool,
2040) -> RKRStatus {
2041    if builder_handle.is_null() || out_xyz.is_null() || has_value.is_null() {
2042        return RKRStatus::RKR_STATUS_NULL_POINTER;
2043    }
2044    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2045    match builder.get_atom_velocity(index) {
2046        Ok(Some(v)) => unsafe {
2047            *out_xyz = v[0];
2048            *out_xyz.add(1) = v[1];
2049            *out_xyz.add(2) = v[2];
2050            *has_value = true;
2051            RKRStatus::RKR_STATUS_SUCCESS
2052        },
2053        Ok(None) => unsafe {
2054            *has_value = false;
2055            RKRStatus::RKR_STATUS_SUCCESS
2056        },
2057        Err(e) => map_builder_err(e),
2058    }
2059}
2060/// # Safety
2061/// builder_handle, out_xyz, has_value must all be valid pointers.
2062#[unsafe(no_mangle)]
2063pub unsafe extern "C" fn rkr_frame_builder_get_atom_force(
2064    builder_handle: *const RKRConFrameBuilder,
2065    index: usize,
2066    out_xyz: *mut f64,
2067    has_value: *mut bool,
2068) -> RKRStatus {
2069    if builder_handle.is_null() || out_xyz.is_null() || has_value.is_null() {
2070        return RKRStatus::RKR_STATUS_NULL_POINTER;
2071    }
2072    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2073    match builder.get_atom_force(index) {
2074        Ok(Some(f)) => unsafe {
2075            *out_xyz = f[0];
2076            *out_xyz.add(1) = f[1];
2077            *out_xyz.add(2) = f[2];
2078            *has_value = true;
2079            RKRStatus::RKR_STATUS_SUCCESS
2080        },
2081        Ok(None) => unsafe {
2082            *has_value = false;
2083            RKRStatus::RKR_STATUS_SUCCESS
2084        },
2085        Err(e) => map_builder_err(e),
2086    }
2087}
2088/// Reads the displacement of an atom (if any). `*has_value` is set to
2089/// `true` if the atom carries a displacement, else `false` and `out_xyz`
2090/// is left untouched.
2091/// # Safety
2092/// builder_handle, out_xyz, has_value must all be valid pointers.
2093#[unsafe(no_mangle)]
2094pub unsafe extern "C" fn rkr_frame_builder_get_atom_displacement(
2095    builder_handle: *const RKRConFrameBuilder,
2096    index: usize,
2097    out_xyz: *mut f64,
2098    has_value: *mut bool,
2099) -> RKRStatus {
2100    if builder_handle.is_null() || out_xyz.is_null() || has_value.is_null() {
2101        return RKRStatus::RKR_STATUS_NULL_POINTER;
2102    }
2103    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2104    match builder.get_atom_displacement(index) {
2105        Ok(Some(d)) => unsafe {
2106            *out_xyz = d[0];
2107            *out_xyz.add(1) = d[1];
2108            *out_xyz.add(2) = d[2];
2109            *has_value = true;
2110            RKRStatus::RKR_STATUS_SUCCESS
2111        },
2112        Ok(None) => unsafe {
2113            *has_value = false;
2114            RKRStatus::RKR_STATUS_SUCCESS
2115        },
2116        Err(e) => map_builder_err(e),
2117    }
2118}
2119/// Reads the spread of an atom (if any). `*has_value` is set to
2120/// `true` if the atom carries a spread, else `false` and `out_xyz`
2121/// is left untouched.
2122/// # Safety
2123/// builder_handle, out_xyz, has_value must all be valid pointers.
2124#[unsafe(no_mangle)]
2125pub unsafe extern "C" fn rkr_frame_builder_get_atom_spread(
2126    builder_handle: *const RKRConFrameBuilder,
2127    index: usize,
2128    out_xyz: *mut f64,
2129    has_value: *mut bool,
2130) -> RKRStatus {
2131    if builder_handle.is_null() || out_xyz.is_null() || has_value.is_null() {
2132        return RKRStatus::RKR_STATUS_NULL_POINTER;
2133    }
2134    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2135    match builder.get_atom_spread(index) {
2136        Ok(Some(d)) => unsafe {
2137            *out_xyz = d[0];
2138            *out_xyz.add(1) = d[1];
2139            *out_xyz.add(2) = d[2];
2140            *has_value = true;
2141            RKRStatus::RKR_STATUS_SUCCESS
2142        },
2143        Ok(None) => unsafe {
2144            *has_value = false;
2145            RKRStatus::RKR_STATUS_SUCCESS
2146        },
2147        Err(e) => map_builder_err(e),
2148    }
2149}
2150/// Reads the per-atom energy of an atom (if any). `*has_value` is set to
2151/// `true` if the atom carries an energy contribution, else `false` and
2152/// `*out_value` is left untouched.
2153/// # Safety
2154/// builder_handle, out_value, has_value must all be valid pointers.
2155#[unsafe(no_mangle)]
2156pub unsafe extern "C" fn rkr_frame_builder_get_atom_energy(
2157    builder_handle: *const RKRConFrameBuilder,
2158    index: usize,
2159    out_value: *mut f64,
2160    has_value: *mut bool,
2161) -> RKRStatus {
2162    if builder_handle.is_null() || out_value.is_null() || has_value.is_null() {
2163        return RKRStatus::RKR_STATUS_NULL_POINTER;
2164    }
2165    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2166    match builder.get_atom_energy(index) {
2167        Ok(Some(e)) => unsafe {
2168            *out_value = e;
2169            *has_value = true;
2170            RKRStatus::RKR_STATUS_SUCCESS
2171        },
2172        Ok(None) => unsafe {
2173            *has_value = false;
2174            RKRStatus::RKR_STATUS_SUCCESS
2175        },
2176        Err(e) => map_builder_err(e),
2177    }
2178}
2179/// Reads the mass of an existing atom.
2180/// # Safety
2181/// builder_handle and out_mass must be valid pointers.
2182#[unsafe(no_mangle)]
2183pub unsafe extern "C" fn rkr_frame_builder_get_atom_mass(
2184    builder_handle: *const RKRConFrameBuilder,
2185    index: usize,
2186    out_mass: *mut f64,
2187) -> RKRStatus {
2188    if builder_handle.is_null() || out_mass.is_null() {
2189        return RKRStatus::RKR_STATUS_NULL_POINTER;
2190    }
2191    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2192    match builder.get_atom_mass(index) {
2193        Ok(m) => unsafe {
2194            *out_mass = m;
2195            RKRStatus::RKR_STATUS_SUCCESS
2196        },
2197        Err(e) => map_builder_err(e),
2198    }
2199}
2200// ----- v0.11.0 DLPack tier-3 export FFI -------------------------------------
2201//
2202// Cross-language zero-copy via the DLPack 1.0 ABI. Each per-atom field of
2203// the builder is exported as an owning `DLManagedTensorVersioned*`. The
2204// caller is responsible for invoking the tensor's deleter callback to
2205// release the backing storage when finished. v0.11 ships the OWNING /
2206// CLONED variant: the tensor carries its own copy of the field data so it
2207// remains valid past the builder's lifetime. This is the conservative
2208// choice for cross-process / language-runtime consumers (Python GC,
2209// Julia GC, ...) where the consumer may outlive the Rust-side
2210// that hands out a non-owning view backed by `Arc<ndarray::Array<...>>`
2211// storage (matches metatensor v2's `Arc<RwLock<ArrayD<T>>>` pattern) for
2212// in-process zero-copy.
2213//
2214// Optional sections (velocities, forces, atom_energies) return
2215// RKR_STATUS_SECTION_ABSENT when the section is not declared on the
2216// builder; the out parameter is left untouched. Always-present fields
2217// (positions, masses, atom_ids) always return a tensor on success.
2218/// Re-export of dlpk's `DLManagedTensorVersioned` for the C ABI surface.
2219/// Defined here so cbindgen emits a forward declaration without pulling
2220/// in the full dlpk header; consumers include `<dlpack/dlpack.h>` (or
2221/// equivalent) and cast through the standard DLPack ABI.
2222pub use dlpk::sys::DLManagedTensorVersioned as RKRDLManagedTensorVersioned;
2223fn map_dlpack_err(e: crate::error::ParseError) -> RKRStatus {
2224    use crate::error::ParseError;
2225    match e {
2226        ParseError::ValidationError(ref msg) if msg.contains("device mismatch") => {
2227            RKRStatus::RKR_STATUS_DEVICE_MISMATCH
2228        }
2229        ParseError::ValidationError(ref msg)
2230            if msg.contains("no device allocator") || msg.contains("allocator") =>
2231        {
2232            RKRStatus::RKR_STATUS_DEVICE_ALLOC_UNSUPPORTED
2233        }
2234        ParseError::ValidationError(_) => RKRStatus::RKR_STATUS_VALIDATION_ERROR,
2235        _ => RKRStatus::RKR_STATUS_INTERNAL_ERROR,
2236    }
2237}
2238/// DLPack `DLDataTypeCode` values (same numerics as `dlpack.h` / dlpk).
2239///
2240/// Use with [`RKRDLDataType::code`]. Common: `RKR_DL_FLOAT = 2`, `RKR_DL_UINT = 1`.
2241pub mod rkr_dl_type_code {
2242    pub const RKR_DL_INT: u8 = 0;
2243    pub const RKR_DL_UINT: u8 = 1;
2244    pub const RKR_DL_FLOAT: u8 = 2;
2245    pub const RKR_DL_OPAQUE_HANDLE: u8 = 3;
2246    pub const RKR_DL_BFLOAT: u8 = 4;
2247    pub const RKR_DL_COMPLEX: u8 = 5;
2248    pub const RKR_DL_BOOL: u8 = 6;
2249}
2250
2251/// DLPack `DLDeviceType` values (same numerics as `dlpack.h`). CPU = 1.
2252pub mod rkr_dl_device_type {
2253    pub const RKR_DL_CPU: i32 = 1;
2254    pub const RKR_DL_CUDA: i32 = 2;
2255    pub const RKR_DL_CUDA_HOST: i32 = 3;
2256}
2257
2258/// Element type request — **layout-identical** to DLPack `DLDataType`
2259/// (`uint8_t code`, `uint8_t bits`, `uint16_t lanes`). Interchangeable with
2260/// `DLDataType` from `<dlpack/dlpack.h>` when that header is included.
2261#[repr(C)]
2262#[derive(Clone, Copy, Debug)]
2263pub struct RKRDLDataType {
2264    /// `DLDataTypeCode` (e.g. [`rkr_dl_type_code::RKR_DL_FLOAT`]).
2265    pub code: u8,
2266    /// Bit width (32 or 64 for float sections today).
2267    pub bits: u8,
2268    /// Vector lanes (must be 1 for current exports).
2269    pub lanes: u16,
2270}
2271
2272/// Device request — **layout-identical** to DLPack `DLDevice`
2273/// (`DLDeviceType device_type`, `int32_t device_id`).
2274#[repr(C)]
2275#[derive(Clone, Copy, Debug)]
2276pub struct RKRDLDevice {
2277    /// `DLDeviceType` (e.g. [`rkr_dl_device_type::RKR_DL_CPU`]).
2278    pub device_type: i32,
2279    pub device_id: i32,
2280}
2281
2282/// Options for DLPack export: requested **DLPack** `DLDataType` + `DLDevice`.
2283///
2284/// Pass to `*_dlpack_ex`. NULL → `kDLFloat` / 64 / lanes 1 on **CPU**.
2285///
2286/// **Dtype (CPU):** any combination dlpk can host from converted CON data —
2287/// signed/unsigned ints (8/16/32/64), IEEE floats (32/64), and bool (8-bit
2288/// DLPack convention). Values are cast from on-disk binary64 (or u64 for atom
2289/// ids). Complex / bfloat / float8 / opaque / multi-lane types return
2290/// `RKR_STATUS_VALIDATION_ERROR` until implemented.
2291///
2292/// **Device:** `kDLCPU` always; with `--features cuda`, `kDLCUDA` performs H2D
2293/// into real device memory then exports DLPack. Other devices return
2294/// `RKR_STATUS_FEATURE_DISABLED` so callers can feature-detect.
2295#[repr(C)]
2296#[derive(Clone, Copy, Debug)]
2297pub struct RKRDlpackExportOptions {
2298    /// Requested element type (DLPack `DLDataType` layout).
2299    pub dtype: RKRDLDataType,
2300    /// Requested placement (DLPack `DLDevice` layout).
2301    pub device: RKRDLDevice,
2302}
2303
2304impl Default for RKRDlpackExportOptions {
2305    fn default() -> Self {
2306        Self {
2307            dtype: RKRDLDataType {
2308                code: rkr_dl_type_code::RKR_DL_FLOAT,
2309                bits: 64,
2310                lanes: 1,
2311            },
2312            device: RKRDLDevice {
2313                device_type: rkr_dl_device_type::RKR_DL_CPU,
2314                device_id: 0,
2315            },
2316        }
2317    }
2318}
2319
2320/// Resolve options; NULL → defaults.
2321/// CPU always; CUDA accepted when built with `--features cuda` (H2D export).
2322fn resolve_dlpack_opts(
2323    opts: *const RKRDlpackExportOptions,
2324) -> Result<RKRDlpackExportOptions, RKRStatus> {
2325    let o = if opts.is_null() {
2326        RKRDlpackExportOptions::default()
2327    } else {
2328        unsafe { *opts }
2329    };
2330    let dt = o.device.device_type;
2331    if dt == rkr_dl_device_type::RKR_DL_CPU {
2332        // ok
2333    } else if dt == rkr_dl_device_type::RKR_DL_CUDA {
2334        #[cfg(not(feature = "cuda"))]
2335        {
2336            return Err(RKRStatus::RKR_STATUS_FEATURE_DISABLED);
2337        }
2338        #[cfg(feature = "cuda")]
2339        {
2340            // accepted — H2D in frame as_dlpack / storage layer
2341        }
2342    } else {
2343        return Err(RKRStatus::RKR_STATUS_FEATURE_DISABLED);
2344    }
2345    if o.dtype.lanes != 1 {
2346        return Err(RKRStatus::RKR_STATUS_VALIDATION_ERROR);
2347    }
2348    Ok(o)
2349}
2350
2351fn finish_dlpack_tensor<E: std::fmt::Display>(
2352    result: Result<dlpk::DLPackTensor, E>,
2353    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2354) -> RKRStatus {
2355    match result {
2356        Ok(tensor) => {
2357            let raw = tensor.into_raw();
2358            unsafe {
2359                *out_tensor = raw.as_ptr();
2360            }
2361            RKRStatus::RKR_STATUS_SUCCESS
2362        }
2363        Err(e) => map_dlpack_err(crate::error::ParseError::ValidationError(format!(
2364            "DLPack export failed: {e}"
2365        ))),
2366    }
2367}
2368
2369/// Cast CON `f64` samples into a DLPack tensor with the requested dtype (CPU).
2370/// `shape` is 1-D `[n]` or 2-D `[rows, cols]` with `rows * cols == data.len()`.
2371fn export_f64_slice_as_dlpack(
2372    data: &[f64],
2373    shape: &[usize],
2374    dtype: RKRDLDataType,
2375    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2376) -> RKRStatus {
2377    use rkr_dl_type_code::*;
2378    if dtype.lanes != 1 {
2379        return RKRStatus::RKR_STATUS_VALIDATION_ERROR;
2380    }
2381    macro_rules! arc_export {
2382        ($ty:ty, $map:expr) => {{
2383            let v: Vec<$ty> = data.iter().map($map).collect();
2384            match *shape {
2385                [r, c] => match ndarray::ArcArray2::from_shape_vec((r, c), v) {
2386                    Ok(a) => dlpk::DLPackTensor::try_from(a),
2387                    Err(_) => return RKRStatus::RKR_STATUS_VALIDATION_ERROR,
2388                },
2389                [_] => dlpk::DLPackTensor::try_from(ndarray::ArcArray1::from_vec(v)),
2390                _ => return RKRStatus::RKR_STATUS_VALIDATION_ERROR,
2391            }
2392        }};
2393    }
2394    let tensor = match (dtype.code, dtype.bits) {
2395        (RKR_DL_FLOAT, 64) => arc_export!(f64, |&x| x),
2396        (RKR_DL_FLOAT, 32) => arc_export!(f32, |&x| x as f32),
2397        (RKR_DL_INT, 8) => arc_export!(i8, |&x| x as i8),
2398        (RKR_DL_INT, 16) => arc_export!(i16, |&x| x as i16),
2399        (RKR_DL_INT, 32) => arc_export!(i32, |&x| x as i32),
2400        (RKR_DL_INT, 64) => arc_export!(i64, |&x| x as i64),
2401        (RKR_DL_UINT, 8) => arc_export!(u8, |&x| x as u8),
2402        (RKR_DL_UINT, 16) => arc_export!(u16, |&x| x as u16),
2403        (RKR_DL_UINT, 32) => arc_export!(u32, |&x| x as u32),
2404        (RKR_DL_UINT, 64) => arc_export!(u64, |&x| x as u64),
2405        (RKR_DL_BOOL, 8) => {
2406            // bool has no ndarray Zero; use Vec → DLPack (1-D length = element count)
2407            let v: Vec<bool> = data.iter().map(|&x| x != 0.0).collect();
2408            return finish_dlpack_tensor(dlpk::DLPackTensor::try_from(v), out_tensor);
2409        }
2410        // Complex / bfloat / float8 / opaque: not hosted from CON f64 yet
2411        _ => return RKRStatus::RKR_STATUS_VALIDATION_ERROR,
2412    };
2413    finish_dlpack_tensor(tensor, out_tensor)
2414}
2415
2416fn export_owned_array2_dlpack_opts(
2417    arr: &ndarray::ArcArray2<f64>,
2418    opts: &RKRDlpackExportOptions,
2419    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2420) -> RKRStatus {
2421    let (r, c) = arr.dim();
2422    let flat: Vec<f64> = arr.iter().copied().collect();
2423    if opts.device.device_type == rkr_dl_device_type::RKR_DL_CUDA {
2424        #[cfg(feature = "cuda")]
2425        {
2426            // H2D into real device memory (f64 only for CUDA path here).
2427            if opts.dtype.code != rkr_dl_type_code::RKR_DL_FLOAT || opts.dtype.bits != 64 {
2428                return RKRStatus::RKR_STATUS_VALIDATION_ERROR;
2429            }
2430            return finish_dlpack_tensor(
2431                crate::cuda_array::export_host_f64_as_cuda_dlpack(
2432                    &[r, c],
2433                    &flat,
2434                    opts.device.device_id,
2435                ),
2436                out_tensor,
2437            );
2438        }
2439        #[cfg(not(feature = "cuda"))]
2440        {
2441            return RKRStatus::RKR_STATUS_FEATURE_DISABLED;
2442        }
2443    }
2444    export_f64_slice_as_dlpack(&flat, &[r, c], opts.dtype, out_tensor)
2445}
2446
2447fn export_owned_array1_f64_dlpack_opts(
2448    arr: &ndarray::ArcArray1<f64>,
2449    opts: &RKRDlpackExportOptions,
2450    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2451) -> RKRStatus {
2452    let flat = arr.to_vec();
2453    let n = flat.len();
2454    if opts.device.device_type == rkr_dl_device_type::RKR_DL_CUDA {
2455        #[cfg(feature = "cuda")]
2456        {
2457            if opts.dtype.code != rkr_dl_type_code::RKR_DL_FLOAT || opts.dtype.bits != 64 {
2458                return RKRStatus::RKR_STATUS_VALIDATION_ERROR;
2459            }
2460            return finish_dlpack_tensor(
2461                crate::cuda_array::export_host_f64_as_cuda_dlpack(
2462                    &[n],
2463                    &flat,
2464                    opts.device.device_id,
2465                ),
2466                out_tensor,
2467            );
2468        }
2469        #[cfg(not(feature = "cuda"))]
2470        {
2471            return RKRStatus::RKR_STATUS_FEATURE_DISABLED;
2472        }
2473    }
2474    export_f64_slice_as_dlpack(&flat, &[n], opts.dtype, out_tensor)
2475}
2476
2477fn export_owned_array1_u64_dlpack(
2478    arr: &ndarray::ArcArray1<u64>,
2479    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2480) -> RKRStatus {
2481    finish_dlpack_tensor(dlpk::DLPackTensor::try_from(arr.clone()), out_tensor)
2482}
2483
2484/// Atom ids: default uint64; or cast via f64 path when `opts.dtype` requests another host type.
2485fn export_owned_array1_u64_dlpack_opts(
2486    arr: &ndarray::ArcArray1<u64>,
2487    opts: &RKRDlpackExportOptions,
2488    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2489) -> RKRStatus {
2490    use rkr_dl_type_code::*;
2491    if opts.dtype.code == RKR_DL_UINT && opts.dtype.bits == 64 && opts.dtype.lanes == 1 {
2492        return export_owned_array1_u64_dlpack(arr, out_tensor);
2493    }
2494    let as_f64: Vec<f64> = arr.iter().map(|&x| x as f64).collect();
2495    let n = as_f64.len();
2496    export_f64_slice_as_dlpack(&as_f64, &[n], opts.dtype, out_tensor)
2497}
2498/// Export builder positions as a DLPack-managed tensor.
2499///
2500/// On success the caller-supplied `*out_tensor` is set to a newly-
2501/// allocated `DLManagedTensorVersioned*` that owns a clone of the
2502/// builder's `(N, 3) f64` row-major positions buffer. The caller MUST
2503/// invoke `(*out_tensor)->deleter(*out_tensor)` to release it.
2504///
2505/// # Safety
2506/// `builder_handle` must be a valid builder handle; `out_tensor` must
2507/// be a valid pointer to a writable `*mut DLManagedTensorVersioned`.
2508#[unsafe(no_mangle)]
2509pub unsafe extern "C" fn rkr_frame_builder_positions_dlpack(
2510    builder_handle: *const RKRConFrameBuilder,
2511    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2512) -> RKRStatus {
2513    unsafe { rkr_frame_builder_positions_dlpack_ex(builder_handle, std::ptr::null(), out_tensor) }
2514}
2515
2516/// Like [`rkr_frame_builder_positions_dlpack`] with explicit precision/device.
2517///
2518/// `opts` may be NULL (float64 / CPU). See [`RKRDlpackExportOptions`].
2519///
2520/// # Safety
2521/// Same as the non-`_ex` entry; `opts` must be null or point at a valid struct.
2522#[unsafe(no_mangle)]
2523pub unsafe extern "C" fn rkr_frame_builder_positions_dlpack_ex(
2524    builder_handle: *const RKRConFrameBuilder,
2525    opts: *const RKRDlpackExportOptions,
2526    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2527) -> RKRStatus {
2528    if builder_handle.is_null() || out_tensor.is_null() {
2529        return RKRStatus::RKR_STATUS_NULL_POINTER;
2530    }
2531    let o = match resolve_dlpack_opts(opts) {
2532        Ok(o) => o,
2533        Err(st) => return st,
2534    };
2535    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2536    export_owned_array2_dlpack_opts(builder.positions_2d_ref(), &o, out_tensor)
2537}
2538
2539/// Export builder velocities as a DLPack-managed tensor.
2540///
2541/// Returns `RKR_STATUS_SECTION_ABSENT` if the velocities section is not
2542/// declared; otherwise `(N, 3) f64`. See positions_dlpack for ownership
2543/// semantics.
2544///
2545/// # Safety
2546/// `builder_handle` must be a valid builder handle; `out_tensor` must
2547/// be a valid pointer to a writable `*mut DLManagedTensorVersioned`.
2548#[unsafe(no_mangle)]
2549pub unsafe extern "C" fn rkr_frame_builder_velocities_dlpack(
2550    builder_handle: *const RKRConFrameBuilder,
2551    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2552) -> RKRStatus {
2553    unsafe { rkr_frame_builder_velocities_dlpack_ex(builder_handle, std::ptr::null(), out_tensor) }
2554}
2555
2556#[unsafe(no_mangle)]
2557pub unsafe extern "C" fn rkr_frame_builder_velocities_dlpack_ex(
2558    builder_handle: *const RKRConFrameBuilder,
2559    opts: *const RKRDlpackExportOptions,
2560    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2561) -> RKRStatus {
2562    if builder_handle.is_null() || out_tensor.is_null() {
2563        return RKRStatus::RKR_STATUS_NULL_POINTER;
2564    }
2565    let o = match resolve_dlpack_opts(opts) {
2566        Ok(o) => o,
2567        Err(st) => return st,
2568    };
2569    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2570    if !builder.has_velocities_section() {
2571        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
2572    }
2573    export_owned_array2_dlpack_opts(builder.velocities_2d_ref(), &o, out_tensor)
2574}
2575
2576/// Export builder forces as a DLPack-managed tensor.
2577///
2578/// Returns `RKR_STATUS_SECTION_ABSENT` if the forces section is not
2579/// declared.
2580///
2581/// # Safety
2582/// `builder_handle` must be a valid builder handle; `out_tensor` must
2583/// be a valid pointer to a writable `*mut DLManagedTensorVersioned`.
2584#[unsafe(no_mangle)]
2585pub unsafe extern "C" fn rkr_frame_builder_forces_dlpack(
2586    builder_handle: *const RKRConFrameBuilder,
2587    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2588) -> RKRStatus {
2589    unsafe { rkr_frame_builder_forces_dlpack_ex(builder_handle, std::ptr::null(), out_tensor) }
2590}
2591
2592#[unsafe(no_mangle)]
2593pub unsafe extern "C" fn rkr_frame_builder_forces_dlpack_ex(
2594    builder_handle: *const RKRConFrameBuilder,
2595    opts: *const RKRDlpackExportOptions,
2596    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2597) -> RKRStatus {
2598    if builder_handle.is_null() || out_tensor.is_null() {
2599        return RKRStatus::RKR_STATUS_NULL_POINTER;
2600    }
2601    let o = match resolve_dlpack_opts(opts) {
2602        Ok(o) => o,
2603        Err(st) => return st,
2604    };
2605    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2606    if !builder.has_forces_section() {
2607        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
2608    }
2609    export_owned_array2_dlpack_opts(builder.forces_2d_ref(), &o, out_tensor)
2610}
2611
2612/// Export builder displacements as a DLPack-managed tensor.
2613///
2614/// Returns `RKR_STATUS_SECTION_ABSENT` if the displacements section is not
2615/// declared.
2616///
2617/// # Safety
2618/// `builder_handle` must be a valid builder handle; `out_tensor` must
2619/// be a valid pointer to a writable `*mut DLManagedTensorVersioned`.
2620#[unsafe(no_mangle)]
2621pub unsafe extern "C" fn rkr_frame_builder_displacements_dlpack(
2622    builder_handle: *const RKRConFrameBuilder,
2623    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2624) -> RKRStatus {
2625    unsafe {
2626        rkr_frame_builder_displacements_dlpack_ex(builder_handle, std::ptr::null(), out_tensor)
2627    }
2628}
2629
2630/// Like [`rkr_frame_builder_displacements_dlpack`] with export options
2631/// (NULL `opts` selects float64 on CPU).
2632///
2633/// # Safety
2634/// Same contract as [`rkr_frame_builder_displacements_dlpack`]; `opts` is
2635/// NULL or points to a valid `RKRDlpackExportOptions`.
2636#[unsafe(no_mangle)]
2637pub unsafe extern "C" fn rkr_frame_builder_displacements_dlpack_ex(
2638    builder_handle: *const RKRConFrameBuilder,
2639    opts: *const RKRDlpackExportOptions,
2640    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2641) -> RKRStatus {
2642    if builder_handle.is_null() || out_tensor.is_null() {
2643        return RKRStatus::RKR_STATUS_NULL_POINTER;
2644    }
2645    let o = match resolve_dlpack_opts(opts) {
2646        Ok(o) => o,
2647        Err(st) => return st,
2648    };
2649    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2650    if !builder.has_displacements_section() {
2651        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
2652    }
2653    export_owned_array2_dlpack_opts(builder.displacements_2d_ref(), &o, out_tensor)
2654}
2655
2656/// Export builder spreads as a DLPack-managed tensor.
2657///
2658/// Returns `RKR_STATUS_SECTION_ABSENT` if the spreads section is not
2659/// declared.
2660///
2661/// # Safety
2662/// `builder_handle` must be a valid builder handle; `out_tensor` must
2663/// be a valid pointer to a writable `*mut DLManagedTensorVersioned`.
2664#[unsafe(no_mangle)]
2665pub unsafe extern "C" fn rkr_frame_builder_spreads_dlpack(
2666    builder_handle: *const RKRConFrameBuilder,
2667    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2668) -> RKRStatus {
2669    unsafe { rkr_frame_builder_spreads_dlpack_ex(builder_handle, std::ptr::null(), out_tensor) }
2670}
2671
2672/// Like [`rkr_frame_builder_spreads_dlpack`] with export options
2673/// (NULL `opts` selects float64 on CPU).
2674///
2675/// # Safety
2676/// Same contract as [`rkr_frame_builder_spreads_dlpack`]; `opts` is
2677/// NULL or points to a valid `RKRDlpackExportOptions`.
2678#[unsafe(no_mangle)]
2679pub unsafe extern "C" fn rkr_frame_builder_spreads_dlpack_ex(
2680    builder_handle: *const RKRConFrameBuilder,
2681    opts: *const RKRDlpackExportOptions,
2682    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2683) -> RKRStatus {
2684    if builder_handle.is_null() || out_tensor.is_null() {
2685        return RKRStatus::RKR_STATUS_NULL_POINTER;
2686    }
2687    let o = match resolve_dlpack_opts(opts) {
2688        Ok(o) => o,
2689        Err(st) => return st,
2690    };
2691    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2692    if !builder.has_spreads_section() {
2693        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
2694    }
2695    export_owned_array2_dlpack_opts(builder.spreads_2d_ref(), &o, out_tensor)
2696}
2697
2698/// Export builder per-atom energies as a DLPack-managed tensor.
2699///
2700/// Returns `RKR_STATUS_SECTION_ABSENT` if the energies section is not
2701/// declared; otherwise `(N,) f64`.
2702///
2703/// # Safety
2704/// `builder_handle` must be a valid builder handle; `out_tensor` must
2705/// be a valid pointer to a writable `*mut DLManagedTensorVersioned`.
2706#[unsafe(no_mangle)]
2707pub unsafe extern "C" fn rkr_frame_builder_atom_energies_dlpack(
2708    builder_handle: *const RKRConFrameBuilder,
2709    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2710) -> RKRStatus {
2711    unsafe {
2712        rkr_frame_builder_atom_energies_dlpack_ex(builder_handle, std::ptr::null(), out_tensor)
2713    }
2714}
2715
2716#[unsafe(no_mangle)]
2717pub unsafe extern "C" fn rkr_frame_builder_atom_energies_dlpack_ex(
2718    builder_handle: *const RKRConFrameBuilder,
2719    opts: *const RKRDlpackExportOptions,
2720    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2721) -> RKRStatus {
2722    if builder_handle.is_null() || out_tensor.is_null() {
2723        return RKRStatus::RKR_STATUS_NULL_POINTER;
2724    }
2725    let o = match resolve_dlpack_opts(opts) {
2726        Ok(o) => o,
2727        Err(st) => return st,
2728    };
2729    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2730    if !builder.has_energies_section() {
2731        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
2732    }
2733    export_owned_array1_f64_dlpack_opts(builder.atom_energies_1d_ref(), &o, out_tensor)
2734}
2735
2736/// Export builder per-atom masses as a DLPack-managed tensor `(N,) f64`.
2737///
2738/// # Safety
2739/// `builder_handle` must be a valid builder handle; `out_tensor` must
2740/// be a valid pointer to a writable `*mut DLManagedTensorVersioned`.
2741#[unsafe(no_mangle)]
2742pub unsafe extern "C" fn rkr_frame_builder_masses_dlpack(
2743    builder_handle: *const RKRConFrameBuilder,
2744    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2745) -> RKRStatus {
2746    unsafe { rkr_frame_builder_masses_dlpack_ex(builder_handle, std::ptr::null(), out_tensor) }
2747}
2748
2749#[unsafe(no_mangle)]
2750pub unsafe extern "C" fn rkr_frame_builder_masses_dlpack_ex(
2751    builder_handle: *const RKRConFrameBuilder,
2752    opts: *const RKRDlpackExportOptions,
2753    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2754) -> RKRStatus {
2755    if builder_handle.is_null() || out_tensor.is_null() {
2756        return RKRStatus::RKR_STATUS_NULL_POINTER;
2757    }
2758    let o = match resolve_dlpack_opts(opts) {
2759        Ok(o) => o,
2760        Err(st) => return st,
2761    };
2762    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2763    export_owned_array1_f64_dlpack_opts(builder.masses_1d_ref(), &o, out_tensor)
2764}
2765/// Export builder per-atom ids as a DLPack-managed tensor `(N,) u64`.
2766///
2767/// # Safety
2768/// `builder_handle` must be a valid builder handle; `out_tensor` must
2769/// be a valid pointer to a writable `*mut DLManagedTensorVersioned`.
2770#[unsafe(no_mangle)]
2771pub unsafe extern "C" fn rkr_frame_builder_atom_ids_dlpack(
2772    builder_handle: *const RKRConFrameBuilder,
2773    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2774) -> RKRStatus {
2775    unsafe { rkr_frame_builder_atom_ids_dlpack_ex(builder_handle, std::ptr::null(), out_tensor) }
2776}
2777
2778#[unsafe(no_mangle)]
2779pub unsafe extern "C" fn rkr_frame_builder_atom_ids_dlpack_ex(
2780    builder_handle: *const RKRConFrameBuilder,
2781    opts: *const RKRDlpackExportOptions,
2782    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
2783) -> RKRStatus {
2784    if builder_handle.is_null() || out_tensor.is_null() {
2785        return RKRStatus::RKR_STATUS_NULL_POINTER;
2786    }
2787    let o = match resolve_dlpack_opts(opts) {
2788        Ok(o) => o,
2789        Err(st) => return st,
2790    };
2791    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2792    export_owned_array1_u64_dlpack_opts(builder.atom_ids_1d_ref(), &o, out_tensor)
2793}
2794// ----- v0.11.1 in-process zero-copy raw-pointer FFI -------------------------
2795//
2796// The DLPack tier-3 export above clones field data into an owning tensor so
2797// the consumer can outlive the builder; this is the right contract for
2798// language-runtime / cross-process consumers (Python GC, Julia GC,
2799// inter-process exchange). For *in-process* zero-copy on the hot path
2800// (LAMMPS-style `lmp->atom->x` direct pointer access used by integrators,
2801// dynamics drivers, eOn's Matter Eigen::Map<RowMajor> views), we expose
2802// raw pointers into the builder's storage. The lifetime contract is
2803// purely caller-managed: the pointer is valid while the builder is alive
2804// and no add_atom call has grown the underlying ndarray. This mirrors
2805// the LAMMPS / OpenMM / GROMACS C-side hot path and is what makes a
2806// thin Matter wrapper over ConFrameBuilder fast.
2807//
2808// Cross-language ML consumers should use the DLPack tier above; raw
2809// pointer access is for in-process hot paths only.
2810/// Borrow the positions buffer as a raw `(N, 3) f64` row-major pointer.
2811/// Returns NULL on invalid handle. Pointer is valid until the builder
2812/// is dropped or `add_atom` reallocates.
2813///
2814/// # Safety
2815/// builder_handle must be valid; the returned pointer must not be
2816/// dereferenced after a call to add_atom on the same builder.
2817#[unsafe(no_mangle)]
2818pub unsafe extern "C" fn rkr_frame_builder_positions_data(
2819    builder_handle: *mut RKRConFrameBuilder,
2820) -> *mut f64 {
2821    if builder_handle.is_null() {
2822        return std::ptr::null_mut();
2823    }
2824    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
2825    builder
2826        .positions_view_mut()
2827        .as_slice_memory_order_mut()
2828        .map(|s| s.as_mut_ptr())
2829        .unwrap_or(std::ptr::null_mut())
2830}
2831/// Borrow the velocities buffer as a raw `(N, 3) f64` row-major pointer.
2832/// Returns NULL if the velocities section is absent or the handle is
2833/// invalid.
2834///
2835/// # Safety
2836/// Same contract as rkr_frame_builder_positions_data.
2837#[unsafe(no_mangle)]
2838pub unsafe extern "C" fn rkr_frame_builder_velocities_data(
2839    builder_handle: *mut RKRConFrameBuilder,
2840) -> *mut f64 {
2841    if builder_handle.is_null() {
2842        return std::ptr::null_mut();
2843    }
2844    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
2845    if !builder.has_velocities_section() {
2846        return std::ptr::null_mut();
2847    }
2848    let slice = builder.velocities_mut();
2849    if slice.is_empty() {
2850        std::ptr::null_mut()
2851    } else {
2852        slice.as_mut_ptr()
2853    }
2854}
2855/// Borrow the forces buffer as a raw `(N, 3) f64` row-major pointer.
2856/// Returns NULL if the forces section is absent.
2857///
2858/// # Safety
2859/// Same contract as rkr_frame_builder_positions_data.
2860#[unsafe(no_mangle)]
2861pub unsafe extern "C" fn rkr_frame_builder_forces_data(
2862    builder_handle: *mut RKRConFrameBuilder,
2863) -> *mut f64 {
2864    if builder_handle.is_null() {
2865        return std::ptr::null_mut();
2866    }
2867    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
2868    if !builder.has_forces_section() {
2869        return std::ptr::null_mut();
2870    }
2871    let slice = builder.forces_mut();
2872    if slice.is_empty() {
2873        std::ptr::null_mut()
2874    } else {
2875        slice.as_mut_ptr()
2876    }
2877}
2878/// Borrow the displacements buffer as a raw `(N, 3) f64` row-major pointer.
2879/// Returns NULL if the displacements section is absent.
2880///
2881/// # Safety
2882/// Same contract as rkr_frame_builder_positions_data.
2883#[unsafe(no_mangle)]
2884pub unsafe extern "C" fn rkr_frame_builder_displacements_data(
2885    builder_handle: *mut RKRConFrameBuilder,
2886) -> *mut f64 {
2887    if builder_handle.is_null() {
2888        return std::ptr::null_mut();
2889    }
2890    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
2891    if !builder.has_displacements_section() {
2892        return std::ptr::null_mut();
2893    }
2894    let slice = builder.displacements_mut();
2895    if slice.is_empty() {
2896        std::ptr::null_mut()
2897    } else {
2898        slice.as_mut_ptr()
2899    }
2900}
2901/// Borrow the spreads buffer as a raw `(N, 3) f64` row-major pointer.
2902/// Returns NULL if the spreads section is absent.
2903///
2904/// # Safety
2905/// Same contract as rkr_frame_builder_positions_data.
2906#[unsafe(no_mangle)]
2907pub unsafe extern "C" fn rkr_frame_builder_spreads_data(
2908    builder_handle: *mut RKRConFrameBuilder,
2909) -> *mut f64 {
2910    if builder_handle.is_null() {
2911        return std::ptr::null_mut();
2912    }
2913    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
2914    if !builder.has_spreads_section() {
2915        return std::ptr::null_mut();
2916    }
2917    let slice = builder.spreads_mut();
2918    if slice.is_empty() {
2919        std::ptr::null_mut()
2920    } else {
2921        slice.as_mut_ptr()
2922    }
2923}
2924/// Borrow the per-atom energies buffer as a raw `(N,) f64` pointer.
2925/// Returns NULL if the energies section is absent.
2926///
2927/// # Safety
2928/// Same contract as rkr_frame_builder_positions_data.
2929#[unsafe(no_mangle)]
2930pub unsafe extern "C" fn rkr_frame_builder_atom_energies_data(
2931    builder_handle: *mut RKRConFrameBuilder,
2932) -> *mut f64 {
2933    if builder_handle.is_null() {
2934        return std::ptr::null_mut();
2935    }
2936    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
2937    if !builder.has_energies_section() {
2938        return std::ptr::null_mut();
2939    }
2940    let slice = builder.atom_energies_mut();
2941    if slice.is_empty() {
2942        std::ptr::null_mut()
2943    } else {
2944        slice.as_mut_ptr()
2945    }
2946}
2947/// Borrow the per-atom masses buffer as a raw `(N,) f64` pointer.
2948///
2949/// # Safety
2950/// Same contract as rkr_frame_builder_positions_data.
2951#[unsafe(no_mangle)]
2952pub unsafe extern "C" fn rkr_frame_builder_masses_data(
2953    builder_handle: *mut RKRConFrameBuilder,
2954) -> *mut f64 {
2955    if builder_handle.is_null() {
2956        return std::ptr::null_mut();
2957    }
2958    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
2959    let slice = builder.masses_mut();
2960    if slice.is_empty() {
2961        std::ptr::null_mut()
2962    } else {
2963        slice.as_mut_ptr()
2964    }
2965}
2966/// Borrow the per-atom atom_ids buffer as a raw `(N,) u64` pointer.
2967///
2968/// # Safety
2969/// Same contract as rkr_frame_builder_positions_data.
2970#[unsafe(no_mangle)]
2971pub unsafe extern "C" fn rkr_frame_builder_atom_ids_data(
2972    builder_handle: *const RKRConFrameBuilder,
2973) -> *const u64 {
2974    if builder_handle.is_null() {
2975        return std::ptr::null();
2976    }
2977    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
2978    let slice = builder.atom_ids();
2979    if slice.is_empty() {
2980        std::ptr::null()
2981    } else {
2982        slice.as_ptr()
2983    }
2984}
2985// ----- end v0.11.0 in-place mutation FFI ------------------------------------
2986/// Adds an atom with optional per-axis fixed mask, velocity, and force vectors.
2987///
2988/// `velocity` and `force` are pointers to 3 contiguous f64 values, or NULL if
2989/// absent. This is the unified entry point that replaces the eight
2990/// `rkr_frame_add_atom_*` convenience functions; callers may continue using
2991/// those for source compatibility.
2992///
2993/// # Safety
2994/// builder_handle and symbol must be valid. velocity (if non-null) must point
2995/// to 3 contiguous f64 values, and force (if non-null) likewise.
2996#[unsafe(no_mangle)]
2997#[allow(clippy::too_many_arguments)]
2998pub unsafe extern "C" fn rkr_frame_add_atom_full(
2999    builder_handle: *mut RKRConFrameBuilder,
3000    symbol: *const c_char,
3001    x: f64,
3002    y: f64,
3003    z: f64,
3004    fixed_x: bool,
3005    fixed_y: bool,
3006    fixed_z: bool,
3007    atom_id: u64,
3008    mass: f64,
3009    velocity: *const f64,
3010    force: *const f64,
3011) -> RKRStatus {
3012    let velocity = if velocity.is_null() {
3013        None
3014    } else {
3015        Some(unsafe { [*velocity, *velocity.add(1), *velocity.add(2)] })
3016    };
3017    let force = if force.is_null() {
3018        None
3019    } else {
3020        Some(unsafe { [*force, *force.add(1), *force.add(2)] })
3021    };
3022    unsafe {
3023        add_builder_atom(
3024            builder_handle,
3025            symbol,
3026            x,
3027            y,
3028            z,
3029            [fixed_x, fixed_y, fixed_z],
3030            atom_id,
3031            mass,
3032            velocity,
3033            force,
3034        )
3035    }
3036}
3037/// Creates a new frame builder with the given cell dimensions, angles,
3038/// and header lines.
3039///
3040/// `prebox1` is accepted for source compatibility but ignored: the
3041/// JSON metadata line is regenerated by the writer from the builder's
3042/// `spec_version`, `metadata`, and `sections`. Pass NULL or any string.
3043/// The caller OWNS the returned pointer and MUST call
3044/// `free_rkr_frame_builder` or consume it via `rkr_frame_builder_build`.
3045/// Returns NULL on error.
3046///
3047/// # Safety
3048/// cell and angles must point to 3 doubles. prebox0, postbox0, and
3049/// postbox1 must be NULL or valid null-terminated strings; prebox1 is
3050/// not dereferenced. The caller takes ownership of the returned
3051/// builder.
3052#[unsafe(no_mangle)]
3053pub unsafe extern "C" fn rkr_frame_new(
3054    cell: *const f64,
3055    angles: *const f64,
3056    prebox0: *const c_char,
3057    prebox1: *const c_char,
3058    postbox0: *const c_char,
3059    postbox1: *const c_char,
3060) -> *mut RKRConFrameBuilder {
3061    if cell.is_null() || angles.is_null() {
3062        return ptr::null_mut();
3063    }
3064    let cell_arr = unsafe { [*cell, *cell.add(1), *cell.add(2)] };
3065    let angles_arr = unsafe { [*angles, *angles.add(1), *angles.add(2)] };
3066    let get_str = |p: *const c_char| -> String {
3067        if p.is_null() {
3068            String::new()
3069        } else {
3070            unsafe { CStr::from_ptr(p) }
3071                .to_str()
3072                .unwrap_or("")
3073                .to_string()
3074        }
3075    };
3076    // prebox1 is the JSON metadata slot, regenerated on write from
3077    // metadata + sections; it is accepted for ABI continuity but ignored.
3078    let _ = get_str(prebox1);
3079    let mut builder = ConFrameBuilder::new(cell_arr, angles_arr);
3080    builder
3081        .prebox_header(get_str(prebox0))
3082        .postbox_header([get_str(postbox0), get_str(postbox1)]);
3083    Box::into_raw(Box::new(builder)) as *mut RKRConFrameBuilder
3084}
3085/// Parses and sets JSON metadata on an existing frame builder.
3086/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3087///
3088/// # Safety
3089/// builder_handle and metadata_json must be valid.
3090#[unsafe(no_mangle)]
3091pub unsafe extern "C" fn rkr_frame_builder_set_metadata_json(
3092    builder_handle: *mut RKRConFrameBuilder,
3093    metadata_json: *const c_char,
3094) -> RKRStatus {
3095    if builder_handle.is_null() || metadata_json.is_null() {
3096        return RKRStatus::RKR_STATUS_NULL_POINTER;
3097    }
3098    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
3099    let metadata_json = match unsafe { CStr::from_ptr(metadata_json).to_str() } {
3100        Ok(s) => s,
3101        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
3102    };
3103    match builder.set_metadata_json(metadata_json) {
3104        Ok(()) => RKRStatus::RKR_STATUS_SUCCESS,
3105        Err(_) => RKRStatus::RKR_STATUS_INVALID_JSON,
3106    }
3107}
3108/// Sets the units object on an existing frame builder.
3109/// `units_json` is a JSON object, for example
3110/// `{"length":"angstrom","energy":"eV","mass":"amu","time":"(amu*angstrom^2/eV)^0.5"}`.
3111/// Returns `RKR_STATUS_SUCCESS` on success, `RKR_STATUS_INVALID_JSON` when
3112/// the text is not a JSON object, or `RKR_STATUS_VALIDATION_ERROR` when a
3113/// unit expression is not valid.
3114///
3115/// # Safety
3116/// builder_handle and units_json must be valid.
3117#[unsafe(no_mangle)]
3118pub unsafe extern "C" fn rkr_frame_builder_set_units(
3119    builder_handle: *mut RKRConFrameBuilder,
3120    units_json: *const c_char,
3121) -> RKRStatus {
3122    if builder_handle.is_null() || units_json.is_null() {
3123        return RKRStatus::RKR_STATUS_NULL_POINTER;
3124    }
3125    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
3126    let units_json = match unsafe { CStr::from_ptr(units_json).to_str() } {
3127        Ok(s) => s,
3128        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
3129    };
3130    let units: serde_json::Value = match serde_json::from_str(units_json) {
3131        Ok(value) => value,
3132        Err(_) => return RKRStatus::RKR_STATUS_INVALID_JSON,
3133    };
3134    match builder.set_units(units) {
3135        Ok(()) => RKRStatus::RKR_STATUS_SUCCESS,
3136        Err(_) => RKRStatus::RKR_STATUS_VALIDATION_ERROR,
3137    }
3138}
3139/// Sets `con_spec_minor` on an existing frame builder.
3140/// A v3 frame built without this call carries minor 1.
3141/// Returns `RKR_STATUS_SUCCESS` on success.
3142///
3143/// # Safety
3144/// builder_handle must be valid.
3145#[unsafe(no_mangle)]
3146pub unsafe extern "C" fn rkr_frame_builder_set_con_spec_minor(
3147    builder_handle: *mut RKRConFrameBuilder,
3148    minor: u32,
3149) -> RKRStatus {
3150    if builder_handle.is_null() {
3151        return RKRStatus::RKR_STATUS_NULL_POINTER;
3152    }
3153    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
3154    builder.set_con_spec_minor(minor);
3155    RKRStatus::RKR_STATUS_SUCCESS
3156}
3157/// Sets a numeric metadata key on an existing frame builder.
3158/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3159///
3160/// # Safety
3161/// builder_handle and key must be valid.
3162#[unsafe(no_mangle)]
3163pub unsafe extern "C" fn rkr_frame_builder_set_scalar_metadata(
3164    builder_handle: *mut RKRConFrameBuilder,
3165    key: *const c_char,
3166    value: f64,
3167) -> RKRStatus {
3168    if builder_handle.is_null() || key.is_null() {
3169        return RKRStatus::RKR_STATUS_NULL_POINTER;
3170    }
3171    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
3172    let key = match unsafe { CStr::from_ptr(key).to_str() } {
3173        Ok(s) => s,
3174        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
3175    };
3176    builder.set_scalar_metadata(key, value);
3177    RKRStatus::RKR_STATUS_SUCCESS
3178}
3179/// Sets a string metadata key on an existing frame builder.
3180/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3181///
3182/// # Safety
3183/// builder_handle, key, and value must be valid.
3184#[unsafe(no_mangle)]
3185pub unsafe extern "C" fn rkr_frame_builder_set_string_metadata(
3186    builder_handle: *mut RKRConFrameBuilder,
3187    key: *const c_char,
3188    value: *const c_char,
3189) -> RKRStatus {
3190    if builder_handle.is_null() || key.is_null() || value.is_null() {
3191        return RKRStatus::RKR_STATUS_NULL_POINTER;
3192    }
3193    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
3194    let key = match unsafe { CStr::from_ptr(key).to_str() } {
3195        Ok(s) => s,
3196        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
3197    };
3198    let value = match unsafe { CStr::from_ptr(value).to_str() } {
3199        Ok(s) => s,
3200        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
3201    };
3202    builder.set_string_metadata(key, value);
3203    RKRStatus::RKR_STATUS_SUCCESS
3204}
3205/// Sets the per-frame total energy metadata on an existing frame builder.
3206/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3207///
3208/// # Safety
3209/// builder_handle must be valid.
3210#[unsafe(no_mangle)]
3211pub unsafe extern "C" fn rkr_frame_builder_set_energy(
3212    builder_handle: *mut RKRConFrameBuilder,
3213    energy: f64,
3214) -> RKRStatus {
3215    if builder_handle.is_null() {
3216        return RKRStatus::RKR_STATUS_NULL_POINTER;
3217    }
3218    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
3219    builder.set_energy(energy);
3220    RKRStatus::RKR_STATUS_SUCCESS
3221}
3222/// Sets the zero-based frame index metadata on an existing frame builder.
3223/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3224///
3225/// # Safety
3226/// builder_handle must be valid.
3227#[unsafe(no_mangle)]
3228pub unsafe extern "C" fn rkr_frame_builder_set_frame_index(
3229    builder_handle: *mut RKRConFrameBuilder,
3230    idx: u64,
3231) -> RKRStatus {
3232    if builder_handle.is_null() {
3233        return RKRStatus::RKR_STATUS_NULL_POINTER;
3234    }
3235    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
3236    builder.set_frame_index(idx);
3237    RKRStatus::RKR_STATUS_SUCCESS
3238}
3239/// Sets the simulation time metadata on an existing frame builder.
3240/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3241///
3242/// # Safety
3243/// builder_handle must be valid.
3244#[unsafe(no_mangle)]
3245pub unsafe extern "C" fn rkr_frame_builder_set_time(
3246    builder_handle: *mut RKRConFrameBuilder,
3247    time: f64,
3248) -> RKRStatus {
3249    if builder_handle.is_null() {
3250        return RKRStatus::RKR_STATUS_NULL_POINTER;
3251    }
3252    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
3253    builder.set_time(time);
3254    RKRStatus::RKR_STATUS_SUCCESS
3255}
3256/// Sets the timestep metadata on an existing frame builder.
3257/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3258///
3259/// # Safety
3260/// builder_handle must be valid.
3261#[unsafe(no_mangle)]
3262pub unsafe extern "C" fn rkr_frame_builder_set_timestep(
3263    builder_handle: *mut RKRConFrameBuilder,
3264    dt: f64,
3265) -> RKRStatus {
3266    if builder_handle.is_null() {
3267        return RKRStatus::RKR_STATUS_NULL_POINTER;
3268    }
3269    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
3270    builder.set_timestep(dt);
3271    RKRStatus::RKR_STATUS_SUCCESS
3272}
3273/// Sets the NEB bead index metadata on an existing frame builder.
3274/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3275///
3276/// # Safety
3277/// builder_handle must be valid.
3278#[unsafe(no_mangle)]
3279pub unsafe extern "C" fn rkr_frame_builder_set_neb_bead(
3280    builder_handle: *mut RKRConFrameBuilder,
3281    bead: u64,
3282) -> RKRStatus {
3283    if builder_handle.is_null() {
3284        return RKRStatus::RKR_STATUS_NULL_POINTER;
3285    }
3286    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
3287    builder.set_neb_bead(bead);
3288    RKRStatus::RKR_STATUS_SUCCESS
3289}
3290/// Sets the NEB band index metadata on an existing frame builder.
3291/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3292///
3293/// # Safety
3294/// builder_handle must be valid.
3295#[unsafe(no_mangle)]
3296pub unsafe extern "C" fn rkr_frame_builder_set_neb_band(
3297    builder_handle: *mut RKRConFrameBuilder,
3298    band: u64,
3299) -> RKRStatus {
3300    if builder_handle.is_null() {
3301        return RKRStatus::RKR_STATUS_NULL_POINTER;
3302    }
3303    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
3304    builder.set_neb_band(band);
3305    RKRStatus::RKR_STATUS_SUCCESS
3306}
3307// -----------------------------------------------------------------------------
3308// Legacy add_atom variants (kept for source compatibility)
3309//
3310// The unified entry point is `rkr_frame_add_atom_full`, which accepts
3311// optional velocity and force pointers. The eight functions below
3312// pre-date the unified call and remain in the API for code that was
3313// written against earlier 0.x releases. New callers should prefer
3314// `rkr_frame_add_atom_full`.
3315// -----------------------------------------------------------------------------
3316/// **Deprecated**: prefer `rkr_frame_add_atom_full` with NULL velocity
3317/// and force pointers. Adds an atom (no velocity, no forces) to the
3318/// builder using a single uniform fixed flag.
3319/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3320///
3321/// # Safety
3322/// builder_handle and symbol must be valid.
3323#[unsafe(no_mangle)]
3324pub unsafe extern "C" fn rkr_frame_add_atom(
3325    builder_handle: *mut RKRConFrameBuilder,
3326    symbol: *const c_char,
3327    x: f64,
3328    y: f64,
3329    z: f64,
3330    is_fixed: bool,
3331    atom_id: u64,
3332    mass: f64,
3333) -> RKRStatus {
3334    unsafe {
3335        add_builder_atom(
3336            builder_handle,
3337            symbol,
3338            x,
3339            y,
3340            z,
3341            [is_fixed; 3],
3342            atom_id,
3343            mass,
3344            None,
3345            None,
3346        )
3347    }
3348}
3349/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom (no
3350/// velocity, no forces) using per-axis fixed flags.
3351/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3352///
3353/// # Safety
3354/// builder_handle and symbol must be valid.
3355#[unsafe(no_mangle)]
3356pub unsafe extern "C" fn rkr_frame_add_atom_with_fixed_mask(
3357    builder_handle: *mut RKRConFrameBuilder,
3358    symbol: *const c_char,
3359    x: f64,
3360    y: f64,
3361    z: f64,
3362    fixed_x: bool,
3363    fixed_y: bool,
3364    fixed_z: bool,
3365    atom_id: u64,
3366    mass: f64,
3367) -> RKRStatus {
3368    unsafe {
3369        add_builder_atom(
3370            builder_handle,
3371            symbol,
3372            x,
3373            y,
3374            z,
3375            [fixed_x, fixed_y, fixed_z],
3376            atom_id,
3377            mass,
3378            None,
3379            None,
3380        )
3381    }
3382}
3383/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
3384/// a velocity vector and a single uniform fixed flag.
3385/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3386///
3387/// # Safety
3388/// builder_handle and symbol must be valid.
3389#[unsafe(no_mangle)]
3390pub unsafe extern "C" fn rkr_frame_add_atom_with_velocity(
3391    builder_handle: *mut RKRConFrameBuilder,
3392    symbol: *const c_char,
3393    x: f64,
3394    y: f64,
3395    z: f64,
3396    is_fixed: bool,
3397    atom_id: u64,
3398    mass: f64,
3399    vx: f64,
3400    vy: f64,
3401    vz: f64,
3402) -> RKRStatus {
3403    unsafe {
3404        add_builder_atom(
3405            builder_handle,
3406            symbol,
3407            x,
3408            y,
3409            z,
3410            [is_fixed; 3],
3411            atom_id,
3412            mass,
3413            Some([vx, vy, vz]),
3414            None,
3415        )
3416    }
3417}
3418/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
3419/// a velocity vector and per-axis fixed flags.
3420/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3421///
3422/// # Safety
3423/// builder_handle and symbol must be valid.
3424#[unsafe(no_mangle)]
3425pub unsafe extern "C" fn rkr_frame_add_atom_with_velocity_fixed_mask(
3426    builder_handle: *mut RKRConFrameBuilder,
3427    symbol: *const c_char,
3428    x: f64,
3429    y: f64,
3430    z: f64,
3431    fixed_x: bool,
3432    fixed_y: bool,
3433    fixed_z: bool,
3434    atom_id: u64,
3435    mass: f64,
3436    vx: f64,
3437    vy: f64,
3438    vz: f64,
3439) -> RKRStatus {
3440    unsafe {
3441        add_builder_atom(
3442            builder_handle,
3443            symbol,
3444            x,
3445            y,
3446            z,
3447            [fixed_x, fixed_y, fixed_z],
3448            atom_id,
3449            mass,
3450            Some([vx, vy, vz]),
3451            None,
3452        )
3453    }
3454}
3455/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
3456/// a force vector and a single uniform fixed flag.
3457/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3458///
3459/// # Safety
3460/// builder_handle and symbol must be valid.
3461#[unsafe(no_mangle)]
3462pub unsafe extern "C" fn rkr_frame_add_atom_with_forces(
3463    builder_handle: *mut RKRConFrameBuilder,
3464    symbol: *const c_char,
3465    x: f64,
3466    y: f64,
3467    z: f64,
3468    is_fixed: bool,
3469    atom_id: u64,
3470    mass: f64,
3471    fx: f64,
3472    fy: f64,
3473    fz: f64,
3474) -> RKRStatus {
3475    unsafe {
3476        add_builder_atom(
3477            builder_handle,
3478            symbol,
3479            x,
3480            y,
3481            z,
3482            [is_fixed; 3],
3483            atom_id,
3484            mass,
3485            None,
3486            Some([fx, fy, fz]),
3487        )
3488    }
3489}
3490/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
3491/// a force vector and per-axis fixed flags.
3492/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3493///
3494/// # Safety
3495/// builder_handle and symbol must be valid.
3496#[unsafe(no_mangle)]
3497pub unsafe extern "C" fn rkr_frame_add_atom_with_forces_fixed_mask(
3498    builder_handle: *mut RKRConFrameBuilder,
3499    symbol: *const c_char,
3500    x: f64,
3501    y: f64,
3502    z: f64,
3503    fixed_x: bool,
3504    fixed_y: bool,
3505    fixed_z: bool,
3506    atom_id: u64,
3507    mass: f64,
3508    fx: f64,
3509    fy: f64,
3510    fz: f64,
3511) -> RKRStatus {
3512    unsafe {
3513        add_builder_atom(
3514            builder_handle,
3515            symbol,
3516            x,
3517            y,
3518            z,
3519            [fixed_x, fixed_y, fixed_z],
3520            atom_id,
3521            mass,
3522            None,
3523            Some([fx, fy, fz]),
3524        )
3525    }
3526}
3527/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
3528/// both velocity and force vectors and a single uniform fixed flag.
3529/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3530///
3531/// # Safety
3532/// builder_handle and symbol must be valid.
3533#[unsafe(no_mangle)]
3534pub unsafe extern "C" fn rkr_frame_add_atom_with_velocity_and_forces(
3535    builder_handle: *mut RKRConFrameBuilder,
3536    symbol: *const c_char,
3537    x: f64,
3538    y: f64,
3539    z: f64,
3540    is_fixed: bool,
3541    atom_id: u64,
3542    mass: f64,
3543    vx: f64,
3544    vy: f64,
3545    vz: f64,
3546    fx: f64,
3547    fy: f64,
3548    fz: f64,
3549) -> RKRStatus {
3550    unsafe {
3551        add_builder_atom(
3552            builder_handle,
3553            symbol,
3554            x,
3555            y,
3556            z,
3557            [is_fixed; 3],
3558            atom_id,
3559            mass,
3560            Some([vx, vy, vz]),
3561            Some([fx, fy, fz]),
3562        )
3563    }
3564}
3565/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
3566/// both velocity and force vectors and per-axis fixed flags.
3567/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
3568///
3569/// # Safety
3570/// builder_handle and symbol must be valid.
3571#[unsafe(no_mangle)]
3572pub unsafe extern "C" fn rkr_frame_add_atom_with_velocity_and_forces_fixed_mask(
3573    builder_handle: *mut RKRConFrameBuilder,
3574    symbol: *const c_char,
3575    x: f64,
3576    y: f64,
3577    z: f64,
3578    fixed_x: bool,
3579    fixed_y: bool,
3580    fixed_z: bool,
3581    atom_id: u64,
3582    mass: f64,
3583    vx: f64,
3584    vy: f64,
3585    vz: f64,
3586    fx: f64,
3587    fy: f64,
3588    fz: f64,
3589) -> RKRStatus {
3590    unsafe {
3591        add_builder_atom(
3592            builder_handle,
3593            symbol,
3594            x,
3595            y,
3596            z,
3597            [fixed_x, fixed_y, fixed_z],
3598            atom_id,
3599            mass,
3600            Some([vx, vy, vz]),
3601            Some([fx, fy, fz]),
3602        )
3603    }
3604}
3605/// Consumes the builder and returns a finalized RKRConFrame handle.
3606/// The builder handle is invalidated after this call.
3607/// The caller OWNS the returned frame and MUST call `free_rkr_frame`.
3608/// Returns NULL on error, including
3609/// [`crate::error::ParseError::MassMismatch`].
3610///
3611/// # Safety
3612/// builder_handle must be valid. The caller takes ownership of the returned frame.
3613#[unsafe(no_mangle)]
3614pub unsafe extern "C" fn rkr_frame_builder_build(
3615    builder_handle: *mut RKRConFrameBuilder,
3616) -> *mut RKRConFrame {
3617    if builder_handle.is_null() {
3618        return ptr::null_mut();
3619    }
3620    let builder = unsafe { *Box::from_raw(builder_handle as *mut ConFrameBuilder) };
3621    match builder.build() {
3622        Ok(frame) => Box::into_raw(Box::new(frame)) as *mut RKRConFrame,
3623        Err(_) => ptr::null_mut(),
3624    }
3625}
3626/// Frees a frame builder without building.
3627///
3628/// # Safety
3629/// builder_handle must be valid or null.
3630#[unsafe(no_mangle)]
3631pub unsafe extern "C" fn free_rkr_frame_builder(builder_handle: *mut RKRConFrameBuilder) {
3632    if !builder_handle.is_null() {
3633        let _ = unsafe { Box::from_raw(builder_handle as *mut ConFrameBuilder) };
3634    }
3635}
3636/// Cheap, copy-on-write clone of a frame builder. Returned handle owns
3637/// a new `ConFrameBuilder` whose per-atom buffers share storage with
3638/// the source via ArcArray; any subsequent mutation triggers a
3639/// per-buffer copy-on-write so writes do not leak across clones.
3640///
3641/// Intended for downstream consumers (NEB image bulk allocation,
3642/// trajectory snapshots) that need many builders carrying the same
3643/// per-atom data without paying N copies up-front. Returns NULL on
3644/// NULL input.
3645///
3646/// The caller OWNS the returned handle and MUST call
3647/// `free_rkr_frame_builder` (or consume via `rkr_frame_builder_build`).
3648///
3649/// # Safety
3650/// `builder_handle` must be a valid pointer returned by `rkr_frame_new`
3651/// (or by an earlier `rkr_frame_builder_clone`) and not yet freed.
3652#[unsafe(no_mangle)]
3653pub unsafe extern "C" fn rkr_frame_builder_clone(
3654    builder_handle: *const RKRConFrameBuilder,
3655) -> *mut RKRConFrameBuilder {
3656    if builder_handle.is_null() {
3657        return std::ptr::null_mut();
3658    }
3659    let builder = unsafe { &*(builder_handle as *const ConFrameBuilder) };
3660    let cloned = builder.clone();
3661    Box::into_raw(Box::new(cloned)) as *mut RKRConFrameBuilder
3662}
3663/// Creates a new gzip-compressed frame writer for the specified file.
3664/// The caller OWNS the returned pointer and MUST call `free_rkr_writer`.
3665///
3666/// # Safety
3667/// filename_c must be valid. The caller takes ownership of the returned writer.
3668#[unsafe(no_mangle)]
3669pub unsafe extern "C" fn create_writer_gzip_c(filename_c: *const c_char) -> *mut RKRConFrameWriter {
3670    let filename = match unsafe { cstr_path(filename_c) } {
3671        Some(s) => s,
3672        None => return ptr::null_mut(),
3673    };
3674    match crate::compression::gzip_writer(Path::new(filename)) {
3675        Ok(encoder) => into_rkr_writer(Box::new(encoder), None),
3676        Err(_) => ptr::null_mut(),
3677    }
3678}
3679/// Creates a gzip-compressed frame writer with a custom floating-point
3680/// precision. The caller OWNS the returned pointer and MUST call
3681/// `free_rkr_writer`.
3682///
3683/// # Safety
3684/// filename_c must be valid. The caller takes ownership of the returned writer.
3685#[unsafe(no_mangle)]
3686pub unsafe extern "C" fn create_writer_gzip_with_precision_c(
3687    filename_c: *const c_char,
3688    precision: u8,
3689) -> *mut RKRConFrameWriter {
3690    let filename = match unsafe { cstr_path(filename_c) } {
3691        Some(s) => s,
3692        None => return ptr::null_mut(),
3693    };
3694    match crate::compression::gzip_writer(Path::new(filename)) {
3695        Ok(encoder) => into_rkr_writer(Box::new(encoder), Some(precision)),
3696        Err(_) => ptr::null_mut(),
3697    }
3698}
3699/// Creates a new zstd-compressed frame writer for the specified file.
3700/// The caller OWNS the returned pointer and MUST call `free_rkr_writer`.
3701///
3702/// Only present when readcon-core is built with the `zstd` Cargo
3703/// feature; the C header guards the declaration with
3704/// `READCON_CORE_HAS_ZSTD`.
3705///
3706/// # Safety
3707/// filename_c must be valid. The caller takes ownership of the returned writer.
3708#[cfg(feature = "zstd")]
3709#[unsafe(no_mangle)]
3710pub unsafe extern "C" fn create_writer_zstd_c(filename_c: *const c_char) -> *mut RKRConFrameWriter {
3711    let filename = match unsafe { cstr_path(filename_c) } {
3712        Some(s) => s,
3713        None => return ptr::null_mut(),
3714    };
3715    match crate::compression::zstd_writer(Path::new(filename)) {
3716        Ok(encoder) => into_rkr_writer(Box::new(encoder), None),
3717        Err(_) => ptr::null_mut(),
3718    }
3719}
3720/// Creates a zstd-compressed frame writer with a custom floating-point
3721/// precision. The caller OWNS the returned pointer and MUST call
3722/// `free_rkr_writer`.
3723///
3724/// Only present when readcon-core is built with the `zstd` Cargo
3725/// feature; the C header guards the declaration with
3726/// `READCON_CORE_HAS_ZSTD`.
3727///
3728/// # Safety
3729/// filename_c must be valid. The caller takes ownership of the returned writer.
3730#[cfg(feature = "zstd")]
3731#[unsafe(no_mangle)]
3732pub unsafe extern "C" fn create_writer_zstd_with_precision_c(
3733    filename_c: *const c_char,
3734    precision: u8,
3735) -> *mut RKRConFrameWriter {
3736    let filename = match unsafe { cstr_path(filename_c) } {
3737        Some(s) => s,
3738        None => return ptr::null_mut(),
3739    };
3740    match crate::compression::zstd_writer(Path::new(filename)) {
3741        Ok(encoder) => into_rkr_writer(Box::new(encoder), Some(precision)),
3742        Err(_) => ptr::null_mut(),
3743    }
3744}
3745//=============================================================================
3746// Direct mmap-based Reader FFI
3747//=============================================================================
3748/// Reads the first frame from a .con file.
3749/// Uses `read_to_string` for small files (< 64 KiB) and mmap for larger ones.
3750/// Stops after the first frame rather than parsing the entire file.
3751/// The caller OWNS the returned handle and MUST call `free_rkr_frame`.
3752/// Returns NULL on error.
3753///
3754/// # Safety
3755/// filename_c must be valid. The caller takes ownership of the returned frame.
3756#[unsafe(no_mangle)]
3757pub unsafe extern "C" fn rkr_read_first_frame(filename_c: *const c_char) -> *mut RKRConFrame {
3758    if filename_c.is_null() {
3759        return ptr::null_mut();
3760    }
3761    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
3762        Ok(s) => s,
3763        Err(_) => return ptr::null_mut(),
3764    };
3765    match iterators::read_first_frame(Path::new(filename)) {
3766        Ok(frame) => Box::into_raw(Box::new(frame)) as *mut RKRConFrame,
3767        Err(_) => ptr::null_mut(),
3768    }
3769}
3770/// Reads all frames from a .con file using mmap.
3771/// Returns an array of frame handles and sets `num_frames` to the count.
3772/// The caller OWNS both the array and each frame handle.
3773/// Free frames with `free_rkr_frame` and the array with `free_rkr_frame_array`.
3774/// Returns NULL on error.
3775///
3776/// # Safety
3777/// filename_c and num_frames must be valid. The caller takes ownership of the returned handles and array.
3778#[unsafe(no_mangle)]
3779pub unsafe extern "C" fn rkr_read_all_frames(
3780    filename_c: *const c_char,
3781    num_frames: *mut usize,
3782) -> *mut *mut RKRConFrame {
3783    if filename_c.is_null() || num_frames.is_null() {
3784        return ptr::null_mut();
3785    }
3786    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
3787        Ok(s) => s,
3788        Err(_) => return ptr::null_mut(),
3789    };
3790    match iterators::read_all_frames(Path::new(filename)) {
3791        Ok(frames) => pack_frame_handles(frames, num_frames),
3792        Err(_) => ptr::null_mut(),
3793    }
3794}
3795
3796/// Like [`rkr_read_all_frames`], with an explicit worker count.
3797///
3798/// `n_threads == 0` is the automatic policy (Rayon when the library is
3799/// built with `parallel` and the file is at least 48 KiB). `n_threads == 1`
3800/// is sequential. `n_threads >= 2` pins a Rayon pool of that size when
3801/// `parallel` is on; otherwise the parse is sequential.
3802///
3803/// # Safety
3804/// Same contract as [`rkr_read_all_frames`].
3805#[unsafe(no_mangle)]
3806pub unsafe extern "C" fn rkr_read_all_frames_n_threads(
3807    filename_c: *const c_char,
3808    num_frames: *mut usize,
3809    n_threads: usize,
3810) -> *mut *mut RKRConFrame {
3811    if filename_c.is_null() || num_frames.is_null() {
3812        return ptr::null_mut();
3813    }
3814    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
3815        Ok(s) => s,
3816        Err(_) => return ptr::null_mut(),
3817    };
3818    let threads = if n_threads == 0 {
3819        None
3820    } else {
3821        Some(n_threads)
3822    };
3823    match iterators::read_all_frames_with_threads(Path::new(filename), threads) {
3824        Ok(frames) => pack_frame_handles(frames, num_frames),
3825        Err(_) => ptr::null_mut(),
3826    }
3827}
3828
3829/// Pack owned frames into a C array (`len == capacity`) and write `num_frames`.
3830pub(crate) fn pack_frame_handles(
3831    frames: Vec<ConFrame>,
3832    num_frames: *mut usize,
3833) -> *mut *mut RKRConFrame {
3834    let count = frames.len();
3835    let mut handles: Vec<*mut RKRConFrame> = frames
3836        .into_iter()
3837        .map(|f| Box::into_raw(Box::new(f)) as *mut RKRConFrame)
3838        .collect();
3839    handles.shrink_to_fit();
3840    debug_assert_eq!(handles.len(), handles.capacity());
3841    let ptr = handles.as_mut_ptr();
3842    std::mem::forget(handles);
3843    unsafe { *num_frames = count };
3844    ptr
3845}
3846/// Frees an array of frame handles returned by `rkr_read_all_frames`.
3847/// Each frame is freed individually, then the array itself.
3848///
3849/// # Safety
3850/// frames must be valid or null.
3851#[unsafe(no_mangle)]
3852pub unsafe extern "C" fn free_rkr_frame_array(frames: *mut *mut RKRConFrame, num_frames: usize) {
3853    if frames.is_null() {
3854        return;
3855    }
3856    unsafe {
3857        let handles = Vec::from_raw_parts(frames, num_frames, num_frames);
3858        for handle in handles {
3859            if !handle.is_null() {
3860                let _ = Box::from_raw(handle as *mut ConFrame);
3861            }
3862        }
3863    }
3864}
3865/// Free only the outer pointer array from `rkr_read_all_frames` (not the frames).
3866///
3867/// # Safety
3868/// `frames` null or from `rkr_read_all_frames` with length `num_frames`. Frame
3869/// pointers must be owned elsewhere (e.g. language wrappers).
3870#[unsafe(no_mangle)]
3871pub unsafe extern "C" fn free_rkr_frame_ptr_array(
3872    frames: *mut *mut RKRConFrame,
3873    num_frames: usize,
3874) {
3875    if frames.is_null() {
3876        return;
3877    }
3878    unsafe {
3879        let _ptrs = Vec::from_raw_parts(frames, num_frames, num_frames);
3880    }
3881}
3882
3883//=============================================================================
3884// ---------------------------------------------------------------------------
3885// Metatensor TensorBlock exports (`metatensor` Cargo feature)
3886//
3887// Boundary types are metatensor-sys only (`metatensor::c_api::mts_block_t`).
3888// Construction: `metatensor_export` (high-level). Transfer/free: single helpers
3889// in that module (`tensor_block_into_raw_mts` / `mts_block_free_sys`).
3890// Lean builds omit these symbols (`READCON_CORE_HAS_METATENSOR` in the header).
3891// ---------------------------------------------------------------------------
3892/// Free an owned block from `rkr_frame_metatensor_*_block`.
3893/// Prefer this or `mts_block_free` (metatensor.h) — not both on the same pointer.
3894///
3895/// # Safety
3896/// `block` is NULL or an owning `mts_block_t*` from this library's transfer helper.
3897#[cfg(feature = "metatensor")]
3898#[unsafe(no_mangle)]
3899pub unsafe extern "C" fn rkr_mts_block_free(block: *mut metatensor::c_api::mts_block_t) {
3900    unsafe { crate::metatensor_export::mts_block_free_sys(block) };
3901}
3902/// Positions `[N,3]` TensorBlock. Caller frees with `rkr_mts_block_free` / `mts_block_free`.
3903#[cfg(feature = "metatensor")]
3904#[unsafe(no_mangle)]
3905pub unsafe extern "C" fn rkr_frame_metatensor_positions_block(
3906    frame_handle: *const RKRConFrame,
3907    out_block: *mut *mut metatensor::c_api::mts_block_t,
3908) -> RKRStatus {
3909    if frame_handle.is_null() || out_block.is_null() {
3910        return RKRStatus::RKR_STATUS_NULL_POINTER;
3911    }
3912    unsafe { *out_block = std::ptr::null_mut() };
3913    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
3914        return RKRStatus::RKR_STATUS_NULL_POINTER;
3915    };
3916    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3917        crate::metatensor_export::frame_positions_block(frame)
3918    })) {
3919        Ok(Ok(b)) => {
3920            unsafe { *out_block = crate::metatensor_export::tensor_block_into_raw_mts(b) };
3921            RKRStatus::RKR_STATUS_SUCCESS
3922        }
3923        Ok(Err(_)) | Err(_) => RKRStatus::RKR_STATUS_INTERNAL_ERROR,
3924    }
3925}
3926#[cfg(feature = "metatensor")]
3927#[unsafe(no_mangle)]
3928pub unsafe extern "C" fn rkr_frame_metatensor_velocities_block(
3929    frame_handle: *const RKRConFrame,
3930    out_block: *mut *mut metatensor::c_api::mts_block_t,
3931) -> RKRStatus {
3932    if frame_handle.is_null() || out_block.is_null() {
3933        return RKRStatus::RKR_STATUS_NULL_POINTER;
3934    }
3935    unsafe { *out_block = std::ptr::null_mut() };
3936    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
3937        return RKRStatus::RKR_STATUS_NULL_POINTER;
3938    };
3939    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3940        crate::metatensor_export::frame_velocities_block(frame)
3941    })) {
3942        Ok(Ok(Some(b))) => {
3943            unsafe { *out_block = crate::metatensor_export::tensor_block_into_raw_mts(b) };
3944            RKRStatus::RKR_STATUS_SUCCESS
3945        }
3946        Ok(Ok(None)) => RKRStatus::RKR_STATUS_SECTION_ABSENT,
3947        Ok(Err(_)) | Err(_) => RKRStatus::RKR_STATUS_INTERNAL_ERROR,
3948    }
3949}
3950#[cfg(feature = "metatensor")]
3951#[unsafe(no_mangle)]
3952pub unsafe extern "C" fn rkr_frame_metatensor_forces_block(
3953    frame_handle: *const RKRConFrame,
3954    out_block: *mut *mut metatensor::c_api::mts_block_t,
3955) -> RKRStatus {
3956    if frame_handle.is_null() || out_block.is_null() {
3957        return RKRStatus::RKR_STATUS_NULL_POINTER;
3958    }
3959    unsafe { *out_block = std::ptr::null_mut() };
3960    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
3961        return RKRStatus::RKR_STATUS_NULL_POINTER;
3962    };
3963    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3964        crate::metatensor_export::frame_forces_block(frame)
3965    })) {
3966        Ok(Ok(Some(b))) => {
3967            unsafe { *out_block = crate::metatensor_export::tensor_block_into_raw_mts(b) };
3968            RKRStatus::RKR_STATUS_SUCCESS
3969        }
3970        Ok(Ok(None)) => RKRStatus::RKR_STATUS_SECTION_ABSENT,
3971        Ok(Err(_)) | Err(_) => RKRStatus::RKR_STATUS_INTERNAL_ERROR,
3972    }
3973}
3974#[cfg(feature = "metatensor")]
3975#[unsafe(no_mangle)]
3976pub unsafe extern "C" fn rkr_frame_metatensor_atom_energies_block(
3977    frame_handle: *const RKRConFrame,
3978    out_block: *mut *mut metatensor::c_api::mts_block_t,
3979) -> RKRStatus {
3980    if frame_handle.is_null() || out_block.is_null() {
3981        return RKRStatus::RKR_STATUS_NULL_POINTER;
3982    }
3983    unsafe { *out_block = std::ptr::null_mut() };
3984    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
3985        return RKRStatus::RKR_STATUS_NULL_POINTER;
3986    };
3987    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3988        crate::metatensor_export::frame_energies_block(frame)
3989    })) {
3990        Ok(Ok(Some(b))) => {
3991            unsafe { *out_block = crate::metatensor_export::tensor_block_into_raw_mts(b) };
3992            RKRStatus::RKR_STATUS_SUCCESS
3993        }
3994        Ok(Ok(None)) => RKRStatus::RKR_STATUS_SECTION_ABSENT,
3995        Ok(Err(_)) | Err(_) => RKRStatus::RKR_STATUS_INTERNAL_ERROR,
3996    }
3997}
3998#[cfg(not(feature = "zstd"))]
3999#[unsafe(no_mangle)]
4000pub unsafe extern "C" fn create_writer_zstd_c(
4001    _filename_c: *const c_char,
4002) -> *mut RKRConFrameWriter {
4003    ptr::null_mut()
4004}
4005#[cfg(not(feature = "zstd"))]
4006#[unsafe(no_mangle)]
4007pub unsafe extern "C" fn create_writer_zstd_with_precision_c(
4008    _filename_c: *const c_char,
4009    _precision: u8,
4010) -> *mut RKRConFrameWriter {
4011    ptr::null_mut()
4012}
4013//=============================================================================
4014/// Lean-build stubs: always export metatensor C symbols so Fortran/C can link without `#ifdef`.
4015/// Real implementations live under `feature = "metatensor"`.
4016#[cfg(not(feature = "metatensor"))]
4017// No #[repr(C)]: cbindgen then emits an incomplete struct, which C and C++
4018// accept under -pedantic. The zero-length field keeps it unconstructible.
4019pub struct mts_block_t {
4020    _private: [u8; 0],
4021}
4022#[cfg(not(feature = "metatensor"))]
4023#[unsafe(no_mangle)]
4024pub unsafe extern "C" fn rkr_mts_block_free(_block: *mut mts_block_t) {}
4025#[cfg(not(feature = "metatensor"))]
4026#[unsafe(no_mangle)]
4027pub unsafe extern "C" fn rkr_frame_metatensor_positions_block(
4028    _frame_handle: *const RKRConFrame,
4029    out_block: *mut *mut mts_block_t,
4030) -> RKRStatus {
4031    if !out_block.is_null() {
4032        unsafe { *out_block = std::ptr::null_mut() };
4033    }
4034    RKRStatus::RKR_STATUS_FEATURE_DISABLED
4035}
4036#[cfg(not(feature = "metatensor"))]
4037#[unsafe(no_mangle)]
4038pub unsafe extern "C" fn rkr_frame_metatensor_velocities_block(
4039    _frame_handle: *const RKRConFrame,
4040    out_block: *mut *mut mts_block_t,
4041) -> RKRStatus {
4042    if !out_block.is_null() {
4043        unsafe { *out_block = std::ptr::null_mut() };
4044    }
4045    RKRStatus::RKR_STATUS_FEATURE_DISABLED
4046}
4047#[cfg(not(feature = "metatensor"))]
4048#[unsafe(no_mangle)]
4049pub unsafe extern "C" fn rkr_frame_metatensor_forces_block(
4050    _frame_handle: *const RKRConFrame,
4051    out_block: *mut *mut mts_block_t,
4052) -> RKRStatus {
4053    if !out_block.is_null() {
4054        unsafe { *out_block = std::ptr::null_mut() };
4055    }
4056    RKRStatus::RKR_STATUS_FEATURE_DISABLED
4057}
4058#[cfg(not(feature = "metatensor"))]
4059#[unsafe(no_mangle)]
4060pub unsafe extern "C" fn rkr_frame_metatensor_atom_energies_block(
4061    _frame_handle: *const RKRConFrame,
4062    out_block: *mut *mut mts_block_t,
4063) -> RKRStatus {
4064    if !out_block.is_null() {
4065        unsafe { *out_block = std::ptr::null_mut() };
4066    }
4067    RKRStatus::RKR_STATUS_FEATURE_DISABLED
4068}
4069// ----- Frame section buffers (no CFrame AoS required) -------------------------
4070/// Number of atoms on the frame (atom_data order).
4071#[unsafe(no_mangle)]
4072pub unsafe extern "C" fn rkr_frame_atom_count(frame_handle: *const RKRConFrame) -> usize {
4073    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4074        return 0;
4075    };
4076    frame.atom_data.len()
4077}
4078/// Borrow positions SoA. `out->data` is valid until `free_rkr_frame`.
4079#[unsafe(no_mangle)]
4080pub unsafe extern "C" fn rkr_frame_xyz_view(
4081    frame_handle: *const RKRConFrame,
4082    out: *mut RKRArrayView,
4083) -> RKRStatus {
4084    fill_array2_view(frame_handle, out, |f| &f.positions, false)
4085}
4086
4087/// Borrow velocities SoA, or `SECTION_ABSENT`.
4088#[unsafe(no_mangle)]
4089pub unsafe extern "C" fn rkr_frame_velocities_view(
4090    frame_handle: *const RKRConFrame,
4091    out: *mut RKRArrayView,
4092) -> RKRStatus {
4093    fill_array2_view(frame_handle, out, |f| &f.velocities, true)
4094}
4095
4096/// Borrow forces SoA, or `SECTION_ABSENT`.
4097#[unsafe(no_mangle)]
4098pub unsafe extern "C" fn rkr_frame_forces_view(
4099    frame_handle: *const RKRConFrame,
4100    out: *mut RKRArrayView,
4101) -> RKRStatus {
4102    fill_array2_view(frame_handle, out, |f| &f.forces, true)
4103}
4104
4105/// True when the frame carries a `"displacements"` section. False for a
4106/// NULL handle.
4107///
4108/// # Safety
4109/// `frame_handle` is NULL or a valid frame handle.
4110#[unsafe(no_mangle)]
4111pub unsafe extern "C" fn rkr_frame_has_displacements(frame_handle: *const RKRConFrame) -> bool {
4112    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
4113        Some(f) => f.has_displacements(),
4114        None => false,
4115    }
4116}
4117
4118/// Borrow displacements SoA, or `SECTION_ABSENT`.
4119///
4120/// # Safety
4121/// `frame_handle` must be a valid frame handle and `out` a writable view.
4122#[unsafe(no_mangle)]
4123pub unsafe extern "C" fn rkr_frame_displacements_view(
4124    frame_handle: *const RKRConFrame,
4125    out: *mut RKRArrayView,
4126) -> RKRStatus {
4127    fill_array2_view(frame_handle, out, |f| &f.displacements, true)
4128}
4129
4130/// True when the frame carries a `"spreads"` section. False for a
4131/// NULL handle.
4132///
4133/// # Safety
4134/// `frame_handle` is NULL or a valid frame handle.
4135#[unsafe(no_mangle)]
4136pub unsafe extern "C" fn rkr_frame_has_spreads(frame_handle: *const RKRConFrame) -> bool {
4137    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
4138        Some(f) => f.has_spreads(),
4139        None => false,
4140    }
4141}
4142
4143/// Borrow spreads SoA, or `SECTION_ABSENT`.
4144///
4145/// # Safety
4146/// `frame_handle` must be a valid frame handle and `out` a writable view.
4147#[unsafe(no_mangle)]
4148pub unsafe extern "C" fn rkr_frame_spreads_view(
4149    frame_handle: *const RKRConFrame,
4150    out: *mut RKRArrayView,
4151) -> RKRStatus {
4152    fill_array2_view(frame_handle, out, |f| &f.spreads, true)
4153}
4154
4155/// Borrow per-atom energies, or `SECTION_ABSENT`.
4156#[unsafe(no_mangle)]
4157pub unsafe extern "C" fn rkr_frame_energies_view(
4158    frame_handle: *const RKRConFrame,
4159    out: *mut RKRArrayView,
4160) -> RKRStatus {
4161    fill_array1_view(frame_handle, out, |f| &f.atom_energies, true)
4162}
4163
4164/// Borrow per-atom masses.
4165#[unsafe(no_mangle)]
4166pub unsafe extern "C" fn rkr_frame_masses_view(
4167    frame_handle: *const RKRConFrame,
4168    out: *mut RKRArrayView,
4169) -> RKRStatus {
4170    fill_array1_view(frame_handle, out, |f| &f.masses, false)
4171}
4172
4173/// Borrow `atom_id` column (always u64).
4174#[unsafe(no_mangle)]
4175pub unsafe extern "C" fn rkr_frame_atom_ids_view(
4176    frame_handle: *const RKRConFrame,
4177    out: *mut RKRArrayView,
4178) -> RKRStatus {
4179    if frame_handle.is_null() || out.is_null() {
4180        return RKRStatus::RKR_STATUS_NULL_POINTER;
4181    }
4182    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4183        return RKRStatus::RKR_STATUS_NULL_POINTER;
4184    };
4185    let slice = frame.atom_ids.as_slice_memory_order();
4186    unsafe {
4187        *out = RKRArrayView {
4188            data: slice
4189                .map(|s| s.as_ptr() as *const std::ffi::c_void)
4190                .unwrap_or(std::ptr::null()),
4191            n: frame.atom_ids.len(),
4192            cols: 1,
4193            dtype_code: 1, // kDLUInt
4194            dtype_bits: 64,
4195        };
4196    }
4197    RKRStatus::RKR_STATUS_SUCCESS
4198}
4199
4200/// Row-major f64 xyz pointer, or NULL if storage is not float64.
4201/// `*n` receives the atom count. Pointer is valid until `free_rkr_frame`.
4202#[unsafe(no_mangle)]
4203pub unsafe extern "C" fn rkr_frame_xyz_f64(
4204    frame_handle: *const RKRConFrame,
4205    n: *mut usize,
4206) -> *const f64 {
4207    f64_col_ptr(frame_handle, n, |f| f.positions.f64_slice())
4208}
4209
4210/// Row-major f64 velocities pointer, or NULL if absent / not float64.
4211#[unsafe(no_mangle)]
4212pub unsafe extern "C" fn rkr_frame_velocities_f64(
4213    frame_handle: *const RKRConFrame,
4214    n: *mut usize,
4215) -> *const f64 {
4216    f64_col_ptr(frame_handle, n, |f| f.velocities.f64_slice())
4217}
4218
4219/// Row-major f64 forces pointer, or NULL if absent / not float64.
4220#[unsafe(no_mangle)]
4221pub unsafe extern "C" fn rkr_frame_forces_f64(
4222    frame_handle: *const RKRConFrame,
4223    n: *mut usize,
4224) -> *const f64 {
4225    f64_col_ptr(frame_handle, n, |f| f.forces.f64_slice())
4226}
4227
4228/// Row-major f64 displacements pointer, or NULL if absent / not float64.
4229///
4230/// # Safety
4231/// `frame_handle` must be a valid frame handle; `n` is NULL or writable.
4232#[unsafe(no_mangle)]
4233pub unsafe extern "C" fn rkr_frame_displacements_f64(
4234    frame_handle: *const RKRConFrame,
4235    n: *mut usize,
4236) -> *const f64 {
4237    f64_col_ptr(frame_handle, n, |f| f.displacements.f64_slice())
4238}
4239
4240/// Row-major f64 spreads pointer, or NULL if absent / not float64.
4241///
4242/// # Safety
4243/// `frame_handle` must be a valid frame handle; `n` is NULL or writable.
4244#[unsafe(no_mangle)]
4245pub unsafe extern "C" fn rkr_frame_spreads_f64(
4246    frame_handle: *const RKRConFrame,
4247    n: *mut usize,
4248) -> *const f64 {
4249    f64_col_ptr(frame_handle, n, |f| f.spreads.f64_slice())
4250}
4251
4252fn fill_array2_view(
4253    frame_handle: *const RKRConFrame,
4254    out: *mut RKRArrayView,
4255    get: impl FnOnce(&ConFrame) -> &crate::storage_dtype::FloatArray2,
4256    section: bool,
4257) -> RKRStatus {
4258    if frame_handle.is_null() || out.is_null() {
4259        return RKRStatus::RKR_STATUS_NULL_POINTER;
4260    }
4261    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4262        return RKRStatus::RKR_STATUS_NULL_POINTER;
4263    };
4264    let arr = get(frame);
4265    if section && arr.nrows() == 0 {
4266        unsafe { *out = RKRArrayView::empty() };
4267        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4268    }
4269    unsafe { *out = RKRArrayView::from_array2(arr) };
4270    RKRStatus::RKR_STATUS_SUCCESS
4271}
4272
4273fn fill_array1_view(
4274    frame_handle: *const RKRConFrame,
4275    out: *mut RKRArrayView,
4276    get: impl FnOnce(&ConFrame) -> &crate::storage_dtype::FloatArray1,
4277    section: bool,
4278) -> RKRStatus {
4279    if frame_handle.is_null() || out.is_null() {
4280        return RKRStatus::RKR_STATUS_NULL_POINTER;
4281    }
4282    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4283        return RKRStatus::RKR_STATUS_NULL_POINTER;
4284    };
4285    let arr = get(frame);
4286    if section && arr.len() == 0 {
4287        unsafe { *out = RKRArrayView::empty() };
4288        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4289    }
4290    unsafe { *out = RKRArrayView::from_array1(arr) };
4291    RKRStatus::RKR_STATUS_SUCCESS
4292}
4293
4294fn f64_col_ptr(
4295    frame_handle: *const RKRConFrame,
4296    n: *mut usize,
4297    get: impl FnOnce(&ConFrame) -> Option<&[f64]>,
4298) -> *const f64 {
4299    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4300        if !n.is_null() {
4301            unsafe { *n = 0 };
4302        }
4303        return std::ptr::null();
4304    };
4305    match get(frame) {
4306        Some(s) if !s.is_empty() => {
4307            if !n.is_null() {
4308                unsafe { *n = s.len() / 3 };
4309            }
4310            s.as_ptr()
4311        }
4312        _ => {
4313            if !n.is_null() {
4314                unsafe { *n = 0 };
4315            }
4316            std::ptr::null()
4317        }
4318    }
4319}
4320
4321/// Copy positions as row-major `[x0,y0,z0,...]` into `out` (length >= 3*N).
4322/// Prefers a memcpy from the SoA column; falls back to AoS only if SoA is empty.
4323#[unsafe(no_mangle)]
4324pub unsafe extern "C" fn rkr_frame_copy_positions(
4325    frame_handle: *const RKRConFrame,
4326    out: *mut f64,
4327    out_len: usize,
4328) -> RKRStatus {
4329    copy_array2_f64(frame_handle, out, out_len, |f| &f.positions, false)
4330}
4331fn copy_array2_f64(
4332    frame_handle: *const RKRConFrame,
4333    out: *mut f64,
4334    out_len: usize,
4335    get: impl FnOnce(&ConFrame) -> &crate::storage_dtype::FloatArray2,
4336    section: bool,
4337) -> RKRStatus {
4338    if frame_handle.is_null() || out.is_null() {
4339        return RKRStatus::RKR_STATUS_NULL_POINTER;
4340    }
4341    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4342        return RKRStatus::RKR_STATUS_NULL_POINTER;
4343    };
4344    let arr = get(frame);
4345    let n = if arr.nrows() > 0 {
4346        arr.nrows()
4347    } else {
4348        frame.atom_data.len()
4349    };
4350    if section && arr.nrows() == 0 {
4351        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4352    }
4353    let need = n.saturating_mul(3);
4354    if out_len < need {
4355        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
4356    }
4357    let dest = unsafe { std::slice::from_raw_parts_mut(out, need) };
4358    if arr.nrows() == n {
4359        if let Some(src) = arr.f64_slice() {
4360            dest.copy_from_slice(&src[..need.min(src.len())]);
4361            return RKRStatus::RKR_STATUS_SUCCESS;
4362        }
4363        for i in 0..n {
4364            let row = arr.as_f64_row(i);
4365            dest[i * 3] = row[0];
4366            dest[i * 3 + 1] = row[1];
4367            dest[i * 3 + 2] = row[2];
4368        }
4369        return RKRStatus::RKR_STATUS_SUCCESS;
4370    }
4371    for (i, a) in frame.atom_data.iter().enumerate() {
4372        dest[i * 3] = a.x;
4373        dest[i * 3 + 1] = a.y;
4374        dest[i * 3 + 2] = a.z;
4375    }
4376    RKRStatus::RKR_STATUS_SUCCESS
4377}
4378
4379fn copy_array1_f64(
4380    frame_handle: *const RKRConFrame,
4381    out: *mut f64,
4382    out_len: usize,
4383    get: impl FnOnce(&ConFrame) -> &crate::storage_dtype::FloatArray1,
4384    section: bool,
4385) -> RKRStatus {
4386    if frame_handle.is_null() || out.is_null() {
4387        return RKRStatus::RKR_STATUS_NULL_POINTER;
4388    }
4389    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4390        return RKRStatus::RKR_STATUS_NULL_POINTER;
4391    };
4392    let arr = get(frame);
4393    let n = if arr.len() > 0 {
4394        arr.len()
4395    } else {
4396        frame.atom_data.len()
4397    };
4398    if section && arr.len() == 0 {
4399        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4400    }
4401    if out_len < n {
4402        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
4403    }
4404    let dest = unsafe { std::slice::from_raw_parts_mut(out, n) };
4405    if let Some(src) = arr.f64_slice() {
4406        dest.copy_from_slice(&src[..n.min(src.len())]);
4407        return RKRStatus::RKR_STATUS_SUCCESS;
4408    }
4409    for i in 0..n {
4410        dest[i] = arr.get_f64(i);
4411    }
4412    RKRStatus::RKR_STATUS_SUCCESS
4413}
4414
4415#[unsafe(no_mangle)]
4416pub unsafe extern "C" fn rkr_frame_copy_velocities(
4417    frame_handle: *const RKRConFrame,
4418    out: *mut f64,
4419    out_len: usize,
4420) -> RKRStatus {
4421    copy_array2_f64(frame_handle, out, out_len, |f| &f.velocities, true)
4422}
4423#[unsafe(no_mangle)]
4424pub unsafe extern "C" fn rkr_frame_copy_forces(
4425    frame_handle: *const RKRConFrame,
4426    out: *mut f64,
4427    out_len: usize,
4428) -> RKRStatus {
4429    copy_array2_f64(frame_handle, out, out_len, |f| &f.forces, true)
4430}
4431/// Copy displacements into row-major `out` (length >= `3 * N`), or
4432/// `SECTION_ABSENT`.
4433///
4434/// # Safety
4435/// `frame_handle` must be a valid frame handle; `out` must hold `out_len` f64.
4436#[unsafe(no_mangle)]
4437pub unsafe extern "C" fn rkr_frame_copy_displacements(
4438    frame_handle: *const RKRConFrame,
4439    out: *mut f64,
4440    out_len: usize,
4441) -> RKRStatus {
4442    copy_array2_f64(frame_handle, out, out_len, |f| &f.displacements, true)
4443}
4444/// Copy spreads into row-major `out` (length >= `3 * N`), or
4445/// `SECTION_ABSENT`.
4446///
4447/// # Safety
4448/// `frame_handle` must be a valid frame handle; `out` must hold `out_len` f64.
4449#[unsafe(no_mangle)]
4450pub unsafe extern "C" fn rkr_frame_copy_spreads(
4451    frame_handle: *const RKRConFrame,
4452    out: *mut f64,
4453    out_len: usize,
4454) -> RKRStatus {
4455    copy_array2_f64(frame_handle, out, out_len, |f| &f.spreads, true)
4456}
4457#[unsafe(no_mangle)]
4458pub unsafe extern "C" fn rkr_frame_copy_atom_energies(
4459    frame_handle: *const RKRConFrame,
4460    out: *mut f64,
4461    out_len: usize,
4462) -> RKRStatus {
4463    copy_array1_f64(frame_handle, out, out_len, |f| &f.atom_energies, true)
4464}
4465#[unsafe(no_mangle)]
4466pub unsafe extern "C" fn rkr_frame_copy_masses(
4467    frame_handle: *const RKRConFrame,
4468    out: *mut f64,
4469    out_len: usize,
4470) -> RKRStatus {
4471    copy_array1_f64(frame_handle, out, out_len, |f| &f.masses, false)
4472}
4473#[unsafe(no_mangle)]
4474pub unsafe extern "C" fn rkr_frame_copy_atom_ids(
4475    frame_handle: *const RKRConFrame,
4476    out: *mut u64,
4477    out_len: usize,
4478) -> RKRStatus {
4479    if frame_handle.is_null() || out.is_null() {
4480        return RKRStatus::RKR_STATUS_NULL_POINTER;
4481    }
4482    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4483        return RKRStatus::RKR_STATUS_NULL_POINTER;
4484    };
4485    let n = frame.atom_ids.len().max(frame.atom_data.len());
4486    if out_len < n {
4487        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
4488    }
4489    let dest = unsafe { std::slice::from_raw_parts_mut(out, n) };
4490    if let Some(src) = frame.atom_ids.as_slice_memory_order() {
4491        dest[..src.len().min(n)].copy_from_slice(&src[..src.len().min(n)]);
4492        return RKRStatus::RKR_STATUS_SUCCESS;
4493    }
4494    for (i, a) in frame.atom_data.iter().enumerate() {
4495        dest[i] = a.atom_id;
4496    }
4497    RKRStatus::RKR_STATUS_SUCCESS
4498}
4499fn frame_positions_arc(frame: &ConFrame) -> ndarray::ArcArray2<f64> {
4500    let n = frame.atom_data.len();
4501    let mut data = Vec::with_capacity(n * 3);
4502    for a in &frame.atom_data {
4503        data.extend_from_slice(&[a.x, a.y, a.z]);
4504    }
4505    ndarray::ArcArray2::from_shape_vec((n, 3), data)
4506        .unwrap_or_else(|_| ndarray::ArcArray2::zeros((0, 3)))
4507}
4508
4509/// Metatensor-style: export positions as they are stored (CPU f64), with
4510/// explicit device request. Non-CPU → `FEATURE_DISABLED`. Prefer this over
4511/// dtype-cast `*_dlpack_ex` for new code.
4512///
4513/// `stream` and `max_version_*` are accepted for ABI alignment with
4514/// metatensor `as_dlpack`; CPU ignores stream / version negotiation for now.
4515///
4516/// # Safety
4517/// Handles and `out_tensor` must be valid.
4518#[unsafe(no_mangle)]
4519pub unsafe extern "C" fn rkr_frame_positions_as_dlpack(
4520    frame_handle: *const RKRConFrame,
4521    device_type: i32,
4522    device_id: i32,
4523    _stream: i64,
4524    _max_version_major: u32,
4525    _max_version_minor: u32,
4526    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4527) -> RKRStatus {
4528    if frame_handle.is_null() || out_tensor.is_null() {
4529        return RKRStatus::RKR_STATUS_NULL_POINTER;
4530    }
4531    unsafe { *out_tensor = std::ptr::null_mut() };
4532    let dl_device = if device_type == rkr_dl_device_type::RKR_DL_CPU {
4533        dlpk::sys::DLDevice::cpu()
4534    } else if device_type == rkr_dl_device_type::RKR_DL_CUDA {
4535        #[cfg(feature = "cuda")]
4536        {
4537            dlpk::sys::DLDevice::cuda(device_id)
4538        }
4539        #[cfg(not(feature = "cuda"))]
4540        {
4541            let _ = device_id;
4542            return RKRStatus::RKR_STATUS_FEATURE_DISABLED;
4543        }
4544    } else {
4545        return RKRStatus::RKR_STATUS_FEATURE_DISABLED;
4546    };
4547    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4548        return RKRStatus::RKR_STATUS_NULL_POINTER;
4549    };
4550    // Opaque SoA storage → DLPack; CUDA requests H2D via storage_dtype / cuda_array.
4551    match frame.positions_as_dlpack(dl_device) {
4552        Ok(tensor) => {
4553            let raw = tensor.into_raw();
4554            unsafe {
4555                *out_tensor = raw.as_ptr();
4556            }
4557            RKRStatus::RKR_STATUS_SUCCESS
4558        }
4559        Err(e) => map_dlpack_err(e),
4560    }
4561}
4562
4563/// Ingest positions from a DLManagedTensorVersioned (CPU float32/64, shape (N,3)
4564/// or length 3N). Metatensor-style write path symmetry.
4565///
4566/// # Safety
4567/// `frame` must be a valid mutable frame; `tensor` non-null managed tensor.
4568#[unsafe(no_mangle)]
4569pub unsafe extern "C" fn rkr_frame_positions_from_dlpack(
4570    frame_handle: *mut RKRConFrame,
4571    tensor: *const RKRDLManagedTensorVersioned,
4572) -> RKRStatus {
4573    if frame_handle.is_null() || tensor.is_null() {
4574        return RKRStatus::RKR_STATUS_NULL_POINTER;
4575    }
4576    let frame = unsafe { &mut *(frame_handle as *mut ConFrame) };
4577    let dl = unsafe { &(*tensor).dl_tensor };
4578    if dl.device.device_type != dlpk::sys::DLDeviceType::kDLCPU {
4579        return RKRStatus::RKR_STATUS_FEATURE_DISABLED;
4580    }
4581    let n = frame.atom_data.len();
4582    let need = n.saturating_mul(3);
4583    let ndim = dl.ndim as usize;
4584    let shape = if dl.shape.is_null() {
4585        return RKRStatus::RKR_STATUS_VALIDATION_ERROR;
4586    } else {
4587        unsafe { std::slice::from_raw_parts(dl.shape, ndim) }
4588    };
4589    let nelem = if ndim == 2 && shape[0] == n as i64 && shape[1] == 3 {
4590        need
4591    } else if ndim == 1 && shape[0] == need as i64 {
4592        need
4593    } else {
4594        return RKRStatus::RKR_STATUS_VALIDATION_ERROR;
4595    };
4596    let code = dl.dtype.code as u8;
4597    let bits = dl.dtype.bits;
4598    if dl.data.is_null() {
4599        return RKRStatus::RKR_STATUS_NULL_POINTER;
4600    }
4601    let vals: Vec<f64> = if code == rkr_dl_type_code::RKR_DL_FLOAT && bits == 64 {
4602        let s = unsafe { std::slice::from_raw_parts(dl.data as *const f64, nelem) };
4603        s.to_vec()
4604    } else if code == rkr_dl_type_code::RKR_DL_FLOAT && bits == 32 {
4605        let s = unsafe { std::slice::from_raw_parts(dl.data as *const f32, nelem) };
4606        s.iter().map(|&x| x as f64).collect()
4607    } else {
4608        return RKRStatus::RKR_STATUS_VALIDATION_ERROR;
4609    };
4610    if frame.positions.nrows() != n {
4611        return RKRStatus::RKR_STATUS_VALIDATION_ERROR;
4612    }
4613    for i in 0..n {
4614        frame
4615            .positions
4616            .set_f64_row(i, [vals[i * 3], vals[i * 3 + 1], vals[i * 3 + 2]]);
4617    }
4618    frame.sync_atom_data_from_arrays();
4619    RKRStatus::RKR_STATUS_SUCCESS
4620}
4621
4622/// DLPack positions from a frame (default float64 / CPU). Prefer
4623/// [`rkr_frame_positions_as_dlpack`] for metatensor-style device negotiation.
4624#[unsafe(no_mangle)]
4625pub unsafe extern "C" fn rkr_frame_positions_dlpack(
4626    frame_handle: *const RKRConFrame,
4627    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4628) -> RKRStatus {
4629    unsafe {
4630        rkr_frame_positions_as_dlpack(
4631            frame_handle,
4632            rkr_dl_device_type::RKR_DL_CPU,
4633            0,
4634            0,
4635            1,
4636            0,
4637            out_tensor,
4638        )
4639    }
4640}
4641
4642/// Frame positions with [`RKRDlpackExportOptions`] (`opts` NULL → f64/CPU).
4643///
4644/// # Safety
4645/// `frame_handle` / `out_tensor` valid; `opts` null or valid.
4646#[unsafe(no_mangle)]
4647pub unsafe extern "C" fn rkr_frame_positions_dlpack_ex(
4648    frame_handle: *const RKRConFrame,
4649    opts: *const RKRDlpackExportOptions,
4650    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4651) -> RKRStatus {
4652    if frame_handle.is_null() || out_tensor.is_null() {
4653        return RKRStatus::RKR_STATUS_NULL_POINTER;
4654    }
4655    unsafe { *out_tensor = std::ptr::null_mut() };
4656    let o = match resolve_dlpack_opts(opts) {
4657        Ok(o) => o,
4658        Err(st) => return st,
4659    };
4660    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4661        return RKRStatus::RKR_STATUS_NULL_POINTER;
4662    };
4663    let arr = frame_positions_arc(frame);
4664    export_owned_array2_dlpack_opts(&arr, &o, out_tensor)
4665}
4666
4667/// DLPack velocities from a frame, or `SECTION_ABSENT` if missing (f64/CPU).
4668#[unsafe(no_mangle)]
4669pub unsafe extern "C" fn rkr_frame_velocities_dlpack(
4670    frame_handle: *const RKRConFrame,
4671    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4672) -> RKRStatus {
4673    unsafe { rkr_frame_velocities_dlpack_ex(frame_handle, std::ptr::null(), out_tensor) }
4674}
4675
4676#[unsafe(no_mangle)]
4677pub unsafe extern "C" fn rkr_frame_velocities_dlpack_ex(
4678    frame_handle: *const RKRConFrame,
4679    opts: *const RKRDlpackExportOptions,
4680    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4681) -> RKRStatus {
4682    if frame_handle.is_null() || out_tensor.is_null() {
4683        return RKRStatus::RKR_STATUS_NULL_POINTER;
4684    }
4685    unsafe { *out_tensor = std::ptr::null_mut() };
4686    let o = match resolve_dlpack_opts(opts) {
4687        Ok(o) => o,
4688        Err(st) => return st,
4689    };
4690    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4691        return RKRStatus::RKR_STATUS_NULL_POINTER;
4692    };
4693    if !frame.has_velocities() {
4694        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4695    }
4696    let n = frame.atom_data.len();
4697    let mut data = Vec::with_capacity(n * 3);
4698    for a in &frame.atom_data {
4699        let v = a.velocity().unwrap_or([0.0; 3]);
4700        data.extend_from_slice(&v);
4701    }
4702    let arr = ndarray::ArcArray2::from_shape_vec((n, 3), data)
4703        .unwrap_or_else(|_| ndarray::ArcArray2::zeros((0, 3)));
4704    export_owned_array2_dlpack_opts(&arr, &o, out_tensor)
4705}
4706
4707/// DLPack forces from a frame, or `SECTION_ABSENT` if missing (f64/CPU default).
4708#[unsafe(no_mangle)]
4709pub unsafe extern "C" fn rkr_frame_forces_dlpack(
4710    frame_handle: *const RKRConFrame,
4711    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4712) -> RKRStatus {
4713    unsafe { rkr_frame_forces_dlpack_ex(frame_handle, std::ptr::null(), out_tensor) }
4714}
4715
4716#[unsafe(no_mangle)]
4717pub unsafe extern "C" fn rkr_frame_forces_dlpack_ex(
4718    frame_handle: *const RKRConFrame,
4719    opts: *const RKRDlpackExportOptions,
4720    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4721) -> RKRStatus {
4722    if frame_handle.is_null() || out_tensor.is_null() {
4723        return RKRStatus::RKR_STATUS_NULL_POINTER;
4724    }
4725    unsafe { *out_tensor = std::ptr::null_mut() };
4726    let o = match resolve_dlpack_opts(opts) {
4727        Ok(o) => o,
4728        Err(st) => return st,
4729    };
4730    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4731        return RKRStatus::RKR_STATUS_NULL_POINTER;
4732    };
4733    if !frame.has_forces() {
4734        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4735    }
4736    let n = frame.atom_data.len();
4737    let mut data = Vec::with_capacity(n * 3);
4738    for a in &frame.atom_data {
4739        let f = a.force().unwrap_or([0.0; 3]);
4740        data.extend_from_slice(&f);
4741    }
4742    let arr = ndarray::ArcArray2::from_shape_vec((n, 3), data)
4743        .unwrap_or_else(|_| ndarray::ArcArray2::zeros((0, 3)));
4744    export_owned_array2_dlpack_opts(&arr, &o, out_tensor)
4745}
4746
4747/// DLPack displacements from a frame, or `SECTION_ABSENT` if missing
4748/// (f64/CPU default).
4749///
4750/// # Safety
4751/// `frame_handle` must be a valid frame handle; `out_tensor` must be a
4752/// writable pointer. Free the tensor with `rkr_dlpack_delete`.
4753#[unsafe(no_mangle)]
4754pub unsafe extern "C" fn rkr_frame_displacements_dlpack(
4755    frame_handle: *const RKRConFrame,
4756    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4757) -> RKRStatus {
4758    unsafe { rkr_frame_displacements_dlpack_ex(frame_handle, std::ptr::null(), out_tensor) }
4759}
4760
4761/// Like [`rkr_frame_displacements_dlpack`] with export options (NULL
4762/// `opts` selects float64 on CPU).
4763///
4764/// # Safety
4765/// Same contract as [`rkr_frame_displacements_dlpack`]; `opts` is NULL or
4766/// points to a valid `RKRDlpackExportOptions`.
4767#[unsafe(no_mangle)]
4768pub unsafe extern "C" fn rkr_frame_displacements_dlpack_ex(
4769    frame_handle: *const RKRConFrame,
4770    opts: *const RKRDlpackExportOptions,
4771    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4772) -> RKRStatus {
4773    if frame_handle.is_null() || out_tensor.is_null() {
4774        return RKRStatus::RKR_STATUS_NULL_POINTER;
4775    }
4776    unsafe { *out_tensor = std::ptr::null_mut() };
4777    let o = match resolve_dlpack_opts(opts) {
4778        Ok(o) => o,
4779        Err(st) => return st,
4780    };
4781    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4782        return RKRStatus::RKR_STATUS_NULL_POINTER;
4783    };
4784    if !frame.has_displacements() {
4785        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4786    }
4787    let n = frame.atom_data.len();
4788    let mut data = Vec::with_capacity(n * 3);
4789    for a in &frame.atom_data {
4790        let d = a.displacement().unwrap_or([0.0; 3]);
4791        data.extend_from_slice(&d);
4792    }
4793    let arr = ndarray::ArcArray2::from_shape_vec((n, 3), data)
4794        .unwrap_or_else(|_| ndarray::ArcArray2::zeros((0, 3)));
4795    export_owned_array2_dlpack_opts(&arr, &o, out_tensor)
4796}
4797
4798/// DLPack spreads from a frame, or `SECTION_ABSENT` if missing
4799/// (f64/CPU default).
4800///
4801/// # Safety
4802/// `frame_handle` must be a valid frame handle; `out_tensor` must be a
4803/// writable pointer. Free the tensor with `rkr_dlpack_delete`.
4804#[unsafe(no_mangle)]
4805pub unsafe extern "C" fn rkr_frame_spreads_dlpack(
4806    frame_handle: *const RKRConFrame,
4807    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4808) -> RKRStatus {
4809    unsafe { rkr_frame_spreads_dlpack_ex(frame_handle, std::ptr::null(), out_tensor) }
4810}
4811
4812/// Like [`rkr_frame_spreads_dlpack`] with export options (NULL
4813/// `opts` selects float64 on CPU).
4814///
4815/// # Safety
4816/// Same contract as [`rkr_frame_spreads_dlpack`]; `opts` is NULL or
4817/// points to a valid `RKRDlpackExportOptions`.
4818#[unsafe(no_mangle)]
4819pub unsafe extern "C" fn rkr_frame_spreads_dlpack_ex(
4820    frame_handle: *const RKRConFrame,
4821    opts: *const RKRDlpackExportOptions,
4822    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4823) -> RKRStatus {
4824    if frame_handle.is_null() || out_tensor.is_null() {
4825        return RKRStatus::RKR_STATUS_NULL_POINTER;
4826    }
4827    unsafe { *out_tensor = std::ptr::null_mut() };
4828    let o = match resolve_dlpack_opts(opts) {
4829        Ok(o) => o,
4830        Err(st) => return st,
4831    };
4832    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4833        return RKRStatus::RKR_STATUS_NULL_POINTER;
4834    };
4835    if !frame.has_spreads() {
4836        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4837    }
4838    let n = frame.atom_data.len();
4839    let mut data = Vec::with_capacity(n * 3);
4840    for a in &frame.atom_data {
4841        let d = a.spread().unwrap_or([0.0; 3]);
4842        data.extend_from_slice(&d);
4843    }
4844    let arr = ndarray::ArcArray2::from_shape_vec((n, 3), data)
4845        .unwrap_or_else(|_| ndarray::ArcArray2::zeros((0, 3)));
4846    export_owned_array2_dlpack_opts(&arr, &o, out_tensor)
4847}
4848
4849/// DLPack per-atom energies, or `SECTION_ABSENT` if missing (f64/CPU default).
4850#[unsafe(no_mangle)]
4851pub unsafe extern "C" fn rkr_frame_atom_energies_dlpack(
4852    frame_handle: *const RKRConFrame,
4853    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4854) -> RKRStatus {
4855    unsafe { rkr_frame_atom_energies_dlpack_ex(frame_handle, std::ptr::null(), out_tensor) }
4856}
4857
4858#[unsafe(no_mangle)]
4859pub unsafe extern "C" fn rkr_frame_atom_energies_dlpack_ex(
4860    frame_handle: *const RKRConFrame,
4861    opts: *const RKRDlpackExportOptions,
4862    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4863) -> RKRStatus {
4864    if frame_handle.is_null() || out_tensor.is_null() {
4865        return RKRStatus::RKR_STATUS_NULL_POINTER;
4866    }
4867    unsafe { *out_tensor = std::ptr::null_mut() };
4868    let o = match resolve_dlpack_opts(opts) {
4869        Ok(o) => o,
4870        Err(st) => return st,
4871    };
4872    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4873        return RKRStatus::RKR_STATUS_NULL_POINTER;
4874    };
4875    if !frame.has_energies() {
4876        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4877    }
4878    let data: Vec<f64> = frame
4879        .atom_data
4880        .iter()
4881        .map(|a| a.energy().unwrap_or(0.0))
4882        .collect();
4883    let arr = ndarray::ArcArray1::from_vec(data);
4884    export_owned_array1_f64_dlpack_opts(&arr, &o, out_tensor)
4885}
4886
4887// Chemfiles selection (always linked; real impl needs --features chemfiles)
4888//=============================================================================
4889/// Opaque handle for a cached selection evaluation result.
4890pub struct RKRSelectionResult;
4891/// Evaluate a chemfiles selection-language string on an `RKRConFrame`.
4892///
4893/// On success writes a heap-allocated result handle to `*out_result` (caller
4894/// frees with [`rkr_selection_result_free`]). Returns
4895/// `RKR_STATUS_SELECTION_ERROR` for invalid grammar, evaluation failure, or
4896/// when this build was compiled without the `chemfiles` feature.
4897///
4898/// # Safety
4899/// `frame_handle`, `selection`, and `out_result` must be non-null; `selection`
4900/// must point to a valid UTF-8 C string.
4901#[unsafe(no_mangle)]
4902pub unsafe extern "C" fn rkr_frame_select(
4903    frame_handle: *const RKRConFrame,
4904    selection: *const c_char,
4905    out_result: *mut *mut RKRSelectionResult,
4906) -> RKRStatus {
4907    if frame_handle.is_null() || selection.is_null() || out_result.is_null() {
4908        return RKRStatus::RKR_STATUS_NULL_POINTER;
4909    }
4910    let frame = unsafe { &*(frame_handle as *const ConFrame) };
4911    let sel_str = match unsafe { CStr::from_ptr(selection) }.to_str() {
4912        Ok(s) => s,
4913        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
4914    };
4915    match crate::chemfiles_selection::evaluate_selection_on_con_frame(sel_str, frame) {
4916        Ok(result) => {
4917            let boxed = Box::new(result);
4918            unsafe {
4919                *out_result = Box::into_raw(boxed) as *mut RKRSelectionResult;
4920            }
4921            RKRStatus::RKR_STATUS_SUCCESS
4922        }
4923        Err(_) => RKRStatus::RKR_STATUS_SELECTION_ERROR,
4924    }
4925}
4926/// Number of matches in a selection result.
4927///
4928/// # Safety
4929/// `result_handle` must be a valid handle from [`rkr_frame_select`] or NULL.
4930#[unsafe(no_mangle)]
4931pub unsafe extern "C" fn rkr_selection_result_match_count(
4932    result_handle: *const RKRSelectionResult,
4933) -> u64 {
4934    if result_handle.is_null() {
4935        return 0;
4936    }
4937    let result = unsafe { &*(result_handle as *const crate::chemfiles_selection::SelectionResult) };
4938    result.matches.len() as u64
4939}
4940/// Selection context size (1=atom, 2=pair, 3=angle, 4=dihedral).
4941///
4942/// # Safety
4943/// `result_handle` must be valid or NULL (returns 0).
4944#[unsafe(no_mangle)]
4945pub unsafe extern "C" fn rkr_selection_result_context_size(
4946    result_handle: *const RKRSelectionResult,
4947) -> u32 {
4948    if result_handle.is_null() {
4949        return 0;
4950    }
4951    let result = unsafe { &*(result_handle as *const crate::chemfiles_selection::SelectionResult) };
4952    result.context_size as u32
4953}
4954/// Copy match `match_index` atom indices into `out_atoms` (up to 4 slots).
4955/// Writes actual arity to `*out_size` when non-null.
4956///
4957/// # Safety
4958/// Handles and `out_atoms` must be valid; `out_atoms` needs space for 4 `uint64_t`.
4959#[unsafe(no_mangle)]
4960pub unsafe extern "C" fn rkr_selection_result_match_at(
4961    result_handle: *const RKRSelectionResult,
4962    match_index: u64,
4963    out_atoms: *mut u64,
4964    out_size: *mut u32,
4965) -> RKRStatus {
4966    if result_handle.is_null() || out_atoms.is_null() {
4967        return RKRStatus::RKR_STATUS_NULL_POINTER;
4968    }
4969    let result = unsafe { &*(result_handle as *const crate::chemfiles_selection::SelectionResult) };
4970    let idx = match_index as usize;
4971    if idx >= result.matches.len() {
4972        return RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS;
4973    }
4974    let m = &result.matches[idx];
4975    unsafe {
4976        for i in 0..4 {
4977            *out_atoms.add(i) = if i < m.size {
4978                m.atoms[i] as u64
4979            } else {
4980                u64::MAX
4981            };
4982        }
4983        if !out_size.is_null() {
4984            *out_size = m.size as u32;
4985        }
4986    }
4987    RKRStatus::RKR_STATUS_SUCCESS
4988}
4989/// Fill `out_indices` with primary atom indices for each match (length =
4990/// match count). Returns `RKR_STATUS_BUFFER_TOO_SMALL` if `capacity` is too small.
4991///
4992/// # Safety
4993/// `result_handle` and `out_indices` must be valid when capacity > 0.
4994#[unsafe(no_mangle)]
4995pub unsafe extern "C" fn rkr_selection_result_primary_indices(
4996    result_handle: *const RKRSelectionResult,
4997    out_indices: *mut u64,
4998    capacity: u64,
4999    out_written: *mut u64,
5000) -> RKRStatus {
5001    if result_handle.is_null() {
5002        return RKRStatus::RKR_STATUS_NULL_POINTER;
5003    }
5004    let result = unsafe { &*(result_handle as *const crate::chemfiles_selection::SelectionResult) };
5005    let n = result.matches.len() as u64;
5006    if !out_written.is_null() {
5007        unsafe {
5008            *out_written = n;
5009        }
5010    }
5011    if n == 0 {
5012        return RKRStatus::RKR_STATUS_SUCCESS;
5013    }
5014    if out_indices.is_null() {
5015        return RKRStatus::RKR_STATUS_NULL_POINTER;
5016    }
5017    if capacity < n {
5018        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
5019    }
5020    unsafe {
5021        for (i, m) in result.matches.iter().enumerate() {
5022            *out_indices.add(i) = m.atoms[0] as u64;
5023        }
5024    }
5025    RKRStatus::RKR_STATUS_SUCCESS
5026}
5027/// Free a selection result from [`rkr_frame_select`]. Safe with NULL.
5028///
5029/// # Safety
5030/// `result_handle` must be from `rkr_frame_select` or NULL.
5031#[unsafe(no_mangle)]
5032pub unsafe extern "C" fn rkr_selection_result_free(result_handle: *mut RKRSelectionResult) {
5033    if result_handle.is_null() {
5034        return;
5035    }
5036    unsafe {
5037        drop(Box::from_raw(
5038            result_handle as *mut crate::chemfiles_selection::SelectionResult,
5039        ));
5040    }
5041}
5042/// Returns 1 when this library build includes chemfiles selection support.
5043#[unsafe(no_mangle)]
5044pub extern "C" fn rkr_has_chemfiles_support() -> u8 {
5045    #[cfg(feature = "chemfiles")]
5046    {
5047        1
5048    }
5049    #[cfg(not(feature = "chemfiles"))]
5050    {
5051        0
5052    }
5053}
5054
5055/// Returns 1 when this library build includes Rayon multi-frame parse.
5056#[unsafe(no_mangle)]
5057pub extern "C" fn rkr_has_parallel_support() -> u8 {
5058    #[cfg(feature = "parallel")]
5059    {
5060        1
5061    }
5062    #[cfg(not(feature = "parallel"))]
5063    {
5064        0
5065    }
5066}
5067/// Read the first frame from a chemfiles-supported path (XYZ, PDB, GRO, …).
5068/// Returns NULL on error or without the `chemfiles` feature. Caller: `free_rkr_frame`.
5069///
5070/// # Safety
5071/// `path_c` must be a valid NUL-terminated UTF-8 path.
5072#[unsafe(no_mangle)]
5073pub unsafe extern "C" fn rkr_read_chemfiles_first(path_c: *const c_char) -> *mut RKRConFrame {
5074    if path_c.is_null() {
5075        return std::ptr::null_mut();
5076    }
5077    let Ok(path_str) = unsafe { CStr::from_ptr(path_c) }.to_str() else {
5078        return std::ptr::null_mut();
5079    };
5080    match crate::chemfiles_import::con_frame_from_trajectory_path(path_str) {
5081        Ok(frame) => Box::into_raw(Box::new(frame)) as *mut RKRConFrame,
5082        Err(_) => std::ptr::null_mut(),
5083    }
5084}
5085
5086/// Read every step from a chemfiles-supported path. Sets `*num_frames`.
5087/// Free with `free_rkr_frame_array`. NULL on error / without chemfiles.
5088///
5089/// # Safety
5090/// `path_c` valid UTF-8; `num_frames` non-null.
5091#[unsafe(no_mangle)]
5092pub unsafe extern "C" fn rkr_read_chemfiles(
5093    path_c: *const c_char,
5094    num_frames: *mut usize,
5095) -> *mut *mut RKRConFrame {
5096    if path_c.is_null() || num_frames.is_null() {
5097        return std::ptr::null_mut();
5098    }
5099    let Ok(path_str) = unsafe { CStr::from_ptr(path_c) }.to_str() else {
5100        return std::ptr::null_mut();
5101    };
5102    match crate::chemfiles_import::con_frames_from_trajectory_path(path_str) {
5103        Ok(frames) => pack_frame_handles(frames, num_frames),
5104        Err(_) => std::ptr::null_mut(),
5105    }
5106}
5107
5108/// Read step `index` via chemfiles `Trajectory::read_step`.
5109/// Returns NULL on error, out of range, or without the `chemfiles` feature.
5110///
5111/// # Safety
5112/// `path_c` must be a valid NUL-terminated UTF-8 path.
5113#[unsafe(no_mangle)]
5114pub unsafe extern "C" fn rkr_read_chemfiles_nth(
5115    path_c: *const c_char,
5116    index: usize,
5117) -> *mut RKRConFrame {
5118    if path_c.is_null() {
5119        return std::ptr::null_mut();
5120    }
5121    let Ok(path_str) = unsafe { CStr::from_ptr(path_c) }.to_str() else {
5122        return std::ptr::null_mut();
5123    };
5124    match crate::chemfiles_import::con_frame_from_trajectory_path_nth(path_str, index) {
5125        Ok(frame) => Box::into_raw(Box::new(frame)) as *mut RKRConFrame,
5126        Err(_) => std::ptr::null_mut(),
5127    }
5128}
5129
5130/// Number of steps in a chemfiles trajectory (`Trajectory::nsteps`).
5131/// Returns `usize::MAX` on error or without the `chemfiles` feature.
5132///
5133/// # Safety
5134/// `path_c` must be a valid NUL-terminated UTF-8 path.
5135#[unsafe(no_mangle)]
5136pub unsafe extern "C" fn rkr_chemfiles_nsteps(path_c: *const c_char) -> usize {
5137    if path_c.is_null() {
5138        return usize::MAX;
5139    }
5140    let Ok(path_str) = unsafe { CStr::from_ptr(path_c) }.to_str() else {
5141        return usize::MAX;
5142    };
5143    crate::chemfiles_import::nsteps_from_trajectory_path(path_str).unwrap_or(usize::MAX)
5144}
5145
5146/// Read a chemfiles window: steps `start, start+step, … < stop`.
5147/// `stop == usize::MAX` means `nsteps`. `guess_bonds != 0` guesses topology
5148/// when the frame has no bonds. Sets `*num_frames`.
5149/// Free with `free_rkr_frame_array`. NULL on error / without chemfiles.
5150///
5151/// # Safety
5152/// `path_c` valid UTF-8; `num_frames` non-null. `topology_c` may be NULL.
5153#[unsafe(no_mangle)]
5154pub unsafe extern "C" fn rkr_read_chemfiles_range(
5155    path_c: *const c_char,
5156    start: usize,
5157    step: usize,
5158    stop: usize,
5159    topology_c: *const c_char,
5160    guess_bonds: u8,
5161    num_frames: *mut usize,
5162) -> *mut *mut RKRConFrame {
5163    if path_c.is_null() || num_frames.is_null() {
5164        return std::ptr::null_mut();
5165    }
5166    let Ok(path_str) = unsafe { CStr::from_ptr(path_c) }.to_str() else {
5167        return std::ptr::null_mut();
5168    };
5169    let topology = if topology_c.is_null() {
5170        None
5171    } else {
5172        match unsafe { CStr::from_ptr(topology_c) }.to_str() {
5173            Ok(s) if !s.is_empty() => Some(std::path::PathBuf::from(s)),
5174            _ => return std::ptr::null_mut(),
5175        }
5176    };
5177    let opts = crate::chemfiles_import::ChemfilesReadOpts {
5178        start,
5179        step,
5180        stop: if stop == usize::MAX { None } else { Some(stop) },
5181        format: None,
5182        topology,
5183        topology_format: None,
5184        guess_bonds: guess_bonds != 0,
5185    };
5186    match crate::chemfiles_import::con_frames_from_trajectory_path_with(path_str, &opts) {
5187        Ok(frames) => pack_frame_handles(frames, num_frames),
5188        Err(_) => std::ptr::null_mut(),
5189    }
5190}
5191/// Read all frames from memory with chemfiles `format` (e.g. `"XYZ"`).
5192/// Sets `*num_frames`. Free frames with `free_rkr_frame` and the array with
5193/// `free_rkr_frame_array`. NULL on error / without chemfiles.
5194///
5195/// # Safety
5196/// `data_c`, `format_c` valid UTF-8 C strings; `num_frames` non-null.
5197#[unsafe(no_mangle)]
5198pub unsafe extern "C" fn rkr_read_chemfiles_memory(
5199    data_c: *const c_char,
5200    format_c: *const c_char,
5201    num_frames: *mut usize,
5202) -> *mut *mut RKRConFrame {
5203    if data_c.is_null() || format_c.is_null() || num_frames.is_null() {
5204        return std::ptr::null_mut();
5205    }
5206    let Ok(data) = unsafe { CStr::from_ptr(data_c) }.to_str() else {
5207        return std::ptr::null_mut();
5208    };
5209    let Ok(format) = unsafe { CStr::from_ptr(format_c) }.to_str() else {
5210        return std::ptr::null_mut();
5211    };
5212    match crate::chemfiles_import::con_frames_from_memory(data, format) {
5213        Ok(frames) => {
5214            let n = frames.len();
5215            unsafe { *num_frames = n };
5216            let mut ptrs: Vec<*mut RKRConFrame> = frames
5217                .into_iter()
5218                .map(|f| Box::into_raw(Box::new(f)) as *mut RKRConFrame)
5219                .collect();
5220            let p = ptrs.as_mut_ptr();
5221            std::mem::forget(ptrs);
5222            p
5223        }
5224        Err(_) => std::ptr::null_mut(),
5225    }
5226}
5227/// Free a DLPack tensor from `rkr_frame_builder_*_dlpack` (calls deleter). Safe with NULL.
5228///
5229/// # Safety
5230/// `tensor` must be NULL or a pointer from a dlpack export of this library.
5231#[unsafe(no_mangle)]
5232pub unsafe extern "C" fn rkr_dlpack_delete(tensor: *mut RKRDLManagedTensorVersioned) {
5233    if tensor.is_null() {
5234        return;
5235    }
5236    unsafe {
5237        let t = &mut *tensor;
5238        if let Some(del) = t.deleter {
5239            del(tensor);
5240        }
5241    }
5242}
5243
5244/// Pack a frame as RCSO bytes for a caller-side `MPI_Bcast`.
5245///
5246/// `buf == NULL` writes the required size to `*out_len` and returns
5247/// success. If `buf` is non-null and `buflen` is too small, returns
5248/// `BUFFER_TOO_SMALL` and still writes the need to `*out_len`.
5249/// This function does not call MPI.
5250///
5251/// # Safety
5252/// `frame_handle` valid. `out_len` non-null. `buf` valid for `buflen` if non-null.
5253#[unsafe(no_mangle)]
5254pub unsafe extern "C" fn rkr_pack_rcso(
5255    frame_handle: *const RKRConFrame,
5256    buf: *mut u8,
5257    buflen: usize,
5258    out_len: *mut usize,
5259) -> RKRStatus {
5260    if out_len.is_null() {
5261        return RKRStatus::RKR_STATUS_NULL_POINTER;
5262    }
5263    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
5264        Some(f) => f,
5265        None => return RKRStatus::RKR_STATUS_NULL_POINTER,
5266    };
5267    let bytes = match crate::rcso::Rcso::encode_frame(frame) {
5268        Ok(b) => b,
5269        Err(_) => return RKRStatus::RKR_STATUS_VALIDATION_ERROR,
5270    };
5271    unsafe { *out_len = bytes.len() };
5272    if buf.is_null() {
5273        return RKRStatus::RKR_STATUS_SUCCESS;
5274    }
5275    if buflen < bytes.len() {
5276        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
5277    }
5278    unsafe {
5279        ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
5280    }
5281    RKRStatus::RKR_STATUS_SUCCESS
5282}
5283
5284/// Read `natoms` from an RCSO blob. No MPI.
5285///
5286/// # Safety
5287/// `buf` valid for `buflen`. `out_natoms` non-null.
5288#[unsafe(no_mangle)]
5289pub unsafe extern "C" fn rkr_unpack_rcso_natoms(
5290    buf: *const u8,
5291    buflen: usize,
5292    out_natoms: *mut u32,
5293) -> RKRStatus {
5294    if buf.is_null() || out_natoms.is_null() {
5295        return RKRStatus::RKR_STATUS_NULL_POINTER;
5296    }
5297    let bytes = unsafe { std::slice::from_raw_parts(buf, buflen) };
5298    match crate::rcso::Rcso::decode(bytes) {
5299        Ok(s) => {
5300            unsafe { *out_natoms = s.natoms };
5301            RKRStatus::RKR_STATUS_SUCCESS
5302        }
5303        Err(_) => RKRStatus::RKR_STATUS_VALIDATION_ERROR,
5304    }
5305}
5306
5307/// Copy RCSO positions into row-major `dest` (`natoms * 3` doubles).
5308///
5309/// # Safety
5310/// `buf` valid for `buflen`. `dest` valid for `dest_natoms * 3` f64.
5311#[unsafe(no_mangle)]
5312pub unsafe extern "C" fn rkr_unpack_rcso_positions(
5313    buf: *const u8,
5314    buflen: usize,
5315    dest: *mut f64,
5316    dest_natoms: u32,
5317) -> RKRStatus {
5318    if buf.is_null() || dest.is_null() {
5319        return RKRStatus::RKR_STATUS_NULL_POINTER;
5320    }
5321    let bytes = unsafe { std::slice::from_raw_parts(buf, buflen) };
5322    let soa = match crate::rcso::Rcso::decode(bytes) {
5323        Ok(s) => s,
5324        Err(_) => return RKRStatus::RKR_STATUS_VALIDATION_ERROR,
5325    };
5326    if soa.natoms != dest_natoms {
5327        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
5328    }
5329    let out = unsafe { std::slice::from_raw_parts_mut(dest, dest_natoms as usize * 3) };
5330    for (i, p) in soa.positions.iter().enumerate() {
5331        out[i * 3] = p[0];
5332        out[i * 3 + 1] = p[1];
5333        out[i * 3 + 2] = p[2];
5334    }
5335    RKRStatus::RKR_STATUS_SUCCESS
5336}
5337
5338/// Copy RCSO forces into row-major `dest`. `SECTION_ABSENT` if the blob
5339/// has no force block.
5340///
5341/// # Safety
5342/// `buf` valid for `buflen`. `dest` valid for `dest_natoms * 3` f64.
5343#[unsafe(no_mangle)]
5344pub unsafe extern "C" fn rkr_unpack_rcso_forces(
5345    buf: *const u8,
5346    buflen: usize,
5347    dest: *mut f64,
5348    dest_natoms: u32,
5349) -> RKRStatus {
5350    if buf.is_null() || dest.is_null() {
5351        return RKRStatus::RKR_STATUS_NULL_POINTER;
5352    }
5353    let bytes = unsafe { std::slice::from_raw_parts(buf, buflen) };
5354    let soa = match crate::rcso::Rcso::decode(bytes) {
5355        Ok(s) => s,
5356        Err(_) => return RKRStatus::RKR_STATUS_VALIDATION_ERROR,
5357    };
5358    let Some(forces) = soa.forces else {
5359        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
5360    };
5361    if soa.natoms != dest_natoms {
5362        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
5363    }
5364    let out = unsafe { std::slice::from_raw_parts_mut(dest, dest_natoms as usize * 3) };
5365    for (i, f) in forces.iter().enumerate() {
5366        out[i * 3] = f[0];
5367        out[i * 3 + 1] = f[1];
5368        out[i * 3 + 2] = f[2];
5369    }
5370    RKRStatus::RKR_STATUS_SUCCESS
5371}
5372
5373#[cfg(test)]
5374mod tests {
5375    use super::*;
5376    use std::ffi::{CStr, CString};
5377    #[test]
5378    fn frame_copy_positions_without_cframe() {
5379        let handle = test_frame_handle();
5380        let n = unsafe { rkr_frame_atom_count(handle) };
5381        assert_eq!(n, 1);
5382        let mut buf = vec![0.0f64; n * 3];
5383        assert_eq!(
5384            unsafe { rkr_frame_copy_positions(handle, buf.as_mut_ptr(), buf.len()) },
5385            RKRStatus::RKR_STATUS_SUCCESS
5386        );
5387        let mut tensor: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5388        assert_eq!(
5389            unsafe { rkr_frame_positions_dlpack(handle, &mut tensor) },
5390            RKRStatus::RKR_STATUS_SUCCESS
5391        );
5392        assert!(!tensor.is_null());
5393        unsafe { rkr_dlpack_delete(tensor) };
5394        assert_eq!(
5395            unsafe { rkr_frame_copy_velocities(handle, buf.as_mut_ptr(), buf.len()) },
5396            RKRStatus::RKR_STATUS_SECTION_ABSENT
5397        );
5398        unsafe { free_rkr_frame(handle) };
5399    }
5400    #[test]
5401    fn read_all_frames_c_abi_tiny() {
5402        let path = std::ffi::CString::new("resources/test/tiny_cuh2.con").unwrap();
5403        let mut n: usize = 0;
5404        let arr = unsafe { rkr_read_all_frames(path.as_ptr(), &mut n) };
5405        assert!(!arr.is_null() && n >= 1);
5406        let first = unsafe { *arr };
5407        let nat = unsafe { rkr_frame_atom_count(first) };
5408        let mut buf = vec![0.0f64; nat * 3];
5409        assert_eq!(
5410            unsafe { rkr_frame_copy_positions(first, buf.as_mut_ptr(), buf.len()) },
5411            RKRStatus::RKR_STATUS_SUCCESS
5412        );
5413        unsafe { free_rkr_frame_array(arr, n) };
5414    }
5415
5416    #[test]
5417    fn read_all_frames_n_threads_matches_auto() {
5418        let path = std::ffi::CString::new("resources/test/tiny_cuh2.con").unwrap();
5419        let mut n_auto: usize = 0;
5420        let mut n_one: usize = 0;
5421        let auto = unsafe { rkr_read_all_frames_n_threads(path.as_ptr(), &mut n_auto, 0) };
5422        let one = unsafe { rkr_read_all_frames_n_threads(path.as_ptr(), &mut n_one, 1) };
5423        assert!(!auto.is_null() && !one.is_null());
5424        assert_eq!(n_auto, n_one);
5425        assert_eq!(n_auto, 1);
5426        let nat_auto = unsafe { rkr_frame_atom_count(*auto) };
5427        let nat_one = unsafe { rkr_frame_atom_count(*one) };
5428        assert_eq!(nat_auto, nat_one);
5429        unsafe { free_rkr_frame_array(auto, n_auto) };
5430        unsafe { free_rkr_frame_array(one, n_one) };
5431    }
5432
5433    #[test]
5434    fn free_rkr_frame_ptr_array_keeps_frames() {
5435        let path = std::ffi::CString::new("resources/test/tiny_cuh2.con").unwrap();
5436        let mut n: usize = 0;
5437        let arr = unsafe { rkr_read_all_frames(path.as_ptr(), &mut n) };
5438        assert!(!arr.is_null() && n >= 1);
5439        let first = unsafe { *arr };
5440        // free only outer array; first frame remains owned
5441        let rest: Vec<*mut RKRConFrame> = (1..n).map(|i| unsafe { *arr.add(i) }).collect();
5442        unsafe { free_rkr_frame_ptr_array(arr, n) };
5443        assert!(unsafe { rkr_frame_atom_count(first) } >= 1);
5444        unsafe { free_rkr_frame(first) };
5445        for h in rest {
5446            if !h.is_null() {
5447                unsafe { free_rkr_frame(h) };
5448            }
5449        }
5450    }
5451    fn test_frame_handle() -> *mut RKRConFrame {
5452        let mut builder = ConFrameBuilder::new([10.0, 10.0, 10.0], [90.0, 90.0, 90.0]);
5453        builder
5454            .prebox_header("Generated by test")
5455            .postbox_header(["0 0".to_string(), "0 0 0".to_string()]);
5456        builder.add_atom("Cu", 0.0, 0.0, 0.0, [false, false, false], 0, 63.546);
5457        Box::into_raw(Box::new(builder.build().unwrap())) as *mut RKRConFrame
5458    }
5459    #[test]
5460    fn header_line_rejects_null_buffer() {
5461        let frame = test_frame_handle();
5462        let status = unsafe { rkr_frame_get_header_line(frame, true, 0, std::ptr::null_mut(), 16) };
5463        unsafe { free_rkr_frame(frame) };
5464        assert_eq!(status, RKRStatus::RKR_STATUS_NULL_POINTER);
5465    }
5466    #[test]
5467    fn header_line_rejects_empty_buffer() {
5468        let frame = test_frame_handle();
5469        let mut buffer = [0 as c_char; 1];
5470        let status = unsafe { rkr_frame_get_header_line(frame, true, 0, buffer.as_mut_ptr(), 0) };
5471        unsafe { free_rkr_frame(frame) };
5472        assert_eq!(status, RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL);
5473    }
5474    #[test]
5475    fn header_line_truncates_and_terminates_buffer() {
5476        let frame = test_frame_handle();
5477        let mut buffer = [0 as c_char; 10];
5478        let status =
5479            unsafe { rkr_frame_get_header_line(frame, true, 0, buffer.as_mut_ptr(), buffer.len()) };
5480        unsafe { free_rkr_frame(frame) };
5481        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
5482        let copied = unsafe { CStr::from_ptr(buffer.as_ptr()) };
5483        assert_eq!(copied.to_str().unwrap(), "Generated");
5484    }
5485
5486    #[test]
5487    fn pack_rcso_size_query_then_dest() {
5488        let path = std::ffi::CString::new("resources/test/tiny_cuh2_forces.con").unwrap();
5489        let frame = unsafe { rkr_read_nth_frame(path.as_ptr(), 0) };
5490        assert!(!frame.is_null());
5491        let mut need = 0usize;
5492        let st = unsafe { rkr_pack_rcso(frame, std::ptr::null_mut(), 0, &mut need) };
5493        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
5494        assert!(need >= 24);
5495        let mut buf = vec![0u8; need];
5496        let mut wrote = 0usize;
5497        let st = unsafe { rkr_pack_rcso(frame, buf.as_mut_ptr(), buf.len(), &mut wrote) };
5498        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
5499        assert_eq!(wrote, need);
5500        assert_eq!(&buf[0..4], b"RCSO");
5501        let mut natoms = 0u32;
5502        let st = unsafe { rkr_unpack_rcso_natoms(buf.as_ptr(), buf.len(), &mut natoms) };
5503        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
5504        assert_eq!(natoms, unsafe { rkr_frame_atom_count(frame) } as u32);
5505        let mut xyz = vec![0.0f64; natoms as usize * 3];
5506        let st =
5507            unsafe { rkr_unpack_rcso_positions(buf.as_ptr(), buf.len(), xyz.as_mut_ptr(), natoms) };
5508        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
5509        let mut frc = vec![0.0f64; natoms as usize * 3];
5510        let st =
5511            unsafe { rkr_unpack_rcso_forces(buf.as_ptr(), buf.len(), frc.as_mut_ptr(), natoms) };
5512        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
5513        unsafe { free_rkr_frame(frame) };
5514    }
5515
5516    fn test_builder_handle() -> *mut RKRConFrameBuilder {
5517        let cell = [10.0, 11.0, 12.0];
5518        let angles = [90.0, 91.0, 92.0];
5519        unsafe {
5520            rkr_frame_new(
5521                cell.as_ptr(),
5522                angles.as_ptr(),
5523                ptr::null(),
5524                ptr::null(),
5525                ptr::null(),
5526                ptr::null(),
5527            )
5528        }
5529    }
5530    fn c_string(s: &str) -> CString {
5531        CString::new(s).unwrap()
5532    }
5533    #[test]
5534    fn builder_set_units_c_abi_stores_the_object() {
5535        let builder = test_builder_handle();
5536        let units = c_string(
5537            r#"{"length":"A","energy":"ev","mass":"amu","time":"(amu*angstrom^2/eV)^0.5"}"#,
5538        );
5539        let status = unsafe { rkr_frame_builder_set_units(builder, units.as_ptr()) };
5540        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
5541        let symbol = c_string("H");
5542        assert_eq!(
5543            unsafe { rkr_frame_add_atom(builder, symbol.as_ptr(), 0.0, 0.0, 0.0, false, 0, 1.0) },
5544            RKRStatus::RKR_STATUS_SUCCESS
5545        );
5546        let frame = unsafe { rkr_frame_builder_build(builder) };
5547        assert!(!frame.is_null());
5548        let json = unsafe { rkr_frame_metadata_json(frame) };
5549        assert!(!json.is_null());
5550        let text = unsafe { CStr::from_ptr(json) }.to_str().unwrap();
5551        assert!(text.contains(r#""length":"angstrom""#), "{text}");
5552        assert!(text.contains(r#""energy":"eV""#), "{text}");
5553        assert!(
5554            text.contains(r#""time":"(amu*angstrom^2/eV)^0.5""#),
5555            "{text}"
5556        );
5557        unsafe {
5558            rkr_free_string(json);
5559            free_rkr_frame(frame);
5560        }
5561    }
5562    #[test]
5563    fn builder_stamps_con_spec_minor_one_unless_set() {
5564        let builder = test_builder_handle();
5565        let symbol = c_string("H");
5566        assert_eq!(
5567            unsafe { rkr_frame_add_atom(builder, symbol.as_ptr(), 0.0, 0.0, 0.0, false, 0, 1.0) },
5568            RKRStatus::RKR_STATUS_SUCCESS
5569        );
5570        let frame = unsafe { rkr_frame_builder_build(builder) };
5571        let json = unsafe { rkr_frame_metadata_json(frame) };
5572        let text = unsafe { CStr::from_ptr(json) }
5573            .to_str()
5574            .unwrap()
5575            .to_string();
5576        assert!(text.contains(r#""con_spec_minor":1"#), "{text}");
5577        unsafe {
5578            rkr_free_string(json);
5579            free_rkr_frame(frame);
5580        }
5581
5582        let builder = test_builder_handle();
5583        assert_eq!(
5584            unsafe { rkr_frame_builder_set_con_spec_minor(builder, 4) },
5585            RKRStatus::RKR_STATUS_SUCCESS
5586        );
5587        let symbol = c_string("H");
5588        assert_eq!(
5589            unsafe { rkr_frame_add_atom(builder, symbol.as_ptr(), 0.0, 0.0, 0.0, false, 0, 1.0) },
5590            RKRStatus::RKR_STATUS_SUCCESS
5591        );
5592        let frame = unsafe { rkr_frame_builder_build(builder) };
5593        let json = unsafe { rkr_frame_metadata_json(frame) };
5594        let text = unsafe { CStr::from_ptr(json) }
5595            .to_str()
5596            .unwrap()
5597            .to_string();
5598        assert!(text.contains(r#""con_spec_minor":4"#), "{text}");
5599        unsafe {
5600            rkr_free_string(json);
5601            free_rkr_frame(frame);
5602        }
5603    }
5604    unsafe fn assert_single_atom(
5605        frame: *mut RKRConFrame,
5606        fixed: [bool; 3],
5607        velocity: Option<[f64; 3]>,
5608        forces: Option<[f64; 3]>,
5609    ) {
5610        let c_frame = unsafe { rkr_frame_to_c_frame(frame) };
5611        assert!(!c_frame.is_null());
5612        let c_frame_ref = unsafe { &*c_frame };
5613        assert_eq!(c_frame_ref.num_atoms, 1);
5614        assert_eq!(c_frame_ref.has_velocities, velocity.is_some());
5615        assert_eq!(c_frame_ref.has_forces, forces.is_some());
5616        let atom = unsafe { &*c_frame_ref.atoms };
5617        assert_eq!(atom.fixed_x, fixed[0]);
5618        assert_eq!(atom.fixed_y, fixed[1]);
5619        assert_eq!(atom.fixed_z, fixed[2]);
5620        assert_eq!(atom.is_fixed, fixed.iter().any(|&value| value));
5621        assert_eq!(atom.has_velocity, velocity.is_some());
5622        assert_eq!(atom.has_forces, forces.is_some());
5623        if let Some([vx, vy, vz]) = velocity {
5624            assert_eq!([atom.vx, atom.vy, atom.vz], [vx, vy, vz]);
5625        }
5626        if let Some([fx, fy, fz]) = forces {
5627            assert_eq!([atom.fx, atom.fy, atom.fz], [fx, fy, fz]);
5628        }
5629        unsafe { free_c_frame(c_frame) };
5630        unsafe { free_rkr_frame(frame) };
5631    }
5632    #[test]
5633    fn builder_preserves_fixed_mask_for_atom_without_velocity_or_forces() {
5634        let builder = test_builder_handle();
5635        let symbol = c_string("Cu");
5636        let status = unsafe {
5637            rkr_frame_add_atom_with_fixed_mask(
5638                builder,
5639                symbol.as_ptr(),
5640                1.0,
5641                2.0,
5642                3.0,
5643                true,
5644                false,
5645                true,
5646                7,
5647                63.546,
5648            )
5649        };
5650        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
5651        let frame = unsafe { rkr_frame_builder_build(builder) };
5652        unsafe { assert_single_atom(frame, [true, false, true], None, None) };
5653    }
5654    #[test]
5655    fn builder_preserves_fixed_mask_for_atom_with_velocity() {
5656        let builder = test_builder_handle();
5657        let symbol = c_string("H");
5658        let status = unsafe {
5659            rkr_frame_add_atom_with_velocity_fixed_mask(
5660                builder,
5661                symbol.as_ptr(),
5662                1.0,
5663                2.0,
5664                3.0,
5665                false,
5666                true,
5667                false,
5668                9,
5669                1.008,
5670                0.1,
5671                0.2,
5672                0.3,
5673            )
5674        };
5675        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
5676        let frame = unsafe { rkr_frame_builder_build(builder) };
5677        unsafe { assert_single_atom(frame, [false, true, false], Some([0.1, 0.2, 0.3]), None) };
5678    }
5679    #[test]
5680    fn builder_preserves_fixed_mask_for_atom_with_forces() {
5681        let builder = test_builder_handle();
5682        let symbol = c_string("O");
5683        let status = unsafe {
5684            rkr_frame_add_atom_with_forces_fixed_mask(
5685                builder,
5686                symbol.as_ptr(),
5687                1.0,
5688                2.0,
5689                3.0,
5690                true,
5691                true,
5692                false,
5693                11,
5694                15.999,
5695                -0.1,
5696                -0.2,
5697                -0.3,
5698            )
5699        };
5700        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
5701        let frame = unsafe { rkr_frame_builder_build(builder) };
5702        unsafe { assert_single_atom(frame, [true, true, false], None, Some([-0.1, -0.2, -0.3])) };
5703    }
5704    #[test]
5705    fn builder_preserves_fixed_mask_for_atom_with_velocity_and_forces() {
5706        let builder = test_builder_handle();
5707        let symbol = c_string("N");
5708        let status = unsafe {
5709            rkr_frame_add_atom_with_velocity_and_forces_fixed_mask(
5710                builder,
5711                symbol.as_ptr(),
5712                1.0,
5713                2.0,
5714                3.0,
5715                false,
5716                true,
5717                true,
5718                13,
5719                14.007,
5720                0.4,
5721                0.5,
5722                0.6,
5723                -0.4,
5724                -0.5,
5725                -0.6,
5726            )
5727        };
5728        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
5729        let frame = unsafe { rkr_frame_builder_build(builder) };
5730        unsafe {
5731            assert_single_atom(
5732                frame,
5733                [false, true, true],
5734                Some([0.4, 0.5, 0.6]),
5735                Some([-0.4, -0.5, -0.6]),
5736            )
5737        };
5738    }
5739    #[test]
5740    fn builder_bool_fixed_functions_set_all_axes_together() {
5741        let builder = test_builder_handle();
5742        let cu = c_string("Cu");
5743        let h = c_string("H");
5744        let atom_status =
5745            unsafe { rkr_frame_add_atom(builder, cu.as_ptr(), 1.0, 2.0, 3.0, true, 1, 63.546) };
5746        assert_eq!(atom_status, RKRStatus::RKR_STATUS_SUCCESS);
5747        let velocity_status = unsafe {
5748            rkr_frame_add_atom_with_velocity(
5749                builder,
5750                h.as_ptr(),
5751                4.0,
5752                5.0,
5753                6.0,
5754                false,
5755                2,
5756                1.008,
5757                0.7,
5758                0.8,
5759                0.9,
5760            )
5761        };
5762        assert_eq!(velocity_status, RKRStatus::RKR_STATUS_SUCCESS);
5763        let frame = unsafe { rkr_frame_builder_build(builder) };
5764        let c_frame = unsafe { rkr_frame_to_c_frame(frame) };
5765        assert!(!c_frame.is_null());
5766        let c_frame_ref = unsafe { &*c_frame };
5767        assert_eq!(c_frame_ref.num_atoms, 2);
5768        let atoms = unsafe { std::slice::from_raw_parts(c_frame_ref.atoms, c_frame_ref.num_atoms) };
5769        assert_eq!(
5770            [atoms[0].fixed_x, atoms[0].fixed_y, atoms[0].fixed_z],
5771            [true, true, true]
5772        );
5773        assert_eq!(
5774            [atoms[1].fixed_x, atoms[1].fixed_y, atoms[1].fixed_z],
5775            [false, false, false]
5776        );
5777        unsafe { free_c_frame(c_frame) };
5778        unsafe { free_rkr_frame(frame) };
5779    }
5780    #[test]
5781    fn status_message_returns_static_strings_for_all_status_values() {
5782        let cases = [
5783            (RKRStatus::RKR_STATUS_SUCCESS, "success"),
5784            (RKRStatus::RKR_STATUS_NULL_POINTER, "null pointer"),
5785            (RKRStatus::RKR_STATUS_INVALID_UTF8, "invalid UTF-8"),
5786            (RKRStatus::RKR_STATUS_INVALID_JSON, "invalid JSON"),
5787            (RKRStatus::RKR_STATUS_IO_ERROR, "I/O error"),
5788            (
5789                RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS,
5790                "index out of bounds",
5791            ),
5792            (RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL, "buffer too small"),
5793            (RKRStatus::RKR_STATUS_INTERNAL_ERROR, "internal error"),
5794            (RKRStatus::RKR_STATUS_SECTION_ABSENT, "section absent"),
5795            (RKRStatus::RKR_STATUS_VALIDATION_ERROR, "validation error"),
5796            (RKRStatus::RKR_STATUS_SELECTION_ERROR, "selection error"),
5797            (
5798                RKRStatus::RKR_STATUS_FEATURE_DISABLED,
5799                "feature disabled in this build",
5800            ),
5801        ];
5802        for (status, expected) in cases {
5803            let message = unsafe { CStr::from_ptr(rkr_status_message(status)) };
5804            assert_eq!(message.to_str().unwrap(), expected);
5805        }
5806    }
5807    // ----- DLPack FFI smoke tests ----------------------------------------------
5808    /// Assert DLPack tensor layout without assuming the host language uses f64
5809    /// buffers — consumers must read `dtype` / `ndim` / `shape` from the tensor.
5810    unsafe fn assert_dlpack_cpu_float(
5811        t: *mut RKRDLManagedTensorVersioned,
5812        expect_ndim: i32,
5813        expect_shape: &[i64],
5814        expect_bits: u8,
5815    ) {
5816        assert!(!t.is_null());
5817        let dl = unsafe { &(*t).dl_tensor };
5818        assert_eq!(dl.ndim, expect_ndim);
5819        let shape = unsafe { std::slice::from_raw_parts(dl.shape, expect_ndim as usize) };
5820        assert_eq!(shape, expect_shape);
5821        assert_eq!(dl.dtype.code, dlpk::sys::DLDataTypeCode::kDLFloat);
5822        assert_eq!(dl.dtype.bits, expect_bits);
5823        assert_eq!(dl.dtype.lanes, 1);
5824        assert_eq!(dl.device, dlpk::sys::DLDevice::cpu());
5825        assert!(!dl.data.is_null());
5826    }
5827
5828    #[test]
5829    fn frame_optional_section_dlpack_present_and_absent() {
5830        // Absent on coordinate-only frame
5831        let handle = test_frame_handle();
5832        let mut t: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5833        assert_eq!(
5834            unsafe { rkr_frame_velocities_dlpack(handle, &mut t) },
5835            RKRStatus::RKR_STATUS_SECTION_ABSENT
5836        );
5837        assert!(t.is_null());
5838        assert_eq!(
5839            unsafe { rkr_frame_forces_dlpack(handle, &mut t) },
5840            RKRStatus::RKR_STATUS_SECTION_ABSENT
5841        );
5842        assert_eq!(
5843            unsafe { rkr_frame_atom_energies_dlpack(handle, &mut t) },
5844            RKRStatus::RKR_STATUS_SECTION_ABSENT
5845        );
5846        unsafe { free_rkr_frame(handle) };
5847
5848        // Present on .convel fixture — (N, 3) float, N from atom_count
5849        let path = CString::new("resources/test/tiny_cuh2.convel").unwrap();
5850        let fr = unsafe { rkr_read_first_frame(path.as_ptr()) };
5851        assert!(!fr.is_null());
5852        let n = unsafe { rkr_frame_atom_count(fr) } as i64;
5853        assert!(n > 0);
5854        let mut vel: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5855        let st = unsafe { rkr_frame_velocities_dlpack(fr, &mut vel) };
5856        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
5857        unsafe {
5858            assert_dlpack_cpu_float(vel, 2, &[n, 3], 64);
5859            rkr_dlpack_delete(vel);
5860            free_rkr_frame(fr);
5861        }
5862
5863        // Forces (N,3) + energies (N,) via builder → frame
5864        let cell = [10.0f64; 3];
5865        let ang = [90.0f64; 3];
5866        let b = unsafe {
5867            rkr_frame_new(
5868                cell.as_ptr(),
5869                ang.as_ptr(),
5870                std::ptr::null(),
5871                std::ptr::null(),
5872                std::ptr::null(),
5873                std::ptr::null(),
5874            )
5875        };
5876        assert!(!b.is_null());
5877        let sym = CString::new("H").unwrap();
5878        unsafe {
5879            rkr_frame_add_atom_with_velocity_and_forces_fixed_mask(
5880                b,
5881                sym.as_ptr(),
5882                0.0,
5883                0.0,
5884                0.0,
5885                false,
5886                false,
5887                false,
5888                0,
5889                1.0,
5890                0.1,
5891                0.0,
5892                0.0,
5893                0.0,
5894                0.0,
5895                -1.0,
5896            );
5897            rkr_frame_builder_set_last_energy(b, -0.5);
5898        }
5899        let built = unsafe { rkr_frame_builder_build(b) };
5900        assert!(!built.is_null());
5901        let n_built = unsafe { rkr_frame_atom_count(built) } as i64;
5902        let mut frc: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5903        let mut eng: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5904        assert_eq!(
5905            unsafe { rkr_frame_forces_dlpack(built, &mut frc) },
5906            RKRStatus::RKR_STATUS_SUCCESS
5907        );
5908        unsafe { assert_dlpack_cpu_float(frc, 2, &[n_built, 3], 64) };
5909        assert_eq!(
5910            unsafe { rkr_frame_atom_energies_dlpack(built, &mut eng) },
5911            RKRStatus::RKR_STATUS_SUCCESS
5912        );
5913        unsafe { assert_dlpack_cpu_float(eng, 1, &[n_built], 64) };
5914        // Explicit f32 via DLPack-shaped DLDataType (code=kDLFloat=2, bits=32)
5915        let opts32 = RKRDlpackExportOptions {
5916            dtype: RKRDLDataType {
5917                code: rkr_dl_type_code::RKR_DL_FLOAT,
5918                bits: 32,
5919                lanes: 1,
5920            },
5921            device: RKRDLDevice {
5922                device_type: rkr_dl_device_type::RKR_DL_CPU,
5923                device_id: 0,
5924            },
5925        };
5926        let mut pos32: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5927        assert_eq!(
5928            unsafe { rkr_frame_positions_dlpack_ex(built, &opts32, &mut pos32) },
5929            RKRStatus::RKR_STATUS_SUCCESS
5930        );
5931        unsafe {
5932            assert_dlpack_cpu_float(pos32, 2, &[n_built, 3], 32);
5933            rkr_dlpack_delete(pos32);
5934        }
5935        // CUDA device requests: FEATURE_DISABLED without --features cuda;
5936        // with cuda, H2D export succeeds (kDLCUDA).
5937        let cuda_dev = RKRDlpackExportOptions {
5938            dtype: RKRDLDataType {
5939                code: rkr_dl_type_code::RKR_DL_FLOAT,
5940                bits: 64,
5941                lanes: 1,
5942            },
5943            device: RKRDLDevice {
5944                device_type: rkr_dl_device_type::RKR_DL_CUDA,
5945                device_id: 0,
5946            },
5947        };
5948        let mut junk: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5949        #[cfg(not(feature = "cuda"))]
5950        assert_eq!(
5951            unsafe { rkr_frame_positions_dlpack_ex(built, &cuda_dev, &mut junk) },
5952            RKRStatus::RKR_STATUS_FEATURE_DISABLED
5953        );
5954        #[cfg(feature = "cuda")]
5955        {
5956            assert_eq!(
5957                unsafe { rkr_frame_positions_dlpack_ex(built, &cuda_dev, &mut junk) },
5958                RKRStatus::RKR_STATUS_SUCCESS
5959            );
5960            assert!(!junk.is_null());
5961            let dl = unsafe { &(*junk).dl_tensor };
5962            assert_eq!(dl.device.device_type, dlpk::sys::DLDeviceType::kDLCUDA);
5963            assert!(!dl.data.is_null());
5964            unsafe { rkr_dlpack_delete(junk) };
5965            // Also rkr_frame_positions_as_dlpack with CUDA
5966            let mut as_cuda: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5967            assert_eq!(
5968                unsafe {
5969                    rkr_frame_positions_as_dlpack(
5970                        built,
5971                        rkr_dl_device_type::RKR_DL_CUDA,
5972                        0,
5973                        0,
5974                        1,
5975                        0,
5976                        &mut as_cuda,
5977                    )
5978                },
5979                RKRStatus::RKR_STATUS_SUCCESS
5980            );
5981            assert!(!as_cuda.is_null());
5982            unsafe {
5983                assert_eq!(
5984                    (*as_cuda).dl_tensor.device.device_type,
5985                    dlpk::sys::DLDeviceType::kDLCUDA
5986                );
5987                rkr_dlpack_delete(as_cuda);
5988            }
5989        }
5990        // Unsupported dtype bits on CPU
5991        let bad_bits = RKRDlpackExportOptions {
5992            dtype: RKRDLDataType {
5993                code: rkr_dl_type_code::RKR_DL_FLOAT,
5994                bits: 16,
5995                lanes: 1,
5996            },
5997            device: RKRDLDevice {
5998                device_type: rkr_dl_device_type::RKR_DL_CPU,
5999                device_id: 0,
6000            },
6001        };
6002        assert_eq!(
6003            unsafe { rkr_frame_positions_dlpack_ex(built, &bad_bits, &mut junk) },
6004            RKRStatus::RKR_STATUS_VALIDATION_ERROR
6005        );
6006        // Metatensor-style as_dlpack reflects storage (f64 CPU), not a cast target
6007        let mut as_t: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
6008        assert_eq!(
6009            unsafe {
6010                rkr_frame_positions_as_dlpack(
6011                    built,
6012                    rkr_dl_device_type::RKR_DL_CPU,
6013                    0,
6014                    0,
6015                    1,
6016                    0,
6017                    &mut as_t,
6018                )
6019            },
6020            RKRStatus::RKR_STATUS_SUCCESS
6021        );
6022        unsafe {
6023            assert_dlpack_cpu_float(as_t, 2, &[n_built, 3], 64);
6024        }
6025        // Mutate frame positions, then ingest tensor and require values restored
6026        {
6027            let fr = unsafe { &mut *(built as *mut ConFrame) };
6028            for a in fr.atom_data.iter_mut() {
6029                a.x = -99.0;
6030                a.y = -99.0;
6031                a.z = -99.0;
6032            }
6033        }
6034        assert_eq!(
6035            unsafe { rkr_frame_positions_from_dlpack(built, as_t) },
6036            RKRStatus::RKR_STATUS_SUCCESS
6037        );
6038        {
6039            let fr = unsafe { &*(built as *const ConFrame) };
6040            assert!((fr.atom_data[0].x - 0.0).abs() < 1e-12);
6041            assert!((fr.atom_data[0].y - 0.0).abs() < 1e-12);
6042            assert!((fr.atom_data[0].z - 0.0).abs() < 1e-12);
6043        }
6044        // Re-export must match ingested coordinates (not zeros / garbage)
6045        let mut again: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
6046        assert_eq!(
6047            unsafe {
6048                rkr_frame_positions_as_dlpack(
6049                    built,
6050                    rkr_dl_device_type::RKR_DL_CPU,
6051                    0,
6052                    0,
6053                    1,
6054                    0,
6055                    &mut again,
6056                )
6057            },
6058            RKRStatus::RKR_STATUS_SUCCESS
6059        );
6060        unsafe {
6061            let dl = &(*again).dl_tensor;
6062            let data = std::slice::from_raw_parts(dl.data as *const f64, 3);
6063            assert!((data[0] - 0.0).abs() < 1e-12);
6064            assert!((data[1] - 0.0).abs() < 1e-12);
6065            assert!((data[2] - 0.0).abs() < 1e-12);
6066            rkr_dlpack_delete(again);
6067            rkr_dlpack_delete(as_t);
6068            rkr_dlpack_delete(frc);
6069            rkr_dlpack_delete(eng);
6070            free_rkr_frame(built);
6071        }
6072    }
6073
6074    #[test]
6075    fn ffi_positions_dlpack_round_trip() {
6076        let handle = test_builder_handle();
6077        let sym = c_string("Cu");
6078        unsafe {
6079            rkr_frame_add_atom_full(
6080                handle,
6081                sym.as_ptr(),
6082                1.0,
6083                2.0,
6084                3.0,
6085                false,
6086                false,
6087                false,
6088                7,
6089                63.5,
6090                ptr::null(),
6091                ptr::null(),
6092            )
6093        };
6094        let mut t: *mut RKRDLManagedTensorVersioned = ptr::null_mut();
6095        let status = unsafe { rkr_frame_builder_positions_dlpack(handle, &mut t) };
6096        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
6097        assert!(!t.is_null());
6098        // Inspect the DLPack tensor: shape (1, 3), dtype kDLFloat / 64, CPU.
6099        let dl = unsafe { &(*t).dl_tensor };
6100        assert_eq!(dl.ndim, 2);
6101        let shape = unsafe { std::slice::from_raw_parts(dl.shape, 2) };
6102        assert_eq!(shape, &[1, 3]);
6103        assert_eq!(dl.dtype.code, dlpk::sys::DLDataTypeCode::kDLFloat);
6104        assert_eq!(dl.dtype.bits, 64);
6105        assert_eq!(dl.dtype.lanes, 1);
6106        assert_eq!(dl.device, dlpk::sys::DLDevice::cpu());
6107        let data = unsafe { std::slice::from_raw_parts(dl.data as *const f64, 3) };
6108        assert_eq!(data, &[1.0, 2.0, 3.0]);
6109        // Invoke the deleter the same way a C consumer would.
6110        let deleter = unsafe { (*t).deleter };
6111        if let Some(del) = deleter {
6112            unsafe { del(t) };
6113        }
6114        unsafe { free_rkr_frame_builder(handle) };
6115    }
6116    #[test]
6117    fn ffi_velocities_dlpack_section_absent() {
6118        let handle = test_builder_handle();
6119        let sym = c_string("Cu");
6120        unsafe {
6121            rkr_frame_add_atom_full(
6122                handle,
6123                sym.as_ptr(),
6124                0.0,
6125                0.0,
6126                0.0,
6127                false,
6128                false,
6129                false,
6130                0,
6131                63.5,
6132                ptr::null(),
6133                ptr::null(),
6134            )
6135        };
6136        let mut t: *mut RKRDLManagedTensorVersioned = ptr::null_mut();
6137        let status = unsafe { rkr_frame_builder_velocities_dlpack(handle, &mut t) };
6138        assert_eq!(status, RKRStatus::RKR_STATUS_SECTION_ABSENT);
6139        assert!(t.is_null());
6140        unsafe { free_rkr_frame_builder(handle) };
6141    }
6142    #[test]
6143    fn ffi_dlpack_null_handle_rejects() {
6144        let mut t: *mut RKRDLManagedTensorVersioned = ptr::null_mut();
6145        let status = unsafe { rkr_frame_builder_positions_dlpack(ptr::null(), &mut t) };
6146        assert_eq!(status, RKRStatus::RKR_STATUS_NULL_POINTER);
6147        assert!(t.is_null());
6148    }
6149    #[cfg(feature = "chemfiles")]
6150    #[test]
6151    fn rkr_frame_select_finds_oxygen() {
6152        use crate::types::ConFrameBuilder;
6153        let mut b = ConFrameBuilder::new([10.0; 3], [90.0; 3]);
6154        b.add_atom("O", 0.0, 0.0, 0.0, [false; 3], 0, 16.0);
6155        b.add_atom("H", 1.0, 0.0, 0.0, [false; 3], 1, 1.0);
6156        let frame = b.build().unwrap();
6157        let frame_ptr = Box::into_raw(Box::new(frame)) as *mut RKRConFrame;
6158        let sel = CString::new("name O").unwrap();
6159        let mut out: *mut RKRSelectionResult = ptr::null_mut();
6160        let st = unsafe { rkr_frame_select(frame_ptr, sel.as_ptr(), &mut out) };
6161        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
6162        assert!(!out.is_null());
6163        let n = unsafe { rkr_selection_result_match_count(out) };
6164        assert_eq!(n, 1);
6165        let mut atoms = [u64::MAX; 4];
6166        let mut size = 0u32;
6167        let st2 = unsafe { rkr_selection_result_match_at(out, 0, atoms.as_mut_ptr(), &mut size) };
6168        assert_eq!(st2, RKRStatus::RKR_STATUS_SUCCESS);
6169        assert_eq!(size, 1);
6170        assert_eq!(atoms[0], 0);
6171        unsafe {
6172            rkr_selection_result_free(out);
6173            free_rkr_frame(frame_ptr);
6174        }
6175    }
6176    /// C surface: chemfiles selection.cpp chain topology via `rkr_frame_select`.
6177    #[cfg(feature = "chemfiles")]
6178    #[test]
6179    fn rkr_frame_select_cpp_topology_bonds_angles_dihedrals() {
6180        use crate::types::{Bond, ConFrameBuilder};
6181        // H-O-O-H chain (chemfiles testing_frame topology), bonds in atom_data order.
6182        let mut b = ConFrameBuilder::new([10.0; 3], [90.0; 3]);
6183        b.add_atom("H", 0.0, 1.0, 2.0, [false; 3], 0, 1.0);
6184        b.add_atom("O", 1.0, 2.0, 3.0, [false; 3], 1, 16.0);
6185        b.add_atom("O", 2.0, 3.0, 4.0, [false; 3], 2, 16.0);
6186        b.add_atom("H", 3.0, 4.0, 5.0, [false; 3], 3, 1.0);
6187        let mut frame = b.build().unwrap();
6188        let id_to = |id: u64| {
6189            frame
6190                .atom_data
6191                .iter()
6192                .position(|a| a.atom_id == id)
6193                .unwrap() as u32
6194        };
6195        frame.header.set_bonds(&[
6196            Bond::new(id_to(0), id_to(1)),
6197            Bond::new(id_to(1), id_to(2)),
6198            Bond::new(id_to(2), id_to(3)),
6199        ]);
6200        let frame_ptr = Box::into_raw(Box::new(frame)) as *mut RKRConFrame;
6201        let run = |sel: &str| -> (u64, u32) {
6202            let csel = CString::new(sel).unwrap();
6203            let mut out: *mut RKRSelectionResult = ptr::null_mut();
6204            let st = unsafe { rkr_frame_select(frame_ptr, csel.as_ptr(), &mut out) };
6205            assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS, "select failed: {sel}");
6206            let n = unsafe { rkr_selection_result_match_count(out) };
6207            let ctx = unsafe { rkr_selection_result_context_size(out) };
6208            unsafe { rkr_selection_result_free(out) };
6209            (n, ctx)
6210        };
6211        assert_eq!(run("bonds: all"), (3, 2));
6212        assert_eq!(run("angles: all"), (2, 3));
6213        assert_eq!(run("dihedrals: all"), (1, 4));
6214        assert_eq!(run("bonds: name(#1) O and type(#2) H").0, 2);
6215        assert_eq!(
6216            run("two: type(#1) H and name(#2) O and is_bonded(#1, #2)").0,
6217            run("bonds: type(#1) H and name(#2) O").0
6218        );
6219        unsafe { free_rkr_frame(frame_ptr) };
6220    }
6221    #[cfg(feature = "metatensor")]
6222    fn assert_mts_block_shape(block: *mut metatensor::c_api::mts_block_t, n: usize, props: usize) {
6223        assert!(!block.is_null());
6224        let mut array = unsafe { std::mem::zeroed::<metatensor::c_api::mts_array_t>() };
6225        let status = unsafe { metatensor::c_api::mts_block_data(block, &mut array) };
6226        assert_eq!(status, metatensor::c_api::MTS_SUCCESS);
6227        let shape_fn = array
6228            .shape
6229            .expect("mts_array_t.shape from metatensor C API");
6230        let mut shape_ptr: *const usize = std::ptr::null();
6231        let mut shape_count: usize = 0;
6232        let st_shape = unsafe { shape_fn(array.ptr, &mut shape_ptr, &mut shape_count) };
6233        assert_eq!(st_shape, metatensor::c_api::MTS_SUCCESS);
6234        assert_eq!(shape_count, 2);
6235        let shape = unsafe { std::slice::from_raw_parts(shape_ptr, shape_count) };
6236        assert_eq!(shape[0], n);
6237        assert_eq!(shape[1], props);
6238        let samples = unsafe { metatensor::c_api::mts_block_labels(block, 0) };
6239        let prop_lab = unsafe { metatensor::c_api::mts_block_labels(block, 1) };
6240        assert!(!samples.is_null() && !prop_lab.is_null());
6241    }
6242    #[cfg(feature = "metatensor")]
6243    #[test]
6244    fn metatensor_positions_via_c_abi() {
6245        let handle = test_frame_handle();
6246        let mut out: *mut metatensor::c_api::mts_block_t = std::ptr::null_mut();
6247        let st = unsafe { rkr_frame_metatensor_positions_block(handle, &mut out) };
6248        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
6249        assert_mts_block_shape(out, 1, 3);
6250        unsafe { rkr_mts_block_free(out) };
6251        for (name, export) in [
6252            (
6253                "velocities",
6254                rkr_frame_metatensor_velocities_block
6255                    as unsafe extern "C" fn(
6256                        *const RKRConFrame,
6257                        *mut *mut metatensor::c_api::mts_block_t,
6258                    ) -> RKRStatus,
6259            ),
6260            (
6261                "forces",
6262                rkr_frame_metatensor_forces_block
6263                    as unsafe extern "C" fn(
6264                        *const RKRConFrame,
6265                        *mut *mut metatensor::c_api::mts_block_t,
6266                    ) -> RKRStatus,
6267            ),
6268            (
6269                "atom_energies",
6270                rkr_frame_metatensor_atom_energies_block
6271                    as unsafe extern "C" fn(
6272                        *const RKRConFrame,
6273                        *mut *mut metatensor::c_api::mts_block_t,
6274                    ) -> RKRStatus,
6275            ),
6276        ] {
6277            let mut o: *mut metatensor::c_api::mts_block_t = std::ptr::null_mut();
6278            let st_abs = unsafe { export(handle, &mut o) };
6279            assert_eq!(
6280                st_abs,
6281                RKRStatus::RKR_STATUS_SECTION_ABSENT,
6282                "{name} must be SECTION_ABSENT on minimal test frame"
6283            );
6284            assert!(o.is_null());
6285        }
6286        unsafe { free_rkr_frame(handle) };
6287    }
6288
6289    #[cfg(feature = "metatensor")]
6290    #[test]
6291    fn metatensor_optional_sections_via_c_abi() {
6292        // Frame with velocities, forces, and per-atom energies — all four block exports
6293        let mut builder = ConFrameBuilder::new([10.0, 10.0, 10.0], [90.0, 90.0, 90.0]);
6294        builder.add_atom("H", 0.0, 0.0, 0.0, [false; 3], 1, 1.0);
6295        builder.add_atom("O", 1.0, 0.0, 0.0, [false; 3], 2, 16.0);
6296        builder.set_atom_velocity(0, [0.1, 0.2, 0.3]).unwrap();
6297        builder.set_atom_velocity(1, [0.0, 0.1, 0.0]).unwrap();
6298        builder.set_atom_force(0, [1.0, 0.0, 0.0]).unwrap();
6299        builder.set_atom_force(1, [0.0, 1.0, 0.0]).unwrap();
6300        builder.set_atom_energy(0, -0.5).unwrap();
6301        builder.set_atom_energy(1, -1.0).unwrap();
6302        let frame = builder.build().unwrap();
6303        let handle = Box::into_raw(Box::new(frame)) as *mut RKRConFrame;
6304        let mut pos: *mut metatensor::c_api::mts_block_t = std::ptr::null_mut();
6305        assert_eq!(
6306            unsafe { rkr_frame_metatensor_positions_block(handle, &mut pos) },
6307            RKRStatus::RKR_STATUS_SUCCESS
6308        );
6309        assert_mts_block_shape(pos, 2, 3);
6310        unsafe { rkr_mts_block_free(pos) };
6311        let mut vel: *mut metatensor::c_api::mts_block_t = std::ptr::null_mut();
6312        assert_eq!(
6313            unsafe { rkr_frame_metatensor_velocities_block(handle, &mut vel) },
6314            RKRStatus::RKR_STATUS_SUCCESS
6315        );
6316        assert_mts_block_shape(vel, 2, 3);
6317        unsafe { rkr_mts_block_free(vel) };
6318        let mut frc: *mut metatensor::c_api::mts_block_t = std::ptr::null_mut();
6319        assert_eq!(
6320            unsafe { rkr_frame_metatensor_forces_block(handle, &mut frc) },
6321            RKRStatus::RKR_STATUS_SUCCESS
6322        );
6323        assert_mts_block_shape(frc, 2, 3);
6324        unsafe { rkr_mts_block_free(frc) };
6325        let mut eng: *mut metatensor::c_api::mts_block_t = std::ptr::null_mut();
6326        assert_eq!(
6327            unsafe { rkr_frame_metatensor_atom_energies_block(handle, &mut eng) },
6328            RKRStatus::RKR_STATUS_SUCCESS
6329        );
6330        assert_mts_block_shape(eng, 2, 1);
6331        unsafe { rkr_mts_block_free(eng) };
6332        unsafe { free_rkr_frame(handle) };
6333    }
6334
6335    #[test]
6336    fn string_iterator_yields_frames_from_buffer() {
6337        let text =
6338            std::fs::read_to_string("resources/test/tiny_cuh2.con").expect("fixture tiny_cuh2.con");
6339        let c_text = CString::new(text.as_str()).unwrap();
6340        let it = unsafe { read_con_string_iterator(c_text.as_ptr()) };
6341        assert!(!it.is_null());
6342        let mut n = 0usize;
6343        loop {
6344            let fr = unsafe { con_frame_iterator_next(it) };
6345            if fr.is_null() {
6346                break;
6347            }
6348            n += 1;
6349            unsafe { free_rkr_frame(fr) };
6350        }
6351        unsafe { free_con_frame_iterator(it) };
6352        assert!(n >= 1, "string iterator should yield >=1 frame");
6353
6354        let bytes = text.as_bytes();
6355        let it2 = unsafe { read_con_buffer_iterator(bytes.as_ptr(), bytes.len()) };
6356        assert!(!it2.is_null());
6357        let fr2 = unsafe { con_frame_iterator_next(it2) };
6358        assert!(!fr2.is_null());
6359        unsafe {
6360            free_rkr_frame(fr2);
6361            free_con_frame_iterator(it2);
6362        }
6363    }
6364
6365    #[test]
6366    fn file_iterator_reads_gzip_when_present() {
6367        use flate2::Compression;
6368        use flate2::write::GzEncoder;
6369        use std::io::Write;
6370        let plain = std::fs::read("resources/test/tiny_cuh2.con").expect("fixture");
6371        let dir = tempfile::tempdir().expect("tempdir");
6372        let gz_path = dir.path().join("tiny_cuh2.con.gz");
6373        {
6374            let f = std::fs::File::create(&gz_path).unwrap();
6375            let mut enc = GzEncoder::new(f, Compression::default());
6376            enc.write_all(&plain).unwrap();
6377            enc.finish().unwrap();
6378        }
6379        let c_path = CString::new(gz_path.to_str().unwrap()).unwrap();
6380        let it = unsafe { read_con_file_iterator(c_path.as_ptr()) };
6381        assert!(
6382            !it.is_null(),
6383            "path iterator must decompress .con.gz transparently"
6384        );
6385        let fr = unsafe { con_frame_iterator_next(it) };
6386        assert!(!fr.is_null());
6387        let n = unsafe { rkr_frame_atom_count(fr) };
6388        assert!(n > 0);
6389        unsafe {
6390            free_rkr_frame(fr);
6391            free_con_frame_iterator(it);
6392        }
6393    }
6394}