Skip to main content

readcon_core/
ffi.rs

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