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 match calculate_zero_angle_with_conditions(
553 ballistic_inputs,
554 zero_distance,
555 target_height,
556 wind_conditions,
557 atmospheric_conditions,
558 ) {
559 Ok(angle) => angle,
560 Err(_) => f64::NAN,
561 }
562}
563
564#[no_mangle]
574pub unsafe extern "C" fn ballistics_calculate_zero_angle(
575 inputs: *const FFIBallisticInputs,
576 wind: *const FFIWindConditions,
577 atmosphere: *const FFIAtmosphericConditions,
578 zero_distance: c_double,
579) -> c_double {
580 unsafe { calculate_zero_angle_impl(inputs, wind, atmosphere, zero_distance, None) }
581}
582
583#[no_mangle]
604pub unsafe extern "C" fn ballistics_calculate_zero_angle_with_drag_table(
605 inputs: *const FFIBallisticInputs,
606 wind: *const FFIWindConditions,
607 atmosphere: *const FFIAtmosphericConditions,
608 zero_distance: c_double,
609 drag_mach: *const c_double,
610 drag_cd: *const c_double,
611 drag_table_len: c_int,
612) -> c_double {
613 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
614 Ok(t) => t,
615 Err(()) => return f64::NAN,
616 };
617 unsafe { calculate_zero_angle_impl(inputs, wind, atmosphere, zero_distance, Some(table)) }
618}
619
620#[no_mangle]
622#[allow(clippy::field_reassign_with_default)] pub extern "C" fn ballistics_quick_trajectory(
624 muzzle_velocity: c_double,
625 bc: c_double,
626 sight_height: c_double,
627 zero_distance: c_double,
628 target_distance: c_double,
629) -> c_double {
630 let mut inputs = BallisticInputs::default();
634 inputs.muzzle_velocity = muzzle_velocity;
635 inputs.bc_value = bc;
636 inputs.sight_height = sight_height;
637 inputs.target_distance = target_distance;
638
639 let wind = WindConditions::default();
640 let atmo = AtmosphericConditions::default();
641
642 let zero_angle = match calculate_zero_angle_with_conditions(
644 inputs.clone(),
645 zero_distance,
646 sight_height,
647 wind.clone(),
648 atmo.clone(),
649 ) {
650 Ok(angle) => angle,
651 Err(_) => return f64::NAN,
652 };
653
654 inputs.muzzle_angle = zero_angle;
656
657 let mut solver = TrajectorySolver::new(inputs, wind, atmo);
658 solver.set_max_range(target_distance * 1.1);
659
660 match solver.solve() {
661 Ok(result) => {
662 for point in result.points {
664 if point.position[0] >= target_distance {
665 return sight_height - point.position[1];
666 }
667 }
668 f64::NAN
669 }
670 Err(_) => f64::NAN,
671 }
672}
673
674#[no_mangle]
686pub unsafe extern "C" fn ballistics_monte_carlo(
687 inputs: *const FFIBallisticInputs,
688 atmosphere: *const FFIAtmosphericConditions,
689 params: *const FFIMonteCarloParams,
690) -> *mut FFIMonteCarloResults {
691 unsafe { ballistics_monte_carlo_impl(inputs, atmosphere, params, 0.0) }
692}
693
694#[no_mangle]
704pub unsafe extern "C" fn ballistics_monte_carlo_with_direction_std_dev(
705 inputs: *const FFIBallisticInputs,
706 atmosphere: *const FFIAtmosphericConditions,
707 params: *const FFIMonteCarloParams,
708 wind_direction_std_dev: c_double,
709) -> *mut FFIMonteCarloResults {
710 unsafe { ballistics_monte_carlo_impl(inputs, atmosphere, params, wind_direction_std_dev) }
711}
712
713unsafe fn ballistics_monte_carlo_impl(
714 inputs: *const FFIBallisticInputs,
715 atmosphere: *const FFIAtmosphericConditions,
716 params: *const FFIMonteCarloParams,
717 wind_direction_std_dev: f64,
718) -> *mut FFIMonteCarloResults {
719 if inputs.is_null() || params.is_null() {
720 return ptr::null_mut();
721 }
722
723 let inputs = unsafe { &*inputs };
724 let params = unsafe { &*params };
725
726 const MAX_SIMULATIONS: c_int = 1_000_000;
732 if params.num_simulations <= 0 || params.num_simulations > MAX_SIMULATIONS {
733 return ptr::null_mut();
734 }
735
736 let mut ballistic_inputs = convert_inputs(inputs);
738 ballistic_inputs.muzzle_height = 1.5;
739 ballistic_inputs.ground_threshold = 0.0;
740 if !atmosphere.is_null() {
741 let atmo = unsafe { &*atmosphere };
742 ballistic_inputs.temperature = atmo.temperature;
743 ballistic_inputs.pressure = atmo.pressure;
744 ballistic_inputs.humidity = (atmo.humidity / 100.0).clamp(0.0, 1.0);
745 ballistic_inputs.altitude = atmo.altitude;
746 }
747
748 let mc_params = MonteCarloParams {
750 num_simulations: params.num_simulations as usize,
751 velocity_std_dev: params.velocity_std_dev,
752 angle_std_dev: params.angle_std_dev,
753 bc_std_dev: params.bc_std_dev,
754 wind_speed_std_dev: params.wind_speed_std_dev,
755 target_distance: if params.target_distance.is_nan() {
756 None
757 } else {
758 Some(params.target_distance)
759 },
760 base_wind_speed: params.base_wind_speed,
761 base_wind_direction: params.base_wind_direction,
762 azimuth_std_dev: params.azimuth_std_dev,
763 };
764
765 match run_monte_carlo_with_direction_std_dev(
767 ballistic_inputs,
768 mc_params,
769 wind_direction_std_dev,
770 ) {
771 Ok(results) => {
772 let num_results = results.ranges.len() as c_int;
773
774 let mean_range: f64 = results.ranges.iter().sum::<f64>() / num_results as f64;
776 let variance_range: f64 = results
777 .ranges
778 .iter()
779 .map(|r| (r - mean_range).powi(2))
780 .sum::<f64>()
781 / num_results as f64;
782 let std_dev_range = variance_range.sqrt();
783
784 let mean_velocity: f64 =
785 results.impact_velocities.iter().sum::<f64>() / num_results as f64;
786 let variance_velocity: f64 = results
787 .impact_velocities
788 .iter()
789 .map(|v| (v - mean_velocity).powi(2))
790 .sum::<f64>()
791 / num_results as f64;
792 let std_dev_velocity = variance_velocity.sqrt();
793
794 let hit_probability = if params.target_distance.is_nan() {
800 0.0
801 } else {
802 results.hit_probability(crate::DEFAULT_HIT_RADIUS_M)
803 };
804
805 let ranges_ptr = unsafe {
807 let ptr = std::alloc::alloc(
808 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
809 ) as *mut c_double;
810 for (i, &range) in results.ranges.iter().enumerate() {
811 *ptr.add(i) = range;
812 }
813 ptr
814 };
815
816 let velocities_ptr = unsafe {
817 let ptr = std::alloc::alloc(
818 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
819 ) as *mut c_double;
820 for (i, &vel) in results.impact_velocities.iter().enumerate() {
821 *ptr.add(i) = vel;
822 }
823 ptr
824 };
825
826 let pos_x_ptr = unsafe {
827 let ptr = std::alloc::alloc(
828 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
829 ) as *mut c_double;
830 for (i, pos) in results.impact_positions.iter().enumerate() {
831 *ptr.add(i) = pos.x;
832 }
833 ptr
834 };
835
836 let pos_y_ptr = unsafe {
837 let ptr = std::alloc::alloc(
838 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
839 ) as *mut c_double;
840 for (i, pos) in results.impact_positions.iter().enumerate() {
841 *ptr.add(i) = pos.y;
842 }
843 ptr
844 };
845
846 let pos_z_ptr = unsafe {
847 let ptr = std::alloc::alloc(
848 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
849 ) as *mut c_double;
850 for (i, pos) in results.impact_positions.iter().enumerate() {
851 *ptr.add(i) = pos.z;
852 }
853 ptr
854 };
855
856 let result = Box::new(FFIMonteCarloResults {
858 ranges: ranges_ptr,
859 impact_velocities: velocities_ptr,
860 impact_positions_x: pos_x_ptr,
861 impact_positions_y: pos_y_ptr,
862 impact_positions_z: pos_z_ptr,
863 num_results,
864 mean_range,
865 std_dev_range,
866 mean_impact_velocity: mean_velocity,
867 std_dev_impact_velocity: std_dev_velocity,
868 hit_probability,
869 });
870
871 Box::into_raw(result)
872 }
873 Err(_) => ptr::null_mut(),
874 }
875}
876
877#[no_mangle]
886pub unsafe extern "C" fn ballistics_free_monte_carlo_results(results: *mut FFIMonteCarloResults) {
887 if results.is_null() {
888 return;
889 }
890
891 unsafe {
892 let results = Box::from_raw(results);
893 let num = results.num_results as usize;
894
895 if !results.ranges.is_null() {
897 std::alloc::dealloc(
898 results.ranges as *mut u8,
899 std::alloc::Layout::array::<c_double>(num).unwrap(),
900 );
901 }
902
903 if !results.impact_velocities.is_null() {
904 std::alloc::dealloc(
905 results.impact_velocities as *mut u8,
906 std::alloc::Layout::array::<c_double>(num).unwrap(),
907 );
908 }
909
910 if !results.impact_positions_x.is_null() {
911 std::alloc::dealloc(
912 results.impact_positions_x as *mut u8,
913 std::alloc::Layout::array::<c_double>(num).unwrap(),
914 );
915 }
916
917 if !results.impact_positions_y.is_null() {
918 std::alloc::dealloc(
919 results.impact_positions_y as *mut u8,
920 std::alloc::Layout::array::<c_double>(num).unwrap(),
921 );
922 }
923
924 if !results.impact_positions_z.is_null() {
925 std::alloc::dealloc(
926 results.impact_positions_z as *mut u8,
927 std::alloc::Layout::array::<c_double>(num).unwrap(),
928 );
929 }
930
931 }
933}
934
935#[no_mangle]
937pub extern "C" fn ballistics_get_version() -> *const c_char {
938 concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
942}
943
944#[cfg(test)]
945mod tests {
946 use super::*;
947
948 fn valid_trajectory_inputs() -> FFIBallisticInputs {
949 FFIBallisticInputs {
950 muzzle_velocity: 800.0,
951 muzzle_angle: 0.0,
952 bc_value: 0.5,
953 bullet_mass: 0.01,
954 bullet_diameter: 0.00762,
955 bc_type: 0,
956 sight_height: 0.05,
957 target_distance: 1.0,
958 temperature: 15.0,
959 twist_rate: 12.0,
960 is_twist_right: 1,
961 shooting_angle: 0.0,
962 altitude: 0.0,
963 latitude: f64::NAN,
964 azimuth_angle: 0.0,
965 use_rk4: 1,
966 use_adaptive_rk45: 0,
967 enable_wind_shear: 0,
968 enable_trajectory_sampling: 0,
969 sample_interval: 10.0,
970 enable_pitch_damping: 0,
971 enable_precession_nutation: 0,
972 enable_spin_drift: 0,
973 enable_magnus: 0,
974 enable_coriolis: 0,
975 shot_azimuth: 0.0,
976 cant_angle: 0.0,
977 }
978 }
979
980 #[allow(dead_code)]
981 #[repr(C)]
982 struct LegacyFFIMonteCarloParams {
983 num_simulations: c_int,
984 velocity_std_dev: c_double,
985 angle_std_dev: c_double,
986 bc_std_dev: c_double,
987 wind_speed_std_dev: c_double,
988 target_distance: c_double,
989 base_wind_speed: c_double,
990 base_wind_direction: c_double,
991 azimuth_std_dev: c_double,
992 }
993
994 #[test]
995 fn monte_carlo_params_legacy_abi_size_is_unchanged() {
996 assert_eq!(
997 std::mem::size_of::<FFIMonteCarloParams>(),
998 std::mem::size_of::<LegacyFFIMonteCarloParams>()
999 );
1000 assert_eq!(
1001 std::mem::align_of::<FFIMonteCarloParams>(),
1002 std::mem::align_of::<LegacyFFIMonteCarloParams>()
1003 );
1004 }
1005
1006 #[test]
1007 fn null_pointer_contracts_return_sentinels_and_free_safely() {
1008 unsafe {
1009 assert!(ballistics_calculate_trajectory(
1010 std::ptr::null(),
1011 std::ptr::null(),
1012 std::ptr::null(),
1013 1_000.0,
1014 1.0,
1015 )
1016 .is_null());
1017 assert!(ballistics_calculate_zero_angle(
1018 std::ptr::null(),
1019 std::ptr::null(),
1020 std::ptr::null(),
1021 100.0,
1022 )
1023 .is_nan());
1024 assert!(ballistics_calculate_trajectory_with_drag_table(
1025 std::ptr::null(),
1026 std::ptr::null(),
1027 std::ptr::null(),
1028 1_000.0,
1029 1.0,
1030 DECK_MACH.as_ptr(),
1031 DECK_CD_LOW.as_ptr(),
1032 DECK_MACH.len() as c_int,
1033 )
1034 .is_null());
1035 assert!(ballistics_calculate_zero_angle_with_drag_table(
1036 std::ptr::null(),
1037 std::ptr::null(),
1038 std::ptr::null(),
1039 100.0,
1040 DECK_MACH.as_ptr(),
1041 DECK_CD_LOW.as_ptr(),
1042 DECK_MACH.len() as c_int,
1043 )
1044 .is_nan());
1045 assert!(
1046 ballistics_monte_carlo(std::ptr::null(), std::ptr::null(), std::ptr::null(),)
1047 .is_null()
1048 );
1049 assert!(ballistics_monte_carlo_with_direction_std_dev(
1050 std::ptr::null(),
1051 std::ptr::null(),
1052 std::ptr::null(),
1053 0.1,
1054 )
1055 .is_null());
1056
1057 ballistics_free_trajectory_result(std::ptr::null_mut());
1058 ballistics_free_monte_carlo_results(std::ptr::null_mut());
1059 }
1060 }
1061
1062 #[test]
1063 fn mba1283_ffi_enforces_step_floor_for_every_solver_mode() {
1064 for (mode, use_rk4, use_adaptive_rk45) in [("Euler", 0, 0), ("RK4", 1, 0), ("RK45", 1, 1)] {
1065 for step_size in [
1066 f64::NAN,
1067 f64::INFINITY,
1068 f64::NEG_INFINITY,
1069 -1.0,
1070 -0.0,
1071 0.0,
1072 0.001,
1073 MIN_FFI_STEP_SIZE_MS - 0.001,
1074 ] {
1075 let mut inputs = valid_trajectory_inputs();
1076 inputs.use_rk4 = use_rk4;
1077 inputs.use_adaptive_rk45 = use_adaptive_rk45;
1078 let result = unsafe {
1079 ballistics_calculate_trajectory(
1080 &inputs,
1081 std::ptr::null(),
1082 std::ptr::null(),
1083 0.01,
1084 step_size,
1085 )
1086 };
1087 assert!(
1088 result.is_null(),
1089 "{mode} step_size={step_size:?} bypassed the FFI floor"
1090 );
1091 }
1092
1093 let mut inputs = valid_trajectory_inputs();
1094 inputs.use_rk4 = use_rk4;
1095 inputs.use_adaptive_rk45 = use_adaptive_rk45;
1096 let result = unsafe {
1097 ballistics_calculate_trajectory(
1098 &inputs,
1099 std::ptr::null(),
1100 std::ptr::null(),
1101 0.01,
1102 MIN_FFI_STEP_SIZE_MS,
1103 )
1104 };
1105 assert!(
1106 !result.is_null(),
1107 "the documented minimum step must remain usable in {mode}"
1108 );
1109 unsafe {
1110 assert!((*result).point_count >= 0);
1111 assert!((*result).point_count as usize <= crate::MAX_TRAJECTORY_POINTS);
1112 ballistics_free_trajectory_result(result);
1113 }
1114 }
1115 }
1116
1117 const DECK_MACH: [f64; 4] = [0.5, 1.0, 2.0, 3.0];
1119 const DECK_CD_LOW: [f64; 4] = [0.05, 0.08, 0.06, 0.05];
1121
1122 #[test]
1123 fn trajectory_with_drag_table_applies_the_deck() {
1124 let inputs = valid_trajectory_inputs();
1125 unsafe {
1126 let plain = ballistics_calculate_trajectory(
1127 &inputs,
1128 std::ptr::null(),
1129 std::ptr::null(),
1130 300.0,
1131 1.0,
1132 );
1133 let decked = ballistics_calculate_trajectory_with_drag_table(
1134 &inputs,
1135 std::ptr::null(),
1136 std::ptr::null(),
1137 300.0,
1138 1.0,
1139 DECK_MACH.as_ptr(),
1140 DECK_CD_LOW.as_ptr(),
1141 DECK_MACH.len() as c_int,
1142 );
1143 assert!(!plain.is_null() && !decked.is_null());
1144 assert!(
1146 (*decked).impact_velocity > (*plain).impact_velocity + 1.0,
1147 "deck did not change the solve: plain={} decked={}",
1148 (*plain).impact_velocity,
1149 (*decked).impact_velocity
1150 );
1151 ballistics_free_trajectory_result(plain);
1152 ballistics_free_trajectory_result(decked);
1153 }
1154 }
1155
1156 #[test]
1157 fn trajectory_with_drag_table_rejects_invalid_decks() {
1158 let inputs = valid_trajectory_inputs();
1159 let descending = [3.0, 2.0, 1.0, 0.5];
1160 let negative_cd = [0.05, -0.08, 0.06, 0.05];
1161 unsafe {
1162 assert!(ballistics_calculate_trajectory_with_drag_table(
1164 &inputs,
1165 std::ptr::null(),
1166 std::ptr::null(),
1167 300.0,
1168 1.0,
1169 std::ptr::null(),
1170 DECK_CD_LOW.as_ptr(),
1171 4,
1172 )
1173 .is_null());
1174 assert!(ballistics_calculate_trajectory_with_drag_table(
1175 &inputs,
1176 std::ptr::null(),
1177 std::ptr::null(),
1178 300.0,
1179 1.0,
1180 DECK_MACH.as_ptr(),
1181 std::ptr::null(),
1182 4,
1183 )
1184 .is_null());
1185 assert!(ballistics_calculate_trajectory_with_drag_table(
1187 &inputs,
1188 std::ptr::null(),
1189 std::ptr::null(),
1190 300.0,
1191 1.0,
1192 DECK_MACH.as_ptr(),
1193 DECK_CD_LOW.as_ptr(),
1194 1,
1195 )
1196 .is_null());
1197 assert!(ballistics_calculate_trajectory_with_drag_table(
1199 &inputs,
1200 std::ptr::null(),
1201 std::ptr::null(),
1202 300.0,
1203 1.0,
1204 descending.as_ptr(),
1205 DECK_CD_LOW.as_ptr(),
1206 4,
1207 )
1208 .is_null());
1209 assert!(ballistics_calculate_trajectory_with_drag_table(
1211 &inputs,
1212 std::ptr::null(),
1213 std::ptr::null(),
1214 300.0,
1215 1.0,
1216 DECK_MACH.as_ptr(),
1217 negative_cd.as_ptr(),
1218 4,
1219 )
1220 .is_null());
1221 assert!(ballistics_calculate_trajectory_with_drag_table(
1223 std::ptr::null(),
1224 std::ptr::null(),
1225 std::ptr::null(),
1226 300.0,
1227 1.0,
1228 DECK_MACH.as_ptr(),
1229 DECK_CD_LOW.as_ptr(),
1230 4,
1231 )
1232 .is_null());
1233 }
1234 }
1235
1236 #[test]
1237 fn zero_angle_with_drag_table_applies_the_deck() {
1238 let inputs = valid_trajectory_inputs();
1240 unsafe {
1241 let plain =
1242 ballistics_calculate_zero_angle(&inputs, std::ptr::null(), std::ptr::null(), 100.0);
1243 let decked = ballistics_calculate_zero_angle_with_drag_table(
1244 &inputs,
1245 std::ptr::null(),
1246 std::ptr::null(),
1247 100.0,
1248 DECK_MACH.as_ptr(),
1249 DECK_CD_LOW.as_ptr(),
1250 DECK_MACH.len() as c_int,
1251 );
1252 assert!(plain.is_finite() && decked.is_finite());
1253 assert!(
1256 (plain - decked).abs() > 1e-6,
1257 "deck did not change the zero: plain={plain} decked={decked}"
1258 );
1259 }
1260 }
1261
1262 #[test]
1263 fn zero_angle_with_drag_table_rejects_invalid_decks() {
1264 let inputs = valid_trajectory_inputs();
1265 let descending = [3.0, 2.0, 1.0, 0.5];
1266 unsafe {
1267 assert!(ballistics_calculate_zero_angle_with_drag_table(
1268 &inputs,
1269 std::ptr::null(),
1270 std::ptr::null(),
1271 100.0,
1272 std::ptr::null(),
1273 DECK_CD_LOW.as_ptr(),
1274 4,
1275 )
1276 .is_nan());
1277 assert!(ballistics_calculate_zero_angle_with_drag_table(
1278 &inputs,
1279 std::ptr::null(),
1280 std::ptr::null(),
1281 100.0,
1282 DECK_MACH.as_ptr(),
1283 DECK_CD_LOW.as_ptr(),
1284 0,
1285 )
1286 .is_nan());
1287 assert!(ballistics_calculate_zero_angle_with_drag_table(
1288 &inputs,
1289 std::ptr::null(),
1290 std::ptr::null(),
1291 100.0,
1292 descending.as_ptr(),
1293 DECK_CD_LOW.as_ptr(),
1294 4,
1295 )
1296 .is_nan());
1297 assert!(ballistics_calculate_zero_angle_with_drag_table(
1299 std::ptr::null(),
1300 std::ptr::null(),
1301 std::ptr::null(),
1302 100.0,
1303 DECK_MACH.as_ptr(),
1304 DECK_CD_LOW.as_ptr(),
1305 4,
1306 )
1307 .is_nan());
1308 }
1309 }
1310
1311 #[test]
1312 fn zero_then_fly_with_same_deck_is_consistent() {
1313 let mut inputs = valid_trajectory_inputs();
1317 unsafe {
1318 let angle = ballistics_calculate_zero_angle_with_drag_table(
1319 &inputs,
1320 std::ptr::null(),
1321 std::ptr::null(),
1322 100.0,
1323 DECK_MACH.as_ptr(),
1324 DECK_CD_LOW.as_ptr(),
1325 DECK_MACH.len() as c_int,
1326 );
1327 assert!(angle.is_finite());
1328 inputs.muzzle_angle = angle;
1329 let result = ballistics_calculate_trajectory_with_drag_table(
1330 &inputs,
1331 std::ptr::null(),
1332 std::ptr::null(),
1333 150.0,
1334 1.0,
1335 DECK_MACH.as_ptr(),
1336 DECK_CD_LOW.as_ptr(),
1337 DECK_MACH.len() as c_int,
1338 );
1339 assert!(!result.is_null());
1340 let zero_distance = 100.0;
1344 let pts = std::slice::from_raw_parts((*result).points, (*result).point_count as usize);
1345 let bracket = pts
1346 .windows(2)
1347 .find(|w| w[0].position_x <= zero_distance && w[1].position_x >= zero_distance)
1348 .expect("trajectory brackets the zero distance");
1349 let (lo, hi) = (&bracket[0], &bracket[1]);
1350 let y_at_zero = if hi.position_x > lo.position_x {
1351 let t = (zero_distance - lo.position_x) / (hi.position_x - lo.position_x);
1352 lo.position_y + t * (hi.position_y - lo.position_y)
1353 } else {
1354 lo.position_y
1355 };
1356 assert!(
1357 (y_at_zero - inputs.sight_height).abs() < 0.002,
1358 "zeroed flight missed the line of sight at 100 m: y={} (sight_height={})",
1359 y_at_zero,
1360 inputs.sight_height
1361 );
1362 ballistics_free_trajectory_result(result);
1363 }
1364 }
1365
1366 #[test]
1367 fn ffi_cant_angle_deflects_laterally() {
1368 let mut level = valid_trajectory_inputs();
1369 level.muzzle_angle = 0.003;
1370 let mut canted = valid_trajectory_inputs();
1371 canted.muzzle_angle = 0.003;
1372 canted.cant_angle = 10f64.to_radians();
1373 unsafe {
1374 let a = ballistics_calculate_trajectory(&level, std::ptr::null(), std::ptr::null(), 400.0, 1.0);
1375 let b = ballistics_calculate_trajectory(&canted, std::ptr::null(), std::ptr::null(), 400.0, 1.0);
1376 assert!(!a.is_null() && !b.is_null());
1377 let za = std::slice::from_raw_parts((*a).points, (*a).point_count as usize).last().unwrap().position_z;
1378 let zb = std::slice::from_raw_parts((*b).points, (*b).point_count as usize).last().unwrap().position_z;
1379 assert!(zb > za + 0.005, "FFI cant must deflect right: level={za} canted={zb}");
1380 ballistics_free_trajectory_result(a);
1381 ballistics_free_trajectory_result(b);
1382 }
1383 }
1384
1385 #[test]
1386 fn ffi_vertical_wind_raises_trajectory() {
1387 let inputs = valid_trajectory_inputs();
1388 let no_wind = FFIWindConditions {
1389 speed: 0.0,
1390 direction: 0.0,
1391 vertical_speed: 0.0,
1392 };
1393 let updraft = FFIWindConditions {
1394 speed: 0.0,
1395 direction: 0.0,
1396 vertical_speed: 5.0,
1397 };
1398 unsafe {
1399 let a = ballistics_calculate_trajectory(&inputs, &no_wind, std::ptr::null(), 400.0, 1.0);
1400 let b = ballistics_calculate_trajectory(&inputs, &updraft, std::ptr::null(), 400.0, 1.0);
1401 assert!(!a.is_null() && !b.is_null());
1402 let ya = std::slice::from_raw_parts((*a).points, (*a).point_count as usize).last().unwrap().position_y;
1403 let yb = std::slice::from_raw_parts((*b).points, (*b).point_count as usize).last().unwrap().position_y;
1404 assert!(yb > ya + 0.01, "FFI updraft must raise the trajectory: no_wind={ya} updraft={yb}");
1405 ballistics_free_trajectory_result(a);
1406 ballistics_free_trajectory_result(b);
1407 }
1408 }
1409}