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};
4use crate::writer::ConFrameWriter;
5use std::ffi::{c_char, CStr, CString};
6use std::fs::{self, File};
7use std::path::Path;
8use std::ptr;
9
10//=============================================================================
11// C-Compatible Structs & Handles
12//=============================================================================
13
14/// An opaque handle to a full, lossless Rust `ConFrame` object.
15/// The C/C++ side needs to treat this as a void pointer
16#[repr(C)]
17pub struct RKRConFrame {
18    _private: [u8; 0],
19}
20
21/// An opaque handle to a Rust `ConFrameWriter` object.
22/// The C/C++ side needs to treat this as a void pointer
23#[repr(C)]
24pub struct RKRConFrameWriter {
25    _private: [u8; 0],
26}
27
28/// A transparent, "lossy" C-struct containing only the core atomic data.
29/// This can be extracted from an `RKRConFrame` handle for direct data access.
30/// The caller is responsible for freeing the `atoms` array using `free_c_frame`.
31#[repr(C)]
32pub struct CFrame {
33    pub atoms: *mut CAtom,
34    pub num_atoms: usize,
35    pub cell: [f64; 3],
36    pub angles: [f64; 3],
37    pub has_velocities: bool,
38}
39
40#[repr(C)]
41pub struct CAtom {
42    pub atomic_number: u64,
43    pub x: f64,
44    pub y: f64,
45    pub z: f64,
46    pub atom_id: u64,
47    pub mass: f64,
48    pub is_fixed: bool,
49    pub vx: f64,
50    pub vy: f64,
51    pub vz: f64,
52    pub has_velocity: bool,
53}
54
55#[repr(C)]
56pub struct CConFrameIterator {
57    iterator: *mut ConFrameIterator<'static>,
58    file_contents: *mut String,
59}
60
61//=============================================================================
62// Iterator and Memory Management
63//=============================================================================
64
65/// Creates a new iterator for a .con file.
66/// The caller OWNS the returned pointer and MUST call `free_con_frame_iterator`.
67/// Returns NULL if there are no more frames or on error.
68#[unsafe(no_mangle)]
69pub unsafe extern "C" fn read_con_file_iterator(
70    filename_c: *const c_char,
71) -> *mut CConFrameIterator {
72    if filename_c.is_null() {
73        return ptr::null_mut();
74    }
75    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
76        Ok(s) => s,
77        Err(_) => return ptr::null_mut(),
78    };
79    let file_contents_box = match fs::read_to_string(filename) {
80        Ok(contents) => Box::new(contents),
81        Err(_) => return ptr::null_mut(),
82    };
83    let file_contents_ptr = Box::into_raw(file_contents_box);
84    let static_file_contents: &'static str = unsafe { &*file_contents_ptr };
85    let iterator = Box::new(ConFrameIterator::new(static_file_contents));
86    let c_iterator = Box::new(CConFrameIterator {
87        iterator: Box::into_raw(iterator),
88        file_contents: file_contents_ptr,
89    });
90    Box::into_raw(c_iterator)
91}
92
93/// Reads the next frame from the iterator, returning an opaque handle.
94/// The caller OWNS the returned handle and must free it with `free_rkr_frame`.
95#[unsafe(no_mangle)]
96pub unsafe extern "C" fn con_frame_iterator_next(
97    iterator: *mut CConFrameIterator,
98) -> *mut RKRConFrame {
99    if iterator.is_null() {
100        return ptr::null_mut();
101    }
102    let iter = unsafe { &mut *(*iterator).iterator };
103    match iter.next() {
104        Some(Ok(frame)) => Box::into_raw(Box::new(frame)) as *mut RKRConFrame,
105        _ => ptr::null_mut(),
106    }
107}
108
109/// Frees the memory for an opaque `RKRConFrame` handle.
110#[unsafe(no_mangle)]
111pub unsafe extern "C" fn free_rkr_frame(frame_handle: *mut RKRConFrame) {
112    if !frame_handle.is_null() {
113        let _ = unsafe { Box::from_raw(frame_handle as *mut ConFrame) };
114    }
115}
116
117/// Frees the memory for a `CConFrameIterator`.
118#[unsafe(no_mangle)]
119pub unsafe extern "C" fn free_con_frame_iterator(iterator: *mut CConFrameIterator) {
120    if iterator.is_null() {
121        return;
122    }
123    unsafe {
124        let c_iterator_box = Box::from_raw(iterator);
125        let _ = Box::from_raw(c_iterator_box.iterator);
126        let _ = Box::from_raw(c_iterator_box.file_contents);
127    }
128}
129
130//=============================================================================
131// Data Accessors (The "Getter" API)
132//=============================================================================
133
134/// Extracts the core atomic data into a transparent `CFrame` struct.
135/// The caller OWNS the returned pointer and MUST call `free_c_frame` on it.
136#[unsafe(no_mangle)]
137pub unsafe extern "C" fn rkr_frame_to_c_frame(frame_handle: *const RKRConFrame) -> *mut CFrame {
138    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
139        Some(f) => f,
140        None => return ptr::null_mut(),
141    };
142
143    let masses_iter = frame
144        .header
145        .natms_per_type
146        .iter()
147        .zip(frame.header.masses_per_type.iter())
148        .flat_map(|(num_atoms, mass)| std::iter::repeat_n(*mass, *num_atoms));
149
150    let has_velocities = frame.has_velocities();
151
152    let mut c_atoms: Vec<CAtom> = frame
153        .atom_data
154        .iter()
155        .zip(masses_iter)
156        .map(|(atom_datum, mass)| CAtom {
157            atomic_number: symbol_to_atomic_number(&atom_datum.symbol),
158            x: atom_datum.x,
159            y: atom_datum.y,
160            z: atom_datum.z,
161            is_fixed: atom_datum.is_fixed,
162            atom_id: atom_datum.atom_id,
163            mass,
164            vx: atom_datum.vx.unwrap_or(0.0),
165            vy: atom_datum.vy.unwrap_or(0.0),
166            vz: atom_datum.vz.unwrap_or(0.0),
167            has_velocity: atom_datum.has_velocity(),
168        })
169        .collect();
170
171    let atoms_ptr = c_atoms.as_mut_ptr();
172    let num_atoms = c_atoms.len();
173    std::mem::forget(c_atoms);
174
175    let c_frame = Box::new(CFrame {
176        atoms: atoms_ptr,
177        num_atoms,
178        cell: frame.header.boxl,
179        angles: frame.header.angles,
180        has_velocities,
181    });
182
183    Box::into_raw(c_frame)
184}
185
186/// Frees the memory of a `CFrame` struct, including its internal atoms array.
187#[unsafe(no_mangle)]
188pub unsafe extern "C" fn free_c_frame(frame: *mut CFrame) {
189    if frame.is_null() {
190        return;
191    }
192    unsafe {
193        let frame_box = Box::from_raw(frame);
194        let _ = Vec::from_raw_parts(frame_box.atoms, frame_box.num_atoms, frame_box.num_atoms);
195    }
196}
197
198/// Copies a header string line into a user-provided buffer.
199/// This is a C style helper... where the user explicitly sets the buffer.
200/// Returns the number of bytes written (excluding null terminator), or -1 on error.
201#[unsafe(no_mangle)]
202pub unsafe extern "C" fn rkr_frame_get_header_line(
203    frame_handle: *const RKRConFrame,
204    is_prebox: bool,
205    line_index: usize,
206    buffer: *mut c_char,
207    buffer_len: usize,
208) -> i32 {
209    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
210        Some(f) => f,
211        None => return -1,
212    };
213    let line_to_copy = if is_prebox {
214        frame.header.prebox_header.get(line_index)
215    } else {
216        frame.header.postbox_header.get(line_index)
217    };
218    if let Some(line) = line_to_copy {
219        let bytes = line.as_bytes();
220        let len_to_copy = std::cmp::min(bytes.len(), buffer_len - 1);
221        unsafe {
222            ptr::copy_nonoverlapping(bytes.as_ptr(), buffer as *mut u8, len_to_copy);
223            *buffer.add(len_to_copy) = 0;
224        }
225        len_to_copy as i32
226    } else {
227        -1
228    }
229}
230
231/// Gets a header string line as a newly allocated, null-terminated C string.
232///
233/// The caller OWNS the returned pointer and MUST call `rkr_free_string` on it
234/// to prevent a memory leak. Returns NULL on error or if the index is invalid.
235#[unsafe(no_mangle)]
236pub unsafe extern "C" fn rkr_frame_get_header_line_cpp(
237    frame_handle: *const RKRConFrame,
238    is_prebox: bool,
239    line_index: usize,
240) -> *mut c_char {
241    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
242        Some(f) => f,
243        None => return ptr::null_mut(),
244    };
245
246    let line_to_copy = if is_prebox {
247        frame.header.prebox_header.get(line_index)
248    } else {
249        frame.header.postbox_header.get(line_index)
250    };
251
252    if let Some(line) = line_to_copy {
253        // Convert the Rust string slice to a C-compatible, heap-allocated string.
254        match CString::new(line.as_str()) {
255            Ok(c_string) => c_string.into_raw(), // Give ownership to the C caller
256            Err(_) => ptr::null_mut(),           // In case the string contains a null byte
257        }
258    } else {
259        ptr::null_mut() // Index out of bounds
260    }
261}
262
263/// Frees a C string that was allocated by Rust (e.g., from `rkr_frame_get_header_line`).
264#[unsafe(no_mangle)]
265pub unsafe extern "C" fn rkr_free_string(s: *mut c_char) {
266    if !s.is_null() {
267        // Retake ownership of the CString to deallocate it properly.
268        let _ = unsafe { CString::from_raw(s) };
269    }
270}
271
272//=============================================================================
273// FFI Writer Functions (Writer Object Model)
274//=============================================================================
275
276/// Creates a new frame writer for the specified file.
277/// The caller OWNS the returned pointer and MUST call `free_rkr_writer`.
278#[unsafe(no_mangle)]
279pub unsafe extern "C" fn create_writer_from_path_c(
280    filename_c: *const c_char,
281) -> *mut RKRConFrameWriter {
282    if filename_c.is_null() {
283        return ptr::null_mut();
284    }
285    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
286        Ok(s) => s,
287        Err(_) => return ptr::null_mut(),
288    };
289    match crate::writer::ConFrameWriter::from_path(filename) {
290        Ok(writer) => Box::into_raw(Box::new(writer)) as *mut RKRConFrameWriter,
291        Err(_) => ptr::null_mut(),
292    }
293}
294
295/// Frees the memory for an `RKRConFrameWriter`, closing the associated file.
296#[unsafe(no_mangle)]
297pub unsafe extern "C" fn free_rkr_writer(writer_handle: *mut RKRConFrameWriter) {
298    if !writer_handle.is_null() {
299        let _ = unsafe { Box::from_raw(writer_handle as *mut ConFrameWriter<File>) };
300    }
301}
302
303/// Writes multiple frames from an array of handles to the file managed by the writer.
304#[unsafe(no_mangle)]
305pub unsafe extern "C" fn rkr_writer_extend(
306    writer_handle: *mut RKRConFrameWriter,
307    frame_handles: *const *const RKRConFrame,
308    num_frames: usize,
309) -> i32 {
310    let writer = match unsafe { (writer_handle as *mut ConFrameWriter<File>).as_mut() } {
311        Some(w) => w,
312        None => return -1,
313    };
314    if frame_handles.is_null() {
315        return -1;
316    }
317
318    let handles_slice = unsafe { std::slice::from_raw_parts(frame_handles, num_frames) };
319    let mut rust_frames: Vec<&ConFrame> = Vec::with_capacity(num_frames);
320    if handles_slice.iter().any(|&handle| handle.is_null()) {
321        // Fail fast if any handle is null, as this indicates a bug on the
322        // caller's side.
323        return -1;
324    }
325    for &handle in handles_slice.iter() {
326        // Assume the handle is valid.
327        match unsafe { (handle as *const ConFrame).as_ref() } {
328            Some(frame) => rust_frames.push(frame),
329            // This case should be unreachable if the handle is not null, but we handle it for safety.
330            None => return -1,
331        }
332    }
333
334    match writer.extend(rust_frames.into_iter()) {
335        Ok(_) => 0,
336        Err(_) => -1,
337    }
338}
339
340//=============================================================================
341// Writer with Precision
342//=============================================================================
343
344/// Creates a new frame writer with custom floating-point precision.
345/// The caller OWNS the returned pointer and MUST call `free_rkr_writer`.
346#[unsafe(no_mangle)]
347pub unsafe extern "C" fn create_writer_from_path_with_precision_c(
348    filename_c: *const c_char,
349    precision: u8,
350) -> *mut RKRConFrameWriter {
351    if filename_c.is_null() {
352        return ptr::null_mut();
353    }
354    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
355        Ok(s) => s,
356        Err(_) => return ptr::null_mut(),
357    };
358    match ConFrameWriter::from_path_with_precision(filename, precision as usize) {
359        Ok(writer) => Box::into_raw(Box::new(writer)) as *mut RKRConFrameWriter,
360        Err(_) => ptr::null_mut(),
361    }
362}
363
364//=============================================================================
365// Frame Builder FFI (construct ConFrame from C data)
366//=============================================================================
367
368/// An opaque handle to a Rust `ConFrameBuilder` object.
369#[repr(C)]
370pub struct RKRConFrameBuilder {
371    _private: [u8; 0],
372}
373
374/// Creates a new frame builder with the given cell dimensions, angles, and header lines.
375/// The caller OWNS the returned pointer and MUST call `free_rkr_frame_builder` or
376/// `rkr_frame_builder_build`.
377/// Returns NULL on error.
378#[unsafe(no_mangle)]
379pub unsafe extern "C" fn rkr_frame_new(
380    cell: *const f64,
381    angles: *const f64,
382    prebox0: *const c_char,
383    prebox1: *const c_char,
384    postbox0: *const c_char,
385    postbox1: *const c_char,
386) -> *mut RKRConFrameBuilder {
387    if cell.is_null() || angles.is_null() {
388        return ptr::null_mut();
389    }
390    let cell_arr = unsafe { [*cell, *cell.add(1), *cell.add(2)] };
391    let angles_arr = unsafe { [*angles, *angles.add(1), *angles.add(2)] };
392
393    let get_str = |p: *const c_char| -> String {
394        if p.is_null() {
395            String::new()
396        } else {
397            unsafe { CStr::from_ptr(p) }
398                .to_str()
399                .unwrap_or("")
400                .to_string()
401        }
402    };
403
404    let builder = ConFrameBuilder::new(cell_arr, angles_arr)
405        .prebox_header([get_str(prebox0), get_str(prebox1)])
406        .postbox_header([get_str(postbox0), get_str(postbox1)]);
407
408    Box::into_raw(Box::new(builder)) as *mut RKRConFrameBuilder
409}
410
411/// Adds an atom (without velocity) to the frame builder.
412/// Returns 0 on success, -1 on error.
413#[unsafe(no_mangle)]
414pub unsafe extern "C" fn rkr_frame_add_atom(
415    builder_handle: *mut RKRConFrameBuilder,
416    symbol: *const c_char,
417    x: f64,
418    y: f64,
419    z: f64,
420    is_fixed: bool,
421    atom_id: u64,
422    mass: f64,
423) -> i32 {
424    if builder_handle.is_null() || symbol.is_null() {
425        return -1;
426    }
427    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
428    let sym = match unsafe { CStr::from_ptr(symbol).to_str() } {
429        Ok(s) => s,
430        Err(_) => return -1,
431    };
432    builder.add_atom(sym, x, y, z, is_fixed, atom_id, mass);
433    0
434}
435
436/// Adds an atom with velocity data to the frame builder.
437/// Returns 0 on success, -1 on error.
438#[unsafe(no_mangle)]
439pub unsafe extern "C" fn rkr_frame_add_atom_with_velocity(
440    builder_handle: *mut RKRConFrameBuilder,
441    symbol: *const c_char,
442    x: f64,
443    y: f64,
444    z: f64,
445    is_fixed: bool,
446    atom_id: u64,
447    mass: f64,
448    vx: f64,
449    vy: f64,
450    vz: f64,
451) -> i32 {
452    if builder_handle.is_null() || symbol.is_null() {
453        return -1;
454    }
455    let builder = unsafe { &mut *(builder_handle as *mut ConFrameBuilder) };
456    let sym = match unsafe { CStr::from_ptr(symbol).to_str() } {
457        Ok(s) => s,
458        Err(_) => return -1,
459    };
460    builder.add_atom_with_velocity(sym, x, y, z, is_fixed, atom_id, mass, vx, vy, vz);
461    0
462}
463
464/// Consumes the builder and returns a finalized RKRConFrame handle.
465/// The builder handle is invalidated after this call.
466/// The caller OWNS the returned frame and MUST call `free_rkr_frame`.
467/// Returns NULL on error.
468#[unsafe(no_mangle)]
469pub unsafe extern "C" fn rkr_frame_builder_build(
470    builder_handle: *mut RKRConFrameBuilder,
471) -> *mut RKRConFrame {
472    if builder_handle.is_null() {
473        return ptr::null_mut();
474    }
475    let builder = unsafe { *Box::from_raw(builder_handle as *mut ConFrameBuilder) };
476    let frame = builder.build();
477    Box::into_raw(Box::new(frame)) as *mut RKRConFrame
478}
479
480/// Frees a frame builder without building.
481#[unsafe(no_mangle)]
482pub unsafe extern "C" fn free_rkr_frame_builder(builder_handle: *mut RKRConFrameBuilder) {
483    if !builder_handle.is_null() {
484        let _ = unsafe { Box::from_raw(builder_handle as *mut ConFrameBuilder) };
485    }
486}
487
488//=============================================================================
489// Direct mmap-based Reader FFI
490//=============================================================================
491
492/// Reads the first frame from a .con file using mmap.
493/// The caller OWNS the returned handle and MUST call `free_rkr_frame`.
494/// Returns NULL on error.
495#[unsafe(no_mangle)]
496pub unsafe extern "C" fn rkr_read_first_frame(
497    filename_c: *const c_char,
498) -> *mut RKRConFrame {
499    if filename_c.is_null() {
500        return ptr::null_mut();
501    }
502    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
503        Ok(s) => s,
504        Err(_) => return ptr::null_mut(),
505    };
506    match iterators::read_all_frames(Path::new(filename)) {
507        Ok(mut frames) if !frames.is_empty() => {
508            let frame = frames.swap_remove(0);
509            Box::into_raw(Box::new(frame)) as *mut RKRConFrame
510        }
511        _ => ptr::null_mut(),
512    }
513}
514
515/// Reads all frames from a .con file using mmap.
516/// Returns an array of frame handles and sets `num_frames` to the count.
517/// The caller OWNS both the array and each frame handle.
518/// Free frames with `free_rkr_frame` and the array with `free_rkr_frame_array`.
519/// Returns NULL on error.
520#[unsafe(no_mangle)]
521pub unsafe extern "C" fn rkr_read_all_frames(
522    filename_c: *const c_char,
523    num_frames: *mut usize,
524) -> *mut *mut RKRConFrame {
525    if filename_c.is_null() || num_frames.is_null() {
526        return ptr::null_mut();
527    }
528    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
529        Ok(s) => s,
530        Err(_) => return ptr::null_mut(),
531    };
532    match iterators::read_all_frames(Path::new(filename)) {
533        Ok(frames) => {
534            let count = frames.len();
535            let mut handles: Vec<*mut RKRConFrame> = frames
536                .into_iter()
537                .map(|f| Box::into_raw(Box::new(f)) as *mut RKRConFrame)
538                .collect();
539            let ptr = handles.as_mut_ptr();
540            std::mem::forget(handles);
541            unsafe { *num_frames = count };
542            ptr
543        }
544        Err(_) => ptr::null_mut(),
545    }
546}
547
548/// Frees an array of frame handles returned by `rkr_read_all_frames`.
549/// Each frame is freed individually, then the array itself.
550#[unsafe(no_mangle)]
551pub unsafe extern "C" fn free_rkr_frame_array(
552    frames: *mut *mut RKRConFrame,
553    num_frames: usize,
554) {
555    if frames.is_null() {
556        return;
557    }
558    unsafe {
559        let handles = Vec::from_raw_parts(frames, num_frames, num_frames);
560        for handle in handles {
561            if !handle.is_null() {
562                let _ = Box::from_raw(handle as *mut ConFrame);
563            }
564        }
565    }
566}