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`.
3629fn pack_frame_handles(frames: Vec<ConFrame>, num_frames: *mut usize) -> *mut *mut RKRConFrame {
3630    let count = frames.len();
3631    let mut handles: Vec<*mut RKRConFrame> = frames
3632        .into_iter()
3633        .map(|f| Box::into_raw(Box::new(f)) as *mut RKRConFrame)
3634        .collect();
3635    handles.shrink_to_fit();
3636    debug_assert_eq!(handles.len(), handles.capacity());
3637    let ptr = handles.as_mut_ptr();
3638    std::mem::forget(handles);
3639    unsafe { *num_frames = count };
3640    ptr
3641}
3642/// Frees an array of frame handles returned by `rkr_read_all_frames`.
3643/// Each frame is freed individually, then the array itself.
3644///
3645/// # Safety
3646/// frames must be valid or null.
3647#[unsafe(no_mangle)]
3648pub unsafe extern "C" fn free_rkr_frame_array(frames: *mut *mut RKRConFrame, num_frames: usize) {
3649    if frames.is_null() {
3650        return;
3651    }
3652    unsafe {
3653        let handles = Vec::from_raw_parts(frames, num_frames, num_frames);
3654        for handle in handles {
3655            if !handle.is_null() {
3656                let _ = Box::from_raw(handle as *mut ConFrame);
3657            }
3658        }
3659    }
3660}
3661/// Free only the outer pointer array from `rkr_read_all_frames` (not the frames).
3662///
3663/// # Safety
3664/// `frames` null or from `rkr_read_all_frames` with length `num_frames`. Frame
3665/// pointers must be owned elsewhere (e.g. language wrappers).
3666#[unsafe(no_mangle)]
3667pub unsafe extern "C" fn free_rkr_frame_ptr_array(
3668    frames: *mut *mut RKRConFrame,
3669    num_frames: usize,
3670) {
3671    if frames.is_null() {
3672        return;
3673    }
3674    unsafe {
3675        let _ptrs = Vec::from_raw_parts(frames, num_frames, num_frames);
3676    }
3677}
3678
3679//=============================================================================
3680// ---------------------------------------------------------------------------
3681// Metatensor TensorBlock exports (`metatensor` Cargo feature)
3682//
3683// Boundary types are metatensor-sys only (`metatensor::c_api::mts_block_t`).
3684// Construction: `metatensor_export` (high-level). Transfer/free: single helpers
3685// in that module (`tensor_block_into_raw_mts` / `mts_block_free_sys`).
3686// Lean builds omit these symbols (`READCON_CORE_HAS_METATENSOR` in the header).
3687// ---------------------------------------------------------------------------
3688/// Free an owned block from `rkr_frame_metatensor_*_block`.
3689/// Prefer this or `mts_block_free` (metatensor.h) — not both on the same pointer.
3690///
3691/// # Safety
3692/// `block` is NULL or an owning `mts_block_t*` from this library's transfer helper.
3693#[cfg(feature = "metatensor")]
3694#[unsafe(no_mangle)]
3695pub unsafe extern "C" fn rkr_mts_block_free(block: *mut metatensor::c_api::mts_block_t) {
3696    unsafe { crate::metatensor_export::mts_block_free_sys(block) };
3697}
3698/// Positions `[N,3]` TensorBlock. Caller frees with `rkr_mts_block_free` / `mts_block_free`.
3699#[cfg(feature = "metatensor")]
3700#[unsafe(no_mangle)]
3701pub unsafe extern "C" fn rkr_frame_metatensor_positions_block(
3702    frame_handle: *const RKRConFrame,
3703    out_block: *mut *mut metatensor::c_api::mts_block_t,
3704) -> RKRStatus {
3705    if frame_handle.is_null() || out_block.is_null() {
3706        return RKRStatus::RKR_STATUS_NULL_POINTER;
3707    }
3708    unsafe { *out_block = std::ptr::null_mut() };
3709    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
3710        return RKRStatus::RKR_STATUS_NULL_POINTER;
3711    };
3712    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3713        crate::metatensor_export::frame_positions_block(frame)
3714    })) {
3715        Ok(Ok(b)) => {
3716            unsafe { *out_block = crate::metatensor_export::tensor_block_into_raw_mts(b) };
3717            RKRStatus::RKR_STATUS_SUCCESS
3718        }
3719        Ok(Err(_)) | Err(_) => RKRStatus::RKR_STATUS_INTERNAL_ERROR,
3720    }
3721}
3722#[cfg(feature = "metatensor")]
3723#[unsafe(no_mangle)]
3724pub unsafe extern "C" fn rkr_frame_metatensor_velocities_block(
3725    frame_handle: *const RKRConFrame,
3726    out_block: *mut *mut metatensor::c_api::mts_block_t,
3727) -> RKRStatus {
3728    if frame_handle.is_null() || out_block.is_null() {
3729        return RKRStatus::RKR_STATUS_NULL_POINTER;
3730    }
3731    unsafe { *out_block = std::ptr::null_mut() };
3732    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
3733        return RKRStatus::RKR_STATUS_NULL_POINTER;
3734    };
3735    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3736        crate::metatensor_export::frame_velocities_block(frame)
3737    })) {
3738        Ok(Ok(Some(b))) => {
3739            unsafe { *out_block = crate::metatensor_export::tensor_block_into_raw_mts(b) };
3740            RKRStatus::RKR_STATUS_SUCCESS
3741        }
3742        Ok(Ok(None)) => RKRStatus::RKR_STATUS_SECTION_ABSENT,
3743        Ok(Err(_)) | Err(_) => RKRStatus::RKR_STATUS_INTERNAL_ERROR,
3744    }
3745}
3746#[cfg(feature = "metatensor")]
3747#[unsafe(no_mangle)]
3748pub unsafe extern "C" fn rkr_frame_metatensor_forces_block(
3749    frame_handle: *const RKRConFrame,
3750    out_block: *mut *mut metatensor::c_api::mts_block_t,
3751) -> RKRStatus {
3752    if frame_handle.is_null() || out_block.is_null() {
3753        return RKRStatus::RKR_STATUS_NULL_POINTER;
3754    }
3755    unsafe { *out_block = std::ptr::null_mut() };
3756    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
3757        return RKRStatus::RKR_STATUS_NULL_POINTER;
3758    };
3759    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3760        crate::metatensor_export::frame_forces_block(frame)
3761    })) {
3762        Ok(Ok(Some(b))) => {
3763            unsafe { *out_block = crate::metatensor_export::tensor_block_into_raw_mts(b) };
3764            RKRStatus::RKR_STATUS_SUCCESS
3765        }
3766        Ok(Ok(None)) => RKRStatus::RKR_STATUS_SECTION_ABSENT,
3767        Ok(Err(_)) | Err(_) => RKRStatus::RKR_STATUS_INTERNAL_ERROR,
3768    }
3769}
3770#[cfg(feature = "metatensor")]
3771#[unsafe(no_mangle)]
3772pub unsafe extern "C" fn rkr_frame_metatensor_atom_energies_block(
3773    frame_handle: *const RKRConFrame,
3774    out_block: *mut *mut metatensor::c_api::mts_block_t,
3775) -> RKRStatus {
3776    if frame_handle.is_null() || out_block.is_null() {
3777        return RKRStatus::RKR_STATUS_NULL_POINTER;
3778    }
3779    unsafe { *out_block = std::ptr::null_mut() };
3780    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
3781        return RKRStatus::RKR_STATUS_NULL_POINTER;
3782    };
3783    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3784        crate::metatensor_export::frame_energies_block(frame)
3785    })) {
3786        Ok(Ok(Some(b))) => {
3787            unsafe { *out_block = crate::metatensor_export::tensor_block_into_raw_mts(b) };
3788            RKRStatus::RKR_STATUS_SUCCESS
3789        }
3790        Ok(Ok(None)) => RKRStatus::RKR_STATUS_SECTION_ABSENT,
3791        Ok(Err(_)) | Err(_) => RKRStatus::RKR_STATUS_INTERNAL_ERROR,
3792    }
3793}
3794#[cfg(not(feature = "zstd"))]
3795#[unsafe(no_mangle)]
3796pub unsafe extern "C" fn create_writer_zstd_c(
3797    _filename_c: *const c_char,
3798) -> *mut RKRConFrameWriter {
3799    ptr::null_mut()
3800}
3801#[cfg(not(feature = "zstd"))]
3802#[unsafe(no_mangle)]
3803pub unsafe extern "C" fn create_writer_zstd_with_precision_c(
3804    _filename_c: *const c_char,
3805    _precision: u8,
3806) -> *mut RKRConFrameWriter {
3807    ptr::null_mut()
3808}
3809//=============================================================================
3810/// Lean-build stubs: always export metatensor C symbols so Fortran/C can link without `#ifdef`.
3811/// Real implementations live under `feature = "metatensor"`.
3812#[cfg(not(feature = "metatensor"))]
3813// No #[repr(C)]: cbindgen then emits an incomplete struct, which C and C++
3814// accept under -pedantic. The zero-length field keeps it unconstructible.
3815pub struct mts_block_t {
3816    _private: [u8; 0],
3817}
3818#[cfg(not(feature = "metatensor"))]
3819#[unsafe(no_mangle)]
3820pub unsafe extern "C" fn rkr_mts_block_free(_block: *mut mts_block_t) {}
3821#[cfg(not(feature = "metatensor"))]
3822#[unsafe(no_mangle)]
3823pub unsafe extern "C" fn rkr_frame_metatensor_positions_block(
3824    _frame_handle: *const RKRConFrame,
3825    out_block: *mut *mut mts_block_t,
3826) -> RKRStatus {
3827    if !out_block.is_null() {
3828        unsafe { *out_block = std::ptr::null_mut() };
3829    }
3830    RKRStatus::RKR_STATUS_FEATURE_DISABLED
3831}
3832#[cfg(not(feature = "metatensor"))]
3833#[unsafe(no_mangle)]
3834pub unsafe extern "C" fn rkr_frame_metatensor_velocities_block(
3835    _frame_handle: *const RKRConFrame,
3836    out_block: *mut *mut mts_block_t,
3837) -> RKRStatus {
3838    if !out_block.is_null() {
3839        unsafe { *out_block = std::ptr::null_mut() };
3840    }
3841    RKRStatus::RKR_STATUS_FEATURE_DISABLED
3842}
3843#[cfg(not(feature = "metatensor"))]
3844#[unsafe(no_mangle)]
3845pub unsafe extern "C" fn rkr_frame_metatensor_forces_block(
3846    _frame_handle: *const RKRConFrame,
3847    out_block: *mut *mut mts_block_t,
3848) -> RKRStatus {
3849    if !out_block.is_null() {
3850        unsafe { *out_block = std::ptr::null_mut() };
3851    }
3852    RKRStatus::RKR_STATUS_FEATURE_DISABLED
3853}
3854#[cfg(not(feature = "metatensor"))]
3855#[unsafe(no_mangle)]
3856pub unsafe extern "C" fn rkr_frame_metatensor_atom_energies_block(
3857    _frame_handle: *const RKRConFrame,
3858    out_block: *mut *mut mts_block_t,
3859) -> RKRStatus {
3860    if !out_block.is_null() {
3861        unsafe { *out_block = std::ptr::null_mut() };
3862    }
3863    RKRStatus::RKR_STATUS_FEATURE_DISABLED
3864}
3865// ----- Frame section buffers (no CFrame AoS required) -------------------------
3866/// Number of atoms on the frame (atom_data order).
3867#[unsafe(no_mangle)]
3868pub unsafe extern "C" fn rkr_frame_atom_count(frame_handle: *const RKRConFrame) -> usize {
3869    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
3870        return 0;
3871    };
3872    frame.atom_data.len()
3873}
3874/// Borrow positions SoA. `out->data` is valid until `free_rkr_frame`.
3875#[unsafe(no_mangle)]
3876pub unsafe extern "C" fn rkr_frame_xyz_view(
3877    frame_handle: *const RKRConFrame,
3878    out: *mut RKRArrayView,
3879) -> RKRStatus {
3880    fill_array2_view(frame_handle, out, |f| &f.positions, false)
3881}
3882
3883/// Borrow velocities SoA, or `SECTION_ABSENT`.
3884#[unsafe(no_mangle)]
3885pub unsafe extern "C" fn rkr_frame_velocities_view(
3886    frame_handle: *const RKRConFrame,
3887    out: *mut RKRArrayView,
3888) -> RKRStatus {
3889    fill_array2_view(frame_handle, out, |f| &f.velocities, true)
3890}
3891
3892/// Borrow forces SoA, or `SECTION_ABSENT`.
3893#[unsafe(no_mangle)]
3894pub unsafe extern "C" fn rkr_frame_forces_view(
3895    frame_handle: *const RKRConFrame,
3896    out: *mut RKRArrayView,
3897) -> RKRStatus {
3898    fill_array2_view(frame_handle, out, |f| &f.forces, true)
3899}
3900
3901/// True when the frame carries a `"displacements"` section. False for a
3902/// NULL handle.
3903///
3904/// # Safety
3905/// `frame_handle` is NULL or a valid frame handle.
3906#[unsafe(no_mangle)]
3907pub unsafe extern "C" fn rkr_frame_has_displacements(frame_handle: *const RKRConFrame) -> bool {
3908    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
3909        Some(f) => f.has_displacements(),
3910        None => false,
3911    }
3912}
3913
3914/// Borrow displacements SoA, or `SECTION_ABSENT`.
3915///
3916/// # Safety
3917/// `frame_handle` must be a valid frame handle and `out` a writable view.
3918#[unsafe(no_mangle)]
3919pub unsafe extern "C" fn rkr_frame_displacements_view(
3920    frame_handle: *const RKRConFrame,
3921    out: *mut RKRArrayView,
3922) -> RKRStatus {
3923    fill_array2_view(frame_handle, out, |f| &f.displacements, true)
3924}
3925
3926/// True when the frame carries a `"spreads"` section. False for a
3927/// NULL handle.
3928///
3929/// # Safety
3930/// `frame_handle` is NULL or a valid frame handle.
3931#[unsafe(no_mangle)]
3932pub unsafe extern "C" fn rkr_frame_has_spreads(frame_handle: *const RKRConFrame) -> bool {
3933    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
3934        Some(f) => f.has_spreads(),
3935        None => false,
3936    }
3937}
3938
3939/// Borrow spreads SoA, or `SECTION_ABSENT`.
3940///
3941/// # Safety
3942/// `frame_handle` must be a valid frame handle and `out` a writable view.
3943#[unsafe(no_mangle)]
3944pub unsafe extern "C" fn rkr_frame_spreads_view(
3945    frame_handle: *const RKRConFrame,
3946    out: *mut RKRArrayView,
3947) -> RKRStatus {
3948    fill_array2_view(frame_handle, out, |f| &f.spreads, true)
3949}
3950
3951/// Borrow per-atom energies, or `SECTION_ABSENT`.
3952#[unsafe(no_mangle)]
3953pub unsafe extern "C" fn rkr_frame_energies_view(
3954    frame_handle: *const RKRConFrame,
3955    out: *mut RKRArrayView,
3956) -> RKRStatus {
3957    fill_array1_view(frame_handle, out, |f| &f.atom_energies, true)
3958}
3959
3960/// Borrow per-atom masses.
3961#[unsafe(no_mangle)]
3962pub unsafe extern "C" fn rkr_frame_masses_view(
3963    frame_handle: *const RKRConFrame,
3964    out: *mut RKRArrayView,
3965) -> RKRStatus {
3966    fill_array1_view(frame_handle, out, |f| &f.masses, false)
3967}
3968
3969/// Borrow `atom_id` column (always u64).
3970#[unsafe(no_mangle)]
3971pub unsafe extern "C" fn rkr_frame_atom_ids_view(
3972    frame_handle: *const RKRConFrame,
3973    out: *mut RKRArrayView,
3974) -> RKRStatus {
3975    if frame_handle.is_null() || out.is_null() {
3976        return RKRStatus::RKR_STATUS_NULL_POINTER;
3977    }
3978    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
3979        return RKRStatus::RKR_STATUS_NULL_POINTER;
3980    };
3981    let slice = frame.atom_ids.as_slice_memory_order();
3982    unsafe {
3983        *out = RKRArrayView {
3984            data: slice
3985                .map(|s| s.as_ptr() as *const std::ffi::c_void)
3986                .unwrap_or(std::ptr::null()),
3987            n: frame.atom_ids.len(),
3988            cols: 1,
3989            dtype_code: 1, // kDLUInt
3990            dtype_bits: 64,
3991        };
3992    }
3993    RKRStatus::RKR_STATUS_SUCCESS
3994}
3995
3996/// Row-major f64 xyz pointer, or NULL if storage is not float64.
3997/// `*n` receives the atom count. Pointer is valid until `free_rkr_frame`.
3998#[unsafe(no_mangle)]
3999pub unsafe extern "C" fn rkr_frame_xyz_f64(
4000    frame_handle: *const RKRConFrame,
4001    n: *mut usize,
4002) -> *const f64 {
4003    f64_col_ptr(frame_handle, n, |f| f.positions.f64_slice())
4004}
4005
4006/// Row-major f64 velocities pointer, or NULL if absent / not float64.
4007#[unsafe(no_mangle)]
4008pub unsafe extern "C" fn rkr_frame_velocities_f64(
4009    frame_handle: *const RKRConFrame,
4010    n: *mut usize,
4011) -> *const f64 {
4012    f64_col_ptr(frame_handle, n, |f| f.velocities.f64_slice())
4013}
4014
4015/// Row-major f64 forces pointer, or NULL if absent / not float64.
4016#[unsafe(no_mangle)]
4017pub unsafe extern "C" fn rkr_frame_forces_f64(
4018    frame_handle: *const RKRConFrame,
4019    n: *mut usize,
4020) -> *const f64 {
4021    f64_col_ptr(frame_handle, n, |f| f.forces.f64_slice())
4022}
4023
4024/// Row-major f64 displacements pointer, or NULL if absent / not float64.
4025///
4026/// # Safety
4027/// `frame_handle` must be a valid frame handle; `n` is NULL or writable.
4028#[unsafe(no_mangle)]
4029pub unsafe extern "C" fn rkr_frame_displacements_f64(
4030    frame_handle: *const RKRConFrame,
4031    n: *mut usize,
4032) -> *const f64 {
4033    f64_col_ptr(frame_handle, n, |f| f.displacements.f64_slice())
4034}
4035
4036/// Row-major f64 spreads pointer, or NULL if absent / not float64.
4037///
4038/// # Safety
4039/// `frame_handle` must be a valid frame handle; `n` is NULL or writable.
4040#[unsafe(no_mangle)]
4041pub unsafe extern "C" fn rkr_frame_spreads_f64(
4042    frame_handle: *const RKRConFrame,
4043    n: *mut usize,
4044) -> *const f64 {
4045    f64_col_ptr(frame_handle, n, |f| f.spreads.f64_slice())
4046}
4047
4048fn fill_array2_view(
4049    frame_handle: *const RKRConFrame,
4050    out: *mut RKRArrayView,
4051    get: impl FnOnce(&ConFrame) -> &crate::storage_dtype::FloatArray2,
4052    section: bool,
4053) -> RKRStatus {
4054    if frame_handle.is_null() || out.is_null() {
4055        return RKRStatus::RKR_STATUS_NULL_POINTER;
4056    }
4057    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4058        return RKRStatus::RKR_STATUS_NULL_POINTER;
4059    };
4060    let arr = get(frame);
4061    if section && arr.nrows() == 0 {
4062        unsafe { *out = RKRArrayView::empty() };
4063        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4064    }
4065    unsafe { *out = RKRArrayView::from_array2(arr) };
4066    RKRStatus::RKR_STATUS_SUCCESS
4067}
4068
4069fn fill_array1_view(
4070    frame_handle: *const RKRConFrame,
4071    out: *mut RKRArrayView,
4072    get: impl FnOnce(&ConFrame) -> &crate::storage_dtype::FloatArray1,
4073    section: bool,
4074) -> RKRStatus {
4075    if frame_handle.is_null() || out.is_null() {
4076        return RKRStatus::RKR_STATUS_NULL_POINTER;
4077    }
4078    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4079        return RKRStatus::RKR_STATUS_NULL_POINTER;
4080    };
4081    let arr = get(frame);
4082    if section && arr.len() == 0 {
4083        unsafe { *out = RKRArrayView::empty() };
4084        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4085    }
4086    unsafe { *out = RKRArrayView::from_array1(arr) };
4087    RKRStatus::RKR_STATUS_SUCCESS
4088}
4089
4090fn f64_col_ptr(
4091    frame_handle: *const RKRConFrame,
4092    n: *mut usize,
4093    get: impl FnOnce(&ConFrame) -> Option<&[f64]>,
4094) -> *const f64 {
4095    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4096        if !n.is_null() {
4097            unsafe { *n = 0 };
4098        }
4099        return std::ptr::null();
4100    };
4101    match get(frame) {
4102        Some(s) if !s.is_empty() => {
4103            if !n.is_null() {
4104                unsafe { *n = s.len() / 3 };
4105            }
4106            s.as_ptr()
4107        }
4108        _ => {
4109            if !n.is_null() {
4110                unsafe { *n = 0 };
4111            }
4112            std::ptr::null()
4113        }
4114    }
4115}
4116
4117/// Copy positions as row-major `[x0,y0,z0,...]` into `out` (length >= 3*N).
4118/// Prefers a memcpy from the SoA column; falls back to AoS only if SoA is empty.
4119#[unsafe(no_mangle)]
4120pub unsafe extern "C" fn rkr_frame_copy_positions(
4121    frame_handle: *const RKRConFrame,
4122    out: *mut f64,
4123    out_len: usize,
4124) -> RKRStatus {
4125    copy_array2_f64(frame_handle, out, out_len, |f| &f.positions, false)
4126}
4127fn copy_array2_f64(
4128    frame_handle: *const RKRConFrame,
4129    out: *mut f64,
4130    out_len: usize,
4131    get: impl FnOnce(&ConFrame) -> &crate::storage_dtype::FloatArray2,
4132    section: bool,
4133) -> RKRStatus {
4134    if frame_handle.is_null() || out.is_null() {
4135        return RKRStatus::RKR_STATUS_NULL_POINTER;
4136    }
4137    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4138        return RKRStatus::RKR_STATUS_NULL_POINTER;
4139    };
4140    let arr = get(frame);
4141    let n = if arr.nrows() > 0 {
4142        arr.nrows()
4143    } else {
4144        frame.atom_data.len()
4145    };
4146    if section && arr.nrows() == 0 {
4147        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4148    }
4149    let need = n.saturating_mul(3);
4150    if out_len < need {
4151        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
4152    }
4153    let dest = unsafe { std::slice::from_raw_parts_mut(out, need) };
4154    if arr.nrows() == n {
4155        if let Some(src) = arr.f64_slice() {
4156            dest.copy_from_slice(&src[..need.min(src.len())]);
4157            return RKRStatus::RKR_STATUS_SUCCESS;
4158        }
4159        for i in 0..n {
4160            let row = arr.as_f64_row(i);
4161            dest[i * 3] = row[0];
4162            dest[i * 3 + 1] = row[1];
4163            dest[i * 3 + 2] = row[2];
4164        }
4165        return RKRStatus::RKR_STATUS_SUCCESS;
4166    }
4167    for (i, a) in frame.atom_data.iter().enumerate() {
4168        dest[i * 3] = a.x;
4169        dest[i * 3 + 1] = a.y;
4170        dest[i * 3 + 2] = a.z;
4171    }
4172    RKRStatus::RKR_STATUS_SUCCESS
4173}
4174
4175fn copy_array1_f64(
4176    frame_handle: *const RKRConFrame,
4177    out: *mut f64,
4178    out_len: usize,
4179    get: impl FnOnce(&ConFrame) -> &crate::storage_dtype::FloatArray1,
4180    section: bool,
4181) -> RKRStatus {
4182    if frame_handle.is_null() || out.is_null() {
4183        return RKRStatus::RKR_STATUS_NULL_POINTER;
4184    }
4185    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4186        return RKRStatus::RKR_STATUS_NULL_POINTER;
4187    };
4188    let arr = get(frame);
4189    let n = if arr.len() > 0 {
4190        arr.len()
4191    } else {
4192        frame.atom_data.len()
4193    };
4194    if section && arr.len() == 0 {
4195        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4196    }
4197    if out_len < n {
4198        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
4199    }
4200    let dest = unsafe { std::slice::from_raw_parts_mut(out, n) };
4201    if let Some(src) = arr.f64_slice() {
4202        dest.copy_from_slice(&src[..n.min(src.len())]);
4203        return RKRStatus::RKR_STATUS_SUCCESS;
4204    }
4205    for i in 0..n {
4206        dest[i] = arr.get_f64(i);
4207    }
4208    RKRStatus::RKR_STATUS_SUCCESS
4209}
4210
4211#[unsafe(no_mangle)]
4212pub unsafe extern "C" fn rkr_frame_copy_velocities(
4213    frame_handle: *const RKRConFrame,
4214    out: *mut f64,
4215    out_len: usize,
4216) -> RKRStatus {
4217    copy_array2_f64(frame_handle, out, out_len, |f| &f.velocities, true)
4218}
4219#[unsafe(no_mangle)]
4220pub unsafe extern "C" fn rkr_frame_copy_forces(
4221    frame_handle: *const RKRConFrame,
4222    out: *mut f64,
4223    out_len: usize,
4224) -> RKRStatus {
4225    copy_array2_f64(frame_handle, out, out_len, |f| &f.forces, true)
4226}
4227/// Copy displacements into row-major `out` (length >= `3 * N`), or
4228/// `SECTION_ABSENT`.
4229///
4230/// # Safety
4231/// `frame_handle` must be a valid frame handle; `out` must hold `out_len` f64.
4232#[unsafe(no_mangle)]
4233pub unsafe extern "C" fn rkr_frame_copy_displacements(
4234    frame_handle: *const RKRConFrame,
4235    out: *mut f64,
4236    out_len: usize,
4237) -> RKRStatus {
4238    copy_array2_f64(frame_handle, out, out_len, |f| &f.displacements, true)
4239}
4240/// Copy spreads into row-major `out` (length >= `3 * N`), or
4241/// `SECTION_ABSENT`.
4242///
4243/// # Safety
4244/// `frame_handle` must be a valid frame handle; `out` must hold `out_len` f64.
4245#[unsafe(no_mangle)]
4246pub unsafe extern "C" fn rkr_frame_copy_spreads(
4247    frame_handle: *const RKRConFrame,
4248    out: *mut f64,
4249    out_len: usize,
4250) -> RKRStatus {
4251    copy_array2_f64(frame_handle, out, out_len, |f| &f.spreads, true)
4252}
4253#[unsafe(no_mangle)]
4254pub unsafe extern "C" fn rkr_frame_copy_atom_energies(
4255    frame_handle: *const RKRConFrame,
4256    out: *mut f64,
4257    out_len: usize,
4258) -> RKRStatus {
4259    copy_array1_f64(frame_handle, out, out_len, |f| &f.atom_energies, true)
4260}
4261#[unsafe(no_mangle)]
4262pub unsafe extern "C" fn rkr_frame_copy_masses(
4263    frame_handle: *const RKRConFrame,
4264    out: *mut f64,
4265    out_len: usize,
4266) -> RKRStatus {
4267    copy_array1_f64(frame_handle, out, out_len, |f| &f.masses, false)
4268}
4269#[unsafe(no_mangle)]
4270pub unsafe extern "C" fn rkr_frame_copy_atom_ids(
4271    frame_handle: *const RKRConFrame,
4272    out: *mut u64,
4273    out_len: usize,
4274) -> RKRStatus {
4275    if frame_handle.is_null() || out.is_null() {
4276        return RKRStatus::RKR_STATUS_NULL_POINTER;
4277    }
4278    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4279        return RKRStatus::RKR_STATUS_NULL_POINTER;
4280    };
4281    let n = frame.atom_ids.len().max(frame.atom_data.len());
4282    if out_len < n {
4283        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
4284    }
4285    let dest = unsafe { std::slice::from_raw_parts_mut(out, n) };
4286    if let Some(src) = frame.atom_ids.as_slice_memory_order() {
4287        dest[..src.len().min(n)].copy_from_slice(&src[..src.len().min(n)]);
4288        return RKRStatus::RKR_STATUS_SUCCESS;
4289    }
4290    for (i, a) in frame.atom_data.iter().enumerate() {
4291        dest[i] = a.atom_id;
4292    }
4293    RKRStatus::RKR_STATUS_SUCCESS
4294}
4295fn frame_positions_arc(frame: &ConFrame) -> ndarray::ArcArray2<f64> {
4296    let n = frame.atom_data.len();
4297    let mut data = Vec::with_capacity(n * 3);
4298    for a in &frame.atom_data {
4299        data.extend_from_slice(&[a.x, a.y, a.z]);
4300    }
4301    ndarray::ArcArray2::from_shape_vec((n, 3), data)
4302        .unwrap_or_else(|_| ndarray::ArcArray2::zeros((0, 3)))
4303}
4304
4305/// Metatensor-style: export positions as they are stored (CPU f64), with
4306/// explicit device request. Non-CPU → `FEATURE_DISABLED`. Prefer this over
4307/// dtype-cast `*_dlpack_ex` for new code.
4308///
4309/// `stream` and `max_version_*` are accepted for ABI alignment with
4310/// metatensor `as_dlpack`; CPU ignores stream / version negotiation for now.
4311///
4312/// # Safety
4313/// Handles and `out_tensor` must be valid.
4314#[unsafe(no_mangle)]
4315pub unsafe extern "C" fn rkr_frame_positions_as_dlpack(
4316    frame_handle: *const RKRConFrame,
4317    device_type: i32,
4318    device_id: i32,
4319    _stream: i64,
4320    _max_version_major: u32,
4321    _max_version_minor: u32,
4322    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4323) -> RKRStatus {
4324    if frame_handle.is_null() || out_tensor.is_null() {
4325        return RKRStatus::RKR_STATUS_NULL_POINTER;
4326    }
4327    unsafe { *out_tensor = std::ptr::null_mut() };
4328    let dl_device = if device_type == rkr_dl_device_type::RKR_DL_CPU {
4329        dlpk::sys::DLDevice::cpu()
4330    } else if device_type == rkr_dl_device_type::RKR_DL_CUDA {
4331        #[cfg(feature = "cuda")]
4332        {
4333            dlpk::sys::DLDevice::cuda(device_id)
4334        }
4335        #[cfg(not(feature = "cuda"))]
4336        {
4337            let _ = device_id;
4338            return RKRStatus::RKR_STATUS_FEATURE_DISABLED;
4339        }
4340    } else {
4341        return RKRStatus::RKR_STATUS_FEATURE_DISABLED;
4342    };
4343    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4344        return RKRStatus::RKR_STATUS_NULL_POINTER;
4345    };
4346    // Opaque SoA storage → DLPack; CUDA requests H2D via storage_dtype / cuda_array.
4347    match frame.positions_as_dlpack(dl_device) {
4348        Ok(tensor) => {
4349            let raw = tensor.into_raw();
4350            unsafe {
4351                *out_tensor = raw.as_ptr();
4352            }
4353            RKRStatus::RKR_STATUS_SUCCESS
4354        }
4355        Err(e) => map_dlpack_err(e),
4356    }
4357}
4358
4359/// Ingest positions from a DLManagedTensorVersioned (CPU float32/64, shape (N,3)
4360/// or length 3N). Metatensor-style write path symmetry.
4361///
4362/// # Safety
4363/// `frame` must be a valid mutable frame; `tensor` non-null managed tensor.
4364#[unsafe(no_mangle)]
4365pub unsafe extern "C" fn rkr_frame_positions_from_dlpack(
4366    frame_handle: *mut RKRConFrame,
4367    tensor: *const RKRDLManagedTensorVersioned,
4368) -> RKRStatus {
4369    if frame_handle.is_null() || tensor.is_null() {
4370        return RKRStatus::RKR_STATUS_NULL_POINTER;
4371    }
4372    let frame = unsafe { &mut *(frame_handle as *mut ConFrame) };
4373    let dl = unsafe { &(*tensor).dl_tensor };
4374    if dl.device.device_type != dlpk::sys::DLDeviceType::kDLCPU {
4375        return RKRStatus::RKR_STATUS_FEATURE_DISABLED;
4376    }
4377    let n = frame.atom_data.len();
4378    let need = n.saturating_mul(3);
4379    let ndim = dl.ndim as usize;
4380    let shape = if dl.shape.is_null() {
4381        return RKRStatus::RKR_STATUS_VALIDATION_ERROR;
4382    } else {
4383        unsafe { std::slice::from_raw_parts(dl.shape, ndim) }
4384    };
4385    let nelem = if ndim == 2 && shape[0] == n as i64 && shape[1] == 3 {
4386        need
4387    } else if ndim == 1 && shape[0] == need as i64 {
4388        need
4389    } else {
4390        return RKRStatus::RKR_STATUS_VALIDATION_ERROR;
4391    };
4392    let code = dl.dtype.code as u8;
4393    let bits = dl.dtype.bits;
4394    if dl.data.is_null() {
4395        return RKRStatus::RKR_STATUS_NULL_POINTER;
4396    }
4397    let vals: Vec<f64> = if code == rkr_dl_type_code::RKR_DL_FLOAT && bits == 64 {
4398        let s = unsafe { std::slice::from_raw_parts(dl.data as *const f64, nelem) };
4399        s.to_vec()
4400    } else if code == rkr_dl_type_code::RKR_DL_FLOAT && bits == 32 {
4401        let s = unsafe { std::slice::from_raw_parts(dl.data as *const f32, nelem) };
4402        s.iter().map(|&x| x as f64).collect()
4403    } else {
4404        return RKRStatus::RKR_STATUS_VALIDATION_ERROR;
4405    };
4406    if frame.positions.nrows() != n {
4407        return RKRStatus::RKR_STATUS_VALIDATION_ERROR;
4408    }
4409    for i in 0..n {
4410        frame
4411            .positions
4412            .set_f64_row(i, [vals[i * 3], vals[i * 3 + 1], vals[i * 3 + 2]]);
4413    }
4414    frame.sync_atom_data_from_arrays();
4415    RKRStatus::RKR_STATUS_SUCCESS
4416}
4417
4418/// DLPack positions from a frame (default float64 / CPU). Prefer
4419/// [`rkr_frame_positions_as_dlpack`] for metatensor-style device negotiation.
4420#[unsafe(no_mangle)]
4421pub unsafe extern "C" fn rkr_frame_positions_dlpack(
4422    frame_handle: *const RKRConFrame,
4423    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4424) -> RKRStatus {
4425    unsafe {
4426        rkr_frame_positions_as_dlpack(
4427            frame_handle,
4428            rkr_dl_device_type::RKR_DL_CPU,
4429            0,
4430            0,
4431            1,
4432            0,
4433            out_tensor,
4434        )
4435    }
4436}
4437
4438/// Frame positions with [`RKRDlpackExportOptions`] (`opts` NULL → f64/CPU).
4439///
4440/// # Safety
4441/// `frame_handle` / `out_tensor` valid; `opts` null or valid.
4442#[unsafe(no_mangle)]
4443pub unsafe extern "C" fn rkr_frame_positions_dlpack_ex(
4444    frame_handle: *const RKRConFrame,
4445    opts: *const RKRDlpackExportOptions,
4446    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4447) -> RKRStatus {
4448    if frame_handle.is_null() || out_tensor.is_null() {
4449        return RKRStatus::RKR_STATUS_NULL_POINTER;
4450    }
4451    unsafe { *out_tensor = std::ptr::null_mut() };
4452    let o = match resolve_dlpack_opts(opts) {
4453        Ok(o) => o,
4454        Err(st) => return st,
4455    };
4456    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4457        return RKRStatus::RKR_STATUS_NULL_POINTER;
4458    };
4459    let arr = frame_positions_arc(frame);
4460    export_owned_array2_dlpack_opts(&arr, &o, out_tensor)
4461}
4462
4463/// DLPack velocities from a frame, or `SECTION_ABSENT` if missing (f64/CPU).
4464#[unsafe(no_mangle)]
4465pub unsafe extern "C" fn rkr_frame_velocities_dlpack(
4466    frame_handle: *const RKRConFrame,
4467    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4468) -> RKRStatus {
4469    unsafe { rkr_frame_velocities_dlpack_ex(frame_handle, std::ptr::null(), out_tensor) }
4470}
4471
4472#[unsafe(no_mangle)]
4473pub unsafe extern "C" fn rkr_frame_velocities_dlpack_ex(
4474    frame_handle: *const RKRConFrame,
4475    opts: *const RKRDlpackExportOptions,
4476    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4477) -> RKRStatus {
4478    if frame_handle.is_null() || out_tensor.is_null() {
4479        return RKRStatus::RKR_STATUS_NULL_POINTER;
4480    }
4481    unsafe { *out_tensor = std::ptr::null_mut() };
4482    let o = match resolve_dlpack_opts(opts) {
4483        Ok(o) => o,
4484        Err(st) => return st,
4485    };
4486    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4487        return RKRStatus::RKR_STATUS_NULL_POINTER;
4488    };
4489    if !frame.has_velocities() {
4490        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4491    }
4492    let n = frame.atom_data.len();
4493    let mut data = Vec::with_capacity(n * 3);
4494    for a in &frame.atom_data {
4495        let v = a.velocity.unwrap_or([0.0; 3]);
4496        data.extend_from_slice(&v);
4497    }
4498    let arr = ndarray::ArcArray2::from_shape_vec((n, 3), data)
4499        .unwrap_or_else(|_| ndarray::ArcArray2::zeros((0, 3)));
4500    export_owned_array2_dlpack_opts(&arr, &o, out_tensor)
4501}
4502
4503/// DLPack forces from a frame, or `SECTION_ABSENT` if missing (f64/CPU default).
4504#[unsafe(no_mangle)]
4505pub unsafe extern "C" fn rkr_frame_forces_dlpack(
4506    frame_handle: *const RKRConFrame,
4507    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4508) -> RKRStatus {
4509    unsafe { rkr_frame_forces_dlpack_ex(frame_handle, std::ptr::null(), out_tensor) }
4510}
4511
4512#[unsafe(no_mangle)]
4513pub unsafe extern "C" fn rkr_frame_forces_dlpack_ex(
4514    frame_handle: *const RKRConFrame,
4515    opts: *const RKRDlpackExportOptions,
4516    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4517) -> RKRStatus {
4518    if frame_handle.is_null() || out_tensor.is_null() {
4519        return RKRStatus::RKR_STATUS_NULL_POINTER;
4520    }
4521    unsafe { *out_tensor = std::ptr::null_mut() };
4522    let o = match resolve_dlpack_opts(opts) {
4523        Ok(o) => o,
4524        Err(st) => return st,
4525    };
4526    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4527        return RKRStatus::RKR_STATUS_NULL_POINTER;
4528    };
4529    if !frame.has_forces() {
4530        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4531    }
4532    let n = frame.atom_data.len();
4533    let mut data = Vec::with_capacity(n * 3);
4534    for a in &frame.atom_data {
4535        let f = a.force.unwrap_or([0.0; 3]);
4536        data.extend_from_slice(&f);
4537    }
4538    let arr = ndarray::ArcArray2::from_shape_vec((n, 3), data)
4539        .unwrap_or_else(|_| ndarray::ArcArray2::zeros((0, 3)));
4540    export_owned_array2_dlpack_opts(&arr, &o, out_tensor)
4541}
4542
4543/// DLPack displacements from a frame, or `SECTION_ABSENT` if missing
4544/// (f64/CPU default).
4545///
4546/// # Safety
4547/// `frame_handle` must be a valid frame handle; `out_tensor` must be a
4548/// writable pointer. Free the tensor with `rkr_dlpack_delete`.
4549#[unsafe(no_mangle)]
4550pub unsafe extern "C" fn rkr_frame_displacements_dlpack(
4551    frame_handle: *const RKRConFrame,
4552    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4553) -> RKRStatus {
4554    unsafe { rkr_frame_displacements_dlpack_ex(frame_handle, std::ptr::null(), out_tensor) }
4555}
4556
4557/// Like [`rkr_frame_displacements_dlpack`] with export options (NULL
4558/// `opts` selects float64 on CPU).
4559///
4560/// # Safety
4561/// Same contract as [`rkr_frame_displacements_dlpack`]; `opts` is NULL or
4562/// points to a valid `RKRDlpackExportOptions`.
4563#[unsafe(no_mangle)]
4564pub unsafe extern "C" fn rkr_frame_displacements_dlpack_ex(
4565    frame_handle: *const RKRConFrame,
4566    opts: *const RKRDlpackExportOptions,
4567    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4568) -> RKRStatus {
4569    if frame_handle.is_null() || out_tensor.is_null() {
4570        return RKRStatus::RKR_STATUS_NULL_POINTER;
4571    }
4572    unsafe { *out_tensor = std::ptr::null_mut() };
4573    let o = match resolve_dlpack_opts(opts) {
4574        Ok(o) => o,
4575        Err(st) => return st,
4576    };
4577    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4578        return RKRStatus::RKR_STATUS_NULL_POINTER;
4579    };
4580    if !frame.has_displacements() {
4581        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4582    }
4583    let n = frame.atom_data.len();
4584    let mut data = Vec::with_capacity(n * 3);
4585    for a in &frame.atom_data {
4586        let d = a.displacement.unwrap_or([0.0; 3]);
4587        data.extend_from_slice(&d);
4588    }
4589    let arr = ndarray::ArcArray2::from_shape_vec((n, 3), data)
4590        .unwrap_or_else(|_| ndarray::ArcArray2::zeros((0, 3)));
4591    export_owned_array2_dlpack_opts(&arr, &o, out_tensor)
4592}
4593
4594/// DLPack spreads from a frame, or `SECTION_ABSENT` if missing
4595/// (f64/CPU default).
4596///
4597/// # Safety
4598/// `frame_handle` must be a valid frame handle; `out_tensor` must be a
4599/// writable pointer. Free the tensor with `rkr_dlpack_delete`.
4600#[unsafe(no_mangle)]
4601pub unsafe extern "C" fn rkr_frame_spreads_dlpack(
4602    frame_handle: *const RKRConFrame,
4603    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4604) -> RKRStatus {
4605    unsafe { rkr_frame_spreads_dlpack_ex(frame_handle, std::ptr::null(), out_tensor) }
4606}
4607
4608/// Like [`rkr_frame_spreads_dlpack`] with export options (NULL
4609/// `opts` selects float64 on CPU).
4610///
4611/// # Safety
4612/// Same contract as [`rkr_frame_spreads_dlpack`]; `opts` is NULL or
4613/// points to a valid `RKRDlpackExportOptions`.
4614#[unsafe(no_mangle)]
4615pub unsafe extern "C" fn rkr_frame_spreads_dlpack_ex(
4616    frame_handle: *const RKRConFrame,
4617    opts: *const RKRDlpackExportOptions,
4618    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4619) -> RKRStatus {
4620    if frame_handle.is_null() || out_tensor.is_null() {
4621        return RKRStatus::RKR_STATUS_NULL_POINTER;
4622    }
4623    unsafe { *out_tensor = std::ptr::null_mut() };
4624    let o = match resolve_dlpack_opts(opts) {
4625        Ok(o) => o,
4626        Err(st) => return st,
4627    };
4628    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4629        return RKRStatus::RKR_STATUS_NULL_POINTER;
4630    };
4631    if !frame.has_spreads() {
4632        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4633    }
4634    let n = frame.atom_data.len();
4635    let mut data = Vec::with_capacity(n * 3);
4636    for a in &frame.atom_data {
4637        let d = a.spread.unwrap_or([0.0; 3]);
4638        data.extend_from_slice(&d);
4639    }
4640    let arr = ndarray::ArcArray2::from_shape_vec((n, 3), data)
4641        .unwrap_or_else(|_| ndarray::ArcArray2::zeros((0, 3)));
4642    export_owned_array2_dlpack_opts(&arr, &o, out_tensor)
4643}
4644
4645/// DLPack per-atom energies, or `SECTION_ABSENT` if missing (f64/CPU default).
4646#[unsafe(no_mangle)]
4647pub unsafe extern "C" fn rkr_frame_atom_energies_dlpack(
4648    frame_handle: *const RKRConFrame,
4649    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4650) -> RKRStatus {
4651    unsafe { rkr_frame_atom_energies_dlpack_ex(frame_handle, std::ptr::null(), out_tensor) }
4652}
4653
4654#[unsafe(no_mangle)]
4655pub unsafe extern "C" fn rkr_frame_atom_energies_dlpack_ex(
4656    frame_handle: *const RKRConFrame,
4657    opts: *const RKRDlpackExportOptions,
4658    out_tensor: *mut *mut RKRDLManagedTensorVersioned,
4659) -> RKRStatus {
4660    if frame_handle.is_null() || out_tensor.is_null() {
4661        return RKRStatus::RKR_STATUS_NULL_POINTER;
4662    }
4663    unsafe { *out_tensor = std::ptr::null_mut() };
4664    let o = match resolve_dlpack_opts(opts) {
4665        Ok(o) => o,
4666        Err(st) => return st,
4667    };
4668    let Some(frame) = (unsafe { (frame_handle as *const ConFrame).as_ref() }) else {
4669        return RKRStatus::RKR_STATUS_NULL_POINTER;
4670    };
4671    if !frame.has_energies() {
4672        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
4673    }
4674    let data: Vec<f64> = frame
4675        .atom_data
4676        .iter()
4677        .map(|a| a.energy.unwrap_or(0.0))
4678        .collect();
4679    let arr = ndarray::ArcArray1::from_vec(data);
4680    export_owned_array1_f64_dlpack_opts(&arr, &o, out_tensor)
4681}
4682
4683// Chemfiles selection (always linked; real impl needs --features chemfiles)
4684//=============================================================================
4685/// Opaque handle for a cached selection evaluation result.
4686pub struct RKRSelectionResult;
4687/// Evaluate a chemfiles selection-language string on an `RKRConFrame`.
4688///
4689/// On success writes a heap-allocated result handle to `*out_result` (caller
4690/// frees with [`rkr_selection_result_free`]). Returns
4691/// `RKR_STATUS_SELECTION_ERROR` for invalid grammar, evaluation failure, or
4692/// when this build was compiled without the `chemfiles` feature.
4693///
4694/// # Safety
4695/// `frame_handle`, `selection`, and `out_result` must be non-null; `selection`
4696/// must point to a valid UTF-8 C string.
4697#[unsafe(no_mangle)]
4698pub unsafe extern "C" fn rkr_frame_select(
4699    frame_handle: *const RKRConFrame,
4700    selection: *const c_char,
4701    out_result: *mut *mut RKRSelectionResult,
4702) -> RKRStatus {
4703    if frame_handle.is_null() || selection.is_null() || out_result.is_null() {
4704        return RKRStatus::RKR_STATUS_NULL_POINTER;
4705    }
4706    let frame = unsafe { &*(frame_handle as *const ConFrame) };
4707    let sel_str = match unsafe { CStr::from_ptr(selection) }.to_str() {
4708        Ok(s) => s,
4709        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
4710    };
4711    match crate::chemfiles_selection::evaluate_selection_on_con_frame(sel_str, frame) {
4712        Ok(result) => {
4713            let boxed = Box::new(result);
4714            unsafe {
4715                *out_result = Box::into_raw(boxed) as *mut RKRSelectionResult;
4716            }
4717            RKRStatus::RKR_STATUS_SUCCESS
4718        }
4719        Err(_) => RKRStatus::RKR_STATUS_SELECTION_ERROR,
4720    }
4721}
4722/// Number of matches in a selection result.
4723///
4724/// # Safety
4725/// `result_handle` must be a valid handle from [`rkr_frame_select`] or NULL.
4726#[unsafe(no_mangle)]
4727pub unsafe extern "C" fn rkr_selection_result_match_count(
4728    result_handle: *const RKRSelectionResult,
4729) -> u64 {
4730    if result_handle.is_null() {
4731        return 0;
4732    }
4733    let result = unsafe { &*(result_handle as *const crate::chemfiles_selection::SelectionResult) };
4734    result.matches.len() as u64
4735}
4736/// Selection context size (1=atom, 2=pair, 3=angle, 4=dihedral).
4737///
4738/// # Safety
4739/// `result_handle` must be valid or NULL (returns 0).
4740#[unsafe(no_mangle)]
4741pub unsafe extern "C" fn rkr_selection_result_context_size(
4742    result_handle: *const RKRSelectionResult,
4743) -> u32 {
4744    if result_handle.is_null() {
4745        return 0;
4746    }
4747    let result = unsafe { &*(result_handle as *const crate::chemfiles_selection::SelectionResult) };
4748    result.context_size as u32
4749}
4750/// Copy match `match_index` atom indices into `out_atoms` (up to 4 slots).
4751/// Writes actual arity to `*out_size` when non-null.
4752///
4753/// # Safety
4754/// Handles and `out_atoms` must be valid; `out_atoms` needs space for 4 `uint64_t`.
4755#[unsafe(no_mangle)]
4756pub unsafe extern "C" fn rkr_selection_result_match_at(
4757    result_handle: *const RKRSelectionResult,
4758    match_index: u64,
4759    out_atoms: *mut u64,
4760    out_size: *mut u32,
4761) -> RKRStatus {
4762    if result_handle.is_null() || out_atoms.is_null() {
4763        return RKRStatus::RKR_STATUS_NULL_POINTER;
4764    }
4765    let result = unsafe { &*(result_handle as *const crate::chemfiles_selection::SelectionResult) };
4766    let idx = match_index as usize;
4767    if idx >= result.matches.len() {
4768        return RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS;
4769    }
4770    let m = &result.matches[idx];
4771    unsafe {
4772        for i in 0..4 {
4773            *out_atoms.add(i) = if i < m.size {
4774                m.atoms[i] as u64
4775            } else {
4776                u64::MAX
4777            };
4778        }
4779        if !out_size.is_null() {
4780            *out_size = m.size as u32;
4781        }
4782    }
4783    RKRStatus::RKR_STATUS_SUCCESS
4784}
4785/// Fill `out_indices` with primary atom indices for each match (length =
4786/// match count). Returns `RKR_STATUS_BUFFER_TOO_SMALL` if `capacity` is too small.
4787///
4788/// # Safety
4789/// `result_handle` and `out_indices` must be valid when capacity > 0.
4790#[unsafe(no_mangle)]
4791pub unsafe extern "C" fn rkr_selection_result_primary_indices(
4792    result_handle: *const RKRSelectionResult,
4793    out_indices: *mut u64,
4794    capacity: u64,
4795    out_written: *mut u64,
4796) -> RKRStatus {
4797    if result_handle.is_null() {
4798        return RKRStatus::RKR_STATUS_NULL_POINTER;
4799    }
4800    let result = unsafe { &*(result_handle as *const crate::chemfiles_selection::SelectionResult) };
4801    let n = result.matches.len() as u64;
4802    if !out_written.is_null() {
4803        unsafe {
4804            *out_written = n;
4805        }
4806    }
4807    if n == 0 {
4808        return RKRStatus::RKR_STATUS_SUCCESS;
4809    }
4810    if out_indices.is_null() {
4811        return RKRStatus::RKR_STATUS_NULL_POINTER;
4812    }
4813    if capacity < n {
4814        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
4815    }
4816    unsafe {
4817        for (i, m) in result.matches.iter().enumerate() {
4818            *out_indices.add(i) = m.atoms[0] as u64;
4819        }
4820    }
4821    RKRStatus::RKR_STATUS_SUCCESS
4822}
4823/// Free a selection result from [`rkr_frame_select`]. Safe with NULL.
4824///
4825/// # Safety
4826/// `result_handle` must be from `rkr_frame_select` or NULL.
4827#[unsafe(no_mangle)]
4828pub unsafe extern "C" fn rkr_selection_result_free(result_handle: *mut RKRSelectionResult) {
4829    if result_handle.is_null() {
4830        return;
4831    }
4832    unsafe {
4833        drop(Box::from_raw(
4834            result_handle as *mut crate::chemfiles_selection::SelectionResult,
4835        ));
4836    }
4837}
4838/// Returns 1 when this library build includes chemfiles selection support.
4839#[unsafe(no_mangle)]
4840pub extern "C" fn rkr_has_chemfiles_support() -> u8 {
4841    #[cfg(feature = "chemfiles")]
4842    {
4843        1
4844    }
4845    #[cfg(not(feature = "chemfiles"))]
4846    {
4847        0
4848    }
4849}
4850
4851/// Returns 1 when this library build includes Rayon multi-frame parse.
4852#[unsafe(no_mangle)]
4853pub extern "C" fn rkr_has_parallel_support() -> u8 {
4854    #[cfg(feature = "parallel")]
4855    {
4856        1
4857    }
4858    #[cfg(not(feature = "parallel"))]
4859    {
4860        0
4861    }
4862}
4863/// Read the first frame from a chemfiles-supported path (XYZ, PDB, GRO, …).
4864/// Returns NULL on error or without the `chemfiles` feature. Caller: `free_rkr_frame`.
4865///
4866/// # Safety
4867/// `path_c` must be a valid NUL-terminated UTF-8 path.
4868#[unsafe(no_mangle)]
4869pub unsafe extern "C" fn rkr_read_chemfiles_first(path_c: *const c_char) -> *mut RKRConFrame {
4870    if path_c.is_null() {
4871        return std::ptr::null_mut();
4872    }
4873    let Ok(path_str) = unsafe { CStr::from_ptr(path_c) }.to_str() else {
4874        return std::ptr::null_mut();
4875    };
4876    match crate::chemfiles_import::con_frame_from_trajectory_path(path_str) {
4877        Ok(frame) => Box::into_raw(Box::new(frame)) as *mut RKRConFrame,
4878        Err(_) => std::ptr::null_mut(),
4879    }
4880}
4881
4882/// Read every step from a chemfiles-supported path. Sets `*num_frames`.
4883/// Free with `free_rkr_frame_array`. NULL on error / without chemfiles.
4884///
4885/// # Safety
4886/// `path_c` valid UTF-8; `num_frames` non-null.
4887#[unsafe(no_mangle)]
4888pub unsafe extern "C" fn rkr_read_chemfiles(
4889    path_c: *const c_char,
4890    num_frames: *mut usize,
4891) -> *mut *mut RKRConFrame {
4892    if path_c.is_null() || num_frames.is_null() {
4893        return std::ptr::null_mut();
4894    }
4895    let Ok(path_str) = unsafe { CStr::from_ptr(path_c) }.to_str() else {
4896        return std::ptr::null_mut();
4897    };
4898    match crate::chemfiles_import::con_frames_from_trajectory_path(path_str) {
4899        Ok(frames) => pack_frame_handles(frames, num_frames),
4900        Err(_) => std::ptr::null_mut(),
4901    }
4902}
4903
4904/// Read step `index` via chemfiles `Trajectory::read_step`.
4905/// Returns NULL on error, out of range, or without the `chemfiles` feature.
4906///
4907/// # Safety
4908/// `path_c` must be a valid NUL-terminated UTF-8 path.
4909#[unsafe(no_mangle)]
4910pub unsafe extern "C" fn rkr_read_chemfiles_nth(
4911    path_c: *const c_char,
4912    index: usize,
4913) -> *mut RKRConFrame {
4914    if path_c.is_null() {
4915        return std::ptr::null_mut();
4916    }
4917    let Ok(path_str) = unsafe { CStr::from_ptr(path_c) }.to_str() else {
4918        return std::ptr::null_mut();
4919    };
4920    match crate::chemfiles_import::con_frame_from_trajectory_path_nth(path_str, index) {
4921        Ok(frame) => Box::into_raw(Box::new(frame)) as *mut RKRConFrame,
4922        Err(_) => std::ptr::null_mut(),
4923    }
4924}
4925
4926/// Number of steps in a chemfiles trajectory (`Trajectory::nsteps`).
4927/// Returns `usize::MAX` on error or without the `chemfiles` feature.
4928///
4929/// # Safety
4930/// `path_c` must be a valid NUL-terminated UTF-8 path.
4931#[unsafe(no_mangle)]
4932pub unsafe extern "C" fn rkr_chemfiles_nsteps(path_c: *const c_char) -> usize {
4933    if path_c.is_null() {
4934        return usize::MAX;
4935    }
4936    let Ok(path_str) = unsafe { CStr::from_ptr(path_c) }.to_str() else {
4937        return usize::MAX;
4938    };
4939    crate::chemfiles_import::nsteps_from_trajectory_path(path_str).unwrap_or(usize::MAX)
4940}
4941
4942/// Read a chemfiles window: steps `start, start+step, … < stop`.
4943/// `stop == usize::MAX` means `nsteps`. `guess_bonds != 0` guesses topology
4944/// when the frame has no bonds. Sets `*num_frames`.
4945/// Free with `free_rkr_frame_array`. NULL on error / without chemfiles.
4946///
4947/// # Safety
4948/// `path_c` valid UTF-8; `num_frames` non-null. `topology_c` may be NULL.
4949#[unsafe(no_mangle)]
4950pub unsafe extern "C" fn rkr_read_chemfiles_range(
4951    path_c: *const c_char,
4952    start: usize,
4953    step: usize,
4954    stop: usize,
4955    topology_c: *const c_char,
4956    guess_bonds: u8,
4957    num_frames: *mut usize,
4958) -> *mut *mut RKRConFrame {
4959    if path_c.is_null() || num_frames.is_null() {
4960        return std::ptr::null_mut();
4961    }
4962    let Ok(path_str) = unsafe { CStr::from_ptr(path_c) }.to_str() else {
4963        return std::ptr::null_mut();
4964    };
4965    let topology = if topology_c.is_null() {
4966        None
4967    } else {
4968        match unsafe { CStr::from_ptr(topology_c) }.to_str() {
4969            Ok(s) if !s.is_empty() => Some(std::path::PathBuf::from(s)),
4970            _ => return std::ptr::null_mut(),
4971        }
4972    };
4973    let opts = crate::chemfiles_import::ChemfilesReadOpts {
4974        start,
4975        step,
4976        stop: if stop == usize::MAX { None } else { Some(stop) },
4977        format: None,
4978        topology,
4979        topology_format: None,
4980        guess_bonds: guess_bonds != 0,
4981    };
4982    match crate::chemfiles_import::con_frames_from_trajectory_path_with(path_str, &opts) {
4983        Ok(frames) => pack_frame_handles(frames, num_frames),
4984        Err(_) => std::ptr::null_mut(),
4985    }
4986}
4987/// Read all frames from memory with chemfiles `format` (e.g. `"XYZ"`).
4988/// Sets `*num_frames`. Free frames with `free_rkr_frame` and the array with
4989/// `free_rkr_frame_array`. NULL on error / without chemfiles.
4990///
4991/// # Safety
4992/// `data_c`, `format_c` valid UTF-8 C strings; `num_frames` non-null.
4993#[unsafe(no_mangle)]
4994pub unsafe extern "C" fn rkr_read_chemfiles_memory(
4995    data_c: *const c_char,
4996    format_c: *const c_char,
4997    num_frames: *mut usize,
4998) -> *mut *mut RKRConFrame {
4999    if data_c.is_null() || format_c.is_null() || num_frames.is_null() {
5000        return std::ptr::null_mut();
5001    }
5002    let Ok(data) = unsafe { CStr::from_ptr(data_c) }.to_str() else {
5003        return std::ptr::null_mut();
5004    };
5005    let Ok(format) = unsafe { CStr::from_ptr(format_c) }.to_str() else {
5006        return std::ptr::null_mut();
5007    };
5008    match crate::chemfiles_import::con_frames_from_memory(data, format) {
5009        Ok(frames) => {
5010            let n = frames.len();
5011            unsafe { *num_frames = n };
5012            let mut ptrs: Vec<*mut RKRConFrame> = frames
5013                .into_iter()
5014                .map(|f| Box::into_raw(Box::new(f)) as *mut RKRConFrame)
5015                .collect();
5016            let p = ptrs.as_mut_ptr();
5017            std::mem::forget(ptrs);
5018            p
5019        }
5020        Err(_) => std::ptr::null_mut(),
5021    }
5022}
5023/// Free a DLPack tensor from `rkr_frame_builder_*_dlpack` (calls deleter). Safe with NULL.
5024///
5025/// # Safety
5026/// `tensor` must be NULL or a pointer from a dlpack export of this library.
5027#[unsafe(no_mangle)]
5028pub unsafe extern "C" fn rkr_dlpack_delete(tensor: *mut RKRDLManagedTensorVersioned) {
5029    if tensor.is_null() {
5030        return;
5031    }
5032    unsafe {
5033        let t = &mut *tensor;
5034        if let Some(del) = t.deleter {
5035            del(tensor);
5036        }
5037    }
5038}
5039
5040/// Pack a frame as RCSO bytes for a caller-side `MPI_Bcast`.
5041///
5042/// `buf == NULL` writes the required size to `*out_len` and returns
5043/// success. If `buf` is non-null and `buflen` is too small, returns
5044/// `BUFFER_TOO_SMALL` and still writes the need to `*out_len`.
5045/// This function does not call MPI.
5046///
5047/// # Safety
5048/// `frame_handle` valid. `out_len` non-null. `buf` valid for `buflen` if non-null.
5049#[unsafe(no_mangle)]
5050pub unsafe extern "C" fn rkr_pack_rcso(
5051    frame_handle: *const RKRConFrame,
5052    buf: *mut u8,
5053    buflen: usize,
5054    out_len: *mut usize,
5055) -> RKRStatus {
5056    if out_len.is_null() {
5057        return RKRStatus::RKR_STATUS_NULL_POINTER;
5058    }
5059    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
5060        Some(f) => f,
5061        None => return RKRStatus::RKR_STATUS_NULL_POINTER,
5062    };
5063    let bytes = match crate::rcso::Rcso::encode_frame(frame) {
5064        Ok(b) => b,
5065        Err(_) => return RKRStatus::RKR_STATUS_VALIDATION_ERROR,
5066    };
5067    unsafe { *out_len = bytes.len() };
5068    if buf.is_null() {
5069        return RKRStatus::RKR_STATUS_SUCCESS;
5070    }
5071    if buflen < bytes.len() {
5072        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
5073    }
5074    unsafe {
5075        ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
5076    }
5077    RKRStatus::RKR_STATUS_SUCCESS
5078}
5079
5080/// Read `natoms` from an RCSO blob. No MPI.
5081///
5082/// # Safety
5083/// `buf` valid for `buflen`. `out_natoms` non-null.
5084#[unsafe(no_mangle)]
5085pub unsafe extern "C" fn rkr_unpack_rcso_natoms(
5086    buf: *const u8,
5087    buflen: usize,
5088    out_natoms: *mut u32,
5089) -> RKRStatus {
5090    if buf.is_null() || out_natoms.is_null() {
5091        return RKRStatus::RKR_STATUS_NULL_POINTER;
5092    }
5093    let bytes = unsafe { std::slice::from_raw_parts(buf, buflen) };
5094    match crate::rcso::Rcso::decode(bytes) {
5095        Ok(s) => {
5096            unsafe { *out_natoms = s.natoms };
5097            RKRStatus::RKR_STATUS_SUCCESS
5098        }
5099        Err(_) => RKRStatus::RKR_STATUS_VALIDATION_ERROR,
5100    }
5101}
5102
5103/// Copy RCSO positions into row-major `dest` (`natoms * 3` doubles).
5104///
5105/// # Safety
5106/// `buf` valid for `buflen`. `dest` valid for `dest_natoms * 3` f64.
5107#[unsafe(no_mangle)]
5108pub unsafe extern "C" fn rkr_unpack_rcso_positions(
5109    buf: *const u8,
5110    buflen: usize,
5111    dest: *mut f64,
5112    dest_natoms: u32,
5113) -> RKRStatus {
5114    if buf.is_null() || dest.is_null() {
5115        return RKRStatus::RKR_STATUS_NULL_POINTER;
5116    }
5117    let bytes = unsafe { std::slice::from_raw_parts(buf, buflen) };
5118    let soa = match crate::rcso::Rcso::decode(bytes) {
5119        Ok(s) => s,
5120        Err(_) => return RKRStatus::RKR_STATUS_VALIDATION_ERROR,
5121    };
5122    if soa.natoms != dest_natoms {
5123        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
5124    }
5125    let out = unsafe { std::slice::from_raw_parts_mut(dest, dest_natoms as usize * 3) };
5126    for (i, p) in soa.positions.iter().enumerate() {
5127        out[i * 3] = p[0];
5128        out[i * 3 + 1] = p[1];
5129        out[i * 3 + 2] = p[2];
5130    }
5131    RKRStatus::RKR_STATUS_SUCCESS
5132}
5133
5134/// Copy RCSO forces into row-major `dest`. `SECTION_ABSENT` if the blob
5135/// has no force block.
5136///
5137/// # Safety
5138/// `buf` valid for `buflen`. `dest` valid for `dest_natoms * 3` f64.
5139#[unsafe(no_mangle)]
5140pub unsafe extern "C" fn rkr_unpack_rcso_forces(
5141    buf: *const u8,
5142    buflen: usize,
5143    dest: *mut f64,
5144    dest_natoms: u32,
5145) -> RKRStatus {
5146    if buf.is_null() || dest.is_null() {
5147        return RKRStatus::RKR_STATUS_NULL_POINTER;
5148    }
5149    let bytes = unsafe { std::slice::from_raw_parts(buf, buflen) };
5150    let soa = match crate::rcso::Rcso::decode(bytes) {
5151        Ok(s) => s,
5152        Err(_) => return RKRStatus::RKR_STATUS_VALIDATION_ERROR,
5153    };
5154    let Some(forces) = soa.forces else {
5155        return RKRStatus::RKR_STATUS_SECTION_ABSENT;
5156    };
5157    if soa.natoms != dest_natoms {
5158        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
5159    }
5160    let out = unsafe { std::slice::from_raw_parts_mut(dest, dest_natoms as usize * 3) };
5161    for (i, f) in forces.iter().enumerate() {
5162        out[i * 3] = f[0];
5163        out[i * 3 + 1] = f[1];
5164        out[i * 3 + 2] = f[2];
5165    }
5166    RKRStatus::RKR_STATUS_SUCCESS
5167}
5168
5169#[cfg(test)]
5170mod tests {
5171    use super::*;
5172    use std::ffi::{CStr, CString};
5173    #[test]
5174    fn frame_copy_positions_without_cframe() {
5175        let handle = test_frame_handle();
5176        let n = unsafe { rkr_frame_atom_count(handle) };
5177        assert_eq!(n, 1);
5178        let mut buf = vec![0.0f64; n * 3];
5179        assert_eq!(
5180            unsafe { rkr_frame_copy_positions(handle, buf.as_mut_ptr(), buf.len()) },
5181            RKRStatus::RKR_STATUS_SUCCESS
5182        );
5183        let mut tensor: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5184        assert_eq!(
5185            unsafe { rkr_frame_positions_dlpack(handle, &mut tensor) },
5186            RKRStatus::RKR_STATUS_SUCCESS
5187        );
5188        assert!(!tensor.is_null());
5189        unsafe { rkr_dlpack_delete(tensor) };
5190        assert_eq!(
5191            unsafe { rkr_frame_copy_velocities(handle, buf.as_mut_ptr(), buf.len()) },
5192            RKRStatus::RKR_STATUS_SECTION_ABSENT
5193        );
5194        unsafe { free_rkr_frame(handle) };
5195    }
5196    #[test]
5197    fn read_all_frames_c_abi_tiny() {
5198        let path = std::ffi::CString::new("resources/test/tiny_cuh2.con").unwrap();
5199        let mut n: usize = 0;
5200        let arr = unsafe { rkr_read_all_frames(path.as_ptr(), &mut n) };
5201        assert!(!arr.is_null() && n >= 1);
5202        let first = unsafe { *arr };
5203        let nat = unsafe { rkr_frame_atom_count(first) };
5204        let mut buf = vec![0.0f64; nat * 3];
5205        assert_eq!(
5206            unsafe { rkr_frame_copy_positions(first, buf.as_mut_ptr(), buf.len()) },
5207            RKRStatus::RKR_STATUS_SUCCESS
5208        );
5209        unsafe { free_rkr_frame_array(arr, n) };
5210    }
5211
5212    #[test]
5213    fn read_all_frames_n_threads_matches_auto() {
5214        let path = std::ffi::CString::new("resources/test/tiny_cuh2.con").unwrap();
5215        let mut n_auto: usize = 0;
5216        let mut n_one: usize = 0;
5217        let auto = unsafe { rkr_read_all_frames_n_threads(path.as_ptr(), &mut n_auto, 0) };
5218        let one = unsafe { rkr_read_all_frames_n_threads(path.as_ptr(), &mut n_one, 1) };
5219        assert!(!auto.is_null() && !one.is_null());
5220        assert_eq!(n_auto, n_one);
5221        assert_eq!(n_auto, 1);
5222        let nat_auto = unsafe { rkr_frame_atom_count(*auto) };
5223        let nat_one = unsafe { rkr_frame_atom_count(*one) };
5224        assert_eq!(nat_auto, nat_one);
5225        unsafe { free_rkr_frame_array(auto, n_auto) };
5226        unsafe { free_rkr_frame_array(one, n_one) };
5227    }
5228
5229    #[test]
5230    fn free_rkr_frame_ptr_array_keeps_frames() {
5231        let path = std::ffi::CString::new("resources/test/tiny_cuh2.con").unwrap();
5232        let mut n: usize = 0;
5233        let arr = unsafe { rkr_read_all_frames(path.as_ptr(), &mut n) };
5234        assert!(!arr.is_null() && n >= 1);
5235        let first = unsafe { *arr };
5236        // free only outer array; first frame remains owned
5237        let rest: Vec<*mut RKRConFrame> = (1..n).map(|i| unsafe { *arr.add(i) }).collect();
5238        unsafe { free_rkr_frame_ptr_array(arr, n) };
5239        assert!(unsafe { rkr_frame_atom_count(first) } >= 1);
5240        unsafe { free_rkr_frame(first) };
5241        for h in rest {
5242            if !h.is_null() {
5243                unsafe { free_rkr_frame(h) };
5244            }
5245        }
5246    }
5247    fn test_frame_handle() -> *mut RKRConFrame {
5248        let mut builder = ConFrameBuilder::new([10.0, 10.0, 10.0], [90.0, 90.0, 90.0]);
5249        builder
5250            .prebox_header("Generated by test")
5251            .postbox_header(["0 0".to_string(), "0 0 0".to_string()]);
5252        builder.add_atom("Cu", 0.0, 0.0, 0.0, [false, false, false], 0, 63.546);
5253        Box::into_raw(Box::new(builder.build().unwrap())) as *mut RKRConFrame
5254    }
5255    #[test]
5256    fn header_line_rejects_null_buffer() {
5257        let frame = test_frame_handle();
5258        let status = unsafe { rkr_frame_get_header_line(frame, true, 0, std::ptr::null_mut(), 16) };
5259        unsafe { free_rkr_frame(frame) };
5260        assert_eq!(status, RKRStatus::RKR_STATUS_NULL_POINTER);
5261    }
5262    #[test]
5263    fn header_line_rejects_empty_buffer() {
5264        let frame = test_frame_handle();
5265        let mut buffer = [0 as c_char; 1];
5266        let status = unsafe { rkr_frame_get_header_line(frame, true, 0, buffer.as_mut_ptr(), 0) };
5267        unsafe { free_rkr_frame(frame) };
5268        assert_eq!(status, RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL);
5269    }
5270    #[test]
5271    fn header_line_truncates_and_terminates_buffer() {
5272        let frame = test_frame_handle();
5273        let mut buffer = [0 as c_char; 10];
5274        let status =
5275            unsafe { rkr_frame_get_header_line(frame, true, 0, buffer.as_mut_ptr(), buffer.len()) };
5276        unsafe { free_rkr_frame(frame) };
5277        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
5278        let copied = unsafe { CStr::from_ptr(buffer.as_ptr()) };
5279        assert_eq!(copied.to_str().unwrap(), "Generated");
5280    }
5281
5282    #[test]
5283    fn pack_rcso_size_query_then_dest() {
5284        let path = std::ffi::CString::new("resources/test/tiny_cuh2_forces.con").unwrap();
5285        let frame = unsafe { rkr_read_nth_frame(path.as_ptr(), 0) };
5286        assert!(!frame.is_null());
5287        let mut need = 0usize;
5288        let st = unsafe { rkr_pack_rcso(frame, std::ptr::null_mut(), 0, &mut need) };
5289        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
5290        assert!(need >= 24);
5291        let mut buf = vec![0u8; need];
5292        let mut wrote = 0usize;
5293        let st = unsafe { rkr_pack_rcso(frame, buf.as_mut_ptr(), buf.len(), &mut wrote) };
5294        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
5295        assert_eq!(wrote, need);
5296        assert_eq!(&buf[0..4], b"RCSO");
5297        let mut natoms = 0u32;
5298        let st = unsafe { rkr_unpack_rcso_natoms(buf.as_ptr(), buf.len(), &mut natoms) };
5299        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
5300        assert_eq!(natoms, unsafe { rkr_frame_atom_count(frame) } as u32);
5301        let mut xyz = vec![0.0f64; natoms as usize * 3];
5302        let st =
5303            unsafe { rkr_unpack_rcso_positions(buf.as_ptr(), buf.len(), xyz.as_mut_ptr(), natoms) };
5304        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
5305        let mut frc = vec![0.0f64; natoms as usize * 3];
5306        let st =
5307            unsafe { rkr_unpack_rcso_forces(buf.as_ptr(), buf.len(), frc.as_mut_ptr(), natoms) };
5308        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
5309        unsafe { free_rkr_frame(frame) };
5310    }
5311
5312    fn test_builder_handle() -> *mut RKRConFrameBuilder {
5313        let cell = [10.0, 11.0, 12.0];
5314        let angles = [90.0, 91.0, 92.0];
5315        unsafe {
5316            rkr_frame_new(
5317                cell.as_ptr(),
5318                angles.as_ptr(),
5319                ptr::null(),
5320                ptr::null(),
5321                ptr::null(),
5322                ptr::null(),
5323            )
5324        }
5325    }
5326    fn c_string(s: &str) -> CString {
5327        CString::new(s).unwrap()
5328    }
5329    #[test]
5330    fn builder_set_units_c_abi_stores_the_object() {
5331        let builder = test_builder_handle();
5332        let units = c_string(
5333            r#"{"length":"A","energy":"ev","mass":"amu","time":"(amu*angstrom^2/eV)^0.5"}"#,
5334        );
5335        let status = unsafe { rkr_frame_builder_set_units(builder, units.as_ptr()) };
5336        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
5337        let symbol = c_string("H");
5338        assert_eq!(
5339            unsafe { rkr_frame_add_atom(builder, symbol.as_ptr(), 0.0, 0.0, 0.0, false, 0, 1.0) },
5340            RKRStatus::RKR_STATUS_SUCCESS
5341        );
5342        let frame = unsafe { rkr_frame_builder_build(builder) };
5343        assert!(!frame.is_null());
5344        let json = unsafe { rkr_frame_metadata_json(frame) };
5345        assert!(!json.is_null());
5346        let text = unsafe { CStr::from_ptr(json) }.to_str().unwrap();
5347        assert!(text.contains(r#""length":"angstrom""#), "{text}");
5348        assert!(text.contains(r#""energy":"eV""#), "{text}");
5349        assert!(
5350            text.contains(r#""time":"(amu*angstrom^2/eV)^0.5""#),
5351            "{text}"
5352        );
5353        unsafe {
5354            rkr_free_string(json);
5355            free_rkr_frame(frame);
5356        }
5357    }
5358    #[test]
5359    fn builder_stamps_con_spec_minor_one_unless_set() {
5360        let builder = test_builder_handle();
5361        let symbol = c_string("H");
5362        assert_eq!(
5363            unsafe { rkr_frame_add_atom(builder, symbol.as_ptr(), 0.0, 0.0, 0.0, false, 0, 1.0) },
5364            RKRStatus::RKR_STATUS_SUCCESS
5365        );
5366        let frame = unsafe { rkr_frame_builder_build(builder) };
5367        let json = unsafe { rkr_frame_metadata_json(frame) };
5368        let text = unsafe { CStr::from_ptr(json) }
5369            .to_str()
5370            .unwrap()
5371            .to_string();
5372        assert!(text.contains(r#""con_spec_minor":1"#), "{text}");
5373        unsafe {
5374            rkr_free_string(json);
5375            free_rkr_frame(frame);
5376        }
5377
5378        let builder = test_builder_handle();
5379        assert_eq!(
5380            unsafe { rkr_frame_builder_set_con_spec_minor(builder, 4) },
5381            RKRStatus::RKR_STATUS_SUCCESS
5382        );
5383        let symbol = c_string("H");
5384        assert_eq!(
5385            unsafe { rkr_frame_add_atom(builder, symbol.as_ptr(), 0.0, 0.0, 0.0, false, 0, 1.0) },
5386            RKRStatus::RKR_STATUS_SUCCESS
5387        );
5388        let frame = unsafe { rkr_frame_builder_build(builder) };
5389        let json = unsafe { rkr_frame_metadata_json(frame) };
5390        let text = unsafe { CStr::from_ptr(json) }
5391            .to_str()
5392            .unwrap()
5393            .to_string();
5394        assert!(text.contains(r#""con_spec_minor":4"#), "{text}");
5395        unsafe {
5396            rkr_free_string(json);
5397            free_rkr_frame(frame);
5398        }
5399    }
5400    unsafe fn assert_single_atom(
5401        frame: *mut RKRConFrame,
5402        fixed: [bool; 3],
5403        velocity: Option<[f64; 3]>,
5404        forces: Option<[f64; 3]>,
5405    ) {
5406        let c_frame = unsafe { rkr_frame_to_c_frame(frame) };
5407        assert!(!c_frame.is_null());
5408        let c_frame_ref = unsafe { &*c_frame };
5409        assert_eq!(c_frame_ref.num_atoms, 1);
5410        assert_eq!(c_frame_ref.has_velocities, velocity.is_some());
5411        assert_eq!(c_frame_ref.has_forces, forces.is_some());
5412        let atom = unsafe { &*c_frame_ref.atoms };
5413        assert_eq!(atom.fixed_x, fixed[0]);
5414        assert_eq!(atom.fixed_y, fixed[1]);
5415        assert_eq!(atom.fixed_z, fixed[2]);
5416        assert_eq!(atom.is_fixed, fixed.iter().any(|&value| value));
5417        assert_eq!(atom.has_velocity, velocity.is_some());
5418        assert_eq!(atom.has_forces, forces.is_some());
5419        if let Some([vx, vy, vz]) = velocity {
5420            assert_eq!([atom.vx, atom.vy, atom.vz], [vx, vy, vz]);
5421        }
5422        if let Some([fx, fy, fz]) = forces {
5423            assert_eq!([atom.fx, atom.fy, atom.fz], [fx, fy, fz]);
5424        }
5425        unsafe { free_c_frame(c_frame) };
5426        unsafe { free_rkr_frame(frame) };
5427    }
5428    #[test]
5429    fn builder_preserves_fixed_mask_for_atom_without_velocity_or_forces() {
5430        let builder = test_builder_handle();
5431        let symbol = c_string("Cu");
5432        let status = unsafe {
5433            rkr_frame_add_atom_with_fixed_mask(
5434                builder,
5435                symbol.as_ptr(),
5436                1.0,
5437                2.0,
5438                3.0,
5439                true,
5440                false,
5441                true,
5442                7,
5443                63.546,
5444            )
5445        };
5446        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
5447        let frame = unsafe { rkr_frame_builder_build(builder) };
5448        unsafe { assert_single_atom(frame, [true, false, true], None, None) };
5449    }
5450    #[test]
5451    fn builder_preserves_fixed_mask_for_atom_with_velocity() {
5452        let builder = test_builder_handle();
5453        let symbol = c_string("H");
5454        let status = unsafe {
5455            rkr_frame_add_atom_with_velocity_fixed_mask(
5456                builder,
5457                symbol.as_ptr(),
5458                1.0,
5459                2.0,
5460                3.0,
5461                false,
5462                true,
5463                false,
5464                9,
5465                1.008,
5466                0.1,
5467                0.2,
5468                0.3,
5469            )
5470        };
5471        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
5472        let frame = unsafe { rkr_frame_builder_build(builder) };
5473        unsafe { assert_single_atom(frame, [false, true, false], Some([0.1, 0.2, 0.3]), None) };
5474    }
5475    #[test]
5476    fn builder_preserves_fixed_mask_for_atom_with_forces() {
5477        let builder = test_builder_handle();
5478        let symbol = c_string("O");
5479        let status = unsafe {
5480            rkr_frame_add_atom_with_forces_fixed_mask(
5481                builder,
5482                symbol.as_ptr(),
5483                1.0,
5484                2.0,
5485                3.0,
5486                true,
5487                true,
5488                false,
5489                11,
5490                15.999,
5491                -0.1,
5492                -0.2,
5493                -0.3,
5494            )
5495        };
5496        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
5497        let frame = unsafe { rkr_frame_builder_build(builder) };
5498        unsafe { assert_single_atom(frame, [true, true, false], None, Some([-0.1, -0.2, -0.3])) };
5499    }
5500    #[test]
5501    fn builder_preserves_fixed_mask_for_atom_with_velocity_and_forces() {
5502        let builder = test_builder_handle();
5503        let symbol = c_string("N");
5504        let status = unsafe {
5505            rkr_frame_add_atom_with_velocity_and_forces_fixed_mask(
5506                builder,
5507                symbol.as_ptr(),
5508                1.0,
5509                2.0,
5510                3.0,
5511                false,
5512                true,
5513                true,
5514                13,
5515                14.007,
5516                0.4,
5517                0.5,
5518                0.6,
5519                -0.4,
5520                -0.5,
5521                -0.6,
5522            )
5523        };
5524        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
5525        let frame = unsafe { rkr_frame_builder_build(builder) };
5526        unsafe {
5527            assert_single_atom(
5528                frame,
5529                [false, true, true],
5530                Some([0.4, 0.5, 0.6]),
5531                Some([-0.4, -0.5, -0.6]),
5532            )
5533        };
5534    }
5535    #[test]
5536    fn builder_bool_fixed_functions_set_all_axes_together() {
5537        let builder = test_builder_handle();
5538        let cu = c_string("Cu");
5539        let h = c_string("H");
5540        let atom_status =
5541            unsafe { rkr_frame_add_atom(builder, cu.as_ptr(), 1.0, 2.0, 3.0, true, 1, 63.546) };
5542        assert_eq!(atom_status, RKRStatus::RKR_STATUS_SUCCESS);
5543        let velocity_status = unsafe {
5544            rkr_frame_add_atom_with_velocity(
5545                builder,
5546                h.as_ptr(),
5547                4.0,
5548                5.0,
5549                6.0,
5550                false,
5551                2,
5552                1.008,
5553                0.7,
5554                0.8,
5555                0.9,
5556            )
5557        };
5558        assert_eq!(velocity_status, RKRStatus::RKR_STATUS_SUCCESS);
5559        let frame = unsafe { rkr_frame_builder_build(builder) };
5560        let c_frame = unsafe { rkr_frame_to_c_frame(frame) };
5561        assert!(!c_frame.is_null());
5562        let c_frame_ref = unsafe { &*c_frame };
5563        assert_eq!(c_frame_ref.num_atoms, 2);
5564        let atoms = unsafe { std::slice::from_raw_parts(c_frame_ref.atoms, c_frame_ref.num_atoms) };
5565        assert_eq!(
5566            [atoms[0].fixed_x, atoms[0].fixed_y, atoms[0].fixed_z],
5567            [true, true, true]
5568        );
5569        assert_eq!(
5570            [atoms[1].fixed_x, atoms[1].fixed_y, atoms[1].fixed_z],
5571            [false, false, false]
5572        );
5573        unsafe { free_c_frame(c_frame) };
5574        unsafe { free_rkr_frame(frame) };
5575    }
5576    #[test]
5577    fn status_message_returns_static_strings_for_all_status_values() {
5578        let cases = [
5579            (RKRStatus::RKR_STATUS_SUCCESS, "success"),
5580            (RKRStatus::RKR_STATUS_NULL_POINTER, "null pointer"),
5581            (RKRStatus::RKR_STATUS_INVALID_UTF8, "invalid UTF-8"),
5582            (RKRStatus::RKR_STATUS_INVALID_JSON, "invalid JSON"),
5583            (RKRStatus::RKR_STATUS_IO_ERROR, "I/O error"),
5584            (
5585                RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS,
5586                "index out of bounds",
5587            ),
5588            (RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL, "buffer too small"),
5589            (RKRStatus::RKR_STATUS_INTERNAL_ERROR, "internal error"),
5590            (RKRStatus::RKR_STATUS_SECTION_ABSENT, "section absent"),
5591            (RKRStatus::RKR_STATUS_VALIDATION_ERROR, "validation error"),
5592            (RKRStatus::RKR_STATUS_SELECTION_ERROR, "selection error"),
5593            (
5594                RKRStatus::RKR_STATUS_FEATURE_DISABLED,
5595                "feature disabled in this build",
5596            ),
5597        ];
5598        for (status, expected) in cases {
5599            let message = unsafe { CStr::from_ptr(rkr_status_message(status)) };
5600            assert_eq!(message.to_str().unwrap(), expected);
5601        }
5602    }
5603    // ----- DLPack FFI smoke tests ----------------------------------------------
5604    /// Assert DLPack tensor layout without assuming the host language uses f64
5605    /// buffers — consumers must read `dtype` / `ndim` / `shape` from the tensor.
5606    unsafe fn assert_dlpack_cpu_float(
5607        t: *mut RKRDLManagedTensorVersioned,
5608        expect_ndim: i32,
5609        expect_shape: &[i64],
5610        expect_bits: u8,
5611    ) {
5612        assert!(!t.is_null());
5613        let dl = unsafe { &(*t).dl_tensor };
5614        assert_eq!(dl.ndim, expect_ndim);
5615        let shape = unsafe { std::slice::from_raw_parts(dl.shape, expect_ndim as usize) };
5616        assert_eq!(shape, expect_shape);
5617        assert_eq!(dl.dtype.code, dlpk::sys::DLDataTypeCode::kDLFloat);
5618        assert_eq!(dl.dtype.bits, expect_bits);
5619        assert_eq!(dl.dtype.lanes, 1);
5620        assert_eq!(dl.device, dlpk::sys::DLDevice::cpu());
5621        assert!(!dl.data.is_null());
5622    }
5623
5624    #[test]
5625    fn frame_optional_section_dlpack_present_and_absent() {
5626        // Absent on coordinate-only frame
5627        let handle = test_frame_handle();
5628        let mut t: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5629        assert_eq!(
5630            unsafe { rkr_frame_velocities_dlpack(handle, &mut t) },
5631            RKRStatus::RKR_STATUS_SECTION_ABSENT
5632        );
5633        assert!(t.is_null());
5634        assert_eq!(
5635            unsafe { rkr_frame_forces_dlpack(handle, &mut t) },
5636            RKRStatus::RKR_STATUS_SECTION_ABSENT
5637        );
5638        assert_eq!(
5639            unsafe { rkr_frame_atom_energies_dlpack(handle, &mut t) },
5640            RKRStatus::RKR_STATUS_SECTION_ABSENT
5641        );
5642        unsafe { free_rkr_frame(handle) };
5643
5644        // Present on .convel fixture — (N, 3) float, N from atom_count
5645        let path = CString::new("resources/test/tiny_cuh2.convel").unwrap();
5646        let fr = unsafe { rkr_read_first_frame(path.as_ptr()) };
5647        assert!(!fr.is_null());
5648        let n = unsafe { rkr_frame_atom_count(fr) } as i64;
5649        assert!(n > 0);
5650        let mut vel: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5651        let st = unsafe { rkr_frame_velocities_dlpack(fr, &mut vel) };
5652        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
5653        unsafe {
5654            assert_dlpack_cpu_float(vel, 2, &[n, 3], 64);
5655            rkr_dlpack_delete(vel);
5656            free_rkr_frame(fr);
5657        }
5658
5659        // Forces (N,3) + energies (N,) via builder → frame
5660        let cell = [10.0f64; 3];
5661        let ang = [90.0f64; 3];
5662        let b = unsafe {
5663            rkr_frame_new(
5664                cell.as_ptr(),
5665                ang.as_ptr(),
5666                std::ptr::null(),
5667                std::ptr::null(),
5668                std::ptr::null(),
5669                std::ptr::null(),
5670            )
5671        };
5672        assert!(!b.is_null());
5673        let sym = CString::new("H").unwrap();
5674        unsafe {
5675            rkr_frame_add_atom_with_velocity_and_forces_fixed_mask(
5676                b,
5677                sym.as_ptr(),
5678                0.0,
5679                0.0,
5680                0.0,
5681                false,
5682                false,
5683                false,
5684                0,
5685                1.0,
5686                0.1,
5687                0.0,
5688                0.0,
5689                0.0,
5690                0.0,
5691                -1.0,
5692            );
5693            rkr_frame_builder_set_last_energy(b, -0.5);
5694        }
5695        let built = unsafe { rkr_frame_builder_build(b) };
5696        assert!(!built.is_null());
5697        let n_built = unsafe { rkr_frame_atom_count(built) } as i64;
5698        let mut frc: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5699        let mut eng: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5700        assert_eq!(
5701            unsafe { rkr_frame_forces_dlpack(built, &mut frc) },
5702            RKRStatus::RKR_STATUS_SUCCESS
5703        );
5704        unsafe { assert_dlpack_cpu_float(frc, 2, &[n_built, 3], 64) };
5705        assert_eq!(
5706            unsafe { rkr_frame_atom_energies_dlpack(built, &mut eng) },
5707            RKRStatus::RKR_STATUS_SUCCESS
5708        );
5709        unsafe { assert_dlpack_cpu_float(eng, 1, &[n_built], 64) };
5710        // Explicit f32 via DLPack-shaped DLDataType (code=kDLFloat=2, bits=32)
5711        let opts32 = RKRDlpackExportOptions {
5712            dtype: RKRDLDataType {
5713                code: rkr_dl_type_code::RKR_DL_FLOAT,
5714                bits: 32,
5715                lanes: 1,
5716            },
5717            device: RKRDLDevice {
5718                device_type: rkr_dl_device_type::RKR_DL_CPU,
5719                device_id: 0,
5720            },
5721        };
5722        let mut pos32: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5723        assert_eq!(
5724            unsafe { rkr_frame_positions_dlpack_ex(built, &opts32, &mut pos32) },
5725            RKRStatus::RKR_STATUS_SUCCESS
5726        );
5727        unsafe {
5728            assert_dlpack_cpu_float(pos32, 2, &[n_built, 3], 32);
5729            rkr_dlpack_delete(pos32);
5730        }
5731        // CUDA device requests: FEATURE_DISABLED without --features cuda;
5732        // with cuda, H2D export succeeds (kDLCUDA).
5733        let cuda_dev = RKRDlpackExportOptions {
5734            dtype: RKRDLDataType {
5735                code: rkr_dl_type_code::RKR_DL_FLOAT,
5736                bits: 64,
5737                lanes: 1,
5738            },
5739            device: RKRDLDevice {
5740                device_type: rkr_dl_device_type::RKR_DL_CUDA,
5741                device_id: 0,
5742            },
5743        };
5744        let mut junk: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5745        #[cfg(not(feature = "cuda"))]
5746        assert_eq!(
5747            unsafe { rkr_frame_positions_dlpack_ex(built, &cuda_dev, &mut junk) },
5748            RKRStatus::RKR_STATUS_FEATURE_DISABLED
5749        );
5750        #[cfg(feature = "cuda")]
5751        {
5752            assert_eq!(
5753                unsafe { rkr_frame_positions_dlpack_ex(built, &cuda_dev, &mut junk) },
5754                RKRStatus::RKR_STATUS_SUCCESS
5755            );
5756            assert!(!junk.is_null());
5757            let dl = unsafe { &(*junk).dl_tensor };
5758            assert_eq!(dl.device.device_type, dlpk::sys::DLDeviceType::kDLCUDA);
5759            assert!(!dl.data.is_null());
5760            unsafe { rkr_dlpack_delete(junk) };
5761            // Also rkr_frame_positions_as_dlpack with CUDA
5762            let mut as_cuda: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5763            assert_eq!(
5764                unsafe {
5765                    rkr_frame_positions_as_dlpack(
5766                        built,
5767                        rkr_dl_device_type::RKR_DL_CUDA,
5768                        0,
5769                        0,
5770                        1,
5771                        0,
5772                        &mut as_cuda,
5773                    )
5774                },
5775                RKRStatus::RKR_STATUS_SUCCESS
5776            );
5777            assert!(!as_cuda.is_null());
5778            unsafe {
5779                assert_eq!(
5780                    (*as_cuda).dl_tensor.device.device_type,
5781                    dlpk::sys::DLDeviceType::kDLCUDA
5782                );
5783                rkr_dlpack_delete(as_cuda);
5784            }
5785        }
5786        // Unsupported dtype bits on CPU
5787        let bad_bits = RKRDlpackExportOptions {
5788            dtype: RKRDLDataType {
5789                code: rkr_dl_type_code::RKR_DL_FLOAT,
5790                bits: 16,
5791                lanes: 1,
5792            },
5793            device: RKRDLDevice {
5794                device_type: rkr_dl_device_type::RKR_DL_CPU,
5795                device_id: 0,
5796            },
5797        };
5798        assert_eq!(
5799            unsafe { rkr_frame_positions_dlpack_ex(built, &bad_bits, &mut junk) },
5800            RKRStatus::RKR_STATUS_VALIDATION_ERROR
5801        );
5802        // Metatensor-style as_dlpack reflects storage (f64 CPU), not a cast target
5803        let mut as_t: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5804        assert_eq!(
5805            unsafe {
5806                rkr_frame_positions_as_dlpack(
5807                    built,
5808                    rkr_dl_device_type::RKR_DL_CPU,
5809                    0,
5810                    0,
5811                    1,
5812                    0,
5813                    &mut as_t,
5814                )
5815            },
5816            RKRStatus::RKR_STATUS_SUCCESS
5817        );
5818        unsafe {
5819            assert_dlpack_cpu_float(as_t, 2, &[n_built, 3], 64);
5820        }
5821        // Mutate frame positions, then ingest tensor and require values restored
5822        {
5823            let fr = unsafe { &mut *(built as *mut ConFrame) };
5824            for a in fr.atom_data.iter_mut() {
5825                a.x = -99.0;
5826                a.y = -99.0;
5827                a.z = -99.0;
5828            }
5829        }
5830        assert_eq!(
5831            unsafe { rkr_frame_positions_from_dlpack(built, as_t) },
5832            RKRStatus::RKR_STATUS_SUCCESS
5833        );
5834        {
5835            let fr = unsafe { &*(built as *const ConFrame) };
5836            assert!((fr.atom_data[0].x - 0.0).abs() < 1e-12);
5837            assert!((fr.atom_data[0].y - 0.0).abs() < 1e-12);
5838            assert!((fr.atom_data[0].z - 0.0).abs() < 1e-12);
5839        }
5840        // Re-export must match ingested coordinates (not zeros / garbage)
5841        let mut again: *mut RKRDLManagedTensorVersioned = std::ptr::null_mut();
5842        assert_eq!(
5843            unsafe {
5844                rkr_frame_positions_as_dlpack(
5845                    built,
5846                    rkr_dl_device_type::RKR_DL_CPU,
5847                    0,
5848                    0,
5849                    1,
5850                    0,
5851                    &mut again,
5852                )
5853            },
5854            RKRStatus::RKR_STATUS_SUCCESS
5855        );
5856        unsafe {
5857            let dl = &(*again).dl_tensor;
5858            let data = std::slice::from_raw_parts(dl.data as *const f64, 3);
5859            assert!((data[0] - 0.0).abs() < 1e-12);
5860            assert!((data[1] - 0.0).abs() < 1e-12);
5861            assert!((data[2] - 0.0).abs() < 1e-12);
5862            rkr_dlpack_delete(again);
5863            rkr_dlpack_delete(as_t);
5864            rkr_dlpack_delete(frc);
5865            rkr_dlpack_delete(eng);
5866            free_rkr_frame(built);
5867        }
5868    }
5869
5870    #[test]
5871    fn ffi_positions_dlpack_round_trip() {
5872        let handle = test_builder_handle();
5873        let sym = c_string("Cu");
5874        unsafe {
5875            rkr_frame_add_atom_full(
5876                handle,
5877                sym.as_ptr(),
5878                1.0,
5879                2.0,
5880                3.0,
5881                false,
5882                false,
5883                false,
5884                7,
5885                63.5,
5886                ptr::null(),
5887                ptr::null(),
5888            )
5889        };
5890        let mut t: *mut RKRDLManagedTensorVersioned = ptr::null_mut();
5891        let status = unsafe { rkr_frame_builder_positions_dlpack(handle, &mut t) };
5892        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
5893        assert!(!t.is_null());
5894        // Inspect the DLPack tensor: shape (1, 3), dtype kDLFloat / 64, CPU.
5895        let dl = unsafe { &(*t).dl_tensor };
5896        assert_eq!(dl.ndim, 2);
5897        let shape = unsafe { std::slice::from_raw_parts(dl.shape, 2) };
5898        assert_eq!(shape, &[1, 3]);
5899        assert_eq!(dl.dtype.code, dlpk::sys::DLDataTypeCode::kDLFloat);
5900        assert_eq!(dl.dtype.bits, 64);
5901        assert_eq!(dl.dtype.lanes, 1);
5902        assert_eq!(dl.device, dlpk::sys::DLDevice::cpu());
5903        let data = unsafe { std::slice::from_raw_parts(dl.data as *const f64, 3) };
5904        assert_eq!(data, &[1.0, 2.0, 3.0]);
5905        // Invoke the deleter the same way a C consumer would.
5906        let deleter = unsafe { (*t).deleter };
5907        if let Some(del) = deleter {
5908            unsafe { del(t) };
5909        }
5910        unsafe { free_rkr_frame_builder(handle) };
5911    }
5912    #[test]
5913    fn ffi_velocities_dlpack_section_absent() {
5914        let handle = test_builder_handle();
5915        let sym = c_string("Cu");
5916        unsafe {
5917            rkr_frame_add_atom_full(
5918                handle,
5919                sym.as_ptr(),
5920                0.0,
5921                0.0,
5922                0.0,
5923                false,
5924                false,
5925                false,
5926                0,
5927                63.5,
5928                ptr::null(),
5929                ptr::null(),
5930            )
5931        };
5932        let mut t: *mut RKRDLManagedTensorVersioned = ptr::null_mut();
5933        let status = unsafe { rkr_frame_builder_velocities_dlpack(handle, &mut t) };
5934        assert_eq!(status, RKRStatus::RKR_STATUS_SECTION_ABSENT);
5935        assert!(t.is_null());
5936        unsafe { free_rkr_frame_builder(handle) };
5937    }
5938    #[test]
5939    fn ffi_dlpack_null_handle_rejects() {
5940        let mut t: *mut RKRDLManagedTensorVersioned = ptr::null_mut();
5941        let status = unsafe { rkr_frame_builder_positions_dlpack(ptr::null(), &mut t) };
5942        assert_eq!(status, RKRStatus::RKR_STATUS_NULL_POINTER);
5943        assert!(t.is_null());
5944    }
5945    #[cfg(feature = "chemfiles")]
5946    #[test]
5947    fn rkr_frame_select_finds_oxygen() {
5948        use crate::types::ConFrameBuilder;
5949        let mut b = ConFrameBuilder::new([10.0; 3], [90.0; 3]);
5950        b.add_atom("O", 0.0, 0.0, 0.0, [false; 3], 0, 16.0);
5951        b.add_atom("H", 1.0, 0.0, 0.0, [false; 3], 1, 1.0);
5952        let frame = b.build().unwrap();
5953        let frame_ptr = Box::into_raw(Box::new(frame)) as *mut RKRConFrame;
5954        let sel = CString::new("name O").unwrap();
5955        let mut out: *mut RKRSelectionResult = ptr::null_mut();
5956        let st = unsafe { rkr_frame_select(frame_ptr, sel.as_ptr(), &mut out) };
5957        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
5958        assert!(!out.is_null());
5959        let n = unsafe { rkr_selection_result_match_count(out) };
5960        assert_eq!(n, 1);
5961        let mut atoms = [u64::MAX; 4];
5962        let mut size = 0u32;
5963        let st2 = unsafe { rkr_selection_result_match_at(out, 0, atoms.as_mut_ptr(), &mut size) };
5964        assert_eq!(st2, RKRStatus::RKR_STATUS_SUCCESS);
5965        assert_eq!(size, 1);
5966        assert_eq!(atoms[0], 0);
5967        unsafe {
5968            rkr_selection_result_free(out);
5969            free_rkr_frame(frame_ptr);
5970        }
5971    }
5972    /// C surface: chemfiles selection.cpp chain topology via `rkr_frame_select`.
5973    #[cfg(feature = "chemfiles")]
5974    #[test]
5975    fn rkr_frame_select_cpp_topology_bonds_angles_dihedrals() {
5976        use crate::types::{Bond, ConFrameBuilder};
5977        // H-O-O-H chain (chemfiles testing_frame topology), bonds in atom_data order.
5978        let mut b = ConFrameBuilder::new([10.0; 3], [90.0; 3]);
5979        b.add_atom("H", 0.0, 1.0, 2.0, [false; 3], 0, 1.0);
5980        b.add_atom("O", 1.0, 2.0, 3.0, [false; 3], 1, 16.0);
5981        b.add_atom("O", 2.0, 3.0, 4.0, [false; 3], 2, 16.0);
5982        b.add_atom("H", 3.0, 4.0, 5.0, [false; 3], 3, 1.0);
5983        let mut frame = b.build().unwrap();
5984        let id_to = |id: u64| {
5985            frame
5986                .atom_data
5987                .iter()
5988                .position(|a| a.atom_id == id)
5989                .unwrap() as u32
5990        };
5991        frame.header.set_bonds(&[
5992            Bond::new(id_to(0), id_to(1)),
5993            Bond::new(id_to(1), id_to(2)),
5994            Bond::new(id_to(2), id_to(3)),
5995        ]);
5996        let frame_ptr = Box::into_raw(Box::new(frame)) as *mut RKRConFrame;
5997        let run = |sel: &str| -> (u64, u32) {
5998            let csel = CString::new(sel).unwrap();
5999            let mut out: *mut RKRSelectionResult = ptr::null_mut();
6000            let st = unsafe { rkr_frame_select(frame_ptr, csel.as_ptr(), &mut out) };
6001            assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS, "select failed: {sel}");
6002            let n = unsafe { rkr_selection_result_match_count(out) };
6003            let ctx = unsafe { rkr_selection_result_context_size(out) };
6004            unsafe { rkr_selection_result_free(out) };
6005            (n, ctx)
6006        };
6007        assert_eq!(run("bonds: all"), (3, 2));
6008        assert_eq!(run("angles: all"), (2, 3));
6009        assert_eq!(run("dihedrals: all"), (1, 4));
6010        assert_eq!(run("bonds: name(#1) O and type(#2) H").0, 2);
6011        assert_eq!(
6012            run("two: type(#1) H and name(#2) O and is_bonded(#1, #2)").0,
6013            run("bonds: type(#1) H and name(#2) O").0
6014        );
6015        unsafe { free_rkr_frame(frame_ptr) };
6016    }
6017    #[cfg(feature = "metatensor")]
6018    fn assert_mts_block_shape(block: *mut metatensor::c_api::mts_block_t, n: usize, props: usize) {
6019        assert!(!block.is_null());
6020        let mut array = unsafe { std::mem::zeroed::<metatensor::c_api::mts_array_t>() };
6021        let status = unsafe { metatensor::c_api::mts_block_data(block, &mut array) };
6022        assert_eq!(status, metatensor::c_api::MTS_SUCCESS);
6023        let shape_fn = array
6024            .shape
6025            .expect("mts_array_t.shape from metatensor C API");
6026        let mut shape_ptr: *const usize = std::ptr::null();
6027        let mut shape_count: usize = 0;
6028        let st_shape = unsafe { shape_fn(array.ptr, &mut shape_ptr, &mut shape_count) };
6029        assert_eq!(st_shape, metatensor::c_api::MTS_SUCCESS);
6030        assert_eq!(shape_count, 2);
6031        let shape = unsafe { std::slice::from_raw_parts(shape_ptr, shape_count) };
6032        assert_eq!(shape[0], n);
6033        assert_eq!(shape[1], props);
6034        let samples = unsafe { metatensor::c_api::mts_block_labels(block, 0) };
6035        let prop_lab = unsafe { metatensor::c_api::mts_block_labels(block, 1) };
6036        assert!(!samples.is_null() && !prop_lab.is_null());
6037    }
6038    #[cfg(feature = "metatensor")]
6039    #[test]
6040    fn metatensor_positions_via_c_abi() {
6041        let handle = test_frame_handle();
6042        let mut out: *mut metatensor::c_api::mts_block_t = std::ptr::null_mut();
6043        let st = unsafe { rkr_frame_metatensor_positions_block(handle, &mut out) };
6044        assert_eq!(st, RKRStatus::RKR_STATUS_SUCCESS);
6045        assert_mts_block_shape(out, 1, 3);
6046        unsafe { rkr_mts_block_free(out) };
6047        for (name, export) in [
6048            (
6049                "velocities",
6050                rkr_frame_metatensor_velocities_block
6051                    as unsafe extern "C" fn(
6052                        *const RKRConFrame,
6053                        *mut *mut metatensor::c_api::mts_block_t,
6054                    ) -> RKRStatus,
6055            ),
6056            (
6057                "forces",
6058                rkr_frame_metatensor_forces_block
6059                    as unsafe extern "C" fn(
6060                        *const RKRConFrame,
6061                        *mut *mut metatensor::c_api::mts_block_t,
6062                    ) -> RKRStatus,
6063            ),
6064            (
6065                "atom_energies",
6066                rkr_frame_metatensor_atom_energies_block
6067                    as unsafe extern "C" fn(
6068                        *const RKRConFrame,
6069                        *mut *mut metatensor::c_api::mts_block_t,
6070                    ) -> RKRStatus,
6071            ),
6072        ] {
6073            let mut o: *mut metatensor::c_api::mts_block_t = std::ptr::null_mut();
6074            let st_abs = unsafe { export(handle, &mut o) };
6075            assert_eq!(
6076                st_abs,
6077                RKRStatus::RKR_STATUS_SECTION_ABSENT,
6078                "{name} must be SECTION_ABSENT on minimal test frame"
6079            );
6080            assert!(o.is_null());
6081        }
6082        unsafe { free_rkr_frame(handle) };
6083    }
6084
6085    #[cfg(feature = "metatensor")]
6086    #[test]
6087    fn metatensor_optional_sections_via_c_abi() {
6088        // Frame with velocities, forces, and per-atom energies — all four block exports
6089        let mut builder = ConFrameBuilder::new([10.0, 10.0, 10.0], [90.0, 90.0, 90.0]);
6090        builder.add_atom("H", 0.0, 0.0, 0.0, [false; 3], 1, 1.0);
6091        builder.add_atom("O", 1.0, 0.0, 0.0, [false; 3], 2, 16.0);
6092        builder.set_atom_velocity(0, [0.1, 0.2, 0.3]).unwrap();
6093        builder.set_atom_velocity(1, [0.0, 0.1, 0.0]).unwrap();
6094        builder.set_atom_force(0, [1.0, 0.0, 0.0]).unwrap();
6095        builder.set_atom_force(1, [0.0, 1.0, 0.0]).unwrap();
6096        builder.set_atom_energy(0, -0.5).unwrap();
6097        builder.set_atom_energy(1, -1.0).unwrap();
6098        let frame = builder.build().unwrap();
6099        let handle = Box::into_raw(Box::new(frame)) as *mut RKRConFrame;
6100        let mut pos: *mut metatensor::c_api::mts_block_t = std::ptr::null_mut();
6101        assert_eq!(
6102            unsafe { rkr_frame_metatensor_positions_block(handle, &mut pos) },
6103            RKRStatus::RKR_STATUS_SUCCESS
6104        );
6105        assert_mts_block_shape(pos, 2, 3);
6106        unsafe { rkr_mts_block_free(pos) };
6107        let mut vel: *mut metatensor::c_api::mts_block_t = std::ptr::null_mut();
6108        assert_eq!(
6109            unsafe { rkr_frame_metatensor_velocities_block(handle, &mut vel) },
6110            RKRStatus::RKR_STATUS_SUCCESS
6111        );
6112        assert_mts_block_shape(vel, 2, 3);
6113        unsafe { rkr_mts_block_free(vel) };
6114        let mut frc: *mut metatensor::c_api::mts_block_t = std::ptr::null_mut();
6115        assert_eq!(
6116            unsafe { rkr_frame_metatensor_forces_block(handle, &mut frc) },
6117            RKRStatus::RKR_STATUS_SUCCESS
6118        );
6119        assert_mts_block_shape(frc, 2, 3);
6120        unsafe { rkr_mts_block_free(frc) };
6121        let mut eng: *mut metatensor::c_api::mts_block_t = std::ptr::null_mut();
6122        assert_eq!(
6123            unsafe { rkr_frame_metatensor_atom_energies_block(handle, &mut eng) },
6124            RKRStatus::RKR_STATUS_SUCCESS
6125        );
6126        assert_mts_block_shape(eng, 2, 1);
6127        unsafe { rkr_mts_block_free(eng) };
6128        unsafe { free_rkr_frame(handle) };
6129    }
6130
6131    #[test]
6132    fn string_iterator_yields_frames_from_buffer() {
6133        let text =
6134            std::fs::read_to_string("resources/test/tiny_cuh2.con").expect("fixture tiny_cuh2.con");
6135        let c_text = CString::new(text.as_str()).unwrap();
6136        let it = unsafe { read_con_string_iterator(c_text.as_ptr()) };
6137        assert!(!it.is_null());
6138        let mut n = 0usize;
6139        loop {
6140            let fr = unsafe { con_frame_iterator_next(it) };
6141            if fr.is_null() {
6142                break;
6143            }
6144            n += 1;
6145            unsafe { free_rkr_frame(fr) };
6146        }
6147        unsafe { free_con_frame_iterator(it) };
6148        assert!(n >= 1, "string iterator should yield >=1 frame");
6149
6150        let bytes = text.as_bytes();
6151        let it2 = unsafe { read_con_buffer_iterator(bytes.as_ptr(), bytes.len()) };
6152        assert!(!it2.is_null());
6153        let fr2 = unsafe { con_frame_iterator_next(it2) };
6154        assert!(!fr2.is_null());
6155        unsafe {
6156            free_rkr_frame(fr2);
6157            free_con_frame_iterator(it2);
6158        }
6159    }
6160
6161    #[test]
6162    fn file_iterator_reads_gzip_when_present() {
6163        use flate2::Compression;
6164        use flate2::write::GzEncoder;
6165        use std::io::Write;
6166        let plain = std::fs::read("resources/test/tiny_cuh2.con").expect("fixture");
6167        let dir = tempfile::tempdir().expect("tempdir");
6168        let gz_path = dir.path().join("tiny_cuh2.con.gz");
6169        {
6170            let f = std::fs::File::create(&gz_path).unwrap();
6171            let mut enc = GzEncoder::new(f, Compression::default());
6172            enc.write_all(&plain).unwrap();
6173            enc.finish().unwrap();
6174        }
6175        let c_path = CString::new(gz_path.to_str().unwrap()).unwrap();
6176        let it = unsafe { read_con_file_iterator(c_path.as_ptr()) };
6177        assert!(
6178            !it.is_null(),
6179            "path iterator must decompress .con.gz transparently"
6180        );
6181        let fr = unsafe { con_frame_iterator_next(it) };
6182        assert!(!fr.is_null());
6183        let n = unsafe { rkr_frame_atom_count(fr) };
6184        assert!(n > 0);
6185        unsafe {
6186            free_rkr_frame(fr);
6187            free_con_frame_iterator(it);
6188        }
6189    }
6190}