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