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