1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
4//! # Safe Rust FFI Wrapper for `libfers`
6//! This module provides a safe, idiomatic Rust interface to the `libfers` C++
7//! simulation library. It bridges the gap between the raw C-style FFI (generated
8//! by `bindgen`) and the higher-level Rust code used in the Tauri application.
10//! ## Safety Guarantees
12//! The module ensures the following safety properties:
14//! 1. **Automatic Resource Management**: All C-allocated resources (contexts, strings)
15//! are wrapped in RAII types that guarantee cleanup via `Drop`.
16//! 2. **Memory Safety**: Raw pointers from C are only dereferenced within `unsafe`
17//! blocks with documented safety invariants.
18//! 3. **String Conversion**: All C strings are converted to Rust `String`s with
19//! proper UTF-8 validation and null-termination handling.
20//! 4. **Thread Safety**: The `FersContext` is marked as `Send + Sync` because it
21//! will be protected by a `Mutex` in the application layer.
25//! All fallible operations return `Result<T, String>`, where the error string
26//! contains a human-readable message retrieved from the C library's thread-local
29use std::ffi::{c_void, CStr, CString};
30use std::os::raw::c_char;
31use tauri::{AppHandle, Emitter};
33/// Raw FFI bindings generated by `bindgen` from `libfers/api.h`.
35/// This inner module is kept private to prevent direct access to unsafe FFI
36/// functions. It contains the raw C function declarations and opaque struct types
37/// that mirror the C-API header.
41/// All items in this module are `unsafe` to use directly. They require:
42/// * Valid, non-null pointers for all context handles.
43/// * Proper null-termination for all C strings.
44/// * Manual memory management (allocation/deallocation).
46/// The parent module (`fers_api`) provides safe wrappers that enforce these invariants.
48 #![allow(non_upper_case_globals)]
49 #![allow(non_camel_case_types)]
50 #![allow(non_snake_case)]
52 include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
55/// A smart pointer wrapper for C-allocated strings returned from `libfers`.
57/// This type ensures that the memory allocated by the C library (via `strdup`)
58/// is properly freed when the wrapper goes out of scope, preventing memory leaks.
60/// # Memory Management
62/// The wrapped pointer must have been allocated by a `libfers` API function that
63/// transfers ownership to the caller (e.g., `fers_get_scenario_as_json`). The
64/// `Drop` implementation calls `fers_free_string` to release the memory back to
69/// If the pointer is null, the wrapper treats it as an empty string. This simplifies
70/// error handling in cases where null indicates "no data" rather than an error.
71struct FersOwnedString(*mut c_char);
73impl Drop for FersOwnedString {
74 /// Frees the underlying C string by calling `fers_free_string`.
76 /// This is automatically invoked when the wrapper goes out of scope,
77 /// ensuring that no manual cleanup is required by the caller.
81 /// The pointer must have been allocated by `libfers` (typically via `strdup`)
82 /// and must not have been freed already. The `Drop` trait ensures this is
83 /// called exactly once per instance.
85 if !self.0.is_null() {
86 // SAFETY: The pointer was allocated by `libfers` and is valid until we call `fers_free_string`.
87 // We are the sole owner of this pointer.
88 unsafe { ffi::fers_free_string(self.0) };
94 /// Converts the owned C string to a Rust `String`, consuming the wrapper.
96 /// This method performs UTF-8 validation and copies the string data into a
97 /// Rust-managed `String`. The C-allocated memory is freed after the conversion.
101 /// * `Ok(String)` - The converted string if valid UTF-8.
102 /// * `Err(std::str::Utf8Error)` - If the C string contains invalid UTF-8 bytes.
107 /// let owned = FersOwnedString(some_c_string_ptr);
108 /// match owned.into_string() {
109 /// Ok(s) => println!("Got string: {}", s),
110 /// Err(e) => eprintln!("Invalid UTF-8: {}", e),
113 fn into_string(self) -> Result<String, std::str::Utf8Error> {
114 if self.0.is_null() {
115 return Ok(String::new());
117 // SAFETY: `self.0` is a valid, null-terminated C string from `libfers`.
118 // The `CStr::from_ptr` is safe as long as the pointer is valid.
119 let c_str = unsafe { CStr::from_ptr(self.0) };
120 c_str.to_str().map(|s| s.to_string())
124/// A safe, RAII-style wrapper for the `fers_context_t*` C handle.
126/// This struct encapsulates the lifetime and ownership of a simulation context
127/// created by the `libfers` C++ library. It ensures that:
128/// * The context is created via `fers_context_create` on initialization.
129/// * The context is destroyed via `fers_context_destroy` when dropped.
130/// * The context is never null after successful creation.
134/// This type implements `Send` and `Sync` because the underlying C++ context will
135/// be protected by a `Mutex` in the Tauri application. The C++ `FersContext` class
136/// is not thread-safe, but by serializing all access through Rust's `Mutex`, we
137/// ensure that only one thread can call methods on the context at a time.
142/// use fers_api::FersContext;
144/// let context = FersContext::new().expect("Failed to create context");
145/// context.load_scenario_from_xml_file("scenario.xml")?;
146/// let json = context.get_scenario_as_json()?;
147/// // Context is automatically destroyed when it goes out of scope
149pub struct FersContext {
150 /// The raw pointer to the C++ context object.
152 /// This must be a raw pointer because `fers_context_t` is an opaque struct.
153 /// The `Send` and `Sync` traits are manually implemented because we'll wrap this
154 /// context in a Mutex, ensuring that access to the non-thread-safe C++ object
155 /// is properly synchronized.
156 ptr: *mut ffi::fers_context_t,
159// SAFETY: The FersContext will be protected by a Mutex. All C-API calls on a single
160// context are not guaranteed to be thread-safe by themselves, but by enforcing
161// serialized access through a Mutex, we make its usage safe across threads.
162unsafe impl Send for FersContext {}
163unsafe impl Sync for FersContext {}
165impl Drop for FersContext {
166 /// Destroys the underlying C++ context and frees all associated resources.
168 /// This method is automatically called when the `FersContext` goes out of scope.
169 /// It delegates to `fers_context_destroy`, which is responsible for cleaning up
170 /// the C++ `FersContext` object and its owned `World`.
174 /// The pointer must be valid and non-null. The `Drop` trait guarantees this is
175 /// called exactly once, preventing double-free errors.
177 if !self.ptr.is_null() {
178 // SAFETY: `self.ptr` is a valid handle created by `fers_context_create`.
179 // The `Drop` trait ensures this is called exactly once.
180 unsafe { ffi::fers_context_destroy(self.ptr) };
185/// Retrieves and formats the last error message from the `libfers` C-API.
187/// This helper function is called whenever a C-API function returns an error code.
188/// It queries the thread-local error storage via `fers_get_last_error_message`,
189/// converts the C string to a Rust `String`, and ensures the memory is freed.
193/// A human-readable error message. If no error message is available (null pointer),
194/// a default message is returned. If the error message contains invalid UTF-8, an
195/// error describing the UTF-8 issue is returned instead.
199/// This function is thread-safe because the error storage in `libfers` is thread-local.
200/// Each thread has its own error message buffer.
201fn get_last_error() -> String {
202 // SAFETY: `fers_get_last_error_message` is a thread-safe FFI function.
203 let error_ptr = unsafe { ffi::fers_get_last_error_message() };
204 if error_ptr.is_null() {
205 "An unknown FFI error occurred.".to_string()
207 // The FersOwnedString wrapper ensures the memory is freed.
208 FersOwnedString(error_ptr)
210 .unwrap_or_else(|e| format!("FFI error message contained invalid UTF-8: {}", e))
214/// Data structure for simulation progress events emitted to the frontend.
215#[derive(serde::Serialize, Clone)]
216struct ProgressPayload {
222/// The C-style callback function passed to `fers_run_simulation`.
224/// This function is invoked by the C++ core to report progress. It reconstructs the
225/// `AppHandle` from the `user_data` pointer and emits a Tauri event to the frontend.
229/// This function is marked `unsafe` because it dereferences raw pointers (`message`, `user_data`).
230/// The caller (the C++ library) must guarantee that `message` is a valid, null-terminated
231/// UTF-8 string and that `user_data` is a valid pointer to an `AppHandle`. The pointer
232/// is only valid for the duration of the `fers_run_simulation` call.
233#[allow(clippy::similar_names)]
234extern "C" fn simulation_progress_callback(
235 message: *const c_char,
238 user_data: *mut c_void,
240 if user_data.is_null() {
243 // SAFETY: This is safe because we know `user_data` is a pointer to the AppHandle,
244 // which is guaranteed to be valid for the lifetime of the simulation call.
245 let app_handle = unsafe { &*(user_data as *const AppHandle) };
247 // SAFETY: `message` is guaranteed by the C-API to be a valid, null-terminated string.
248 let message_str = unsafe { CStr::from_ptr(message) }.to_string_lossy().into_owned();
250 let payload = ProgressPayload { message: message_str, current, total };
252 // Emit the event to the frontend. If this fails, there's little we can do
253 // from the callback, so we just let it panic in debug builds.
255 .emit("simulation-progress", payload)
256 .expect("Failed to emit simulation-progress event");
259/// A safe RAII wrapper for the antenna pattern data returned by the C-API.
260struct FersAntennaPatternData(*mut ffi::fers_antenna_pattern_data_t);
261impl Drop for FersAntennaPatternData {
263 if !self.0.is_null() {
264 // SAFETY: The pointer is valid and owned by this struct.
265 unsafe { ffi::fers_free_antenna_pattern_data(self.0) };
270/// Data structure for antenna pattern data sent to the frontend.
272/// This struct flattens the 2D gain data into a 1D vector for easy serialization.
273#[derive(serde::Serialize)]
274pub struct AntennaPatternData {
275 /// Flattened array of linear gain values (normalized 0.0 to 1.0).
276 /// Ordered row-major: Elevation rows, then Azimuth columns.
278 /// Number of samples along the azimuth axis (360 degrees).
280 /// Number of samples along the elevation axis (180 degrees).
282 /// The peak linear gain found in the pattern, used for normalization.
286// Helper wrapper for the visual link list
287struct FersVisualLinkList(*mut ffi::fers_visual_link_list_t);
289impl Drop for FersVisualLinkList {
291 if !self.0.is_null() {
292 unsafe { ffi::fers_free_preview_links(self.0) };
297/// Represents a visual link segment for 3D rendering.
299/// This struct maps C-style enums to integers for consumption by the TypeScript frontend.
300#[derive(serde::Serialize)]
301pub struct VisualLink {
302 /// The type of radio link.
303 /// * `0`: Monostatic (Tx -> Tgt -> Rx, where Tx==Rx)
304 /// * `1`: Bistatic Illuminator (Tx -> Tgt)
305 /// * `2`: Bistatic Scattered (Tgt -> Rx)
306 /// * `3`: Direct Interference (Tx -> Rx)
308 /// The radiometric quality of the link.
309 /// * `0`: Strong (SNR > 0 dB)
310 /// * `1`: Weak (SNR < 0 dB, visible but sub-noise)
312 /// A pre-formatted label string (e.g., "-95 dBm").
314 /// The name of the start component for this segment.
315 pub source_name: String,
316 /// The name of the end component for this segment.
317 pub dest_name: String,
318 /// The name of the original transmitter (useful for scattered paths).
319 pub origin_name: String,
323 /// Creates a new `FersContext` by calling the C-API constructor.
325 /// This function allocates a new C++ `FersContext` object on the heap via
326 /// `fers_context_create`. The returned context is initially empty and must be
327 /// populated by loading a scenario (via `load_scenario_from_xml_file` or
328 /// `update_scenario_from_json`).
332 /// * `Some(FersContext)` - If the context was successfully created.
333 /// * `None` - If allocation failed (e.g., out of memory) or if the C++ constructor
334 /// threw an exception.
339 /// let context = FersContext::new().expect("Failed to create FERS context");
341 pub fn new() -> Option<Self> {
342 // SAFETY: `fers_context_create` is a simple constructor function with no preconditions.
343 let ptr = unsafe { ffi::fers_context_create() };
351 /// Loads a FERS scenario from an XML file into the context.
353 /// This method replaces any existing scenario in the context with the one parsed
354 /// from the specified file. The XML is validated against the FERS schema if
355 /// validation is enabled in the C++ library.
359 /// * `filepath` - A UTF-8 string containing the absolute or relative path to the
360 /// FERS XML scenario file.
364 /// * `Ok(())` - If the scenario was successfully loaded and parsed.
365 /// * `Err(String)` - If the file could not be read, the XML was invalid, or a
366 /// C++ exception was thrown. The error string contains details.
371 /// context.load_scenario_from_xml_file("/path/to/scenario.xml")?;
373 pub fn load_scenario_from_xml_file(&self, filepath: &str) -> Result<(), String> {
374 let c_filepath = CString::new(filepath).map_err(|e| e.to_string())?;
375 // SAFETY: We pass a valid context pointer and a null-terminated C string.
376 // The function returns 0 on success.
378 unsafe { ffi::fers_load_scenario_from_xml_file(self.ptr, c_filepath.as_ptr(), 1) };
382 Err(get_last_error())
386 /// Retrieves the current in-memory scenario as a JSON string.
388 /// This method serializes the C++ `World` object into JSON format, which mirrors
389 /// the structure used by the frontend. It is typically used to populate the UI
390 /// after loading a scenario from XML.
394 /// * `Ok(String)` - The JSON representation of the scenario.
395 /// * `Err(String)` - If serialization failed or the JSON contains invalid UTF-8.
397 /// # Memory Management
399 /// The returned string is a Rust-owned `String`. The underlying C-allocated memory
400 /// is automatically freed by the `FersOwnedString` wrapper.
405 /// let json = context.get_scenario_as_json()?;
406 /// let scenario: serde_json::Value = serde_json::from_str(&json)?;
408 pub fn get_scenario_as_json(&self) -> Result<String, String> {
409 // SAFETY: We pass a valid context pointer. The function returns a C string
410 // that we must free.
411 let json_ptr = unsafe { ffi::fers_get_scenario_as_json(self.ptr) };
412 if json_ptr.is_null() {
413 return Err(get_last_error());
415 // FersOwnedString takes ownership and will free the memory on drop.
416 FersOwnedString(json_ptr).into_string().map_err(|e| e.to_string())
419 /// Retrieves the current in-memory scenario as a FERS XML string.
421 /// This method serializes the C++ `World` object into the standard FERS XML format.
422 /// It is typically used when the user wants to export the scenario (potentially
423 /// modified in the UI) back to a file.
427 /// * `Ok(String)` - The XML representation of the scenario.
428 /// * `Err(String)` - If serialization failed or the XML contains invalid UTF-8.
430 /// # Memory Management
432 /// The returned string is a Rust-owned `String`. The underlying C-allocated memory
433 /// is automatically freed by the `FersOwnedString` wrapper.
438 /// let xml = context.get_scenario_as_xml()?;
439 /// std::fs::write("exported_scenario.xml", xml)?;
441 pub fn get_scenario_as_xml(&self) -> Result<String, String> {
442 // SAFETY: We pass a valid context pointer. The function returns a C string
443 // that we must free.
444 let xml_ptr = unsafe { ffi::fers_get_scenario_as_xml(self.ptr) };
445 if xml_ptr.is_null() {
446 return Err(get_last_error());
448 // FersOwnedString takes ownership and will free the memory on drop.
449 FersOwnedString(xml_ptr).into_string().map_err(|e| e.to_string())
452 /// Updates the in-memory scenario from a JSON string.
454 /// This method is the primary way for the UI to push modified scenario data back
455 /// to the C++ simulation engine. It deserializes the JSON and rebuilds the internal
456 /// `World` object, replacing any existing scenario.
460 /// * `json` - A UTF-8 JSON string representing the scenario. The structure must
461 /// match the schema expected by `libfers` (the same format returned by
462 /// `get_scenario_as_json`).
466 /// * `Ok(())` - If the scenario was successfully deserialized and loaded.
467 /// * `Err(String)` - If the JSON was malformed, contained invalid data, or a C++
468 /// exception was thrown. The error string contains details.
473 /// let modified_json = /* JSON from UI */;
474 /// context.update_scenario_from_json(&modified_json)?;
476 pub fn update_scenario_from_json(&self, json: &str) -> Result<(), String> {
477 let c_json = CString::new(json).map_err(|e| e.to_string())?;
478 // SAFETY: We pass a valid context pointer and a null-terminated C string.
479 // The function returns 0 on success.
480 let result = unsafe { ffi::fers_update_scenario_from_json(self.ptr, c_json.as_ptr()) };
484 Err(get_last_error())
488 /// Runs the simulation defined in the context.
490 /// This is a blocking call that executes the simulation on a separate thread pool
491 /// managed by the C++ core. It accepts a Tauri `AppHandle` to enable progress
492 /// reporting via events.
496 /// * `app_handle` - A reference to the Tauri application handle, used for emitting events.
500 /// * `Ok(())` - If the simulation completed successfully.
501 /// * `Err(String)` - If the simulation failed.
502 pub fn run_simulation(&self, app_handle: &AppHandle) -> Result<(), String> {
503 // The AppHandle is passed as a raw pointer through the `user_data` argument.
504 // This is safe because this function is blocking, and the app_handle reference
505 // will be valid for the entire duration of the C++ call.
506 let user_data_ptr = app_handle as *const _ as *mut c_void;
508 // SAFETY: We pass a valid context pointer, a valid function pointer for the callback,
509 // and a valid user_data pointer that points to the AppHandle.
510 let result = unsafe {
511 ffi::fers_run_simulation(self.ptr, Some(simulation_progress_callback), user_data_ptr)
517 Err(get_last_error())
521 /// Generates a KML file for the current scenario.
525 /// * `output_path` - The path where the KML file will be saved.
529 /// * `Ok(())` - If the KML file was generated successfully.
530 /// * `Err(String)` - If KML generation failed.
531 pub fn generate_kml(&self, output_path: &str) -> Result<(), String> {
532 let c_output_path = CString::new(output_path).map_err(|e| e.to_string())?;
533 // SAFETY: We pass a valid context pointer and a null-terminated C string for the path.
534 let result = unsafe { ffi::fers_generate_kml(self.ptr, c_output_path.as_ptr()) };
538 Err(get_last_error())
542 /// Retrieves a sampled gain pattern for a specified antenna.
546 /// * `antenna_name` - The name of the antenna asset to sample.
547 /// * `az_samples` - The resolution along the azimuth axis.
548 /// * `el_samples` - The resolution along the elevation axis.
549 /// * `frequency` - The frequency in Hz to use for calculation.
553 /// * `Ok(AntennaPatternData)` - If the pattern was successfully sampled.
554 /// * `Err(String)` - If the antenna was not found or an error occurred.
555 pub fn get_antenna_pattern(
561 ) -> Result<AntennaPatternData, String> {
562 let c_antenna_name = CString::new(antenna_name).map_err(|e| e.to_string())?;
563 // SAFETY: We pass a valid context pointer and valid arguments.
564 let result_ptr = unsafe {
565 ffi::fers_get_antenna_pattern(
567 c_antenna_name.as_ptr(),
574 if result_ptr.is_null() {
575 return Err(get_last_error());
578 let owned_data = FersAntennaPatternData(result_ptr);
580 // SAFETY: Dereferencing the non-null pointer returned by the FFI.
581 // The data is valid for the lifetime of `owned_data`.
582 let (gains_ptr, az_count, el_count, max_gain) = unsafe {
584 (*owned_data.0).gains,
585 (*owned_data.0).az_count,
586 (*owned_data.0).el_count,
587 (*owned_data.0).max_gain,
590 let total_samples = az_count * el_count;
592 // SAFETY: The gains_ptr is valid for `total_samples` elements.
593 let gains_slice = unsafe { std::slice::from_raw_parts(gains_ptr, total_samples) };
595 Ok(AntennaPatternData { gains: gains_slice.to_vec(), az_count, el_count, max_gain })
598 pub fn calculate_preview_links(&self, time: f64) -> Result<Vec<VisualLink>, String> {
599 let list_ptr = unsafe { ffi::fers_calculate_preview_links(self.ptr, time) };
600 if list_ptr.is_null() {
601 let err_msg = get_last_error();
602 // If we receive a NULL ptr but no error message, it might be a logic error
603 // or an unhandled edge case in C++. We default to the retrieved message.
607 let owned_list = FersVisualLinkList(list_ptr);
608 let count = unsafe { (*owned_list.0).count };
609 let links_ptr = unsafe { (*owned_list.0).links };
611 let mut result = Vec::with_capacity(count);
612 if count > 0 && !links_ptr.is_null() {
613 let slice = unsafe { std::slice::from_raw_parts(links_ptr, count) };
615 let link_type_val = match l.type_ {
616 ffi::fers_link_type_t_FERS_LINK_MONOSTATIC => 0,
617 ffi::fers_link_type_t_FERS_LINK_BISTATIC_TX_TGT => 1,
618 ffi::fers_link_type_t_FERS_LINK_BISTATIC_TGT_RX => 2,
619 ffi::fers_link_type_t_FERS_LINK_DIRECT_TX_RX => 3,
623 let quality_val = match l.quality {
624 ffi::fers_link_quality_t_FERS_LINK_STRONG => 0,
629 unsafe { CStr::from_ptr(l.label.as_ptr()) }.to_string_lossy().into_owned();
630 let source_name = unsafe { CStr::from_ptr(l.source_name.as_ptr()) }
634 unsafe { CStr::from_ptr(l.dest_name.as_ptr()) }.to_string_lossy().into_owned();
635 let origin_name = unsafe { CStr::from_ptr(l.origin_name.as_ptr()) }
639 result.push(VisualLink {
640 link_type: link_type_val,
641 quality: quality_val,
653/// A safe wrapper for the stateless `fers_get_interpolated_motion_path` C-API function.
655/// This function converts Rust-native waypoint data into C-compatible types,
656/// calls the FFI function, and then converts the result back into a `Vec` of points,
657/// ensuring that all C-allocated memory is properly freed.
660/// * `waypoints` - A vector of motion waypoints from the frontend.
661/// * `interp_type` - The interpolation algorithm to use.
662/// * `num_points` - The desired number of points in the output path.
665/// * `Ok(Vec<InterpolatedPoint>)` - A vector of points representing the calculated path.
666/// * `Err(String)` - An error message if the FFI call failed.
667pub fn get_interpolated_motion_path(
668 waypoints: Vec<crate::MotionWaypoint>,
669 interp_type: crate::InterpolationType,
671) -> Result<Vec<crate::InterpolatedMotionPoint>, String> {
672 if waypoints.is_empty() || num_points == 0 {
673 return Ok(Vec::new());
676 let c_waypoints: Vec<ffi::fers_motion_waypoint_t> = waypoints
678 .map(|wp| ffi::fers_motion_waypoint_t { time: wp.time, x: wp.x, y: wp.y, z: wp.altitude })
681 let c_interp_type = match interp_type {
682 crate::InterpolationType::Static => ffi::fers_interp_type_t_FERS_INTERP_STATIC,
683 crate::InterpolationType::Linear => ffi::fers_interp_type_t_FERS_INTERP_LINEAR,
684 crate::InterpolationType::Cubic => ffi::fers_interp_type_t_FERS_INTERP_CUBIC,
687 // SAFETY: We are calling the stateless FFI function with valid, well-formed arguments.
688 // The pointer returned is owned by us and must be freed.
689 let result_ptr = unsafe {
690 ffi::fers_get_interpolated_motion_path(
691 c_waypoints.as_ptr(),
698 if result_ptr.is_null() {
699 return Err(get_last_error());
702 // RAII wrapper to ensure the C-allocated path is freed.
703 struct FersInterpolatedMotionPath(*mut ffi::fers_interpolated_path_t);
705 impl Drop for FersInterpolatedMotionPath {
707 if !self.0.is_null() {
708 // SAFETY: The pointer is valid and owned by this struct.
709 unsafe { ffi::fers_free_interpolated_motion_path(self.0) };
714 let owned_path = FersInterpolatedMotionPath(result_ptr);
716 // SAFETY: We are accessing the fields of a non-null pointer returned by the FFI.
717 // The `count` and `points` fields are guaranteed to be valid for the lifetime of `owned_path`.
719 unsafe { std::slice::from_raw_parts((*owned_path.0).points, (*owned_path.0).count) };
721 let points: Vec<crate::InterpolatedMotionPoint> = result_slice
723 .map(|p| crate::InterpolatedMotionPoint {
736/// A safe wrapper for the stateless `fers_get_interpolated_rotation_path` C-API function.
738/// This function converts Rust-native rotation waypoints into C-compatible types,
739/// calls the FFI function, and converts the result back into a `Vec` of points.
740/// It handles the mapping between Rust enums and C enums for interpolation types
741/// and ensures that the C-allocated array is properly freed.
744/// * `waypoints` - A vector of `RotationWaypoint`s.
745/// * `interp_type` - The interpolation algorithm to use.
746/// * `num_points` - The desired number of points in the output path.
749/// * `Ok(Vec<InterpolatedRotationPoint>)` - A vector of interpolated points.
750/// * `Err(String)` - An error message if the FFI call failed.
751pub fn get_interpolated_rotation_path(
752 waypoints: Vec<crate::RotationWaypoint>,
753 interp_type: crate::InterpolationType,
755) -> Result<Vec<crate::InterpolatedRotationPoint>, String> {
756 if waypoints.is_empty() || num_points == 0 {
757 return Ok(Vec::new());
760 let c_waypoints: Vec<ffi::fers_rotation_waypoint_t> = waypoints
762 .map(|wp| ffi::fers_rotation_waypoint_t {
764 azimuth_deg: wp.azimuth,
765 elevation_deg: wp.elevation,
769 let c_interp_type = match interp_type {
770 crate::InterpolationType::Static => ffi::fers_interp_type_t_FERS_INTERP_STATIC,
771 crate::InterpolationType::Linear => ffi::fers_interp_type_t_FERS_INTERP_LINEAR,
772 crate::InterpolationType::Cubic => ffi::fers_interp_type_t_FERS_INTERP_CUBIC,
775 let result_ptr = unsafe {
776 ffi::fers_get_interpolated_rotation_path(
777 c_waypoints.as_ptr(),
784 if result_ptr.is_null() {
785 return Err(get_last_error());
788 struct FersInterpolatedRotationPath(*mut ffi::fers_interpolated_rotation_path_t);
790 impl Drop for FersInterpolatedRotationPath {
792 if !self.0.is_null() {
793 unsafe { ffi::fers_free_interpolated_rotation_path(self.0) };
798 let owned_path = FersInterpolatedRotationPath(result_ptr);
801 unsafe { std::slice::from_raw_parts((*owned_path.0).points, (*owned_path.0).count) };
803 let points: Vec<crate::InterpolatedRotationPoint> = result_slice
805 .map(|p| crate::InterpolatedRotationPoint {
806 azimuth_deg: p.azimuth_deg,
807 elevation_deg: p.elevation_deg,