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