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