1use crate::{
4 calculate_zero_angle_with_conditions, run_monte_carlo_with_direction_std_dev,
5 AtmosphericConditions, BallisticInputs, DragModel, MonteCarloParams, TrajectorySolver,
6 WindConditions,
7};
8use std::os::raw::{c_char, c_double, c_int};
9use std::ptr;
10
11pub const MIN_FFI_STEP_SIZE_MS: c_double = 0.1;
16
17#[repr(C)]
20pub struct FFIBallisticInputs {
21 pub muzzle_velocity: c_double, pub muzzle_angle: c_double, pub bc_value: c_double, pub bullet_mass: c_double, pub bullet_diameter: c_double, pub bc_type: c_int, pub sight_height: c_double, pub target_distance: c_double, pub temperature: c_double, pub twist_rate: c_double, pub is_twist_right: c_int, pub shooting_angle: c_double, pub altitude: c_double, pub latitude: c_double, pub azimuth_angle: c_double, pub use_rk4: c_int, pub use_adaptive_rk45: c_int, pub enable_wind_shear: c_int, pub enable_trajectory_sampling: c_int, pub sample_interval: c_double, pub enable_pitch_damping: c_int, pub enable_precession_nutation: c_int, pub enable_spin_drift: c_int, pub enable_magnus: c_int, pub enable_coriolis: c_int, pub shot_azimuth: c_double,
50 pub cant_angle: c_double,
54}
55
56#[repr(C)]
57pub struct FFIWindConditions {
58 pub speed: c_double, pub direction: c_double,
62 pub vertical_speed: c_double,
65}
66
67#[repr(C)]
68pub struct FFIAtmosphericConditions {
69 pub temperature: c_double, pub pressure: c_double, pub humidity: c_double, pub altitude: c_double, }
74
75#[repr(C)]
76pub struct FFITrajectorySample {
77 pub distance: c_double, pub time: c_double, pub velocity_mps: c_double, pub energy_joules: c_double, pub drop_meters: c_double, pub windage_meters: c_double, pub mach: c_double, pub spin_rate_rps: c_double, }
86
87#[repr(C)]
88pub struct FFITrajectoryPoint {
89 pub time: c_double,
90 pub position_x: c_double,
91 pub position_y: c_double,
92 pub position_z: c_double,
93 pub velocity_magnitude: c_double,
94 pub kinetic_energy: c_double,
95}
96
97#[repr(C)]
98pub struct FFITrajectoryResult {
99 pub max_range: c_double,
100 pub max_height: c_double,
101 pub time_of_flight: c_double,
102 pub impact_velocity: c_double,
103 pub impact_energy: c_double,
104 pub points: *mut FFITrajectoryPoint,
105 pub point_count: c_int,
106 pub sampled_points: *mut FFITrajectorySample,
107 pub sampled_point_count: c_int,
108 pub min_pitch_damping: c_double, pub transonic_mach: c_double, pub final_pitch_angle: c_double, pub final_yaw_angle: c_double, pub max_yaw_angle: c_double, pub max_precession_angle: c_double, }
115
116#[repr(C)]
118pub struct FFIMonteCarloParams {
119 pub num_simulations: c_int,
120 pub velocity_std_dev: c_double,
121 pub angle_std_dev: c_double,
122 pub bc_std_dev: c_double,
123 pub wind_speed_std_dev: c_double,
124 pub target_distance: c_double, pub base_wind_speed: c_double, pub base_wind_direction: c_double, pub azimuth_std_dev: c_double, }
129
130#[repr(C)]
132pub struct FFIMonteCarloResults {
133 pub ranges: *mut c_double,
134 pub impact_velocities: *mut c_double,
135 pub impact_positions_x: *mut c_double,
136 pub impact_positions_y: *mut c_double,
139 pub impact_positions_z: *mut c_double,
140 pub num_results: c_int,
141 pub mean_range: c_double,
142 pub std_dev_range: c_double,
143 pub mean_impact_velocity: c_double,
144 pub std_dev_impact_velocity: c_double,
145 pub hit_probability: c_double, }
147
148#[allow(clippy::field_reassign_with_default)] fn convert_inputs(inputs: &FFIBallisticInputs) -> BallisticInputs {
151 let mut ballistic_inputs = BallisticInputs::default();
152
153 ballistic_inputs.muzzle_velocity = inputs.muzzle_velocity;
154 ballistic_inputs.muzzle_angle = inputs.muzzle_angle;
155 ballistic_inputs.azimuth_angle = inputs.azimuth_angle;
156 ballistic_inputs.shot_azimuth = inputs.shot_azimuth;
157 ballistic_inputs.cant_angle = inputs.cant_angle;
158 ballistic_inputs.use_rk4 = inputs.use_rk4 != 0;
159 ballistic_inputs.use_adaptive_rk45 = inputs.use_adaptive_rk45 != 0;
160 ballistic_inputs.bc_value = inputs.bc_value;
161 ballistic_inputs.bullet_mass = inputs.bullet_mass;
162 ballistic_inputs.bullet_diameter = inputs.bullet_diameter;
163 ballistic_inputs.bc_type = match inputs.bc_type {
164 1 => DragModel::G7,
165 2 => DragModel::G2,
166 3 => DragModel::G5,
167 4 => DragModel::G6,
168 5 => DragModel::G8,
169 6 => DragModel::GI,
170 7 => DragModel::GS,
171 _ => DragModel::G1,
172 };
173 ballistic_inputs.sight_height = inputs.sight_height;
174 ballistic_inputs.target_distance = inputs.target_distance;
175 ballistic_inputs.temperature = inputs.temperature;
176 ballistic_inputs.twist_rate = inputs.twist_rate;
177 ballistic_inputs.is_twist_right = inputs.is_twist_right != 0;
178 ballistic_inputs.shooting_angle = inputs.shooting_angle;
179 ballistic_inputs.altitude = inputs.altitude;
180
181 if !inputs.latitude.is_nan() {
182 ballistic_inputs.latitude = Some(inputs.latitude);
183 }
184
185 ballistic_inputs.caliber_inches = inputs.bullet_diameter / 0.0254;
187 ballistic_inputs.weight_grains = inputs.bullet_mass / 0.00006479891;
188 ballistic_inputs.bullet_length = {
191 let est = crate::stability::estimate_bullet_length_m(
192 ballistic_inputs.bullet_diameter,
193 ballistic_inputs.bullet_mass,
194 );
195 if est > 0.0 {
196 est
197 } else {
198 ballistic_inputs.bullet_diameter * 4.5
199 }
200 };
201
202 ballistic_inputs.enable_wind_shear = inputs.enable_wind_shear != 0;
204 ballistic_inputs.enable_trajectory_sampling = inputs.enable_trajectory_sampling != 0;
205 ballistic_inputs.sample_interval = inputs.sample_interval;
206 ballistic_inputs.enable_pitch_damping = inputs.enable_pitch_damping != 0;
207 ballistic_inputs.enable_precession_nutation = inputs.enable_precession_nutation != 0;
208 ballistic_inputs.use_enhanced_spin_drift = inputs.enable_spin_drift != 0;
209 ballistic_inputs.enable_advanced_effects =
210 inputs.enable_magnus != 0 || inputs.enable_coriolis != 0;
211 ballistic_inputs.enable_magnus = inputs.enable_magnus != 0;
213 ballistic_inputs.enable_coriolis = inputs.enable_coriolis != 0;
214
215 ballistic_inputs
216}
217
218unsafe fn drag_table_from_raw(
231 mach: *const c_double,
232 cd: *const c_double,
233 len: c_int,
234) -> Result<crate::drag::DragTable, ()> {
235 if mach.is_null() || cd.is_null() || len < 2 {
236 return Err(());
237 }
238 let len = len as usize;
239 let mach = unsafe { std::slice::from_raw_parts(mach, len) }.to_vec();
240 let cd = unsafe { std::slice::from_raw_parts(cd, len) }.to_vec();
241 crate::drag::DragTable::try_new(mach, cd).map_err(|_| ())
242}
243
244unsafe fn calculate_trajectory_impl(
248 inputs: *const FFIBallisticInputs,
249 wind: *const FFIWindConditions,
250 atmosphere: *const FFIAtmosphericConditions,
251 max_range: c_double,
252 step_size: c_double,
253 custom_drag_table: Option<crate::drag::DragTable>,
254) -> *mut FFITrajectoryResult {
255 if inputs.is_null() {
256 return ptr::null_mut();
257 }
258 if !step_size.is_finite() || step_size < MIN_FFI_STEP_SIZE_MS {
259 return ptr::null_mut();
260 }
261
262 let inputs = unsafe { &*inputs };
263 let mut ballistic_inputs = convert_inputs(inputs);
264 ballistic_inputs.custom_drag_table = custom_drag_table;
265 let twist_rate_in = ballistic_inputs.twist_rate;
266
267 let wind_conditions = if wind.is_null() {
268 WindConditions::default()
269 } else {
270 let wind = unsafe { &*wind };
271 WindConditions {
272 speed: wind.speed,
273 direction: wind.direction,
274 vertical_speed: wind.vertical_speed,
275 }
276 };
277
278 let atmospheric_conditions = if atmosphere.is_null() {
279 AtmosphericConditions::default()
280 } else {
281 let atmo = unsafe { &*atmosphere };
282 AtmosphericConditions {
283 temperature: atmo.temperature,
284 pressure: atmo.pressure,
285 humidity: atmo.humidity,
286 altitude: atmo.altitude,
287 }
288 };
289
290 let (sample_temp_c, sample_pressure_hpa) = crate::atmosphere::resolve_station_conditions(
292 atmospheric_conditions.temperature,
293 atmospheric_conditions.pressure,
294 atmospheric_conditions.altitude,
295 );
296 let (_, sample_speed_of_sound) = crate::atmosphere::calculate_atmosphere(
297 atmospheric_conditions.altitude,
298 Some(sample_temp_c),
299 Some(sample_pressure_hpa),
300 atmospheric_conditions.humidity,
301 );
302
303 let mut solver =
304 TrajectorySolver::new(ballistic_inputs, wind_conditions, atmospheric_conditions);
305
306 solver.set_max_range(max_range);
308 solver.set_time_step(step_size / 1000.0); match solver.solve() {
311 Ok(result) => {
312 let point_count = result.points.len();
314 let points = if point_count > 0 {
315 let mut ffi_points = Vec::with_capacity(point_count);
316 for point in result.points.iter() {
317 ffi_points.push(FFITrajectoryPoint {
318 time: point.time,
319 position_x: point.position[0],
320 position_y: point.position[1],
321 position_z: point.position[2],
322 velocity_magnitude: point.velocity_magnitude,
323 kinetic_energy: point.kinetic_energy,
324 });
325 }
326 let points_ptr = ffi_points.as_mut_ptr();
327 std::mem::forget(ffi_points); points_ptr
329 } else {
330 ptr::null_mut()
331 };
332
333 let (sampled_points, sampled_point_count) =
335 if let Some(ref samples) = result.sampled_points {
336 let mut ffi_samples = Vec::with_capacity(samples.len());
337 for sample in samples {
338 ffi_samples.push(FFITrajectorySample {
339 distance: sample.distance_m,
340 time: sample.time_s,
341 velocity_mps: sample.velocity_mps,
342 energy_joules: sample.energy_j,
343 drop_meters: sample.drop_m,
344 windage_meters: sample.wind_drift_m,
345 mach: if sample_speed_of_sound > 0.0 {
346 sample.velocity_mps / sample_speed_of_sound
347 } else {
348 0.0
349 },
350 spin_rate_rps: if twist_rate_in > 0.0 {
351 sample.velocity_mps / (twist_rate_in * 0.0254)
352 } else {
353 0.0
354 },
355 });
356 }
357 let count = ffi_samples.len() as c_int;
358 let samples_ptr = ffi_samples.as_mut_ptr();
359 std::mem::forget(ffi_samples);
360 (samples_ptr, count)
361 } else {
362 (ptr::null_mut(), 0)
363 };
364
365 let (final_pitch, final_yaw, max_yaw, max_prec) =
367 if let Some(ref angular) = result.angular_state {
368 (
369 angular.pitch_angle,
370 angular.yaw_angle,
371 result.max_yaw_angle.unwrap_or(f64::NAN),
372 result.max_precession_angle.unwrap_or(f64::NAN),
373 )
374 } else {
375 (f64::NAN, f64::NAN, f64::NAN, f64::NAN)
376 };
377
378 let ffi_result = Box::new(FFITrajectoryResult {
380 max_range: result.max_range,
381 max_height: result.max_height,
382 time_of_flight: result.time_of_flight,
383 impact_velocity: result.impact_velocity,
384 impact_energy: result.impact_energy,
385 points,
386 point_count: point_count as c_int,
387 sampled_points,
388 sampled_point_count,
389 min_pitch_damping: result.min_pitch_damping.unwrap_or(f64::NAN),
390 transonic_mach: result.transonic_mach.unwrap_or(f64::NAN),
391 final_pitch_angle: final_pitch,
392 final_yaw_angle: final_yaw,
393 max_yaw_angle: max_yaw,
394 max_precession_angle: max_prec,
395 });
396
397 Box::into_raw(ffi_result)
398 }
399 Err(_) => ptr::null_mut(),
400 }
401}
402
403#[no_mangle]
422pub unsafe extern "C" fn ballistics_calculate_trajectory(
423 inputs: *const FFIBallisticInputs,
424 wind: *const FFIWindConditions,
425 atmosphere: *const FFIAtmosphericConditions,
426 max_range: c_double,
427 step_size: c_double,
428) -> *mut FFITrajectoryResult {
429 unsafe { calculate_trajectory_impl(inputs, wind, atmosphere, max_range, step_size, None) }
430}
431
432#[no_mangle]
452pub unsafe extern "C" fn ballistics_calculate_trajectory_with_drag_table(
453 inputs: *const FFIBallisticInputs,
454 wind: *const FFIWindConditions,
455 atmosphere: *const FFIAtmosphericConditions,
456 max_range: c_double,
457 step_size: c_double,
458 drag_mach: *const c_double,
459 drag_cd: *const c_double,
460 drag_table_len: c_int,
461) -> *mut FFITrajectoryResult {
462 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
463 Ok(t) => t,
464 Err(()) => return ptr::null_mut(),
465 };
466 unsafe {
467 calculate_trajectory_impl(inputs, wind, atmosphere, max_range, step_size, Some(table))
468 }
469}
470
471#[no_mangle]
480pub unsafe extern "C" fn ballistics_free_trajectory_result(result: *mut FFITrajectoryResult) {
481 if !result.is_null() {
482 unsafe {
483 let result = Box::from_raw(result);
484 if !result.points.is_null() && result.point_count > 0 {
485 let points = Vec::from_raw_parts(
486 result.points,
487 result.point_count as usize,
488 result.point_count as usize,
489 );
490 drop(points);
491 }
492 if !result.sampled_points.is_null() && result.sampled_point_count > 0 {
493 let samples = Vec::from_raw_parts(
494 result.sampled_points,
495 result.sampled_point_count as usize,
496 result.sampled_point_count as usize,
497 );
498 drop(samples);
499 }
500 drop(result);
501 }
502 }
503}
504
505unsafe fn calculate_zero_angle_impl(
511 inputs: *const FFIBallisticInputs,
512 wind: *const FFIWindConditions,
513 atmosphere: *const FFIAtmosphericConditions,
514 zero_distance: c_double,
515 custom_drag_table: Option<crate::drag::DragTable>,
516) -> c_double {
517 if inputs.is_null() {
518 return f64::NAN;
519 }
520
521 let inputs = unsafe { &*inputs };
522 let mut ballistic_inputs = convert_inputs(inputs);
523 ballistic_inputs.custom_drag_table = custom_drag_table;
524
525 let wind_conditions = if wind.is_null() {
526 WindConditions::default()
527 } else {
528 let wind = unsafe { &*wind };
529 WindConditions {
530 speed: wind.speed,
531 direction: wind.direction,
532 vertical_speed: wind.vertical_speed,
533 }
534 };
535
536 let atmospheric_conditions = if atmosphere.is_null() {
537 AtmosphericConditions::default()
538 } else {
539 let atmo = unsafe { &*atmosphere };
540 AtmosphericConditions {
541 temperature: atmo.temperature,
542 pressure: atmo.pressure,
543 humidity: atmo.humidity,
544 altitude: atmo.altitude,
545 }
546 };
547
548 let target_height = ballistic_inputs.sight_height;
551
552 calculate_zero_angle_with_conditions(
553 ballistic_inputs,
554 zero_distance,
555 target_height,
556 wind_conditions,
557 atmospheric_conditions,
558 )
559 .unwrap_or(f64::NAN)
560}
561
562#[no_mangle]
572pub unsafe extern "C" fn ballistics_calculate_zero_angle(
573 inputs: *const FFIBallisticInputs,
574 wind: *const FFIWindConditions,
575 atmosphere: *const FFIAtmosphericConditions,
576 zero_distance: c_double,
577) -> c_double {
578 unsafe { calculate_zero_angle_impl(inputs, wind, atmosphere, zero_distance, None) }
579}
580
581#[no_mangle]
602pub unsafe extern "C" fn ballistics_calculate_zero_angle_with_drag_table(
603 inputs: *const FFIBallisticInputs,
604 wind: *const FFIWindConditions,
605 atmosphere: *const FFIAtmosphericConditions,
606 zero_distance: c_double,
607 drag_mach: *const c_double,
608 drag_cd: *const c_double,
609 drag_table_len: c_int,
610) -> c_double {
611 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
612 Ok(t) => t,
613 Err(()) => return f64::NAN,
614 };
615 unsafe { calculate_zero_angle_impl(inputs, wind, atmosphere, zero_distance, Some(table)) }
616}
617
618#[no_mangle]
620#[allow(clippy::field_reassign_with_default)] pub extern "C" fn ballistics_quick_trajectory(
622 muzzle_velocity: c_double,
623 bc: c_double,
624 sight_height: c_double,
625 zero_distance: c_double,
626 target_distance: c_double,
627) -> c_double {
628 let mut inputs = BallisticInputs::default();
632 inputs.muzzle_velocity = muzzle_velocity;
633 inputs.bc_value = bc;
634 inputs.sight_height = sight_height;
635 inputs.target_distance = target_distance;
636
637 let wind = WindConditions::default();
638 let atmo = AtmosphericConditions::default();
639
640 let zero_angle = match calculate_zero_angle_with_conditions(
642 inputs.clone(),
643 zero_distance,
644 sight_height,
645 wind.clone(),
646 atmo.clone(),
647 ) {
648 Ok(angle) => angle,
649 Err(_) => return f64::NAN,
650 };
651
652 inputs.muzzle_angle = zero_angle;
654
655 let mut solver = TrajectorySolver::new(inputs, wind, atmo);
656 solver.set_max_range(target_distance * 1.1);
657
658 match solver.solve() {
659 Ok(result) => {
660 for point in result.points {
662 if point.position[0] >= target_distance {
663 return sight_height - point.position[1];
664 }
665 }
666 f64::NAN
667 }
668 Err(_) => f64::NAN,
669 }
670}
671
672#[no_mangle]
684pub unsafe extern "C" fn ballistics_monte_carlo(
685 inputs: *const FFIBallisticInputs,
686 atmosphere: *const FFIAtmosphericConditions,
687 params: *const FFIMonteCarloParams,
688) -> *mut FFIMonteCarloResults {
689 unsafe { ballistics_monte_carlo_impl(inputs, atmosphere, params, 0.0) }
690}
691
692#[no_mangle]
702pub unsafe extern "C" fn ballistics_monte_carlo_with_direction_std_dev(
703 inputs: *const FFIBallisticInputs,
704 atmosphere: *const FFIAtmosphericConditions,
705 params: *const FFIMonteCarloParams,
706 wind_direction_std_dev: c_double,
707) -> *mut FFIMonteCarloResults {
708 unsafe { ballistics_monte_carlo_impl(inputs, atmosphere, params, wind_direction_std_dev) }
709}
710
711unsafe fn ballistics_monte_carlo_impl(
712 inputs: *const FFIBallisticInputs,
713 atmosphere: *const FFIAtmosphericConditions,
714 params: *const FFIMonteCarloParams,
715 wind_direction_std_dev: f64,
716) -> *mut FFIMonteCarloResults {
717 if inputs.is_null() || params.is_null() {
718 return ptr::null_mut();
719 }
720
721 let inputs = unsafe { &*inputs };
722 let params = unsafe { &*params };
723
724 const MAX_SIMULATIONS: c_int = 1_000_000;
730 if params.num_simulations <= 0 || params.num_simulations > MAX_SIMULATIONS {
731 return ptr::null_mut();
732 }
733
734 let mut ballistic_inputs = convert_inputs(inputs);
736 ballistic_inputs.muzzle_height = 1.5;
737 ballistic_inputs.ground_threshold = 0.0;
738 if !atmosphere.is_null() {
739 let atmo = unsafe { &*atmosphere };
740 ballistic_inputs.temperature = atmo.temperature;
741 ballistic_inputs.pressure = atmo.pressure;
742 ballistic_inputs.humidity = (atmo.humidity / 100.0).clamp(0.0, 1.0);
743 ballistic_inputs.altitude = atmo.altitude;
744 }
745
746 let mc_params = MonteCarloParams {
748 num_simulations: params.num_simulations as usize,
749 velocity_std_dev: params.velocity_std_dev,
750 angle_std_dev: params.angle_std_dev,
751 bc_std_dev: params.bc_std_dev,
752 wind_speed_std_dev: params.wind_speed_std_dev,
753 target_distance: if params.target_distance.is_nan() {
754 None
755 } else {
756 Some(params.target_distance)
757 },
758 base_wind_speed: params.base_wind_speed,
759 base_wind_direction: params.base_wind_direction,
760 azimuth_std_dev: params.azimuth_std_dev,
761 };
762
763 match run_monte_carlo_with_direction_std_dev(
765 ballistic_inputs,
766 mc_params,
767 wind_direction_std_dev,
768 ) {
769 Ok(results) => {
770 let num_results = results.ranges.len() as c_int;
771
772 let mean_range: f64 = results.ranges.iter().sum::<f64>() / num_results as f64;
774 let variance_range: f64 = results
775 .ranges
776 .iter()
777 .map(|r| (r - mean_range).powi(2))
778 .sum::<f64>()
779 / num_results as f64;
780 let std_dev_range = variance_range.sqrt();
781
782 let mean_velocity: f64 =
783 results.impact_velocities.iter().sum::<f64>() / num_results as f64;
784 let variance_velocity: f64 = results
785 .impact_velocities
786 .iter()
787 .map(|v| (v - mean_velocity).powi(2))
788 .sum::<f64>()
789 / num_results as f64;
790 let std_dev_velocity = variance_velocity.sqrt();
791
792 let hit_probability = if params.target_distance.is_nan() {
798 0.0
799 } else {
800 results.hit_probability(crate::DEFAULT_HIT_RADIUS_M)
801 };
802
803 let ranges_ptr = unsafe {
805 let ptr = std::alloc::alloc(
806 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
807 ) as *mut c_double;
808 for (i, &range) in results.ranges.iter().enumerate() {
809 *ptr.add(i) = range;
810 }
811 ptr
812 };
813
814 let velocities_ptr = unsafe {
815 let ptr = std::alloc::alloc(
816 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
817 ) as *mut c_double;
818 for (i, &vel) in results.impact_velocities.iter().enumerate() {
819 *ptr.add(i) = vel;
820 }
821 ptr
822 };
823
824 let pos_x_ptr = unsafe {
825 let ptr = std::alloc::alloc(
826 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
827 ) as *mut c_double;
828 for (i, pos) in results.impact_positions.iter().enumerate() {
829 *ptr.add(i) = pos.x;
830 }
831 ptr
832 };
833
834 let pos_y_ptr = unsafe {
835 let ptr = std::alloc::alloc(
836 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
837 ) as *mut c_double;
838 for (i, pos) in results.impact_positions.iter().enumerate() {
839 *ptr.add(i) = pos.y;
840 }
841 ptr
842 };
843
844 let pos_z_ptr = unsafe {
845 let ptr = std::alloc::alloc(
846 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
847 ) as *mut c_double;
848 for (i, pos) in results.impact_positions.iter().enumerate() {
849 *ptr.add(i) = pos.z;
850 }
851 ptr
852 };
853
854 let result = Box::new(FFIMonteCarloResults {
856 ranges: ranges_ptr,
857 impact_velocities: velocities_ptr,
858 impact_positions_x: pos_x_ptr,
859 impact_positions_y: pos_y_ptr,
860 impact_positions_z: pos_z_ptr,
861 num_results,
862 mean_range,
863 std_dev_range,
864 mean_impact_velocity: mean_velocity,
865 std_dev_impact_velocity: std_dev_velocity,
866 hit_probability,
867 });
868
869 Box::into_raw(result)
870 }
871 Err(_) => ptr::null_mut(),
872 }
873}
874
875#[no_mangle]
884pub unsafe extern "C" fn ballistics_free_monte_carlo_results(results: *mut FFIMonteCarloResults) {
885 if results.is_null() {
886 return;
887 }
888
889 unsafe {
890 let results = Box::from_raw(results);
891 let num = results.num_results as usize;
892
893 if !results.ranges.is_null() {
895 std::alloc::dealloc(
896 results.ranges as *mut u8,
897 std::alloc::Layout::array::<c_double>(num).unwrap(),
898 );
899 }
900
901 if !results.impact_velocities.is_null() {
902 std::alloc::dealloc(
903 results.impact_velocities as *mut u8,
904 std::alloc::Layout::array::<c_double>(num).unwrap(),
905 );
906 }
907
908 if !results.impact_positions_x.is_null() {
909 std::alloc::dealloc(
910 results.impact_positions_x as *mut u8,
911 std::alloc::Layout::array::<c_double>(num).unwrap(),
912 );
913 }
914
915 if !results.impact_positions_y.is_null() {
916 std::alloc::dealloc(
917 results.impact_positions_y as *mut u8,
918 std::alloc::Layout::array::<c_double>(num).unwrap(),
919 );
920 }
921
922 if !results.impact_positions_z.is_null() {
923 std::alloc::dealloc(
924 results.impact_positions_z as *mut u8,
925 std::alloc::Layout::array::<c_double>(num).unwrap(),
926 );
927 }
928
929 }
931}
932
933#[no_mangle]
935pub extern "C" fn ballistics_get_version() -> *const c_char {
936 concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
940}
941
942#[cfg(test)]
943mod tests {
944 use super::*;
945
946 fn valid_trajectory_inputs() -> FFIBallisticInputs {
947 FFIBallisticInputs {
948 muzzle_velocity: 800.0,
949 muzzle_angle: 0.0,
950 bc_value: 0.5,
951 bullet_mass: 0.01,
952 bullet_diameter: 0.00762,
953 bc_type: 0,
954 sight_height: 0.05,
955 target_distance: 1.0,
956 temperature: 15.0,
957 twist_rate: 12.0,
958 is_twist_right: 1,
959 shooting_angle: 0.0,
960 altitude: 0.0,
961 latitude: f64::NAN,
962 azimuth_angle: 0.0,
963 use_rk4: 1,
964 use_adaptive_rk45: 0,
965 enable_wind_shear: 0,
966 enable_trajectory_sampling: 0,
967 sample_interval: 10.0,
968 enable_pitch_damping: 0,
969 enable_precession_nutation: 0,
970 enable_spin_drift: 0,
971 enable_magnus: 0,
972 enable_coriolis: 0,
973 shot_azimuth: 0.0,
974 cant_angle: 0.0,
975 }
976 }
977
978 #[allow(dead_code)]
979 #[repr(C)]
980 struct LegacyFFIMonteCarloParams {
981 num_simulations: c_int,
982 velocity_std_dev: c_double,
983 angle_std_dev: c_double,
984 bc_std_dev: c_double,
985 wind_speed_std_dev: c_double,
986 target_distance: c_double,
987 base_wind_speed: c_double,
988 base_wind_direction: c_double,
989 azimuth_std_dev: c_double,
990 }
991
992 #[test]
993 fn monte_carlo_params_legacy_abi_size_is_unchanged() {
994 assert_eq!(
995 std::mem::size_of::<FFIMonteCarloParams>(),
996 std::mem::size_of::<LegacyFFIMonteCarloParams>()
997 );
998 assert_eq!(
999 std::mem::align_of::<FFIMonteCarloParams>(),
1000 std::mem::align_of::<LegacyFFIMonteCarloParams>()
1001 );
1002 }
1003
1004 #[test]
1005 fn null_pointer_contracts_return_sentinels_and_free_safely() {
1006 unsafe {
1007 assert!(ballistics_calculate_trajectory(
1008 std::ptr::null(),
1009 std::ptr::null(),
1010 std::ptr::null(),
1011 1_000.0,
1012 1.0,
1013 )
1014 .is_null());
1015 assert!(ballistics_calculate_zero_angle(
1016 std::ptr::null(),
1017 std::ptr::null(),
1018 std::ptr::null(),
1019 100.0,
1020 )
1021 .is_nan());
1022 assert!(ballistics_calculate_trajectory_with_drag_table(
1023 std::ptr::null(),
1024 std::ptr::null(),
1025 std::ptr::null(),
1026 1_000.0,
1027 1.0,
1028 DECK_MACH.as_ptr(),
1029 DECK_CD_LOW.as_ptr(),
1030 DECK_MACH.len() as c_int,
1031 )
1032 .is_null());
1033 assert!(ballistics_calculate_zero_angle_with_drag_table(
1034 std::ptr::null(),
1035 std::ptr::null(),
1036 std::ptr::null(),
1037 100.0,
1038 DECK_MACH.as_ptr(),
1039 DECK_CD_LOW.as_ptr(),
1040 DECK_MACH.len() as c_int,
1041 )
1042 .is_nan());
1043 assert!(
1044 ballistics_monte_carlo(std::ptr::null(), std::ptr::null(), std::ptr::null(),)
1045 .is_null()
1046 );
1047 assert!(ballistics_monte_carlo_with_direction_std_dev(
1048 std::ptr::null(),
1049 std::ptr::null(),
1050 std::ptr::null(),
1051 0.1,
1052 )
1053 .is_null());
1054
1055 ballistics_free_trajectory_result(std::ptr::null_mut());
1056 ballistics_free_monte_carlo_results(std::ptr::null_mut());
1057 }
1058 }
1059
1060 #[test]
1061 fn mba1283_ffi_enforces_step_floor_for_every_solver_mode() {
1062 for (mode, use_rk4, use_adaptive_rk45) in [("Euler", 0, 0), ("RK4", 1, 0), ("RK45", 1, 1)] {
1063 for step_size in [
1064 f64::NAN,
1065 f64::INFINITY,
1066 f64::NEG_INFINITY,
1067 -1.0,
1068 -0.0,
1069 0.0,
1070 0.001,
1071 MIN_FFI_STEP_SIZE_MS - 0.001,
1072 ] {
1073 let mut inputs = valid_trajectory_inputs();
1074 inputs.use_rk4 = use_rk4;
1075 inputs.use_adaptive_rk45 = use_adaptive_rk45;
1076 let result = unsafe {
1077 ballistics_calculate_trajectory(
1078 &inputs,
1079 std::ptr::null(),
1080 std::ptr::null(),
1081 0.01,
1082 step_size,
1083 )
1084 };
1085 assert!(
1086 result.is_null(),
1087 "{mode} step_size={step_size:?} bypassed the FFI floor"
1088 );
1089 }
1090
1091 let mut inputs = valid_trajectory_inputs();
1092 inputs.use_rk4 = use_rk4;
1093 inputs.use_adaptive_rk45 = use_adaptive_rk45;
1094 let result = unsafe {
1095 ballistics_calculate_trajectory(
1096 &inputs,
1097 std::ptr::null(),
1098 std::ptr::null(),
1099 0.01,
1100 MIN_FFI_STEP_SIZE_MS,
1101 )
1102 };
1103 assert!(
1104 !result.is_null(),
1105 "the documented minimum step must remain usable in {mode}"
1106 );
1107 unsafe {
1108 assert!((*result).point_count >= 0);
1109 assert!((*result).point_count as usize <= crate::MAX_TRAJECTORY_POINTS);
1110 ballistics_free_trajectory_result(result);
1111 }
1112 }
1113 }
1114
1115 const DECK_MACH: [f64; 4] = [0.5, 1.0, 2.0, 3.0];
1117 const DECK_CD_LOW: [f64; 4] = [0.05, 0.08, 0.06, 0.05];
1119
1120 #[test]
1121 fn trajectory_with_drag_table_applies_the_deck() {
1122 let inputs = valid_trajectory_inputs();
1123 unsafe {
1124 let plain = ballistics_calculate_trajectory(
1125 &inputs,
1126 std::ptr::null(),
1127 std::ptr::null(),
1128 300.0,
1129 1.0,
1130 );
1131 let decked = ballistics_calculate_trajectory_with_drag_table(
1132 &inputs,
1133 std::ptr::null(),
1134 std::ptr::null(),
1135 300.0,
1136 1.0,
1137 DECK_MACH.as_ptr(),
1138 DECK_CD_LOW.as_ptr(),
1139 DECK_MACH.len() as c_int,
1140 );
1141 assert!(!plain.is_null() && !decked.is_null());
1142 assert!(
1144 (*decked).impact_velocity > (*plain).impact_velocity + 1.0,
1145 "deck did not change the solve: plain={} decked={}",
1146 (*plain).impact_velocity,
1147 (*decked).impact_velocity
1148 );
1149 ballistics_free_trajectory_result(plain);
1150 ballistics_free_trajectory_result(decked);
1151 }
1152 }
1153
1154 #[test]
1155 fn trajectory_with_drag_table_rejects_invalid_decks() {
1156 let inputs = valid_trajectory_inputs();
1157 let descending = [3.0, 2.0, 1.0, 0.5];
1158 let negative_cd = [0.05, -0.08, 0.06, 0.05];
1159 unsafe {
1160 assert!(ballistics_calculate_trajectory_with_drag_table(
1162 &inputs,
1163 std::ptr::null(),
1164 std::ptr::null(),
1165 300.0,
1166 1.0,
1167 std::ptr::null(),
1168 DECK_CD_LOW.as_ptr(),
1169 4,
1170 )
1171 .is_null());
1172 assert!(ballistics_calculate_trajectory_with_drag_table(
1173 &inputs,
1174 std::ptr::null(),
1175 std::ptr::null(),
1176 300.0,
1177 1.0,
1178 DECK_MACH.as_ptr(),
1179 std::ptr::null(),
1180 4,
1181 )
1182 .is_null());
1183 assert!(ballistics_calculate_trajectory_with_drag_table(
1185 &inputs,
1186 std::ptr::null(),
1187 std::ptr::null(),
1188 300.0,
1189 1.0,
1190 DECK_MACH.as_ptr(),
1191 DECK_CD_LOW.as_ptr(),
1192 1,
1193 )
1194 .is_null());
1195 assert!(ballistics_calculate_trajectory_with_drag_table(
1197 &inputs,
1198 std::ptr::null(),
1199 std::ptr::null(),
1200 300.0,
1201 1.0,
1202 descending.as_ptr(),
1203 DECK_CD_LOW.as_ptr(),
1204 4,
1205 )
1206 .is_null());
1207 assert!(ballistics_calculate_trajectory_with_drag_table(
1209 &inputs,
1210 std::ptr::null(),
1211 std::ptr::null(),
1212 300.0,
1213 1.0,
1214 DECK_MACH.as_ptr(),
1215 negative_cd.as_ptr(),
1216 4,
1217 )
1218 .is_null());
1219 assert!(ballistics_calculate_trajectory_with_drag_table(
1221 std::ptr::null(),
1222 std::ptr::null(),
1223 std::ptr::null(),
1224 300.0,
1225 1.0,
1226 DECK_MACH.as_ptr(),
1227 DECK_CD_LOW.as_ptr(),
1228 4,
1229 )
1230 .is_null());
1231 }
1232 }
1233
1234 #[test]
1235 fn zero_angle_with_drag_table_applies_the_deck() {
1236 let inputs = valid_trajectory_inputs();
1238 unsafe {
1239 let plain =
1240 ballistics_calculate_zero_angle(&inputs, std::ptr::null(), std::ptr::null(), 100.0);
1241 let decked = ballistics_calculate_zero_angle_with_drag_table(
1242 &inputs,
1243 std::ptr::null(),
1244 std::ptr::null(),
1245 100.0,
1246 DECK_MACH.as_ptr(),
1247 DECK_CD_LOW.as_ptr(),
1248 DECK_MACH.len() as c_int,
1249 );
1250 assert!(plain.is_finite() && decked.is_finite());
1251 assert!(
1254 (plain - decked).abs() > 1e-6,
1255 "deck did not change the zero: plain={plain} decked={decked}"
1256 );
1257 }
1258 }
1259
1260 #[test]
1261 fn zero_angle_with_drag_table_rejects_invalid_decks() {
1262 let inputs = valid_trajectory_inputs();
1263 let descending = [3.0, 2.0, 1.0, 0.5];
1264 unsafe {
1265 assert!(ballistics_calculate_zero_angle_with_drag_table(
1266 &inputs,
1267 std::ptr::null(),
1268 std::ptr::null(),
1269 100.0,
1270 std::ptr::null(),
1271 DECK_CD_LOW.as_ptr(),
1272 4,
1273 )
1274 .is_nan());
1275 assert!(ballistics_calculate_zero_angle_with_drag_table(
1276 &inputs,
1277 std::ptr::null(),
1278 std::ptr::null(),
1279 100.0,
1280 DECK_MACH.as_ptr(),
1281 DECK_CD_LOW.as_ptr(),
1282 0,
1283 )
1284 .is_nan());
1285 assert!(ballistics_calculate_zero_angle_with_drag_table(
1286 &inputs,
1287 std::ptr::null(),
1288 std::ptr::null(),
1289 100.0,
1290 descending.as_ptr(),
1291 DECK_CD_LOW.as_ptr(),
1292 4,
1293 )
1294 .is_nan());
1295 assert!(ballistics_calculate_zero_angle_with_drag_table(
1297 std::ptr::null(),
1298 std::ptr::null(),
1299 std::ptr::null(),
1300 100.0,
1301 DECK_MACH.as_ptr(),
1302 DECK_CD_LOW.as_ptr(),
1303 4,
1304 )
1305 .is_nan());
1306 }
1307 }
1308
1309 #[test]
1310 fn zero_then_fly_with_same_deck_is_consistent() {
1311 let mut inputs = valid_trajectory_inputs();
1315 unsafe {
1316 let angle = ballistics_calculate_zero_angle_with_drag_table(
1317 &inputs,
1318 std::ptr::null(),
1319 std::ptr::null(),
1320 100.0,
1321 DECK_MACH.as_ptr(),
1322 DECK_CD_LOW.as_ptr(),
1323 DECK_MACH.len() as c_int,
1324 );
1325 assert!(angle.is_finite());
1326 inputs.muzzle_angle = angle;
1327 let result = ballistics_calculate_trajectory_with_drag_table(
1328 &inputs,
1329 std::ptr::null(),
1330 std::ptr::null(),
1331 150.0,
1332 1.0,
1333 DECK_MACH.as_ptr(),
1334 DECK_CD_LOW.as_ptr(),
1335 DECK_MACH.len() as c_int,
1336 );
1337 assert!(!result.is_null());
1338 let zero_distance = 100.0;
1342 let pts = std::slice::from_raw_parts((*result).points, (*result).point_count as usize);
1343 let bracket = pts
1344 .windows(2)
1345 .find(|w| w[0].position_x <= zero_distance && w[1].position_x >= zero_distance)
1346 .expect("trajectory brackets the zero distance");
1347 let (lo, hi) = (&bracket[0], &bracket[1]);
1348 let y_at_zero = if hi.position_x > lo.position_x {
1349 let t = (zero_distance - lo.position_x) / (hi.position_x - lo.position_x);
1350 lo.position_y + t * (hi.position_y - lo.position_y)
1351 } else {
1352 lo.position_y
1353 };
1354 assert!(
1355 (y_at_zero - inputs.sight_height).abs() < 0.002,
1356 "zeroed flight missed the line of sight at 100 m: y={} (sight_height={})",
1357 y_at_zero,
1358 inputs.sight_height
1359 );
1360 ballistics_free_trajectory_result(result);
1361 }
1362 }
1363
1364 #[test]
1365 fn ffi_cant_angle_deflects_laterally() {
1366 let mut level = valid_trajectory_inputs();
1367 level.muzzle_angle = 0.003;
1368 let mut canted = valid_trajectory_inputs();
1369 canted.muzzle_angle = 0.003;
1370 canted.cant_angle = 10f64.to_radians();
1371 unsafe {
1372 let a = ballistics_calculate_trajectory(&level, std::ptr::null(), std::ptr::null(), 400.0, 1.0);
1373 let b = ballistics_calculate_trajectory(&canted, std::ptr::null(), std::ptr::null(), 400.0, 1.0);
1374 assert!(!a.is_null() && !b.is_null());
1375 let za = std::slice::from_raw_parts((*a).points, (*a).point_count as usize).last().unwrap().position_z;
1376 let zb = std::slice::from_raw_parts((*b).points, (*b).point_count as usize).last().unwrap().position_z;
1377 assert!(zb > za + 0.005, "FFI cant must deflect right: level={za} canted={zb}");
1378 ballistics_free_trajectory_result(a);
1379 ballistics_free_trajectory_result(b);
1380 }
1381 }
1382
1383 #[test]
1384 fn ffi_vertical_wind_raises_trajectory() {
1385 let inputs = valid_trajectory_inputs();
1386 let no_wind = FFIWindConditions {
1387 speed: 0.0,
1388 direction: 0.0,
1389 vertical_speed: 0.0,
1390 };
1391 let updraft = FFIWindConditions {
1392 speed: 0.0,
1393 direction: 0.0,
1394 vertical_speed: 5.0,
1395 };
1396 unsafe {
1397 let a = ballistics_calculate_trajectory(&inputs, &no_wind, std::ptr::null(), 400.0, 1.0);
1398 let b = ballistics_calculate_trajectory(&inputs, &updraft, std::ptr::null(), 400.0, 1.0);
1399 assert!(!a.is_null() && !b.is_null());
1400 let ya = std::slice::from_raw_parts((*a).points, (*a).point_count as usize).last().unwrap().position_y;
1401 let yb = std::slice::from_raw_parts((*b).points, (*b).point_count as usize).last().unwrap().position_y;
1402 assert!(yb > ya + 0.01, "FFI updraft must raise the trajectory: no_wind={ya} updraft={yb}");
1403 ballistics_free_trajectory_result(a);
1404 ballistics_free_trajectory_result(b);
1405 }
1406 }
1407}