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