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