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