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