Skip to main content

readcon_core/
ffi.rs

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