Skip to main content

readcon_core/
ffi.rs

1use crate::helpers::symbol_to_atomic_number;
2use crate::iterators::ConFrameIterator;
3use crate::types::ConFrame;
4use crate::writer::ConFrameWriter;
5use std::ffi::{c_char, CStr, CString};
6use std::fs::{self, File};
7use std::ptr;
8
9//=============================================================================
10// C-Compatible Structs & Handles
11//=============================================================================
12
13/// An opaque handle to a full, lossless Rust `ConFrame` object.
14/// The C/C++ side needs to treat this as a void pointer
15#[repr(C)]
16pub struct RKRConFrame {
17    _private: [u8; 0],
18}
19
20/// An opaque handle to a Rust `ConFrameWriter` object.
21/// The C/C++ side needs to treat this as a void pointer
22#[repr(C)]
23pub struct RKRConFrameWriter {
24    _private: [u8; 0],
25}
26
27/// A transparent, "lossy" C-struct containing only the core atomic data.
28/// This can be extracted from an `RKRConFrame` handle for direct data access.
29/// The caller is responsible for freeing the `atoms` array using `free_c_frame`.
30#[repr(C)]
31pub struct CFrame {
32    pub atoms: *mut CAtom,
33    pub num_atoms: usize,
34    pub cell: [f64; 3],
35    pub angles: [f64; 3],
36    pub has_velocities: bool,
37}
38
39#[repr(C)]
40pub struct CAtom {
41    pub atomic_number: u64,
42    pub x: f64,
43    pub y: f64,
44    pub z: f64,
45    pub atom_id: u64,
46    pub mass: f64,
47    pub is_fixed: bool,
48    pub vx: f64,
49    pub vy: f64,
50    pub vz: f64,
51    pub has_velocity: bool,
52}
53
54#[repr(C)]
55pub struct CConFrameIterator {
56    iterator: *mut ConFrameIterator<'static>,
57    file_contents: *mut String,
58}
59
60//=============================================================================
61// Iterator and Memory Management
62//=============================================================================
63
64/// Creates a new iterator for a .con file.
65/// The caller OWNS the returned pointer and MUST call `free_con_frame_iterator`.
66/// Returns NULL if there are no more frames or on error.
67#[unsafe(no_mangle)]
68pub unsafe extern "C" fn read_con_file_iterator(
69    filename_c: *const c_char,
70) -> *mut CConFrameIterator {
71    if filename_c.is_null() {
72        return ptr::null_mut();
73    }
74    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
75        Ok(s) => s,
76        Err(_) => return ptr::null_mut(),
77    };
78    let file_contents_box = match fs::read_to_string(filename) {
79        Ok(contents) => Box::new(contents),
80        Err(_) => return ptr::null_mut(),
81    };
82    let file_contents_ptr = Box::into_raw(file_contents_box);
83    let static_file_contents: &'static str = unsafe { &*file_contents_ptr };
84    let iterator = Box::new(ConFrameIterator::new(static_file_contents));
85    let c_iterator = Box::new(CConFrameIterator {
86        iterator: Box::into_raw(iterator),
87        file_contents: file_contents_ptr,
88    });
89    Box::into_raw(c_iterator)
90}
91
92/// Reads the next frame from the iterator, returning an opaque handle.
93/// The caller OWNS the returned handle and must free it with `free_rkr_frame`.
94#[unsafe(no_mangle)]
95pub unsafe extern "C" fn con_frame_iterator_next(
96    iterator: *mut CConFrameIterator,
97) -> *mut RKRConFrame {
98    if iterator.is_null() {
99        return ptr::null_mut();
100    }
101    let iter = unsafe { &mut *(*iterator).iterator };
102    match iter.next() {
103        Some(Ok(frame)) => Box::into_raw(Box::new(frame)) as *mut RKRConFrame,
104        _ => ptr::null_mut(),
105    }
106}
107
108/// Frees the memory for an opaque `RKRConFrame` handle.
109#[unsafe(no_mangle)]
110pub unsafe extern "C" fn free_rkr_frame(frame_handle: *mut RKRConFrame) {
111    if !frame_handle.is_null() {
112        let _ = unsafe { Box::from_raw(frame_handle as *mut ConFrame) };
113    }
114}
115
116/// Frees the memory for a `CConFrameIterator`.
117#[unsafe(no_mangle)]
118pub unsafe extern "C" fn free_con_frame_iterator(iterator: *mut CConFrameIterator) {
119    if iterator.is_null() {
120        return;
121    }
122    unsafe {
123        let c_iterator_box = Box::from_raw(iterator);
124        let _ = Box::from_raw(c_iterator_box.iterator);
125        let _ = Box::from_raw(c_iterator_box.file_contents);
126    }
127}
128
129//=============================================================================
130// Data Accessors (The "Getter" API)
131//=============================================================================
132
133/// Extracts the core atomic data into a transparent `CFrame` struct.
134/// The caller OWNS the returned pointer and MUST call `free_c_frame` on it.
135#[unsafe(no_mangle)]
136pub unsafe extern "C" fn rkr_frame_to_c_frame(frame_handle: *const RKRConFrame) -> *mut CFrame {
137    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
138        Some(f) => f,
139        None => return ptr::null_mut(),
140    };
141
142    let masses_iter = frame
143        .header
144        .natms_per_type
145        .iter()
146        .zip(frame.header.masses_per_type.iter())
147        .flat_map(|(num_atoms, mass)| std::iter::repeat_n(*mass, *num_atoms));
148
149    let has_velocities = frame.has_velocities();
150
151    let mut c_atoms: Vec<CAtom> = frame
152        .atom_data
153        .iter()
154        .zip(masses_iter)
155        .map(|(atom_datum, mass)| CAtom {
156            atomic_number: symbol_to_atomic_number(&atom_datum.symbol),
157            x: atom_datum.x,
158            y: atom_datum.y,
159            z: atom_datum.z,
160            is_fixed: atom_datum.is_fixed,
161            atom_id: atom_datum.atom_id,
162            mass,
163            vx: atom_datum.vx.unwrap_or(0.0),
164            vy: atom_datum.vy.unwrap_or(0.0),
165            vz: atom_datum.vz.unwrap_or(0.0),
166            has_velocity: atom_datum.has_velocity(),
167        })
168        .collect();
169
170    let atoms_ptr = c_atoms.as_mut_ptr();
171    let num_atoms = c_atoms.len();
172    std::mem::forget(c_atoms);
173
174    let c_frame = Box::new(CFrame {
175        atoms: atoms_ptr,
176        num_atoms,
177        cell: frame.header.boxl,
178        angles: frame.header.angles,
179        has_velocities,
180    });
181
182    Box::into_raw(c_frame)
183}
184
185/// Frees the memory of a `CFrame` struct, including its internal atoms array.
186#[unsafe(no_mangle)]
187pub unsafe extern "C" fn free_c_frame(frame: *mut CFrame) {
188    if frame.is_null() {
189        return;
190    }
191    unsafe {
192        let frame_box = Box::from_raw(frame);
193        let _ = Vec::from_raw_parts(frame_box.atoms, frame_box.num_atoms, frame_box.num_atoms);
194    }
195}
196
197/// Copies a header string line into a user-provided buffer.
198/// This is a C style helper... where the user explicitly sets the buffer.
199/// Returns the number of bytes written (excluding null terminator), or -1 on error.
200#[unsafe(no_mangle)]
201pub unsafe extern "C" fn rkr_frame_get_header_line(
202    frame_handle: *const RKRConFrame,
203    is_prebox: bool,
204    line_index: usize,
205    buffer: *mut c_char,
206    buffer_len: usize,
207) -> i32 {
208    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
209        Some(f) => f,
210        None => return -1,
211    };
212    let line_to_copy = if is_prebox {
213        frame.header.prebox_header.get(line_index)
214    } else {
215        frame.header.postbox_header.get(line_index)
216    };
217    if let Some(line) = line_to_copy {
218        let bytes = line.as_bytes();
219        let len_to_copy = std::cmp::min(bytes.len(), buffer_len - 1);
220        unsafe {
221            ptr::copy_nonoverlapping(bytes.as_ptr(), buffer as *mut u8, len_to_copy);
222            *buffer.add(len_to_copy) = 0;
223        }
224        len_to_copy as i32
225    } else {
226        -1
227    }
228}
229
230/// Gets a header string line as a newly allocated, null-terminated C string.
231///
232/// The caller OWNS the returned pointer and MUST call `rkr_free_string` on it
233/// to prevent a memory leak. Returns NULL on error or if the index is invalid.
234#[unsafe(no_mangle)]
235pub unsafe extern "C" fn rkr_frame_get_header_line_cpp(
236    frame_handle: *const RKRConFrame,
237    is_prebox: bool,
238    line_index: usize,
239) -> *mut c_char {
240    let frame = match unsafe { (frame_handle as *const ConFrame).as_ref() } {
241        Some(f) => f,
242        None => return ptr::null_mut(),
243    };
244
245    let line_to_copy = if is_prebox {
246        frame.header.prebox_header.get(line_index)
247    } else {
248        frame.header.postbox_header.get(line_index)
249    };
250
251    if let Some(line) = line_to_copy {
252        // Convert the Rust string slice to a C-compatible, heap-allocated string.
253        match CString::new(line.as_str()) {
254            Ok(c_string) => c_string.into_raw(), // Give ownership to the C caller
255            Err(_) => ptr::null_mut(),           // In case the string contains a null byte
256        }
257    } else {
258        ptr::null_mut() // Index out of bounds
259    }
260}
261
262/// Frees a C string that was allocated by Rust (e.g., from `rkr_frame_get_header_line`).
263#[unsafe(no_mangle)]
264pub unsafe extern "C" fn rkr_free_string(s: *mut c_char) {
265    if !s.is_null() {
266        // Retake ownership of the CString to deallocate it properly.
267        let _ = unsafe { CString::from_raw(s) };
268    }
269}
270
271//=============================================================================
272// FFI Writer Functions (Writer Object Model)
273//=============================================================================
274
275/// Creates a new frame writer for the specified file.
276/// The caller OWNS the returned pointer and MUST call `free_rkr_writer`.
277#[unsafe(no_mangle)]
278pub unsafe extern "C" fn create_writer_from_path_c(
279    filename_c: *const c_char,
280) -> *mut RKRConFrameWriter {
281    if filename_c.is_null() {
282        return ptr::null_mut();
283    }
284    let filename = match unsafe { CStr::from_ptr(filename_c).to_str() } {
285        Ok(s) => s,
286        Err(_) => return ptr::null_mut(),
287    };
288    match crate::writer::ConFrameWriter::from_path(filename) {
289        Ok(writer) => Box::into_raw(Box::new(writer)) as *mut RKRConFrameWriter,
290        Err(_) => ptr::null_mut(),
291    }
292}
293
294/// Frees the memory for an `RKRConFrameWriter`, closing the associated file.
295#[unsafe(no_mangle)]
296pub unsafe extern "C" fn free_rkr_writer(writer_handle: *mut RKRConFrameWriter) {
297    if !writer_handle.is_null() {
298        let _ = unsafe { Box::from_raw(writer_handle as *mut ConFrameWriter<File>) };
299    }
300}
301
302/// Writes multiple frames from an array of handles to the file managed by the writer.
303#[unsafe(no_mangle)]
304pub unsafe extern "C" fn rkr_writer_extend(
305    writer_handle: *mut RKRConFrameWriter,
306    frame_handles: *const *const RKRConFrame,
307    num_frames: usize,
308) -> i32 {
309    let writer = match unsafe { (writer_handle as *mut ConFrameWriter<File>).as_mut() } {
310        Some(w) => w,
311        None => return -1,
312    };
313    if frame_handles.is_null() {
314        return -1;
315    }
316
317    let handles_slice = unsafe { std::slice::from_raw_parts(frame_handles, num_frames) };
318    let mut rust_frames: Vec<&ConFrame> = Vec::with_capacity(num_frames);
319    if handles_slice.iter().any(|&handle| handle.is_null()) {
320        // Fail fast if any handle is null, as this indicates a bug on the
321        // caller's side.
322        return -1;
323    }
324    for &handle in handles_slice.iter() {
325        // Assume the handle is valid.
326        match unsafe { (handle as *const ConFrame).as_ref() } {
327            Some(frame) => rust_frames.push(frame),
328            // This case should be unreachable if the handle is not null, but we handle it for safety.
329            None => return -1,
330        }
331    }
332
333    match writer.extend(rust_frames.into_iter()) {
334        Ok(_) => 0,
335        Err(_) => -1,
336    }
337}