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
176    ballistic_inputs
177}
178
179// Main trajectory calculation function for FFI
180#[no_mangle]
181pub extern "C" fn ballistics_calculate_trajectory(
182    inputs: *const FFIBallisticInputs,
183    wind: *const FFIWindConditions,
184    atmosphere: *const FFIAtmosphericConditions,
185    max_range: c_double,
186    step_size: c_double,
187) -> *mut FFITrajectoryResult {
188    if inputs.is_null() {
189        return ptr::null_mut();
190    }
191
192    let inputs = unsafe { &*inputs };
193    let ballistic_inputs = convert_inputs(inputs);
194
195    let wind_conditions = if wind.is_null() {
196        WindConditions::default()
197    } else {
198        let wind = unsafe { &*wind };
199        WindConditions {
200            speed: wind.speed,
201            direction: wind.direction,
202        }
203    };
204
205    let atmospheric_conditions = if atmosphere.is_null() {
206        AtmosphericConditions::default()
207    } else {
208        let atmo = unsafe { &*atmosphere };
209        AtmosphericConditions {
210            temperature: atmo.temperature,
211            pressure: atmo.pressure,
212            humidity: atmo.humidity,
213            altitude: atmo.altitude,
214        }
215    };
216
217    // Create solver and calculate trajectory
218    let mut solver =
219        TrajectorySolver::new(ballistic_inputs, wind_conditions, atmospheric_conditions);
220
221    // Set max range and time step
222    solver.set_max_range(max_range);
223    solver.set_time_step(step_size / 1000.0); // Convert to time step
224
225    match solver.solve() {
226        Ok(result) => {
227            // Convert trajectory points to FFI format
228            let point_count = result.points.len();
229            let points = if point_count > 0 {
230                let mut ffi_points = Vec::with_capacity(point_count);
231                for (i, point) in result.points.iter().enumerate() {
232                    ffi_points.push(FFITrajectoryPoint {
233                        time: point.time,
234                        position_x: point.position[0],
235                        position_y: point.position[1],
236                        position_z: point.position[2],
237                        velocity_magnitude: point.velocity_magnitude,
238                        kinetic_energy: point.kinetic_energy,
239                    });
240
241                    // Debug: Log first, last, and every 100th point
242                    // Standard ballistics coordinate system: X=lateral, Y=vertical, Z=downrange
243                    #[cfg(debug_assertions)]
244                    if i == 0 || i == result.points.len() - 1 || i % 100 == 0 {
245                        eprintln!(
246                            "FFI point {}: lateral={:.2}m, vertical={:.2}m, downrange={:.2}m",
247                            i, point.position[0], point.position[1], point.position[2]
248                        );
249                    }
250                }
251                let points_ptr = ffi_points.as_mut_ptr();
252                std::mem::forget(ffi_points); // Prevent deallocation
253                points_ptr
254            } else {
255                ptr::null_mut()
256            };
257
258            // Convert sampled points if available
259            let (sampled_points, sampled_point_count) =
260                if let Some(ref samples) = result.sampled_points {
261                    let mut ffi_samples = Vec::with_capacity(samples.len());
262                    for sample in samples {
263                        ffi_samples.push(FFITrajectorySample {
264                            distance: sample.distance_m,
265                            time: sample.time_s,
266                            velocity_mps: sample.velocity_mps,
267                            energy_joules: sample.energy_j,
268                            drop_meters: sample.drop_m,
269                            windage_meters: sample.wind_drift_m,
270                            mach: 0.0,          // Would need to calculate from velocity
271                            spin_rate_rps: 0.0, // Not available in TrajectorySample
272                        });
273                    }
274                    let count = ffi_samples.len() as c_int;
275                    let samples_ptr = ffi_samples.as_mut_ptr();
276                    std::mem::forget(ffi_samples);
277                    (samples_ptr, count)
278                } else {
279                    (ptr::null_mut(), 0)
280                };
281
282            // Extract angular state values if available
283            let (final_pitch, final_yaw, max_yaw, max_prec) =
284                if let Some(ref angular) = result.angular_state {
285                    (
286                        angular.pitch_angle,
287                        angular.yaw_angle,
288                        result.max_yaw_angle.unwrap_or(std::f64::NAN),
289                        result.max_precession_angle.unwrap_or(std::f64::NAN),
290                    )
291                } else {
292                    (std::f64::NAN, std::f64::NAN, std::f64::NAN, std::f64::NAN)
293                };
294
295            // Create result on heap
296            let ffi_result = Box::new(FFITrajectoryResult {
297                max_range: result.max_range,
298                max_height: result.max_height,
299                time_of_flight: result.time_of_flight,
300                impact_velocity: result.impact_velocity,
301                impact_energy: result.impact_energy,
302                points,
303                point_count: point_count as c_int,
304                sampled_points,
305                sampled_point_count,
306                min_pitch_damping: result.min_pitch_damping.unwrap_or(std::f64::NAN),
307                transonic_mach: result.transonic_mach.unwrap_or(std::f64::NAN),
308                final_pitch_angle: final_pitch,
309                final_yaw_angle: final_yaw,
310                max_yaw_angle: max_yaw,
311                max_precession_angle: max_prec,
312            });
313
314            Box::into_raw(ffi_result)
315        }
316        Err(_) => ptr::null_mut(),
317    }
318}
319
320// Free allocated trajectory result
321#[no_mangle]
322pub extern "C" fn ballistics_free_trajectory_result(result: *mut FFITrajectoryResult) {
323    if !result.is_null() {
324        unsafe {
325            let result = Box::from_raw(result);
326            if !result.points.is_null() && result.point_count > 0 {
327                let points = Vec::from_raw_parts(
328                    result.points,
329                    result.point_count as usize,
330                    result.point_count as usize,
331                );
332                drop(points);
333            }
334            if !result.sampled_points.is_null() && result.sampled_point_count > 0 {
335                let samples = Vec::from_raw_parts(
336                    result.sampled_points,
337                    result.sampled_point_count as usize,
338                    result.sampled_point_count as usize,
339                );
340                drop(samples);
341            }
342            drop(result);
343        }
344    }
345}
346
347// Calculate zero angle for a given target distance
348#[no_mangle]
349pub extern "C" fn ballistics_calculate_zero_angle(
350    inputs: *const FFIBallisticInputs,
351    wind: *const FFIWindConditions,
352    atmosphere: *const FFIAtmosphericConditions,
353    zero_distance: c_double,
354) -> c_double {
355    if inputs.is_null() {
356        return f64::NAN;
357    }
358
359    let inputs = unsafe { &*inputs };
360    let ballistic_inputs = convert_inputs(inputs);
361
362    let wind_conditions = if wind.is_null() {
363        WindConditions::default()
364    } else {
365        let wind = unsafe { &*wind };
366        WindConditions {
367            speed: wind.speed,
368            direction: wind.direction,
369        }
370    };
371
372    let atmospheric_conditions = if atmosphere.is_null() {
373        AtmosphericConditions::default()
374    } else {
375        let atmo = unsafe { &*atmosphere };
376        AtmosphericConditions {
377            temperature: atmo.temperature,
378            pressure: atmo.pressure,
379            humidity: atmo.humidity,
380            altitude: atmo.altitude,
381        }
382    };
383
384    // For zero angle, we want the bullet to hit at sight height at the zero distance
385    // This means the bullet crosses the line of sight at the zero distance
386    let target_height = ballistic_inputs.sight_height;
387
388    #[cfg(debug_assertions)]
389    {
390        eprintln!("FFI: Calculating zero angle for:");
391        eprintln!("  Zero distance: {} m", zero_distance);
392        eprintln!("  Target height: {} m", target_height);
393        eprintln!("  Sight height: {} m", ballistic_inputs.sight_height);
394        eprintln!("  Wind speed: {} m/s", wind_conditions.speed);
395        eprintln!("  Temperature: {} C", atmospheric_conditions.temperature);
396    }
397
398    match calculate_zero_angle_with_conditions(
399        ballistic_inputs,
400        zero_distance,
401        target_height,
402        wind_conditions,
403        atmospheric_conditions,
404    ) {
405        Ok(angle) => {
406            #[cfg(debug_assertions)]
407            eprintln!(
408                "  Calculated angle: {} rad ({} deg)",
409                angle,
410                angle * 180.0 / std::f64::consts::PI
411            );
412            angle
413        }
414        Err(e) => {
415            #[cfg(debug_assertions)]
416            eprintln!("  Error: {:?}", e);
417            f64::NAN
418        }
419    }
420}
421
422// Simple trajectory calculation for quick results
423#[no_mangle]
424pub extern "C" fn ballistics_quick_trajectory(
425    muzzle_velocity: c_double,
426    bc: c_double,
427    sight_height: c_double,
428    zero_distance: c_double,
429    target_distance: c_double,
430) -> c_double {
431    // This provides a simple drop calculation at target distance
432    // Using simplified ballistic calculations
433
434    let mut inputs = BallisticInputs::default();
435    inputs.muzzle_velocity = muzzle_velocity;
436    inputs.bc_value = bc;
437    inputs.sight_height = sight_height;
438    inputs.target_distance = target_distance;
439
440    let wind = WindConditions::default();
441    let atmo = AtmosphericConditions::default();
442
443    // First calculate zero angle
444    let zero_angle = match calculate_zero_angle_with_conditions(
445        inputs.clone(),
446        zero_distance,
447        sight_height,
448        wind.clone(),
449        atmo.clone(),
450    ) {
451        Ok(angle) => angle,
452        Err(_) => return f64::NAN,
453    };
454
455    // Now calculate trajectory with that zero angle
456    inputs.muzzle_angle = zero_angle;
457
458    let mut solver = TrajectorySolver::new(inputs, wind, atmo);
459    solver.set_max_range(target_distance * 1.1);
460
461    match solver.solve() {
462        Ok(result) => {
463            // Find the drop at target distance
464            for point in result.points {
465                if point.position[0] >= target_distance {
466                    return -point.position[1]; // Return drop (negative y is drop)
467                }
468            }
469            f64::NAN
470        }
471        Err(_) => f64::NAN,
472    }
473}
474
475// Monte Carlo simulation
476#[no_mangle]
477pub extern "C" fn ballistics_monte_carlo(
478    inputs: *const FFIBallisticInputs,
479    _atmosphere: *const FFIAtmosphericConditions,
480    params: *const FFIMonteCarloParams,
481) -> *mut FFIMonteCarloResults {
482    if inputs.is_null() || params.is_null() {
483        return ptr::null_mut();
484    }
485
486    let inputs = unsafe { &*inputs };
487    let params = unsafe { &*params };
488
489    // Convert FFI inputs to internal types
490    let ballistic_inputs = convert_inputs(inputs);
491
492    // Note: Atmospheric conditions are already included in the conversion
493    // from FFIBallisticInputs (temperature, altitude, etc.)
494
495    // Create Monte Carlo parameters
496    let mc_params = MonteCarloParams {
497        num_simulations: params.num_simulations as usize,
498        velocity_std_dev: params.velocity_std_dev,
499        angle_std_dev: params.angle_std_dev,
500        bc_std_dev: params.bc_std_dev,
501        wind_speed_std_dev: params.wind_speed_std_dev,
502        target_distance: if params.target_distance.is_nan() {
503            None
504        } else {
505            Some(params.target_distance)
506        },
507        base_wind_speed: params.base_wind_speed,
508        base_wind_direction: params.base_wind_direction,
509        azimuth_std_dev: params.azimuth_std_dev,
510    };
511
512    // Run Monte Carlo simulation
513    match run_monte_carlo(ballistic_inputs, mc_params) {
514        Ok(results) => {
515            let num_results = results.ranges.len() as c_int;
516
517            // Calculate statistics
518            let mean_range: f64 = results.ranges.iter().sum::<f64>() / num_results as f64;
519            let variance_range: f64 = results
520                .ranges
521                .iter()
522                .map(|r| (r - mean_range).powi(2))
523                .sum::<f64>()
524                / num_results as f64;
525            let std_dev_range = variance_range.sqrt();
526
527            let mean_velocity: f64 =
528                results.impact_velocities.iter().sum::<f64>() / num_results as f64;
529            let variance_velocity: f64 = results
530                .impact_velocities
531                .iter()
532                .map(|v| (v - mean_velocity).powi(2))
533                .sum::<f64>()
534                / num_results as f64;
535            let std_dev_velocity = variance_velocity.sqrt();
536
537            // Calculate hit probability if target distance was specified
538            let hit_probability = if params.target_distance.is_nan() {
539                0.0
540            } else {
541                let target = params.target_distance;
542                let hit_radius = 0.3; // 30cm radius for hit zone
543                let hits = results
544                    .impact_positions
545                    .iter()
546                    .filter(|pos| {
547                        let distance = (pos.x.powi(2) + pos.y.powi(2)).sqrt();
548                        distance < target && pos.norm() < hit_radius
549                    })
550                    .count();
551                hits as f64 / num_results as f64
552            };
553
554            // Allocate memory for arrays
555            let ranges_ptr = unsafe {
556                let ptr = std::alloc::alloc(
557                    std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
558                ) as *mut c_double;
559                for (i, &range) in results.ranges.iter().enumerate() {
560                    *ptr.add(i) = range;
561                }
562                ptr
563            };
564
565            let velocities_ptr = unsafe {
566                let ptr = std::alloc::alloc(
567                    std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
568                ) as *mut c_double;
569                for (i, &vel) in results.impact_velocities.iter().enumerate() {
570                    *ptr.add(i) = vel;
571                }
572                ptr
573            };
574
575            let pos_x_ptr = unsafe {
576                let ptr = std::alloc::alloc(
577                    std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
578                ) as *mut c_double;
579                for (i, pos) in results.impact_positions.iter().enumerate() {
580                    *ptr.add(i) = pos.x;
581                }
582                ptr
583            };
584
585            let pos_y_ptr = unsafe {
586                let ptr = std::alloc::alloc(
587                    std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
588                ) as *mut c_double;
589                for (i, pos) in results.impact_positions.iter().enumerate() {
590                    *ptr.add(i) = pos.y;
591                }
592                ptr
593            };
594
595            let pos_z_ptr = unsafe {
596                let ptr = std::alloc::alloc(
597                    std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
598                ) as *mut c_double;
599                for (i, pos) in results.impact_positions.iter().enumerate() {
600                    *ptr.add(i) = pos.z;
601                }
602                ptr
603            };
604
605            // Create result structure
606            let result = Box::new(FFIMonteCarloResults {
607                ranges: ranges_ptr,
608                impact_velocities: velocities_ptr,
609                impact_positions_x: pos_x_ptr,
610                impact_positions_y: pos_y_ptr,
611                impact_positions_z: pos_z_ptr,
612                num_results,
613                mean_range,
614                std_dev_range,
615                mean_impact_velocity: mean_velocity,
616                std_dev_impact_velocity: std_dev_velocity,
617                hit_probability,
618            });
619
620            Box::into_raw(result)
621        }
622        Err(_) => ptr::null_mut(),
623    }
624}
625
626// Free Monte Carlo results
627#[no_mangle]
628pub extern "C" fn ballistics_free_monte_carlo_results(results: *mut FFIMonteCarloResults) {
629    if results.is_null() {
630        return;
631    }
632
633    unsafe {
634        let results = Box::from_raw(results);
635        let num = results.num_results as usize;
636
637        // Free arrays
638        if !results.ranges.is_null() {
639            std::alloc::dealloc(
640                results.ranges as *mut u8,
641                std::alloc::Layout::array::<c_double>(num).unwrap(),
642            );
643        }
644
645        if !results.impact_velocities.is_null() {
646            std::alloc::dealloc(
647                results.impact_velocities as *mut u8,
648                std::alloc::Layout::array::<c_double>(num).unwrap(),
649            );
650        }
651
652        if !results.impact_positions_x.is_null() {
653            std::alloc::dealloc(
654                results.impact_positions_x as *mut u8,
655                std::alloc::Layout::array::<c_double>(num).unwrap(),
656            );
657        }
658
659        if !results.impact_positions_y.is_null() {
660            std::alloc::dealloc(
661                results.impact_positions_y as *mut u8,
662                std::alloc::Layout::array::<c_double>(num).unwrap(),
663            );
664        }
665
666        if !results.impact_positions_z.is_null() {
667            std::alloc::dealloc(
668                results.impact_positions_z as *mut u8,
669                std::alloc::Layout::array::<c_double>(num).unwrap(),
670            );
671        }
672
673        // Box automatically deallocates the result structure
674    }
675}
676
677// Get library version
678#[no_mangle]
679pub extern "C" fn ballistics_get_version() -> *const c_char {
680    let version = CString::new("0.3.0").unwrap();
681    let ptr = version.as_ptr();
682    std::mem::forget(version);
683    ptr
684}