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::{self, File};
7use std::path::Path;
8use std::ptr;
9
10//=============================================================================
11// Version & Spec Constants (exported as #define by cbindgen)
12//=============================================================================
13
14/// CON/convel format spec version. Use `#if RKR_CON_SPEC_VERSION >= 2` in C/C++
15/// to gate code that depends on atom_index semantics.
16///
17/// Tracks `crate::CON_SPEC_VERSION` (which the Rust API exposes as
18/// `CON_SPEC_VERSION`). Both macros are emitted into the C header for
19/// the convenience of either naming convention; they always carry the
20/// same value.
21pub const RKR_CON_SPEC_VERSION: u32 = 2;
22
23/// Returns the spec version at runtime (for dynamically linked consumers).
24#[unsafe(no_mangle)]
25pub extern "C" fn rkr_con_spec_version() -> u32 {
26    crate::CON_SPEC_VERSION
27}
28
29/// Returns a pointer to a static, null-terminated library version string.
30/// The returned pointer is valid for the lifetime of the process. Do NOT free it.
31#[unsafe(no_mangle)]
32pub extern "C" fn rkr_library_version() -> *const c_char {
33    // concat! produces a &'static str with a trailing NUL byte
34    const VERSION_NUL: &[u8] = concat!(env!("CARGO_PKG_VERSION"), "\0").as_bytes();
35    VERSION_NUL.as_ptr() as *const c_char
36}
37
38/// Returns the position of an atom inside the frame's `atom_data` array
39/// matching the given `atom_id`. Returns `UINT64_MAX` if no atom with
40/// that id exists or `frame_handle` is NULL.
41///
42/// O(N) per call. C/C++ consumers performing many lookups should cache
43/// a `std::unordered_map<uint64_t, size_t>` from a single sweep over
44/// the frame.
45///
46/// # Safety
47///
48/// `frame_handle` must point to a valid `RKRConFrame` allocation.
49#[unsafe(no_mangle)]
50pub unsafe extern "C" fn rkr_frame_atom_index_by_id(
51    frame_handle: *const RKRConFrame,
52    atom_id: u64,
53) -> u64 {
54    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
55        Some(f) => f,
56        None => return u64::MAX,
57    };
58    match frame.atom_index_by_id(atom_id) {
59        Some(idx) => idx as u64,
60        None => u64::MAX,
61    }
62}
63
64/// Returns the atomic number for a chemical symbol, or 0 if the symbol
65/// is unknown or `symbol` is NULL. Lookup covers H..U (Z = 1..=92) and
66/// is case-sensitive: "Fe" works, "fe" does not.
67///
68/// # Safety
69///
70/// `symbol` must be either NULL or a pointer to a NUL-terminated UTF-8
71/// C string valid for reads up to the terminating NUL byte.
72#[unsafe(no_mangle)]
73pub unsafe extern "C" fn rkr_symbol_to_z(symbol: *const c_char) -> u64 {
74    if symbol.is_null() {
75        return 0;
76    }
77    match unsafe { CStr::from_ptr(symbol) }.to_str() {
78        Ok(s) => symbol_to_atomic_number(s),
79        Err(_) => 0,
80    }
81}
82
83/// Returns a pointer to a static, NUL-terminated chemical symbol for an
84/// atomic number, or "X" for unknown values. Coverage is H..U
85/// (Z = 1..=92). The returned pointer is valid for the lifetime of the
86/// process; do NOT free it.
87#[unsafe(no_mangle)]
88pub extern "C" fn rkr_z_to_symbol(z: u64) -> *const c_char {
89    // The static &str returned by helpers::atomic_number_to_symbol is
90    // not NUL-terminated, so the FFI mirrors the table with literals
91    // that have a trailing NUL. Index 0 holds "X" for unknown Z; indices
92    // 1..=92 hold H..U in order.
93    macro_rules! cstrs {
94        ($($lit:literal),* $(,)?) => {
95            [$(concat!($lit, "\0").as_bytes()),*]
96        };
97    }
98    const TABLE: [&[u8]; 93] = cstrs![
99        "X", "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P",
100        "S", "Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn",
101        "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh",
102        "Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd",
103        "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re",
104        "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th",
105        "Pa", "U",
106    ];
107    let idx = if (1..=92).contains(&z) { z as usize } else { 0 };
108    TABLE[idx].as_ptr() as *const c_char
109}
110
111/// Returns the spec version stored in a parsed frame's header.
112/// Returns 0 on error (null handle).
113#[unsafe(no_mangle)]
114pub extern "C" fn rkr_frame_spec_version(frame_handle: *const RKRConFrame) -> u32 {
115    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
116        Some(f) => f.header.spec_version,
117        None => 0,
118    }
119}
120
121/// Returns the JSON metadata line from a parsed frame as a heap-allocated
122/// null-terminated C string. The caller MUST free with `rkr_free_string`.
123/// Returns NULL on error.
124///
125/// # Safety
126/// frame_handle must be valid. The caller takes ownership of the returned string.
127#[unsafe(no_mangle)]
128pub unsafe extern "C" fn rkr_frame_metadata_json(frame_handle: *const RKRConFrame) -> *mut c_char {
129    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
130        Some(f) => f,
131        None => return ptr::null_mut(),
132    };
133    let mut obj = serde_json::Map::new();
134    obj.insert(
135        meta::CON_SPEC_VERSION.into(),
136        serde_json::Value::from(frame.header.spec_version),
137    );
138    for (k, v) in &frame.header.metadata {
139        obj.insert(k.clone(), v.clone());
140    }
141    let json_str = serde_json::Value::Object(obj).to_string();
142    match CString::new(json_str) {
143        Ok(cs) => cs.into_raw(),
144        Err(_) => ptr::null_mut(),
145    }
146}
147
148/// Returns the per-frame energy from metadata, or NaN if absent.
149#[unsafe(no_mangle)]
150pub extern "C" fn rkr_frame_energy(frame_handle: *const RKRConFrame) -> f64 {
151    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
152        Some(f) => f.header.energy().unwrap_or(f64::NAN),
153        None => f64::NAN,
154    }
155}
156
157/// Returns the potential type string from metadata as a heap-allocated
158/// null-terminated C string. The caller MUST free with `rkr_free_string`.
159/// Returns NULL if absent or on error.
160///
161/// # Safety
162/// frame_handle must be valid. The caller takes ownership of the returned string.
163#[unsafe(no_mangle)]
164pub unsafe extern "C" fn rkr_frame_potential_type(frame_handle: *const RKRConFrame) -> *mut c_char {
165    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
166        Some(f) => f,
167        None => return ptr::null_mut(),
168    };
169    match frame.header.potential_type() {
170        Some(pot_type) => match CString::new(pot_type) {
171            Ok(cs) => cs.into_raw(),
172            Err(_) => ptr::null_mut(),
173        },
174        None => ptr::null_mut(),
175    }
176}
177
178/// Returns the zero-based frame index from metadata, or UINT64_MAX if absent.
179#[unsafe(no_mangle)]
180pub extern "C" fn rkr_frame_frame_index(frame_handle: *const RKRConFrame) -> u64 {
181    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
182        Some(f) => f.header.frame_index().unwrap_or(u64::MAX),
183        None => u64::MAX,
184    }
185}
186
187/// Returns the simulation time from metadata, or NaN if absent.
188#[unsafe(no_mangle)]
189pub extern "C" fn rkr_frame_time(frame_handle: *const RKRConFrame) -> f64 {
190    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
191        Some(f) => f.header.time().unwrap_or(f64::NAN),
192        None => f64::NAN,
193    }
194}
195
196/// Returns the integration timestep from metadata, or NaN if absent.
197#[unsafe(no_mangle)]
198pub extern "C" fn rkr_frame_timestep(frame_handle: *const RKRConFrame) -> f64 {
199    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
200        Some(f) => f.header.timestep().unwrap_or(f64::NAN),
201        None => f64::NAN,
202    }
203}
204
205/// Returns the NEB bead index from metadata, or UINT64_MAX if absent.
206#[unsafe(no_mangle)]
207pub extern "C" fn rkr_frame_neb_bead(frame_handle: *const RKRConFrame) -> u64 {
208    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
209        Some(f) => f.header.neb_bead().unwrap_or(u64::MAX),
210        None => u64::MAX,
211    }
212}
213
214/// Returns the NEB band index from metadata, or UINT64_MAX if absent.
215#[unsafe(no_mangle)]
216pub extern "C" fn rkr_frame_neb_band(frame_handle: *const RKRConFrame) -> u64 {
217    match unsafe { (frame_handle as *const ConFrame).as_ref() } {
218        Some(f) => f.header.neb_band().unwrap_or(u64::MAX),
219        None => u64::MAX,
220    }
221}
222
223//=============================================================================
224// C-Compatible Structs & Handles
225//=============================================================================
226
227/// Error codes for RKR functions.
228#[repr(C)]
229#[allow(non_camel_case_types)]
230#[derive(Debug, PartialEq, Eq)]
231pub enum RKRStatus {
232    /// Function completed successfully.
233    RKR_STATUS_SUCCESS = 0,
234    /// A null pointer was passed for a required argument.
235    RKR_STATUS_NULL_POINTER = -1,
236    /// An input string was not valid UTF-8.
237    RKR_STATUS_INVALID_UTF8 = -2,
238    /// JSON parsing or serialization failed.
239    RKR_STATUS_INVALID_JSON = -3,
240    /// File I/O error.
241    RKR_STATUS_IO_ERROR = -4,
242    /// Index out of bounds.
243    RKR_STATUS_INDEX_OUT_OF_BOUNDS = -5,
244    /// The destination buffer cannot hold a null-terminated string.
245    RKR_STATUS_BUFFER_TOO_SMALL = -6,
246    /// An internal logic error or unhandled state.
247    RKR_STATUS_INTERNAL_ERROR = -7,
248}
249
250/// Returns a stable, static message for a status code.
251/// The returned pointer is valid for the lifetime of the process. Do NOT free it.
252#[unsafe(no_mangle)]
253pub extern "C" fn rkr_status_message(status: RKRStatus) -> *const c_char {
254    match status {
255        RKRStatus::RKR_STATUS_SUCCESS => c"success".as_ptr(),
256        RKRStatus::RKR_STATUS_NULL_POINTER => c"null pointer".as_ptr(),
257        RKRStatus::RKR_STATUS_INVALID_UTF8 => c"invalid UTF-8".as_ptr(),
258        RKRStatus::RKR_STATUS_INVALID_JSON => c"invalid JSON".as_ptr(),
259        RKRStatus::RKR_STATUS_IO_ERROR => c"I/O error".as_ptr(),
260        RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS => c"index out of bounds".as_ptr(),
261        RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL => c"buffer too small".as_ptr(),
262        RKRStatus::RKR_STATUS_INTERNAL_ERROR => c"internal error".as_ptr(),
263    }
264}
265
266/// An opaque handle to a full, lossless Rust `ConFrame` object.
267/// The C/C++ side needs to treat this as a void pointer
268#[repr(C)]
269pub struct RKRConFrame {
270    _private: [u8; 0],
271}
272
273/// An opaque handle to a Rust `ConFrameWriter` object.
274/// The C/C++ side needs to treat this as a void pointer
275#[repr(C)]
276pub struct RKRConFrameWriter {
277    _private: [u8; 0],
278}
279
280/// A transparent, "lossy" C-struct containing only the core atomic data.
281/// This can be extracted from an `RKRConFrame` handle for direct data access.
282/// The caller is responsible for freeing the `atoms` array using `free_c_frame`.
283#[repr(C)]
284pub struct CFrame {
285    pub atoms: *mut CAtom,
286    pub num_atoms: usize,
287    pub cell: [f64; 3],
288    pub angles: [f64; 3],
289    pub has_velocities: bool,
290    pub has_forces: bool,
291    pub has_energies: bool,
292}
293
294/// Transparent atom record extracted via [`rkr_frame_to_c_frame`].
295///
296/// `is_fixed` is the OR of `fixed_x`, `fixed_y`, `fixed_z`; it is kept
297/// for source compatibility with pre-spec-v2 callers that did not have
298/// per-axis flags. New code should use the per-axis fields.
299///
300/// `vx`/`vy`/`vz`, `fx`/`fy`/`fz`, and `energy` carry meaningful values
301/// only when `has_velocity`, `has_forces`, or `has_energy` is true
302/// respectively; the values are zeroed otherwise.
303#[repr(C)]
304pub struct CAtom {
305    pub atomic_number: u64,
306    pub x: f64,
307    pub y: f64,
308    pub z: f64,
309    pub atom_id: u64,
310    pub mass: f64,
311    /// True when any of `fixed_x`, `fixed_y`, `fixed_z` is true.
312    /// Kept for source compatibility; prefer the per-axis fields.
313    pub is_fixed: bool,
314    pub fixed_x: bool,
315    pub fixed_y: bool,
316    pub fixed_z: bool,
317    pub vx: f64,
318    pub vy: f64,
319    pub vz: f64,
320    pub has_velocity: bool,
321    pub fx: f64,
322    pub fy: f64,
323    pub fz: f64,
324    pub has_forces: bool,
325    /// Per-atom energy contribution; meaningful only when
326    /// `has_energy` is true. See [`crate::types::SECTION_ENERGIES`].
327    pub energy: f64,
328    pub has_energy: bool,
329}
330
331#[repr(C)]
332pub struct CConFrameIterator {
333    iterator: *mut ConFrameIterator<'static>,
334    file_contents: *mut String,
335}
336
337//=============================================================================
338// Iterator and Memory Management
339//=============================================================================
340
341/// Creates a new iterator for a .con or .convel file.
342///
343/// Returns NULL if the file cannot be read (missing, unreadable, or
344/// not valid UTF-8). A successfully-opened file with zero frames
345/// returns a non-NULL iterator that yields NULL on the first call to
346/// [`con_frame_iterator_next`]. The caller OWNS the returned pointer
347/// and MUST call [`free_con_frame_iterator`].
348///
349/// # Safety
350/// filename_c must be a valid null-terminated string. The caller takes
351/// ownership of the returned iterator.
352#[unsafe(no_mangle)]
353pub unsafe extern "C" fn read_con_file_iterator(
354    filename_c: *const c_char,
355) -> *mut CConFrameIterator {
356    if filename_c.is_null() {
357        return ptr::null_mut();
358    }
359    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
360        Ok(s) => s,
361        Err(_) => return ptr::null_mut(),
362    };
363    let file_contents_box = match fs::read_to_string(filename) {
364        Ok(contents) => Box::new(contents),
365        Err(_) => return ptr::null_mut(),
366    };
367    let file_contents_ptr = Box::into_raw(file_contents_box);
368    let static_file_contents: &'static str = unsafe { &*file_contents_ptr };
369    let iterator = Box::new(ConFrameIterator::new(static_file_contents));
370    let c_iterator = Box::new(CConFrameIterator {
371        iterator: Box::into_raw(iterator),
372        file_contents: file_contents_ptr,
373    });
374    Box::into_raw(c_iterator)
375}
376
377/// Reads the next frame from the iterator, returning an opaque handle.
378/// The caller OWNS the returned handle and must free it with `free_rkr_frame`.
379///
380/// # Safety
381/// iterator must be valid. The caller takes ownership of the returned frame.
382#[unsafe(no_mangle)]
383pub unsafe extern "C" fn con_frame_iterator_next(
384    iterator: *mut CConFrameIterator,
385) -> *mut RKRConFrame {
386    if iterator.is_null() {
387        return ptr::null_mut();
388    }
389    let iter = unsafe { &mut *(*iterator).iterator };
390    match iter.next() {
391        Some(Ok(frame)) => Box::into_raw(Box::new(frame)) as *mut RKRConFrame,
392        _ => ptr::null_mut(),
393    }
394}
395
396/// Frees the memory for an opaque `RKRConFrame` handle.
397///
398/// # Safety
399/// frame_handle must be valid or null.
400#[unsafe(no_mangle)]
401pub unsafe extern "C" fn free_rkr_frame(frame_handle: *mut RKRConFrame) {
402    if !frame_handle.is_null() {
403        let _ = unsafe { Box::from_raw(frame_handle as *mut ConFrame) };
404    }
405}
406
407/// Frees the memory for a `CConFrameIterator`.
408///
409/// # Safety
410/// iterator must be valid or null.
411#[unsafe(no_mangle)]
412pub unsafe extern "C" fn free_con_frame_iterator(iterator: *mut CConFrameIterator) {
413    if iterator.is_null() {
414        return;
415    }
416    unsafe {
417        let c_iterator_box = Box::from_raw(iterator);
418        let _ = Box::from_raw(c_iterator_box.iterator);
419        let _ = Box::from_raw(c_iterator_box.file_contents);
420    }
421}
422
423//=============================================================================
424// Data Accessors (The "Getter" API)
425//=============================================================================
426
427/// Extracts the core atomic data into a transparent `CFrame` struct.
428/// The caller OWNS the returned pointer and MUST call `free_c_frame` on it.
429///
430/// # Safety
431/// frame_handle must be valid. The caller takes ownership of the returned CFrame.
432#[unsafe(no_mangle)]
433pub unsafe extern "C" fn rkr_frame_to_c_frame(frame_handle: *const RKRConFrame) -> *mut CFrame {
434    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
435        Some(f) => f,
436        None => return ptr::null_mut(),
437    };
438
439    let masses_iter = frame
440        .header
441        .natms_per_type
442        .iter()
443        .zip(frame.header.masses_per_type.iter())
444        .flat_map(|(num_atoms, mass)| std::iter::repeat_n(*mass, *num_atoms));
445
446    let has_velocities = frame.has_velocities();
447
448    let mut c_atoms: Vec<CAtom> = frame
449        .atom_data
450        .iter()
451        .zip(masses_iter)
452        .map(|(atom_datum, mass)| {
453            let [vx, vy, vz] = atom_datum.velocity.unwrap_or([0.0; 3]);
454            let [fx, fy, fz] = atom_datum.force.unwrap_or([0.0; 3]);
455            CAtom {
456                atomic_number: symbol_to_atomic_number(&atom_datum.symbol),
457                x: atom_datum.x,
458                y: atom_datum.y,
459                z: atom_datum.z,
460                is_fixed: atom_datum.is_fixed(),
461                fixed_x: atom_datum.fixed[0],
462                fixed_y: atom_datum.fixed[1],
463                fixed_z: atom_datum.fixed[2],
464                atom_id: atom_datum.atom_id,
465                mass,
466                vx,
467                vy,
468                vz,
469                has_velocity: atom_datum.has_velocity(),
470                fx,
471                fy,
472                fz,
473                has_forces: atom_datum.has_forces(),
474                energy: atom_datum.energy.unwrap_or(0.0),
475                has_energy: atom_datum.has_energy(),
476            }
477        })
478        .collect();
479
480    let atoms_ptr = c_atoms.as_mut_ptr();
481    let num_atoms = c_atoms.len();
482    std::mem::forget(c_atoms);
483
484    let has_forces = frame.has_forces();
485    let has_energies = frame.has_energies();
486
487    let c_frame = Box::new(CFrame {
488        atoms: atoms_ptr,
489        num_atoms,
490        cell: frame.header.boxl,
491        angles: frame.header.angles,
492        has_velocities,
493        has_forces,
494        has_energies,
495    });
496
497    Box::into_raw(c_frame)
498}
499
500/// Frees the memory of a `CFrame` struct, including its internal atoms array.
501///
502/// # Safety
503/// frame must be valid or null.
504#[unsafe(no_mangle)]
505pub unsafe extern "C" fn free_c_frame(frame: *mut CFrame) {
506    if frame.is_null() {
507        return;
508    }
509    unsafe {
510        let frame_box = Box::from_raw(frame);
511        let _ = Vec::from_raw_parts(frame_box.atoms, frame_box.num_atoms, frame_box.num_atoms);
512    }
513}
514
515/// Copies a header string line into a caller-provided buffer.
516///
517/// `is_prebox=true` selects from the two prebox lines (line 0 = user
518/// text, line 1 = JSON metadata); `false` selects from the two postbox
519/// lines. Strings longer than `buffer_len - 1` bytes are truncated; the
520/// final byte is always set to NUL.
521///
522/// Returns `RKR_STATUS_SUCCESS` on success,
523/// `RKR_STATUS_INDEX_OUT_OF_BOUNDS` if `line_index >= 2`,
524/// `RKR_STATUS_NULL_POINTER` if `frame_handle` or `buffer` is NULL,
525/// `RKR_STATUS_BUFFER_TOO_SMALL` if `buffer_len == 0`.
526///
527/// Pair with [`rkr_frame_get_header_line_cpp`] when the caller prefers
528/// an allocated string with no fixed length cap; that variant returns
529/// NULL for the same out-of-bounds condition.
530///
531/// # Safety
532/// frame_handle must be valid. buffer must be at least buffer_len bytes.
533#[unsafe(no_mangle)]
534pub unsafe extern "C" fn rkr_frame_get_header_line(
535    frame_handle: *const RKRConFrame,
536    is_prebox: bool,
537    line_index: usize,
538    buffer: *mut c_char,
539    buffer_len: usize,
540) -> RKRStatus {
541    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
542        Some(f) => f,
543        None => return RKRStatus::RKR_STATUS_NULL_POINTER,
544    };
545    if buffer.is_null() {
546        return RKRStatus::RKR_STATUS_NULL_POINTER;
547    }
548    if buffer_len == 0 {
549        return RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL;
550    }
551    let line_to_copy: Option<&str> = if is_prebox {
552        match line_index {
553            0 => Some(frame.header.prebox_header.user.as_str()),
554            1 => Some(frame.header.prebox_header.metadata_line()),
555            _ => None,
556        }
557    } else {
558        frame.header.postbox_header.get(line_index).map(String::as_str)
559    };
560    if let Some(line) = line_to_copy {
561        let bytes = line.as_bytes();
562        let len_to_copy = std::cmp::min(bytes.len(), buffer_len - 1);
563        unsafe {
564            ptr::copy_nonoverlapping(bytes.as_ptr(), buffer as *mut u8, len_to_copy);
565            *buffer.add(len_to_copy) = 0;
566        }
567        RKRStatus::RKR_STATUS_SUCCESS
568    } else {
569        RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS
570    }
571}
572
573/// Gets a header string line as a newly allocated, null-terminated C string.
574///
575/// The caller OWNS the returned pointer and MUST call `rkr_free_string`
576/// on it to prevent a memory leak. Returns NULL on error or if the
577/// index is invalid (use [`rkr_frame_get_header_line`] when a status
578/// code is preferred to NULL-vs-success disambiguation).
579///
580/// The `_cpp` suffix is historical; the function is callable from both
581/// C and C++.
582///
583/// # Safety
584/// frame_handle must be valid. The caller takes ownership of the returned string.
585#[unsafe(no_mangle)]
586pub unsafe extern "C" fn rkr_frame_get_header_line_cpp(
587    frame_handle: *const RKRConFrame,
588    is_prebox: bool,
589    line_index: usize,
590) -> *mut c_char {
591    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
592        Some(f) => f,
593        None => return ptr::null_mut(),
594    };
595
596    let line_to_copy: Option<&str> = if is_prebox {
597        match line_index {
598            0 => Some(frame.header.prebox_header.user.as_str()),
599            1 => Some(frame.header.prebox_header.metadata_line()),
600            _ => None,
601        }
602    } else {
603        frame.header.postbox_header.get(line_index).map(String::as_str)
604    };
605
606    if let Some(line) = line_to_copy {
607        // Convert the Rust string slice to a C-compatible, heap-allocated string.
608        match CString::new(line) {
609            Ok(c_string) => c_string.into_raw(), // Give ownership to the C caller
610            Err(_) => ptr::null_mut(),           // In case the string contains a null byte
611        }
612    } else {
613        ptr::null_mut() // Index out of bounds
614    }
615}
616
617/// Frees a C string that was allocated by Rust (e.g., from
618/// `rkr_frame_metadata_json`, `rkr_frame_potential_type`, or
619/// `rkr_frame_get_header_line_cpp`). Safe to call with NULL (no-op).
620///
621/// # Safety
622/// s must be either NULL or a pointer previously returned by an
623/// allocating Rust FFI function in this crate.
624#[unsafe(no_mangle)]
625pub unsafe extern "C" fn rkr_free_string(s: *mut c_char) {
626    if !s.is_null() {
627        // Retake ownership of the CString to deallocate it properly.
628        let _ = unsafe { CString::from_raw(s) };
629    }
630}
631
632//=============================================================================
633// FFI Writer Functions (Writer Object Model)
634//=============================================================================
635
636/// Creates a new frame writer for the specified file.
637/// The caller OWNS the returned pointer and MUST call `free_rkr_writer`.
638///
639/// # Safety
640/// filename_c must be valid. The caller takes ownership of the returned writer.
641#[unsafe(no_mangle)]
642pub unsafe extern "C" fn create_writer_from_path_c(
643    filename_c: *const c_char,
644) -> *mut RKRConFrameWriter {
645    if filename_c.is_null() {
646        return ptr::null_mut();
647    }
648    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
649        Ok(s) => s,
650        Err(_) => return ptr::null_mut(),
651    };
652    match crate::writer::ConFrameWriter::from_path(filename) {
653        Ok(writer) => Box::into_raw(Box::new(writer)) as *mut RKRConFrameWriter,
654        Err(_) => ptr::null_mut(),
655    }
656}
657
658/// Frees the memory for an `RKRConFrameWriter`, closing the associated file.
659///
660/// # Safety
661/// writer_handle must be valid or null.
662#[unsafe(no_mangle)]
663pub unsafe extern "C" fn free_rkr_writer(writer_handle: *mut RKRConFrameWriter) {
664    if !writer_handle.is_null() {
665        let _ = unsafe { Box::from_raw(writer_handle as *mut ConFrameWriter<File>) };
666    }
667}
668
669/// Writes multiple frames from an array of handles to the file managed by the writer.
670/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
671///
672/// # Safety
673/// writer_handle and frame_handles must be valid.
674#[unsafe(no_mangle)]
675pub unsafe extern "C" fn rkr_writer_extend(
676    writer_handle: *mut RKRConFrameWriter,
677    frame_handles: *const *const RKRConFrame,
678    num_frames: usize,
679) -> RKRStatus {
680    let writer = match unsafe { (writer_handle as *mut ConFrameWriter<File>).as_mut() } {
681        Some(w) => w,
682        None => return RKRStatus::RKR_STATUS_NULL_POINTER,
683    };
684    if frame_handles.is_null() {
685        return RKRStatus::RKR_STATUS_NULL_POINTER;
686    }
687
688    let handles_slice = unsafe { std::slice::from_raw_parts(frame_handles, num_frames) };
689    let mut rust_frames: Vec<&ConFrame> = Vec::with_capacity(num_frames);
690    if handles_slice.iter().any(|&handle| handle.is_null()) {
691        // Fail fast if any handle is null, as this indicates a bug on the
692        // caller's side.
693        return RKRStatus::RKR_STATUS_NULL_POINTER;
694    }
695    for &handle in handles_slice.iter() {
696        // Assume the handle is valid.
697        match unsafe { (handle as *const ConFrame).as_ref() } {
698            Some(frame) => rust_frames.push(frame),
699            // This case should be unreachable if the handle is not null, but we handle it for safety.
700            None => return RKRStatus::RKR_STATUS_NULL_POINTER,
701        }
702    }
703
704    match writer.extend(rust_frames.into_iter()) {
705        Ok(_) => RKRStatus::RKR_STATUS_SUCCESS,
706        Err(_) => RKRStatus::RKR_STATUS_IO_ERROR,
707    }
708}
709
710//=============================================================================
711// Writer with Precision
712//=============================================================================
713
714/// Creates a new frame writer with custom floating-point precision.
715/// The caller OWNS the returned pointer and MUST call `free_rkr_writer`.
716///
717/// # Safety
718/// filename_c must be valid. The caller takes ownership of the returned writer.
719#[unsafe(no_mangle)]
720pub unsafe extern "C" fn create_writer_from_path_with_precision_c(
721    filename_c: *const c_char,
722    precision: u8,
723) -> *mut RKRConFrameWriter {
724    if filename_c.is_null() {
725        return ptr::null_mut();
726    }
727    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
728        Ok(s) => s,
729        Err(_) => return ptr::null_mut(),
730    };
731    match ConFrameWriter::from_path_with_precision(filename, precision as usize) {
732        Ok(writer) => Box::into_raw(Box::new(writer)) as *mut RKRConFrameWriter,
733        Err(_) => ptr::null_mut(),
734    }
735}
736
737//=============================================================================
738// Frame Builder FFI (construct ConFrame from C data)
739//=============================================================================
740
741/// An opaque handle to a Rust `ConFrameBuilder` object.
742#[repr(C)]
743pub struct RKRConFrameBuilder {
744    _private: [u8; 0],
745}
746
747#[allow(clippy::too_many_arguments)]
748unsafe fn add_builder_atom(
749    builder_handle: *mut RKRConFrameBuilder,
750    symbol: *const c_char,
751    x: f64,
752    y: f64,
753    z: f64,
754    fixed: [bool; 3],
755    atom_id: u64,
756    mass: f64,
757    velocity: Option<[f64; 3]>,
758    forces: Option<[f64; 3]>,
759) -> RKRStatus {
760    if builder_handle.is_null() || symbol.is_null() {
761        return RKRStatus::RKR_STATUS_NULL_POINTER;
762    }
763    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
764    let sym = match unsafe { CStr::from_ptr(symbol).to_str() } {
765        Ok(s) => s,
766        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
767    };
768
769    builder.add_atom(sym, x, y, z, fixed, atom_id, mass);
770    if let Some(v) = velocity {
771        builder.with_velocity(v);
772    }
773    if let Some(f) = forces {
774        builder.with_force(f);
775    }
776
777    RKRStatus::RKR_STATUS_SUCCESS
778}
779
780/// Attaches a velocity vector to the most recently added atom on a builder.
781/// No-op if no atom has been added yet.
782///
783/// # Safety
784/// builder_handle must be valid. velocity must point to 3 contiguous f64 values.
785#[unsafe(no_mangle)]
786pub unsafe extern "C" fn rkr_frame_builder_set_last_velocity(
787    builder_handle: *mut RKRConFrameBuilder,
788    velocity: *const f64,
789) -> RKRStatus {
790    if builder_handle.is_null() || velocity.is_null() {
791        return RKRStatus::RKR_STATUS_NULL_POINTER;
792    }
793    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
794    let v = unsafe { [*velocity, *velocity.add(1), *velocity.add(2)] };
795    builder.with_velocity(v);
796    RKRStatus::RKR_STATUS_SUCCESS
797}
798
799/// Attaches a force vector to the most recently added atom on a builder.
800/// No-op if no atom has been added yet.
801///
802/// # Safety
803/// builder_handle must be valid. force must point to 3 contiguous f64 values.
804#[unsafe(no_mangle)]
805pub unsafe extern "C" fn rkr_frame_builder_set_last_force(
806    builder_handle: *mut RKRConFrameBuilder,
807    force: *const f64,
808) -> RKRStatus {
809    if builder_handle.is_null() || force.is_null() {
810        return RKRStatus::RKR_STATUS_NULL_POINTER;
811    }
812    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
813    let f = unsafe { [*force, *force.add(1), *force.add(2)] };
814    builder.with_force(f);
815    RKRStatus::RKR_STATUS_SUCCESS
816}
817
818/// Attaches a per-atom energy to the most recently added atom on a
819/// builder. No-op if no atom has been added yet.
820///
821/// Use this together with the per-frame `energy` metadata key when a
822/// caller wants to round-trip an "Energies of Component" decomposition
823/// alongside the total.
824///
825/// # Safety
826/// builder_handle must be valid.
827#[unsafe(no_mangle)]
828pub unsafe extern "C" fn rkr_frame_builder_set_last_energy(
829    builder_handle: *mut RKRConFrameBuilder,
830    energy: f64,
831) -> RKRStatus {
832    if builder_handle.is_null() {
833        return RKRStatus::RKR_STATUS_NULL_POINTER;
834    }
835    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
836    builder.with_energy(energy);
837    RKRStatus::RKR_STATUS_SUCCESS
838}
839
840/// Adds an atom with optional per-axis fixed mask, velocity, and force vectors.
841///
842/// `velocity` and `force` are pointers to 3 contiguous f64 values, or NULL if
843/// absent. This is the unified entry point that replaces the eight
844/// `rkr_frame_add_atom_*` convenience functions; callers may continue using
845/// those for source compatibility.
846///
847/// # Safety
848/// builder_handle and symbol must be valid. velocity (if non-null) must point
849/// to 3 contiguous f64 values, and force (if non-null) likewise.
850#[unsafe(no_mangle)]
851#[allow(clippy::too_many_arguments)]
852pub unsafe extern "C" fn rkr_frame_add_atom_full(
853    builder_handle: *mut RKRConFrameBuilder,
854    symbol: *const c_char,
855    x: f64,
856    y: f64,
857    z: f64,
858    fixed_x: bool,
859    fixed_y: bool,
860    fixed_z: bool,
861    atom_id: u64,
862    mass: f64,
863    velocity: *const f64,
864    force: *const f64,
865) -> RKRStatus {
866    let velocity = if velocity.is_null() {
867        None
868    } else {
869        Some(unsafe { [*velocity, *velocity.add(1), *velocity.add(2)] })
870    };
871    let force = if force.is_null() {
872        None
873    } else {
874        Some(unsafe { [*force, *force.add(1), *force.add(2)] })
875    };
876    unsafe {
877        add_builder_atom(
878            builder_handle,
879            symbol,
880            x,
881            y,
882            z,
883            [fixed_x, fixed_y, fixed_z],
884            atom_id,
885            mass,
886            velocity,
887            force,
888        )
889    }
890}
891
892/// Creates a new frame builder with the given cell dimensions, angles,
893/// and header lines.
894///
895/// `prebox1` is accepted for source compatibility but ignored: the
896/// JSON metadata line is regenerated by the writer from the builder's
897/// `spec_version`, `metadata`, and `sections`. Pass NULL or any string.
898/// The caller OWNS the returned pointer and MUST call
899/// `free_rkr_frame_builder` or consume it via `rkr_frame_builder_build`.
900/// Returns NULL on error.
901///
902/// # Safety
903/// cell and angles must point to 3 doubles. prebox0, postbox0, and
904/// postbox1 must be NULL or valid null-terminated strings; prebox1 is
905/// not dereferenced. The caller takes ownership of the returned
906/// builder.
907#[unsafe(no_mangle)]
908pub unsafe extern "C" fn rkr_frame_new(
909    cell: *const f64,
910    angles: *const f64,
911    prebox0: *const c_char,
912    prebox1: *const c_char,
913    postbox0: *const c_char,
914    postbox1: *const c_char,
915) -> *mut RKRConFrameBuilder {
916    if cell.is_null() || angles.is_null() {
917        return ptr::null_mut();
918    }
919    let cell_arr = unsafe { [*cell, *cell.add(1), *cell.add(2)] };
920    let angles_arr = unsafe { [*angles, *angles.add(1), *angles.add(2)] };
921
922    let get_str = |p: *const c_char| -> String {
923        if p.is_null() {
924            String::new()
925        } else {
926            unsafe { CStr::from_ptr(p) }
927                .to_str()
928                .unwrap_or("")
929                .to_string()
930        }
931    };
932
933    // prebox1 is the JSON metadata slot, regenerated on write from
934    // metadata + sections; it is accepted for ABI continuity but ignored.
935    let _ = get_str(prebox1);
936    let mut builder = ConFrameBuilder::new(cell_arr, angles_arr);
937    builder
938        .prebox_header(get_str(prebox0))
939        .postbox_header([get_str(postbox0), get_str(postbox1)]);
940
941    Box::into_raw(Box::new(builder)) as *mut RKRConFrameBuilder
942}
943
944/// Parses and sets JSON metadata on an existing frame builder.
945/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
946///
947/// # Safety
948/// builder_handle and metadata_json must be valid.
949#[unsafe(no_mangle)]
950pub unsafe extern "C" fn rkr_frame_builder_set_metadata_json(
951    builder_handle: *mut RKRConFrameBuilder,
952    metadata_json: *const c_char,
953) -> RKRStatus {
954    if builder_handle.is_null() || metadata_json.is_null() {
955        return RKRStatus::RKR_STATUS_NULL_POINTER;
956    }
957    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
958    let metadata_json = match unsafe { CStr::from_ptr(metadata_json).to_str() } {
959        Ok(s) => s,
960        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
961    };
962    match builder.set_metadata_json(metadata_json) {
963        Ok(()) => RKRStatus::RKR_STATUS_SUCCESS,
964        Err(_) => RKRStatus::RKR_STATUS_INVALID_JSON,
965    }
966}
967
968/// Sets a numeric metadata key on an existing frame builder.
969/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
970///
971/// # Safety
972/// builder_handle and key must be valid.
973#[unsafe(no_mangle)]
974pub unsafe extern "C" fn rkr_frame_builder_set_scalar_metadata(
975    builder_handle: *mut RKRConFrameBuilder,
976    key: *const c_char,
977    value: f64,
978) -> RKRStatus {
979    if builder_handle.is_null() || key.is_null() {
980        return RKRStatus::RKR_STATUS_NULL_POINTER;
981    }
982    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
983    let key = match unsafe { CStr::from_ptr(key).to_str() } {
984        Ok(s) => s,
985        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
986    };
987    builder.set_scalar_metadata(key, value);
988    RKRStatus::RKR_STATUS_SUCCESS
989}
990
991/// Sets a string metadata key on an existing frame builder.
992/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
993///
994/// # Safety
995/// builder_handle, key, and value must be valid.
996#[unsafe(no_mangle)]
997pub unsafe extern "C" fn rkr_frame_builder_set_string_metadata(
998    builder_handle: *mut RKRConFrameBuilder,
999    key: *const c_char,
1000    value: *const c_char,
1001) -> RKRStatus {
1002    if builder_handle.is_null() || key.is_null() || value.is_null() {
1003        return RKRStatus::RKR_STATUS_NULL_POINTER;
1004    }
1005    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1006    let key = match unsafe { CStr::from_ptr(key).to_str() } {
1007        Ok(s) => s,
1008        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
1009    };
1010    let value = match unsafe { CStr::from_ptr(value).to_str() } {
1011        Ok(s) => s,
1012        Err(_) => return RKRStatus::RKR_STATUS_INVALID_UTF8,
1013    };
1014    builder.set_string_metadata(key, value);
1015    RKRStatus::RKR_STATUS_SUCCESS
1016}
1017
1018/// Sets the per-frame total energy metadata on an existing frame builder.
1019/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1020///
1021/// # Safety
1022/// builder_handle must be valid.
1023#[unsafe(no_mangle)]
1024pub unsafe extern "C" fn rkr_frame_builder_set_energy(
1025    builder_handle: *mut RKRConFrameBuilder,
1026    energy: f64,
1027) -> RKRStatus {
1028    if builder_handle.is_null() {
1029        return RKRStatus::RKR_STATUS_NULL_POINTER;
1030    }
1031    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1032    builder.set_energy(energy);
1033    RKRStatus::RKR_STATUS_SUCCESS
1034}
1035
1036/// Sets the zero-based frame index metadata on an existing frame builder.
1037/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1038///
1039/// # Safety
1040/// builder_handle must be valid.
1041#[unsafe(no_mangle)]
1042pub unsafe extern "C" fn rkr_frame_builder_set_frame_index(
1043    builder_handle: *mut RKRConFrameBuilder,
1044    idx: u64,
1045) -> RKRStatus {
1046    if builder_handle.is_null() {
1047        return RKRStatus::RKR_STATUS_NULL_POINTER;
1048    }
1049    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1050    builder.set_frame_index(idx);
1051    RKRStatus::RKR_STATUS_SUCCESS
1052}
1053
1054/// Sets the simulation time metadata on an existing frame builder.
1055/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1056///
1057/// # Safety
1058/// builder_handle must be valid.
1059#[unsafe(no_mangle)]
1060pub unsafe extern "C" fn rkr_frame_builder_set_time(
1061    builder_handle: *mut RKRConFrameBuilder,
1062    time: f64,
1063) -> RKRStatus {
1064    if builder_handle.is_null() {
1065        return RKRStatus::RKR_STATUS_NULL_POINTER;
1066    }
1067    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1068    builder.set_time(time);
1069    RKRStatus::RKR_STATUS_SUCCESS
1070}
1071
1072/// Sets the timestep metadata on an existing frame builder.
1073/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1074///
1075/// # Safety
1076/// builder_handle must be valid.
1077#[unsafe(no_mangle)]
1078pub unsafe extern "C" fn rkr_frame_builder_set_timestep(
1079    builder_handle: *mut RKRConFrameBuilder,
1080    dt: f64,
1081) -> RKRStatus {
1082    if builder_handle.is_null() {
1083        return RKRStatus::RKR_STATUS_NULL_POINTER;
1084    }
1085    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1086    builder.set_timestep(dt);
1087    RKRStatus::RKR_STATUS_SUCCESS
1088}
1089
1090/// Sets the NEB bead index metadata on an existing frame builder.
1091/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1092///
1093/// # Safety
1094/// builder_handle must be valid.
1095#[unsafe(no_mangle)]
1096pub unsafe extern "C" fn rkr_frame_builder_set_neb_bead(
1097    builder_handle: *mut RKRConFrameBuilder,
1098    bead: u64,
1099) -> RKRStatus {
1100    if builder_handle.is_null() {
1101        return RKRStatus::RKR_STATUS_NULL_POINTER;
1102    }
1103    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1104    builder.set_neb_bead(bead);
1105    RKRStatus::RKR_STATUS_SUCCESS
1106}
1107
1108/// Sets the NEB band index metadata on an existing frame builder.
1109/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1110///
1111/// # Safety
1112/// builder_handle must be valid.
1113#[unsafe(no_mangle)]
1114pub unsafe extern "C" fn rkr_frame_builder_set_neb_band(
1115    builder_handle: *mut RKRConFrameBuilder,
1116    band: u64,
1117) -> RKRStatus {
1118    if builder_handle.is_null() {
1119        return RKRStatus::RKR_STATUS_NULL_POINTER;
1120    }
1121    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
1122    builder.set_neb_band(band);
1123    RKRStatus::RKR_STATUS_SUCCESS
1124}
1125
1126// -----------------------------------------------------------------------------
1127// Legacy add_atom variants (kept for source compatibility)
1128//
1129// The unified entry point is `rkr_frame_add_atom_full`, which accepts
1130// optional velocity and force pointers. The eight functions below
1131// pre-date the unified call and remain in the API for code that was
1132// written against earlier 0.x releases. New callers should prefer
1133// `rkr_frame_add_atom_full`.
1134// -----------------------------------------------------------------------------
1135
1136/// **Deprecated**: prefer `rkr_frame_add_atom_full` with NULL velocity
1137/// and force pointers. Adds an atom (no velocity, no forces) to the
1138/// builder using a single uniform fixed flag.
1139/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1140///
1141/// # Safety
1142/// builder_handle and symbol must be valid.
1143#[unsafe(no_mangle)]
1144pub unsafe extern "C" fn rkr_frame_add_atom(
1145    builder_handle: *mut RKRConFrameBuilder,
1146    symbol: *const c_char,
1147    x: f64,
1148    y: f64,
1149    z: f64,
1150    is_fixed: bool,
1151    atom_id: u64,
1152    mass: f64,
1153) -> RKRStatus {
1154    unsafe {
1155        add_builder_atom(
1156            builder_handle,
1157            symbol,
1158            x,
1159            y,
1160            z,
1161            [is_fixed; 3],
1162            atom_id,
1163            mass,
1164            None,
1165            None,
1166        )
1167    }
1168}
1169
1170/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom (no
1171/// velocity, no forces) using per-axis fixed flags.
1172/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1173///
1174/// # Safety
1175/// builder_handle and symbol must be valid.
1176#[unsafe(no_mangle)]
1177pub unsafe extern "C" fn rkr_frame_add_atom_with_fixed_mask(
1178    builder_handle: *mut RKRConFrameBuilder,
1179    symbol: *const c_char,
1180    x: f64,
1181    y: f64,
1182    z: f64,
1183    fixed_x: bool,
1184    fixed_y: bool,
1185    fixed_z: bool,
1186    atom_id: u64,
1187    mass: f64,
1188) -> RKRStatus {
1189    unsafe {
1190        add_builder_atom(
1191            builder_handle,
1192            symbol,
1193            x,
1194            y,
1195            z,
1196            [fixed_x, fixed_y, fixed_z],
1197            atom_id,
1198            mass,
1199            None,
1200            None,
1201        )
1202    }
1203}
1204
1205/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
1206/// a velocity vector and a single uniform fixed flag.
1207/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1208///
1209/// # Safety
1210/// builder_handle and symbol must be valid.
1211#[unsafe(no_mangle)]
1212pub unsafe extern "C" fn rkr_frame_add_atom_with_velocity(
1213    builder_handle: *mut RKRConFrameBuilder,
1214    symbol: *const c_char,
1215    x: f64,
1216    y: f64,
1217    z: f64,
1218    is_fixed: bool,
1219    atom_id: u64,
1220    mass: f64,
1221    vx: f64,
1222    vy: f64,
1223    vz: f64,
1224) -> RKRStatus {
1225    unsafe {
1226        add_builder_atom(
1227            builder_handle,
1228            symbol,
1229            x,
1230            y,
1231            z,
1232            [is_fixed; 3],
1233            atom_id,
1234            mass,
1235            Some([vx, vy, vz]),
1236            None,
1237        )
1238    }
1239}
1240
1241/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
1242/// a velocity vector and per-axis fixed flags.
1243/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1244///
1245/// # Safety
1246/// builder_handle and symbol must be valid.
1247#[unsafe(no_mangle)]
1248pub unsafe extern "C" fn rkr_frame_add_atom_with_velocity_fixed_mask(
1249    builder_handle: *mut RKRConFrameBuilder,
1250    symbol: *const c_char,
1251    x: f64,
1252    y: f64,
1253    z: f64,
1254    fixed_x: bool,
1255    fixed_y: bool,
1256    fixed_z: bool,
1257    atom_id: u64,
1258    mass: f64,
1259    vx: f64,
1260    vy: f64,
1261    vz: f64,
1262) -> RKRStatus {
1263    unsafe {
1264        add_builder_atom(
1265            builder_handle,
1266            symbol,
1267            x,
1268            y,
1269            z,
1270            [fixed_x, fixed_y, fixed_z],
1271            atom_id,
1272            mass,
1273            Some([vx, vy, vz]),
1274            None,
1275        )
1276    }
1277}
1278
1279/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
1280/// a force vector and a single uniform fixed flag.
1281/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1282///
1283/// # Safety
1284/// builder_handle and symbol must be valid.
1285#[unsafe(no_mangle)]
1286pub unsafe extern "C" fn rkr_frame_add_atom_with_forces(
1287    builder_handle: *mut RKRConFrameBuilder,
1288    symbol: *const c_char,
1289    x: f64,
1290    y: f64,
1291    z: f64,
1292    is_fixed: bool,
1293    atom_id: u64,
1294    mass: f64,
1295    fx: f64,
1296    fy: f64,
1297    fz: f64,
1298) -> RKRStatus {
1299    unsafe {
1300        add_builder_atom(
1301            builder_handle,
1302            symbol,
1303            x,
1304            y,
1305            z,
1306            [is_fixed; 3],
1307            atom_id,
1308            mass,
1309            None,
1310            Some([fx, fy, fz]),
1311        )
1312    }
1313}
1314
1315/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
1316/// a force vector and per-axis fixed flags.
1317/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1318///
1319/// # Safety
1320/// builder_handle and symbol must be valid.
1321#[unsafe(no_mangle)]
1322pub unsafe extern "C" fn rkr_frame_add_atom_with_forces_fixed_mask(
1323    builder_handle: *mut RKRConFrameBuilder,
1324    symbol: *const c_char,
1325    x: f64,
1326    y: f64,
1327    z: f64,
1328    fixed_x: bool,
1329    fixed_y: bool,
1330    fixed_z: bool,
1331    atom_id: u64,
1332    mass: f64,
1333    fx: f64,
1334    fy: f64,
1335    fz: f64,
1336) -> RKRStatus {
1337    unsafe {
1338        add_builder_atom(
1339            builder_handle,
1340            symbol,
1341            x,
1342            y,
1343            z,
1344            [fixed_x, fixed_y, fixed_z],
1345            atom_id,
1346            mass,
1347            None,
1348            Some([fx, fy, fz]),
1349        )
1350    }
1351}
1352
1353/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
1354/// both velocity and force vectors and a single uniform fixed flag.
1355/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1356///
1357/// # Safety
1358/// builder_handle and symbol must be valid.
1359#[unsafe(no_mangle)]
1360pub unsafe extern "C" fn rkr_frame_add_atom_with_velocity_and_forces(
1361    builder_handle: *mut RKRConFrameBuilder,
1362    symbol: *const c_char,
1363    x: f64,
1364    y: f64,
1365    z: f64,
1366    is_fixed: bool,
1367    atom_id: u64,
1368    mass: f64,
1369    vx: f64,
1370    vy: f64,
1371    vz: f64,
1372    fx: f64,
1373    fy: f64,
1374    fz: f64,
1375) -> RKRStatus {
1376    unsafe {
1377        add_builder_atom(
1378            builder_handle,
1379            symbol,
1380            x,
1381            y,
1382            z,
1383            [is_fixed; 3],
1384            atom_id,
1385            mass,
1386            Some([vx, vy, vz]),
1387            Some([fx, fy, fz]),
1388        )
1389    }
1390}
1391
1392/// **Deprecated**: prefer `rkr_frame_add_atom_full`. Adds an atom with
1393/// both velocity and force vectors and per-axis fixed flags.
1394/// Returns `RKR_STATUS_SUCCESS` on success, or an error code.
1395///
1396/// # Safety
1397/// builder_handle and symbol must be valid.
1398#[unsafe(no_mangle)]
1399pub unsafe extern "C" fn rkr_frame_add_atom_with_velocity_and_forces_fixed_mask(
1400    builder_handle: *mut RKRConFrameBuilder,
1401    symbol: *const c_char,
1402    x: f64,
1403    y: f64,
1404    z: f64,
1405    fixed_x: bool,
1406    fixed_y: bool,
1407    fixed_z: bool,
1408    atom_id: u64,
1409    mass: f64,
1410    vx: f64,
1411    vy: f64,
1412    vz: f64,
1413    fx: f64,
1414    fy: f64,
1415    fz: f64,
1416) -> RKRStatus {
1417    unsafe {
1418        add_builder_atom(
1419            builder_handle,
1420            symbol,
1421            x,
1422            y,
1423            z,
1424            [fixed_x, fixed_y, fixed_z],
1425            atom_id,
1426            mass,
1427            Some([vx, vy, vz]),
1428            Some([fx, fy, fz]),
1429        )
1430    }
1431}
1432
1433/// Consumes the builder and returns a finalized RKRConFrame handle.
1434/// The builder handle is invalidated after this call.
1435/// The caller OWNS the returned frame and MUST call `free_rkr_frame`.
1436/// Returns NULL on error.
1437///
1438/// # Safety
1439/// builder_handle must be valid. The caller takes ownership of the returned frame.
1440#[unsafe(no_mangle)]
1441pub unsafe extern "C" fn rkr_frame_builder_build(
1442    builder_handle: *mut RKRConFrameBuilder,
1443) -> *mut RKRConFrame {
1444    if builder_handle.is_null() {
1445        return ptr::null_mut();
1446    }
1447    let builder = unsafe { *Box::from_raw(builder_handle as *mut ConFrameBuilder) };
1448    let frame = builder.build();
1449    Box::into_raw(Box::new(frame)) as *mut RKRConFrame
1450}
1451
1452/// Frees a frame builder without building.
1453///
1454/// # Safety
1455/// builder_handle must be valid or null.
1456#[unsafe(no_mangle)]
1457pub unsafe extern "C" fn free_rkr_frame_builder(builder_handle: *mut RKRConFrameBuilder) {
1458    if !builder_handle.is_null() {
1459        let _ = unsafe { Box::from_raw(builder_handle as *mut ConFrameBuilder) };
1460    }
1461}
1462
1463/// Creates a new gzip-compressed frame writer for the specified file.
1464/// The caller OWNS the returned pointer and MUST call `free_rkr_writer`.
1465///
1466/// # Safety
1467/// filename_c must be valid. The caller takes ownership of the returned writer.
1468#[unsafe(no_mangle)]
1469pub unsafe extern "C" fn create_writer_gzip_c(filename_c: *const c_char) -> *mut RKRConFrameWriter {
1470    if filename_c.is_null() {
1471        return ptr::null_mut();
1472    }
1473    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
1474        Ok(s) => s,
1475        Err(_) => return ptr::null_mut(),
1476    };
1477    match ConFrameWriter::from_path_gzip(filename) {
1478        Ok(writer) => Box::into_raw(Box::new(writer)) as *mut RKRConFrameWriter,
1479        Err(_) => ptr::null_mut(),
1480    }
1481}
1482
1483//=============================================================================
1484// Direct mmap-based Reader FFI
1485//=============================================================================
1486
1487/// Reads the first frame from a .con file.
1488/// Uses `read_to_string` for small files (< 64 KiB) and mmap for larger ones.
1489/// Stops after the first frame rather than parsing the entire file.
1490/// The caller OWNS the returned handle and MUST call `free_rkr_frame`.
1491/// Returns NULL on error.
1492///
1493/// # Safety
1494/// filename_c must be valid. The caller takes ownership of the returned frame.
1495#[unsafe(no_mangle)]
1496pub unsafe extern "C" fn rkr_read_first_frame(filename_c: *const c_char) -> *mut RKRConFrame {
1497    if filename_c.is_null() {
1498        return ptr::null_mut();
1499    }
1500    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
1501        Ok(s) => s,
1502        Err(_) => return ptr::null_mut(),
1503    };
1504    match iterators::read_first_frame(Path::new(filename)) {
1505        Ok(frame) => Box::into_raw(Box::new(frame)) as *mut RKRConFrame,
1506        Err(_) => ptr::null_mut(),
1507    }
1508}
1509
1510/// Reads all frames from a .con file using mmap.
1511/// Returns an array of frame handles and sets `num_frames` to the count.
1512/// The caller OWNS both the array and each frame handle.
1513/// Free frames with `free_rkr_frame` and the array with `free_rkr_frame_array`.
1514/// Returns NULL on error.
1515///
1516/// # Safety
1517/// filename_c and num_frames must be valid. The caller takes ownership of the returned handles and array.
1518#[unsafe(no_mangle)]
1519pub unsafe extern "C" fn rkr_read_all_frames(
1520    filename_c: *const c_char,
1521    num_frames: *mut usize,
1522) -> *mut *mut RKRConFrame {
1523    if filename_c.is_null() || num_frames.is_null() {
1524        return ptr::null_mut();
1525    }
1526    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
1527        Ok(s) => s,
1528        Err(_) => return ptr::null_mut(),
1529    };
1530    match iterators::read_all_frames(Path::new(filename)) {
1531        Ok(frames) => {
1532            let count = frames.len();
1533            // shrink_to_fit ensures len == capacity so the matching
1534            // free_rkr_frame_array can soundly call Vec::from_raw_parts
1535            // with len == cap.
1536            let mut handles: Vec<*mut RKRConFrame> = frames
1537                .into_iter()
1538                .map(|f| Box::into_raw(Box::new(f)) as *mut RKRConFrame)
1539                .collect();
1540            handles.shrink_to_fit();
1541            debug_assert_eq!(handles.len(), handles.capacity());
1542            let ptr = handles.as_mut_ptr();
1543            std::mem::forget(handles);
1544            unsafe { *num_frames = count };
1545            ptr
1546        }
1547        Err(_) => ptr::null_mut(),
1548    }
1549}
1550
1551/// Frees an array of frame handles returned by `rkr_read_all_frames`.
1552/// Each frame is freed individually, then the array itself.
1553///
1554/// # Safety
1555/// frames must be valid or null.
1556#[unsafe(no_mangle)]
1557pub unsafe extern "C" fn free_rkr_frame_array(frames: *mut *mut RKRConFrame, num_frames: usize) {
1558    if frames.is_null() {
1559        return;
1560    }
1561    unsafe {
1562        let handles = Vec::from_raw_parts(frames, num_frames, num_frames);
1563        for handle in handles {
1564            if !handle.is_null() {
1565                let _ = Box::from_raw(handle as *mut ConFrame);
1566            }
1567        }
1568    }
1569}
1570
1571#[cfg(test)]
1572mod tests {
1573    use super::*;
1574    use std::ffi::{CStr, CString};
1575
1576    fn test_frame_handle() -> *mut RKRConFrame {
1577        let mut builder = ConFrameBuilder::new([10.0, 10.0, 10.0], [90.0, 90.0, 90.0]);
1578        builder
1579            .prebox_header("Generated by test")
1580            .postbox_header(["0 0".to_string(), "0 0 0".to_string()]);
1581        builder.add_atom("Cu", 0.0, 0.0, 0.0, [false, false, false], 0, 63.546);
1582        Box::into_raw(Box::new(builder.build())) as *mut RKRConFrame
1583    }
1584
1585    #[test]
1586    fn header_line_rejects_null_buffer() {
1587        let frame = test_frame_handle();
1588        let status = unsafe { rkr_frame_get_header_line(frame, true, 0, std::ptr::null_mut(), 16) };
1589        unsafe { free_rkr_frame(frame) };
1590
1591        assert_eq!(status, RKRStatus::RKR_STATUS_NULL_POINTER);
1592    }
1593
1594    #[test]
1595    fn header_line_rejects_empty_buffer() {
1596        let frame = test_frame_handle();
1597        let mut buffer = [0 as c_char; 1];
1598        let status = unsafe { rkr_frame_get_header_line(frame, true, 0, buffer.as_mut_ptr(), 0) };
1599        unsafe { free_rkr_frame(frame) };
1600
1601        assert_eq!(status, RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL);
1602    }
1603
1604    #[test]
1605    fn header_line_truncates_and_terminates_buffer() {
1606        let frame = test_frame_handle();
1607        let mut buffer = [0 as c_char; 10];
1608        let status =
1609            unsafe { rkr_frame_get_header_line(frame, true, 0, buffer.as_mut_ptr(), buffer.len()) };
1610        unsafe { free_rkr_frame(frame) };
1611
1612        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
1613        let copied = unsafe { CStr::from_ptr(buffer.as_ptr()) };
1614        assert_eq!(copied.to_str().unwrap(), "Generated");
1615    }
1616
1617    fn test_builder_handle() -> *mut RKRConFrameBuilder {
1618        let cell = [10.0, 11.0, 12.0];
1619        let angles = [90.0, 91.0, 92.0];
1620        unsafe {
1621            rkr_frame_new(
1622                cell.as_ptr(),
1623                angles.as_ptr(),
1624                ptr::null(),
1625                ptr::null(),
1626                ptr::null(),
1627                ptr::null(),
1628            )
1629        }
1630    }
1631
1632    fn c_string(s: &str) -> CString {
1633        CString::new(s).unwrap()
1634    }
1635
1636    unsafe fn assert_single_atom(
1637        frame: *mut RKRConFrame,
1638        fixed: [bool; 3],
1639        velocity: Option<[f64; 3]>,
1640        forces: Option<[f64; 3]>,
1641    ) {
1642        let c_frame = unsafe { rkr_frame_to_c_frame(frame) };
1643        assert!(!c_frame.is_null());
1644        let c_frame_ref = unsafe { &*c_frame };
1645        assert_eq!(c_frame_ref.num_atoms, 1);
1646        assert_eq!(c_frame_ref.has_velocities, velocity.is_some());
1647        assert_eq!(c_frame_ref.has_forces, forces.is_some());
1648
1649        let atom = unsafe { &*c_frame_ref.atoms };
1650        assert_eq!(atom.fixed_x, fixed[0]);
1651        assert_eq!(atom.fixed_y, fixed[1]);
1652        assert_eq!(atom.fixed_z, fixed[2]);
1653        assert_eq!(atom.is_fixed, fixed.iter().any(|&value| value));
1654        assert_eq!(atom.has_velocity, velocity.is_some());
1655        assert_eq!(atom.has_forces, forces.is_some());
1656        if let Some([vx, vy, vz]) = velocity {
1657            assert_eq!([atom.vx, atom.vy, atom.vz], [vx, vy, vz]);
1658        }
1659        if let Some([fx, fy, fz]) = forces {
1660            assert_eq!([atom.fx, atom.fy, atom.fz], [fx, fy, fz]);
1661        }
1662
1663        unsafe { free_c_frame(c_frame) };
1664        unsafe { free_rkr_frame(frame) };
1665    }
1666
1667    #[test]
1668    fn builder_preserves_fixed_mask_for_atom_without_velocity_or_forces() {
1669        let builder = test_builder_handle();
1670        let symbol = c_string("Cu");
1671
1672        let status = unsafe {
1673            rkr_frame_add_atom_with_fixed_mask(
1674                builder,
1675                symbol.as_ptr(),
1676                1.0,
1677                2.0,
1678                3.0,
1679                true,
1680                false,
1681                true,
1682                7,
1683                63.546,
1684            )
1685        };
1686        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
1687
1688        let frame = unsafe { rkr_frame_builder_build(builder) };
1689        unsafe { assert_single_atom(frame, [true, false, true], None, None) };
1690    }
1691
1692    #[test]
1693    fn builder_preserves_fixed_mask_for_atom_with_velocity() {
1694        let builder = test_builder_handle();
1695        let symbol = c_string("H");
1696
1697        let status = unsafe {
1698            rkr_frame_add_atom_with_velocity_fixed_mask(
1699                builder,
1700                symbol.as_ptr(),
1701                1.0,
1702                2.0,
1703                3.0,
1704                false,
1705                true,
1706                false,
1707                9,
1708                1.008,
1709                0.1,
1710                0.2,
1711                0.3,
1712            )
1713        };
1714        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
1715
1716        let frame = unsafe { rkr_frame_builder_build(builder) };
1717        unsafe { assert_single_atom(frame, [false, true, false], Some([0.1, 0.2, 0.3]), None) };
1718    }
1719
1720    #[test]
1721    fn builder_preserves_fixed_mask_for_atom_with_forces() {
1722        let builder = test_builder_handle();
1723        let symbol = c_string("O");
1724
1725        let status = unsafe {
1726            rkr_frame_add_atom_with_forces_fixed_mask(
1727                builder,
1728                symbol.as_ptr(),
1729                1.0,
1730                2.0,
1731                3.0,
1732                true,
1733                true,
1734                false,
1735                11,
1736                15.999,
1737                -0.1,
1738                -0.2,
1739                -0.3,
1740            )
1741        };
1742        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
1743
1744        let frame = unsafe { rkr_frame_builder_build(builder) };
1745        unsafe { assert_single_atom(frame, [true, true, false], None, Some([-0.1, -0.2, -0.3])) };
1746    }
1747
1748    #[test]
1749    fn builder_preserves_fixed_mask_for_atom_with_velocity_and_forces() {
1750        let builder = test_builder_handle();
1751        let symbol = c_string("N");
1752
1753        let status = unsafe {
1754            rkr_frame_add_atom_with_velocity_and_forces_fixed_mask(
1755                builder,
1756                symbol.as_ptr(),
1757                1.0,
1758                2.0,
1759                3.0,
1760                false,
1761                true,
1762                true,
1763                13,
1764                14.007,
1765                0.4,
1766                0.5,
1767                0.6,
1768                -0.4,
1769                -0.5,
1770                -0.6,
1771            )
1772        };
1773        assert_eq!(status, RKRStatus::RKR_STATUS_SUCCESS);
1774
1775        let frame = unsafe { rkr_frame_builder_build(builder) };
1776        unsafe {
1777            assert_single_atom(
1778                frame,
1779                [false, true, true],
1780                Some([0.4, 0.5, 0.6]),
1781                Some([-0.4, -0.5, -0.6]),
1782            )
1783        };
1784    }
1785
1786    #[test]
1787    fn builder_bool_fixed_functions_set_all_axes_together() {
1788        let builder = test_builder_handle();
1789        let cu = c_string("Cu");
1790        let h = c_string("H");
1791
1792        let atom_status =
1793            unsafe { rkr_frame_add_atom(builder, cu.as_ptr(), 1.0, 2.0, 3.0, true, 1, 63.546) };
1794        assert_eq!(atom_status, RKRStatus::RKR_STATUS_SUCCESS);
1795
1796        let velocity_status = unsafe {
1797            rkr_frame_add_atom_with_velocity(
1798                builder,
1799                h.as_ptr(),
1800                4.0,
1801                5.0,
1802                6.0,
1803                false,
1804                2,
1805                1.008,
1806                0.7,
1807                0.8,
1808                0.9,
1809            )
1810        };
1811        assert_eq!(velocity_status, RKRStatus::RKR_STATUS_SUCCESS);
1812
1813        let frame = unsafe { rkr_frame_builder_build(builder) };
1814        let c_frame = unsafe { rkr_frame_to_c_frame(frame) };
1815        assert!(!c_frame.is_null());
1816        let c_frame_ref = unsafe { &*c_frame };
1817        assert_eq!(c_frame_ref.num_atoms, 2);
1818
1819        let atoms = unsafe { std::slice::from_raw_parts(c_frame_ref.atoms, c_frame_ref.num_atoms) };
1820        assert_eq!(
1821            [atoms[0].fixed_x, atoms[0].fixed_y, atoms[0].fixed_z],
1822            [true, true, true]
1823        );
1824        assert_eq!(
1825            [atoms[1].fixed_x, atoms[1].fixed_y, atoms[1].fixed_z],
1826            [false, false, false]
1827        );
1828
1829        unsafe { free_c_frame(c_frame) };
1830        unsafe { free_rkr_frame(frame) };
1831    }
1832
1833    #[test]
1834    fn status_message_returns_static_strings_for_all_status_values() {
1835        let cases = [
1836            (RKRStatus::RKR_STATUS_SUCCESS, "success"),
1837            (RKRStatus::RKR_STATUS_NULL_POINTER, "null pointer"),
1838            (RKRStatus::RKR_STATUS_INVALID_UTF8, "invalid UTF-8"),
1839            (RKRStatus::RKR_STATUS_INVALID_JSON, "invalid JSON"),
1840            (RKRStatus::RKR_STATUS_IO_ERROR, "I/O error"),
1841            (
1842                RKRStatus::RKR_STATUS_INDEX_OUT_OF_BOUNDS,
1843                "index out of bounds",
1844            ),
1845            (RKRStatus::RKR_STATUS_BUFFER_TOO_SMALL, "buffer too small"),
1846            (RKRStatus::RKR_STATUS_INTERNAL_ERROR, "internal error"),
1847        ];
1848
1849        for (status, expected) in cases {
1850            let message = unsafe { CStr::from_ptr(rkr_status_message(status)) };
1851            assert_eq!(message.to_str().unwrap(), expected);
1852        }
1853    }
1854}