Skip to main content

readcon_core/
ffi.rs

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