Skip to main content

ballistics_engine/
ffi.rs

1//! FFI bindings for iOS/Swift integration
2
3use crate::{
4    calculate_zero_angle_with_conditions, run_monte_carlo, AtmosphericConditions, BallisticInputs,
5    DragModel, MonteCarloParams, TrajectorySolver, WindConditions,
6};
7use std::ffi::CString;
8use std::os::raw::{c_char, c_double, c_int};
9use std::ptr;
10
11// FFI-safe structures with C-compatible layouts
12
13#[repr(C)]
14pub struct FFIBallisticInputs {
15    pub muzzle_velocity: c_double,         // m/s
16    pub muzzle_angle: c_double,            // radians (launch angle)
17    pub bc_value: c_double,                // ballistic coefficient
18    pub bullet_mass: c_double,             // kg
19    pub bullet_diameter: c_double,         // meters
20    pub bc_type: c_int,                    // 0=G1, 1=G7, etc.
21    pub sight_height: c_double,            // meters
22    pub target_distance: c_double,         // meters
23    pub temperature: c_double,             // Celsius
24    pub twist_rate: c_double,              // inches per turn
25    pub is_twist_right: c_int,             // 0=false, 1=true
26    pub shooting_angle: c_double,          // uphill/downhill angle in radians
27    pub altitude: c_double,                // meters
28    pub latitude: c_double,                // degrees (use NAN if not provided)
29    pub azimuth_angle: c_double,           // horizontal aiming angle in radians
30    pub use_rk4: c_int,                    // 0=Euler, 1=RK4
31    pub use_adaptive_rk45: c_int,          // 0=false, 1=true (adaptive RK45)
32    pub enable_wind_shear: c_int,          // 0=false, 1=true
33    pub enable_trajectory_sampling: c_int, // 0=false, 1=true
34    pub sample_interval: c_double,         // meters
35    pub enable_pitch_damping: c_int,       // 0=false, 1=true
36    pub enable_precession_nutation: c_int, // 0=false, 1=true
37    pub enable_spin_drift: c_int,          // 0=false, 1=true
38    pub enable_magnus: c_int,              // 0=false, 1=true
39    pub enable_coriolis: c_int,            // 0=false, 1=true
40}
41
42#[repr(C)]
43pub struct FFIWindConditions {
44    pub speed: c_double,     // m/s
45    pub direction: c_double, // radians (0 = North, PI/2 = East)
46}
47
48#[repr(C)]
49pub struct FFIAtmosphericConditions {
50    pub temperature: c_double, // Celsius
51    pub pressure: c_double,    // hPa
52    pub humidity: c_double,    // percentage (0-100)
53    pub altitude: c_double,    // meters
54}
55
56#[repr(C)]
57pub struct FFITrajectorySample {
58    pub distance: c_double,       // meters
59    pub time: c_double,           // seconds
60    pub velocity_mps: c_double,   // meters per second
61    pub energy_joules: c_double,  // joules
62    pub drop_meters: c_double,    // meters
63    pub windage_meters: c_double, // meters
64    pub mach: c_double,           // Mach number
65    pub spin_rate_rps: c_double,  // revolutions per second
66}
67
68#[repr(C)]
69pub struct FFITrajectoryPoint {
70    pub time: c_double,
71    pub position_x: c_double,
72    pub position_y: c_double,
73    pub position_z: c_double,
74    pub velocity_magnitude: c_double,
75    pub kinetic_energy: c_double,
76}
77
78#[repr(C)]
79pub struct FFITrajectoryResult {
80    pub max_range: c_double,
81    pub max_height: c_double,
82    pub time_of_flight: c_double,
83    pub impact_velocity: c_double,
84    pub impact_energy: c_double,
85    pub points: *mut FFITrajectoryPoint,
86    pub point_count: c_int,
87    pub sampled_points: *mut FFITrajectorySample,
88    pub sampled_point_count: c_int,
89    pub min_pitch_damping: c_double,    // NAN if not calculated
90    pub transonic_mach: c_double,       // NAN if not reached
91    pub final_pitch_angle: c_double,    // NAN if not calculated
92    pub final_yaw_angle: c_double,      // NAN if not calculated
93    pub max_yaw_angle: c_double,        // NAN if not calculated
94    pub max_precession_angle: c_double, // NAN if not calculated
95}
96
97// Monte Carlo simulation parameters
98#[repr(C)]
99pub struct FFIMonteCarloParams {
100    pub num_simulations: c_int,
101    pub velocity_std_dev: c_double,
102    pub angle_std_dev: c_double,
103    pub bc_std_dev: c_double,
104    pub wind_speed_std_dev: c_double,
105    pub target_distance: c_double,     // Use NAN if not specified
106    pub base_wind_speed: c_double,     // Base wind speed in m/s
107    pub base_wind_direction: c_double, // Base wind direction in radians
108    pub azimuth_std_dev: c_double,     // Horizontal aiming variation in radians
109}
110
111// Monte Carlo simulation results
112#[repr(C)]
113pub struct FFIMonteCarloResults {
114    pub ranges: *mut c_double,
115    pub impact_velocities: *mut c_double,
116    pub impact_positions_x: *mut c_double,
117    pub impact_positions_y: *mut c_double,
118    pub impact_positions_z: *mut c_double,
119    pub num_results: c_int,
120    pub mean_range: c_double,
121    pub std_dev_range: c_double,
122    pub mean_impact_velocity: c_double,
123    pub std_dev_impact_velocity: c_double,
124    pub hit_probability: c_double, // If target_distance was specified
125}
126
127// Helper function to convert FFI inputs to internal types
128fn convert_inputs(inputs: &FFIBallisticInputs) -> BallisticInputs {
129    let mut ballistic_inputs = BallisticInputs::default();
130
131    ballistic_inputs.muzzle_velocity = inputs.muzzle_velocity;
132    ballistic_inputs.muzzle_angle = inputs.muzzle_angle;
133    ballistic_inputs.azimuth_angle = inputs.azimuth_angle;
134    ballistic_inputs.use_rk4 = inputs.use_rk4 != 0;
135    ballistic_inputs.use_adaptive_rk45 = inputs.use_adaptive_rk45 != 0;
136    ballistic_inputs.bc_value = inputs.bc_value;
137    ballistic_inputs.bullet_mass = inputs.bullet_mass;
138    ballistic_inputs.bullet_diameter = inputs.bullet_diameter;
139    ballistic_inputs.bc_type = match inputs.bc_type {
140        1 => DragModel::G7,
141        2 => DragModel::G2,
142        3 => DragModel::G5,
143        4 => DragModel::G6,
144        5 => DragModel::G8,
145        6 => DragModel::GI,
146        7 => DragModel::GS,
147        _ => DragModel::G1,
148    };
149    ballistic_inputs.sight_height = inputs.sight_height;
150    ballistic_inputs.target_distance = inputs.target_distance;
151    ballistic_inputs.temperature = inputs.temperature;
152    ballistic_inputs.twist_rate = inputs.twist_rate;
153    ballistic_inputs.is_twist_right = inputs.is_twist_right != 0;
154    ballistic_inputs.shooting_angle = inputs.shooting_angle;
155    ballistic_inputs.altitude = inputs.altitude;
156
157    if !inputs.latitude.is_nan() {
158        ballistic_inputs.latitude = Some(inputs.latitude);
159    }
160
161    // Set derived values
162    ballistic_inputs.caliber_inches = inputs.bullet_diameter / 0.0254;
163    ballistic_inputs.weight_grains = inputs.bullet_mass / 0.00006479891;
164    ballistic_inputs.bullet_length = inputs.bullet_diameter * 4.0;
165
166    // New advanced physics flags
167    ballistic_inputs.enable_wind_shear = inputs.enable_wind_shear != 0;
168    ballistic_inputs.enable_trajectory_sampling = inputs.enable_trajectory_sampling != 0;
169    ballistic_inputs.sample_interval = inputs.sample_interval;
170    ballistic_inputs.enable_pitch_damping = inputs.enable_pitch_damping != 0;
171    ballistic_inputs.enable_precession_nutation = inputs.enable_precession_nutation != 0;
172    ballistic_inputs.use_enhanced_spin_drift = inputs.enable_spin_drift != 0;
173    ballistic_inputs.enable_advanced_effects =
174        inputs.enable_magnus != 0 || inputs.enable_coriolis != 0;
175    // Gate Magnus and Coriolis independently so enabling one does not enable the other.
176    ballistic_inputs.enable_magnus = inputs.enable_magnus != 0;
177    ballistic_inputs.enable_coriolis = inputs.enable_coriolis != 0;
178
179    ballistic_inputs
180}
181
182// Main trajectory calculation function for FFI
183#[no_mangle]
184pub extern "C" fn ballistics_calculate_trajectory(
185    inputs: *const FFIBallisticInputs,
186    wind: *const FFIWindConditions,
187    atmosphere: *const FFIAtmosphericConditions,
188    max_range: c_double,
189    step_size: c_double,
190) -> *mut FFITrajectoryResult {
191    if inputs.is_null() {
192        return ptr::null_mut();
193    }
194
195    let inputs = unsafe { &*inputs };
196    let ballistic_inputs = convert_inputs(inputs);
197
198    let wind_conditions = if wind.is_null() {
199        WindConditions::default()
200    } else {
201        let wind = unsafe { &*wind };
202        WindConditions {
203            speed: wind.speed,
204            direction: wind.direction,
205        }
206    };
207
208    let atmospheric_conditions = if atmosphere.is_null() {
209        AtmosphericConditions::default()
210    } else {
211        let atmo = unsafe { &*atmosphere };
212        AtmosphericConditions {
213            temperature: atmo.temperature,
214            pressure: atmo.pressure,
215            humidity: atmo.humidity,
216            altitude: atmo.altitude,
217        }
218    };
219
220    // Create solver and calculate trajectory
221    let mut solver =
222        TrajectorySolver::new(ballistic_inputs, wind_conditions, atmospheric_conditions);
223
224    // Set max range and time step
225    solver.set_max_range(max_range);
226    solver.set_time_step(step_size / 1000.0); // Convert to time step
227
228    match solver.solve() {
229        Ok(result) => {
230            // Convert trajectory points to FFI format
231            let point_count = result.points.len();
232            let points = if point_count > 0 {
233                let mut ffi_points = Vec::with_capacity(point_count);
234                for (i, point) in result.points.iter().enumerate() {
235                    ffi_points.push(FFITrajectoryPoint {
236                        time: point.time,
237                        position_x: point.position[0],
238                        position_y: point.position[1],
239                        position_z: point.position[2],
240                        velocity_magnitude: point.velocity_magnitude,
241                        kinetic_energy: point.kinetic_energy,
242                    });
243
244                    // Debug: Log first, last, and every 100th point.
245                    // McCoy coordinate system: X=downrange, Y=vertical, Z=lateral.
246                    // Raw position_x/_y/_z exported above are McCoy-ordered (X=downrange).
247                    #[cfg(debug_assertions)]
248                    if i == 0 || i == result.points.len() - 1 || i % 100 == 0 {
249                        eprintln!(
250                            "FFI point {}: lateral={:.2}m, vertical={:.2}m, downrange={:.2}m",
251                            i, point.position[2], point.position[1], point.position[0]
252                        );
253                    }
254                }
255                let points_ptr = ffi_points.as_mut_ptr();
256                std::mem::forget(ffi_points); // Prevent deallocation
257                points_ptr
258            } else {
259                ptr::null_mut()
260            };
261
262            // Convert sampled points if available
263            let (sampled_points, sampled_point_count) =
264                if let Some(ref samples) = result.sampled_points {
265                    let mut ffi_samples = Vec::with_capacity(samples.len());
266                    for sample in samples {
267                        ffi_samples.push(FFITrajectorySample {
268                            distance: sample.distance_m,
269                            time: sample.time_s,
270                            velocity_mps: sample.velocity_mps,
271                            energy_joules: sample.energy_j,
272                            drop_meters: sample.drop_m,
273                            windage_meters: sample.wind_drift_m,
274                            mach: 0.0,          // Would need to calculate from velocity
275                            spin_rate_rps: 0.0, // Not available in TrajectorySample
276                        });
277                    }
278                    let count = ffi_samples.len() as c_int;
279                    let samples_ptr = ffi_samples.as_mut_ptr();
280                    std::mem::forget(ffi_samples);
281                    (samples_ptr, count)
282                } else {
283                    (ptr::null_mut(), 0)
284                };
285
286            // Extract angular state values if available
287            let (final_pitch, final_yaw, max_yaw, max_prec) =
288                if let Some(ref angular) = result.angular_state {
289                    (
290                        angular.pitch_angle,
291                        angular.yaw_angle,
292                        result.max_yaw_angle.unwrap_or(std::f64::NAN),
293                        result.max_precession_angle.unwrap_or(std::f64::NAN),
294                    )
295                } else {
296                    (std::f64::NAN, std::f64::NAN, std::f64::NAN, std::f64::NAN)
297                };
298
299            // Create result on heap
300            let ffi_result = Box::new(FFITrajectoryResult {
301                max_range: result.max_range,
302                max_height: result.max_height,
303                time_of_flight: result.time_of_flight,
304                impact_velocity: result.impact_velocity,
305                impact_energy: result.impact_energy,
306                points,
307                point_count: point_count as c_int,
308                sampled_points,
309                sampled_point_count,
310                min_pitch_damping: result.min_pitch_damping.unwrap_or(std::f64::NAN),
311                transonic_mach: result.transonic_mach.unwrap_or(std::f64::NAN),
312                final_pitch_angle: final_pitch,
313                final_yaw_angle: final_yaw,
314                max_yaw_angle: max_yaw,
315                max_precession_angle: max_prec,
316            });
317
318            Box::into_raw(ffi_result)
319        }
320        Err(_) => ptr::null_mut(),
321    }
322}
323
324// Free allocated trajectory result
325#[no_mangle]
326pub extern "C" fn ballistics_free_trajectory_result(result: *mut FFITrajectoryResult) {
327    if !result.is_null() {
328        unsafe {
329            let result = Box::from_raw(result);
330            if !result.points.is_null() && result.point_count > 0 {
331                let points = Vec::from_raw_parts(
332                    result.points,
333                    result.point_count as usize,
334                    result.point_count as usize,
335                );
336                drop(points);
337            }
338            if !result.sampled_points.is_null() && result.sampled_point_count > 0 {
339                let samples = Vec::from_raw_parts(
340                    result.sampled_points,
341                    result.sampled_point_count as usize,
342                    result.sampled_point_count as usize,
343                );
344                drop(samples);
345            }
346            drop(result);
347        }
348    }
349}
350
351// Calculate zero angle for a given target distance
352#[no_mangle]
353pub extern "C" fn ballistics_calculate_zero_angle(
354    inputs: *const FFIBallisticInputs,
355    wind: *const FFIWindConditions,
356    atmosphere: *const FFIAtmosphericConditions,
357    zero_distance: c_double,
358) -> c_double {
359    if inputs.is_null() {
360        return f64::NAN;
361    }
362
363    let inputs = unsafe { &*inputs };
364    let ballistic_inputs = convert_inputs(inputs);
365
366    let wind_conditions = if wind.is_null() {
367        WindConditions::default()
368    } else {
369        let wind = unsafe { &*wind };
370        WindConditions {
371            speed: wind.speed,
372            direction: wind.direction,
373        }
374    };
375
376    let atmospheric_conditions = if atmosphere.is_null() {
377        AtmosphericConditions::default()
378    } else {
379        let atmo = unsafe { &*atmosphere };
380        AtmosphericConditions {
381            temperature: atmo.temperature,
382            pressure: atmo.pressure,
383            humidity: atmo.humidity,
384            altitude: atmo.altitude,
385        }
386    };
387
388    // For zero angle, we want the bullet to hit at sight height at the zero distance
389    // This means the bullet crosses the line of sight at the zero distance
390    let target_height = ballistic_inputs.sight_height;
391
392    #[cfg(debug_assertions)]
393    {
394        eprintln!("FFI: Calculating zero angle for:");
395        eprintln!("  Zero distance: {} m", zero_distance);
396        eprintln!("  Target height: {} m", target_height);
397        eprintln!("  Sight height: {} m", ballistic_inputs.sight_height);
398        eprintln!("  Wind speed: {} m/s", wind_conditions.speed);
399        eprintln!("  Temperature: {} C", atmospheric_conditions.temperature);
400    }
401
402    match calculate_zero_angle_with_conditions(
403        ballistic_inputs,
404        zero_distance,
405        target_height,
406        wind_conditions,
407        atmospheric_conditions,
408    ) {
409        Ok(angle) => {
410            #[cfg(debug_assertions)]
411            eprintln!(
412                "  Calculated angle: {} rad ({} deg)",
413                angle,
414                angle * 180.0 / std::f64::consts::PI
415            );
416            angle
417        }
418        Err(e) => {
419            #[cfg(debug_assertions)]
420            eprintln!("  Error: {:?}", e);
421            f64::NAN
422        }
423    }
424}
425
426// Simple trajectory calculation for quick results
427#[no_mangle]
428pub extern "C" fn ballistics_quick_trajectory(
429    muzzle_velocity: c_double,
430    bc: c_double,
431    sight_height: c_double,
432    zero_distance: c_double,
433    target_distance: c_double,
434) -> c_double {
435    // This provides a simple drop calculation at target distance
436    // Using simplified ballistic calculations
437
438    let mut inputs = BallisticInputs::default();
439    inputs.muzzle_velocity = muzzle_velocity;
440    inputs.bc_value = bc;
441    inputs.sight_height = sight_height;
442    inputs.target_distance = target_distance;
443
444    let wind = WindConditions::default();
445    let atmo = AtmosphericConditions::default();
446
447    // First calculate zero angle
448    let zero_angle = match calculate_zero_angle_with_conditions(
449        inputs.clone(),
450        zero_distance,
451        sight_height,
452        wind.clone(),
453        atmo.clone(),
454    ) {
455        Ok(angle) => angle,
456        Err(_) => return f64::NAN,
457    };
458
459    // Now calculate trajectory with that zero angle
460    inputs.muzzle_angle = zero_angle;
461
462    let mut solver = TrajectorySolver::new(inputs, wind, atmo);
463    solver.set_max_range(target_distance * 1.1);
464
465    match solver.solve() {
466        Ok(result) => {
467            // Find the drop at target distance
468            for point in result.points {
469                if point.position[0] >= target_distance {
470                    return -point.position[1]; // Return drop (negative y is drop)
471                }
472            }
473            f64::NAN
474        }
475        Err(_) => f64::NAN,
476    }
477}
478
479// Monte Carlo simulation
480#[no_mangle]
481pub extern "C" fn ballistics_monte_carlo(
482    inputs: *const FFIBallisticInputs,
483    _atmosphere: *const FFIAtmosphericConditions,
484    params: *const FFIMonteCarloParams,
485) -> *mut FFIMonteCarloResults {
486    if inputs.is_null() || params.is_null() {
487        return ptr::null_mut();
488    }
489
490    let inputs = unsafe { &*inputs };
491    let params = unsafe { &*params };
492
493    // Convert FFI inputs to internal types
494    let ballistic_inputs = convert_inputs(inputs);
495
496    // Note: Atmospheric conditions are already included in the conversion
497    // from FFIBallisticInputs (temperature, altitude, etc.)
498
499    // Create Monte Carlo parameters
500    let mc_params = MonteCarloParams {
501        num_simulations: params.num_simulations as usize,
502        velocity_std_dev: params.velocity_std_dev,
503        angle_std_dev: params.angle_std_dev,
504        bc_std_dev: params.bc_std_dev,
505        wind_speed_std_dev: params.wind_speed_std_dev,
506        target_distance: if params.target_distance.is_nan() {
507            None
508        } else {
509            Some(params.target_distance)
510        },
511        base_wind_speed: params.base_wind_speed,
512        base_wind_direction: params.base_wind_direction,
513        azimuth_std_dev: params.azimuth_std_dev,
514    };
515
516    // Run Monte Carlo simulation
517    match run_monte_carlo(ballistic_inputs, mc_params) {
518        Ok(results) => {
519            let num_results = results.ranges.len() as c_int;
520
521            // Calculate statistics
522            let mean_range: f64 = results.ranges.iter().sum::<f64>() / num_results as f64;
523            let variance_range: f64 = results
524                .ranges
525                .iter()
526                .map(|r| (r - mean_range).powi(2))
527                .sum::<f64>()
528                / num_results as f64;
529            let std_dev_range = variance_range.sqrt();
530
531            let mean_velocity: f64 =
532                results.impact_velocities.iter().sum::<f64>() / num_results as f64;
533            let variance_velocity: f64 = results
534                .impact_velocities
535                .iter()
536                .map(|v| (v - mean_velocity).powi(2))
537                .sum::<f64>()
538                / num_results as f64;
539            let std_dev_velocity = variance_velocity.sqrt();
540
541            // Calculate hit probability if target distance was specified
542            let hit_probability = if params.target_distance.is_nan() {
543                0.0
544            } else {
545                let target = params.target_distance;
546                let hit_radius = 0.3; // 30cm radius for hit zone
547                let hits = results
548                    .impact_positions
549                    .iter()
550                    .filter(|pos| {
551                        let distance = (pos.x.powi(2) + pos.y.powi(2)).sqrt();
552                        distance < target && pos.norm() < hit_radius
553                    })
554                    .count();
555                hits as f64 / num_results as f64
556            };
557
558            // Allocate memory for arrays
559            let ranges_ptr = unsafe {
560                let ptr = std::alloc::alloc(
561                    std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
562                ) as *mut c_double;
563                for (i, &range) in results.ranges.iter().enumerate() {
564                    *ptr.add(i) = range;
565                }
566                ptr
567            };
568
569            let velocities_ptr = unsafe {
570                let ptr = std::alloc::alloc(
571                    std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
572                ) as *mut c_double;
573                for (i, &vel) in results.impact_velocities.iter().enumerate() {
574                    *ptr.add(i) = vel;
575                }
576                ptr
577            };
578
579            let pos_x_ptr = unsafe {
580                let ptr = std::alloc::alloc(
581                    std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
582                ) as *mut c_double;
583                for (i, pos) in results.impact_positions.iter().enumerate() {
584                    *ptr.add(i) = pos.x;
585                }
586                ptr
587            };
588
589            let pos_y_ptr = unsafe {
590                let ptr = std::alloc::alloc(
591                    std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
592                ) as *mut c_double;
593                for (i, pos) in results.impact_positions.iter().enumerate() {
594                    *ptr.add(i) = pos.y;
595                }
596                ptr
597            };
598
599            let pos_z_ptr = unsafe {
600                let ptr = std::alloc::alloc(
601                    std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
602                ) as *mut c_double;
603                for (i, pos) in results.impact_positions.iter().enumerate() {
604                    *ptr.add(i) = pos.z;
605                }
606                ptr
607            };
608
609            // Create result structure
610            let result = Box::new(FFIMonteCarloResults {
611                ranges: ranges_ptr,
612                impact_velocities: velocities_ptr,
613                impact_positions_x: pos_x_ptr,
614                impact_positions_y: pos_y_ptr,
615                impact_positions_z: pos_z_ptr,
616                num_results,
617                mean_range,
618                std_dev_range,
619                mean_impact_velocity: mean_velocity,
620                std_dev_impact_velocity: std_dev_velocity,
621                hit_probability,
622            });
623
624            Box::into_raw(result)
625        }
626        Err(_) => ptr::null_mut(),
627    }
628}
629
630// Free Monte Carlo results
631#[no_mangle]
632pub extern "C" fn ballistics_free_monte_carlo_results(results: *mut FFIMonteCarloResults) {
633    if results.is_null() {
634        return;
635    }
636
637    unsafe {
638        let results = Box::from_raw(results);
639        let num = results.num_results as usize;
640
641        // Free arrays
642        if !results.ranges.is_null() {
643            std::alloc::dealloc(
644                results.ranges as *mut u8,
645                std::alloc::Layout::array::<c_double>(num).unwrap(),
646            );
647        }
648
649        if !results.impact_velocities.is_null() {
650            std::alloc::dealloc(
651                results.impact_velocities as *mut u8,
652                std::alloc::Layout::array::<c_double>(num).unwrap(),
653            );
654        }
655
656        if !results.impact_positions_x.is_null() {
657            std::alloc::dealloc(
658                results.impact_positions_x as *mut u8,
659                std::alloc::Layout::array::<c_double>(num).unwrap(),
660            );
661        }
662
663        if !results.impact_positions_y.is_null() {
664            std::alloc::dealloc(
665                results.impact_positions_y as *mut u8,
666                std::alloc::Layout::array::<c_double>(num).unwrap(),
667            );
668        }
669
670        if !results.impact_positions_z.is_null() {
671            std::alloc::dealloc(
672                results.impact_positions_z as *mut u8,
673                std::alloc::Layout::array::<c_double>(num).unwrap(),
674            );
675        }
676
677        // Box automatically deallocates the result structure
678    }
679}
680
681// Get library version
682#[no_mangle]
683pub extern "C" fn ballistics_get_version() -> *const c_char {
684    let version = CString::new("0.3.0").unwrap();
685    let ptr = version.as_ptr();
686    std::mem::forget(version);
687    ptr
688}