Skip to main content

readcon_core/
ffi.rs

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