Skip to main content

readcon_core/
ffi.rs

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