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