1use crate::{
4 calculate_zero_angle_with_conditions, run_monte_carlo, AtmosphericConditions, BallisticInputs,
5 DragModel, MonteCarloParams, TrajectorySolver, WindConditions,
6};
7use std::ffi::CString;
8use std::os::raw::{c_char, c_double, c_int};
9use std::ptr;
10
11#[repr(C)]
14pub struct FFIBallisticInputs {
15 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, }
41
42#[repr(C)]
43pub struct FFIWindConditions {
44 pub speed: c_double, pub direction: c_double, }
47
48#[repr(C)]
49pub struct FFIAtmosphericConditions {
50 pub temperature: c_double, pub pressure: c_double, pub humidity: c_double, pub altitude: c_double, }
55
56#[repr(C)]
57pub struct FFITrajectorySample {
58 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, }
67
68#[repr(C)]
69pub struct FFITrajectoryPoint {
70 pub time: c_double,
71 pub position_x: c_double,
72 pub position_y: c_double,
73 pub position_z: c_double,
74 pub velocity_magnitude: c_double,
75 pub kinetic_energy: c_double,
76}
77
78#[repr(C)]
79pub struct FFITrajectoryResult {
80 pub max_range: c_double,
81 pub max_height: c_double,
82 pub time_of_flight: c_double,
83 pub impact_velocity: c_double,
84 pub impact_energy: c_double,
85 pub points: *mut FFITrajectoryPoint,
86 pub point_count: c_int,
87 pub sampled_points: *mut FFITrajectorySample,
88 pub sampled_point_count: c_int,
89 pub min_pitch_damping: c_double, 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, }
96
97#[repr(C)]
99pub struct FFIMonteCarloParams {
100 pub num_simulations: c_int,
101 pub velocity_std_dev: c_double,
102 pub angle_std_dev: c_double,
103 pub bc_std_dev: c_double,
104 pub wind_speed_std_dev: c_double,
105 pub target_distance: c_double, pub base_wind_speed: c_double, pub base_wind_direction: c_double, pub azimuth_std_dev: c_double, }
110
111#[repr(C)]
113pub struct FFIMonteCarloResults {
114 pub ranges: *mut c_double,
115 pub impact_velocities: *mut c_double,
116 pub impact_positions_x: *mut c_double,
117 pub impact_positions_y: *mut c_double,
118 pub impact_positions_z: *mut c_double,
119 pub num_results: c_int,
120 pub mean_range: c_double,
121 pub std_dev_range: c_double,
122 pub mean_impact_velocity: c_double,
123 pub std_dev_impact_velocity: c_double,
124 pub hit_probability: c_double, }
126
127fn convert_inputs(inputs: &FFIBallisticInputs) -> BallisticInputs {
129 let mut ballistic_inputs = BallisticInputs::default();
130
131 ballistic_inputs.muzzle_velocity = inputs.muzzle_velocity;
132 ballistic_inputs.muzzle_angle = inputs.muzzle_angle;
133 ballistic_inputs.azimuth_angle = inputs.azimuth_angle;
134 ballistic_inputs.use_rk4 = inputs.use_rk4 != 0;
135 ballistic_inputs.use_adaptive_rk45 = inputs.use_adaptive_rk45 != 0;
136 ballistic_inputs.bc_value = inputs.bc_value;
137 ballistic_inputs.bullet_mass = inputs.bullet_mass;
138 ballistic_inputs.bullet_diameter = inputs.bullet_diameter;
139 ballistic_inputs.bc_type = match inputs.bc_type {
140 1 => DragModel::G7,
141 2 => DragModel::G2,
142 3 => DragModel::G5,
143 4 => DragModel::G6,
144 5 => DragModel::G8,
145 6 => DragModel::GI,
146 7 => DragModel::GS,
147 _ => DragModel::G1,
148 };
149 ballistic_inputs.sight_height = inputs.sight_height;
150 ballistic_inputs.target_distance = inputs.target_distance;
151 ballistic_inputs.temperature = inputs.temperature;
152 ballistic_inputs.twist_rate = inputs.twist_rate;
153 ballistic_inputs.is_twist_right = inputs.is_twist_right != 0;
154 ballistic_inputs.shooting_angle = inputs.shooting_angle;
155 ballistic_inputs.altitude = inputs.altitude;
156
157 if !inputs.latitude.is_nan() {
158 ballistic_inputs.latitude = Some(inputs.latitude);
159 }
160
161 ballistic_inputs.caliber_inches = inputs.bullet_diameter / 0.0254;
163 ballistic_inputs.weight_grains = inputs.bullet_mass / 0.00006479891;
164 ballistic_inputs.bullet_length = inputs.bullet_diameter * 4.0;
165
166 ballistic_inputs.enable_wind_shear = inputs.enable_wind_shear != 0;
168 ballistic_inputs.enable_trajectory_sampling = inputs.enable_trajectory_sampling != 0;
169 ballistic_inputs.sample_interval = inputs.sample_interval;
170 ballistic_inputs.enable_pitch_damping = inputs.enable_pitch_damping != 0;
171 ballistic_inputs.enable_precession_nutation = inputs.enable_precession_nutation != 0;
172 ballistic_inputs.use_enhanced_spin_drift = inputs.enable_spin_drift != 0;
173 ballistic_inputs.enable_advanced_effects =
174 inputs.enable_magnus != 0 || inputs.enable_coriolis != 0;
175
176 ballistic_inputs
177}
178
179#[no_mangle]
181pub extern "C" fn ballistics_calculate_trajectory(
182 inputs: *const FFIBallisticInputs,
183 wind: *const FFIWindConditions,
184 atmosphere: *const FFIAtmosphericConditions,
185 max_range: c_double,
186 step_size: c_double,
187) -> *mut FFITrajectoryResult {
188 if inputs.is_null() {
189 return ptr::null_mut();
190 }
191
192 let inputs = unsafe { &*inputs };
193 let ballistic_inputs = convert_inputs(inputs);
194
195 let wind_conditions = if wind.is_null() {
196 WindConditions::default()
197 } else {
198 let wind = unsafe { &*wind };
199 WindConditions {
200 speed: wind.speed,
201 direction: wind.direction,
202 }
203 };
204
205 let atmospheric_conditions = if atmosphere.is_null() {
206 AtmosphericConditions::default()
207 } else {
208 let atmo = unsafe { &*atmosphere };
209 AtmosphericConditions {
210 temperature: atmo.temperature,
211 pressure: atmo.pressure,
212 humidity: atmo.humidity,
213 altitude: atmo.altitude,
214 }
215 };
216
217 let mut solver =
219 TrajectorySolver::new(ballistic_inputs, wind_conditions, atmospheric_conditions);
220
221 solver.set_max_range(max_range);
223 solver.set_time_step(step_size / 1000.0); match solver.solve() {
226 Ok(result) => {
227 let point_count = result.points.len();
229 let points = if point_count > 0 {
230 let mut ffi_points = Vec::with_capacity(point_count);
231 for (i, point) in result.points.iter().enumerate() {
232 ffi_points.push(FFITrajectoryPoint {
233 time: point.time,
234 position_x: point.position[0],
235 position_y: point.position[1],
236 position_z: point.position[2],
237 velocity_magnitude: point.velocity_magnitude,
238 kinetic_energy: point.kinetic_energy,
239 });
240
241 #[cfg(debug_assertions)]
244 if i == 0 || i == result.points.len() - 1 || i % 100 == 0 {
245 eprintln!(
246 "FFI point {}: lateral={:.2}m, vertical={:.2}m, downrange={:.2}m",
247 i, point.position[0], point.position[1], point.position[2]
248 );
249 }
250 }
251 let points_ptr = ffi_points.as_mut_ptr();
252 std::mem::forget(ffi_points); points_ptr
254 } else {
255 ptr::null_mut()
256 };
257
258 let (sampled_points, sampled_point_count) =
260 if let Some(ref samples) = result.sampled_points {
261 let mut ffi_samples = Vec::with_capacity(samples.len());
262 for sample in samples {
263 ffi_samples.push(FFITrajectorySample {
264 distance: sample.distance_m,
265 time: sample.time_s,
266 velocity_mps: sample.velocity_mps,
267 energy_joules: sample.energy_j,
268 drop_meters: sample.drop_m,
269 windage_meters: sample.wind_drift_m,
270 mach: 0.0, spin_rate_rps: 0.0, });
273 }
274 let count = ffi_samples.len() as c_int;
275 let samples_ptr = ffi_samples.as_mut_ptr();
276 std::mem::forget(ffi_samples);
277 (samples_ptr, count)
278 } else {
279 (ptr::null_mut(), 0)
280 };
281
282 let (final_pitch, final_yaw, max_yaw, max_prec) =
284 if let Some(ref angular) = result.angular_state {
285 (
286 angular.pitch_angle,
287 angular.yaw_angle,
288 result.max_yaw_angle.unwrap_or(std::f64::NAN),
289 result.max_precession_angle.unwrap_or(std::f64::NAN),
290 )
291 } else {
292 (std::f64::NAN, std::f64::NAN, std::f64::NAN, std::f64::NAN)
293 };
294
295 let ffi_result = Box::new(FFITrajectoryResult {
297 max_range: result.max_range,
298 max_height: result.max_height,
299 time_of_flight: result.time_of_flight,
300 impact_velocity: result.impact_velocity,
301 impact_energy: result.impact_energy,
302 points,
303 point_count: point_count as c_int,
304 sampled_points,
305 sampled_point_count,
306 min_pitch_damping: result.min_pitch_damping.unwrap_or(std::f64::NAN),
307 transonic_mach: result.transonic_mach.unwrap_or(std::f64::NAN),
308 final_pitch_angle: final_pitch,
309 final_yaw_angle: final_yaw,
310 max_yaw_angle: max_yaw,
311 max_precession_angle: max_prec,
312 });
313
314 Box::into_raw(ffi_result)
315 }
316 Err(_) => ptr::null_mut(),
317 }
318}
319
320#[no_mangle]
322pub extern "C" fn ballistics_free_trajectory_result(result: *mut FFITrajectoryResult) {
323 if !result.is_null() {
324 unsafe {
325 let result = Box::from_raw(result);
326 if !result.points.is_null() && result.point_count > 0 {
327 let points = Vec::from_raw_parts(
328 result.points,
329 result.point_count as usize,
330 result.point_count as usize,
331 );
332 drop(points);
333 }
334 if !result.sampled_points.is_null() && result.sampled_point_count > 0 {
335 let samples = Vec::from_raw_parts(
336 result.sampled_points,
337 result.sampled_point_count as usize,
338 result.sampled_point_count as usize,
339 );
340 drop(samples);
341 }
342 drop(result);
343 }
344 }
345}
346
347#[no_mangle]
349pub extern "C" fn ballistics_calculate_zero_angle(
350 inputs: *const FFIBallisticInputs,
351 wind: *const FFIWindConditions,
352 atmosphere: *const FFIAtmosphericConditions,
353 zero_distance: c_double,
354) -> c_double {
355 if inputs.is_null() {
356 return f64::NAN;
357 }
358
359 let inputs = unsafe { &*inputs };
360 let ballistic_inputs = convert_inputs(inputs);
361
362 let wind_conditions = if wind.is_null() {
363 WindConditions::default()
364 } else {
365 let wind = unsafe { &*wind };
366 WindConditions {
367 speed: wind.speed,
368 direction: wind.direction,
369 }
370 };
371
372 let atmospheric_conditions = if atmosphere.is_null() {
373 AtmosphericConditions::default()
374 } else {
375 let atmo = unsafe { &*atmosphere };
376 AtmosphericConditions {
377 temperature: atmo.temperature,
378 pressure: atmo.pressure,
379 humidity: atmo.humidity,
380 altitude: atmo.altitude,
381 }
382 };
383
384 let target_height = ballistic_inputs.sight_height;
387
388 #[cfg(debug_assertions)]
389 {
390 eprintln!("FFI: Calculating zero angle for:");
391 eprintln!(" Zero distance: {} m", zero_distance);
392 eprintln!(" Target height: {} m", target_height);
393 eprintln!(" Sight height: {} m", ballistic_inputs.sight_height);
394 eprintln!(" Wind speed: {} m/s", wind_conditions.speed);
395 eprintln!(" Temperature: {} C", atmospheric_conditions.temperature);
396 }
397
398 match calculate_zero_angle_with_conditions(
399 ballistic_inputs,
400 zero_distance,
401 target_height,
402 wind_conditions,
403 atmospheric_conditions,
404 ) {
405 Ok(angle) => {
406 #[cfg(debug_assertions)]
407 eprintln!(
408 " Calculated angle: {} rad ({} deg)",
409 angle,
410 angle * 180.0 / std::f64::consts::PI
411 );
412 angle
413 }
414 Err(e) => {
415 #[cfg(debug_assertions)]
416 eprintln!(" Error: {:?}", e);
417 f64::NAN
418 }
419 }
420}
421
422#[no_mangle]
424pub extern "C" fn ballistics_quick_trajectory(
425 muzzle_velocity: c_double,
426 bc: c_double,
427 sight_height: c_double,
428 zero_distance: c_double,
429 target_distance: c_double,
430) -> c_double {
431 let mut inputs = BallisticInputs::default();
435 inputs.muzzle_velocity = muzzle_velocity;
436 inputs.bc_value = bc;
437 inputs.sight_height = sight_height;
438 inputs.target_distance = target_distance;
439
440 let wind = WindConditions::default();
441 let atmo = AtmosphericConditions::default();
442
443 let zero_angle = match calculate_zero_angle_with_conditions(
445 inputs.clone(),
446 zero_distance,
447 sight_height,
448 wind.clone(),
449 atmo.clone(),
450 ) {
451 Ok(angle) => angle,
452 Err(_) => return f64::NAN,
453 };
454
455 inputs.muzzle_angle = zero_angle;
457
458 let mut solver = TrajectorySolver::new(inputs, wind, atmo);
459 solver.set_max_range(target_distance * 1.1);
460
461 match solver.solve() {
462 Ok(result) => {
463 for point in result.points {
465 if point.position[0] >= target_distance {
466 return -point.position[1]; }
468 }
469 f64::NAN
470 }
471 Err(_) => f64::NAN,
472 }
473}
474
475#[no_mangle]
477pub extern "C" fn ballistics_monte_carlo(
478 inputs: *const FFIBallisticInputs,
479 _atmosphere: *const FFIAtmosphericConditions,
480 params: *const FFIMonteCarloParams,
481) -> *mut FFIMonteCarloResults {
482 if inputs.is_null() || params.is_null() {
483 return ptr::null_mut();
484 }
485
486 let inputs = unsafe { &*inputs };
487 let params = unsafe { &*params };
488
489 let ballistic_inputs = convert_inputs(inputs);
491
492 let mc_params = MonteCarloParams {
497 num_simulations: params.num_simulations as usize,
498 velocity_std_dev: params.velocity_std_dev,
499 angle_std_dev: params.angle_std_dev,
500 bc_std_dev: params.bc_std_dev,
501 wind_speed_std_dev: params.wind_speed_std_dev,
502 target_distance: if params.target_distance.is_nan() {
503 None
504 } else {
505 Some(params.target_distance)
506 },
507 base_wind_speed: params.base_wind_speed,
508 base_wind_direction: params.base_wind_direction,
509 azimuth_std_dev: params.azimuth_std_dev,
510 };
511
512 match run_monte_carlo(ballistic_inputs, mc_params) {
514 Ok(results) => {
515 let num_results = results.ranges.len() as c_int;
516
517 let mean_range: f64 = results.ranges.iter().sum::<f64>() / num_results as f64;
519 let variance_range: f64 = results
520 .ranges
521 .iter()
522 .map(|r| (r - mean_range).powi(2))
523 .sum::<f64>()
524 / num_results as f64;
525 let std_dev_range = variance_range.sqrt();
526
527 let mean_velocity: f64 =
528 results.impact_velocities.iter().sum::<f64>() / num_results as f64;
529 let variance_velocity: f64 = results
530 .impact_velocities
531 .iter()
532 .map(|v| (v - mean_velocity).powi(2))
533 .sum::<f64>()
534 / num_results as f64;
535 let std_dev_velocity = variance_velocity.sqrt();
536
537 let hit_probability = if params.target_distance.is_nan() {
539 0.0
540 } else {
541 let target = params.target_distance;
542 let hit_radius = 0.3; let hits = results
544 .impact_positions
545 .iter()
546 .filter(|pos| {
547 let distance = (pos.x.powi(2) + pos.y.powi(2)).sqrt();
548 distance < target && pos.norm() < hit_radius
549 })
550 .count();
551 hits as f64 / num_results as f64
552 };
553
554 let ranges_ptr = unsafe {
556 let ptr = std::alloc::alloc(
557 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
558 ) as *mut c_double;
559 for (i, &range) in results.ranges.iter().enumerate() {
560 *ptr.add(i) = range;
561 }
562 ptr
563 };
564
565 let velocities_ptr = unsafe {
566 let ptr = std::alloc::alloc(
567 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
568 ) as *mut c_double;
569 for (i, &vel) in results.impact_velocities.iter().enumerate() {
570 *ptr.add(i) = vel;
571 }
572 ptr
573 };
574
575 let pos_x_ptr = unsafe {
576 let ptr = std::alloc::alloc(
577 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
578 ) as *mut c_double;
579 for (i, pos) in results.impact_positions.iter().enumerate() {
580 *ptr.add(i) = pos.x;
581 }
582 ptr
583 };
584
585 let pos_y_ptr = unsafe {
586 let ptr = std::alloc::alloc(
587 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
588 ) as *mut c_double;
589 for (i, pos) in results.impact_positions.iter().enumerate() {
590 *ptr.add(i) = pos.y;
591 }
592 ptr
593 };
594
595 let pos_z_ptr = unsafe {
596 let ptr = std::alloc::alloc(
597 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
598 ) as *mut c_double;
599 for (i, pos) in results.impact_positions.iter().enumerate() {
600 *ptr.add(i) = pos.z;
601 }
602 ptr
603 };
604
605 let result = Box::new(FFIMonteCarloResults {
607 ranges: ranges_ptr,
608 impact_velocities: velocities_ptr,
609 impact_positions_x: pos_x_ptr,
610 impact_positions_y: pos_y_ptr,
611 impact_positions_z: pos_z_ptr,
612 num_results,
613 mean_range,
614 std_dev_range,
615 mean_impact_velocity: mean_velocity,
616 std_dev_impact_velocity: std_dev_velocity,
617 hit_probability,
618 });
619
620 Box::into_raw(result)
621 }
622 Err(_) => ptr::null_mut(),
623 }
624}
625
626#[no_mangle]
628pub extern "C" fn ballistics_free_monte_carlo_results(results: *mut FFIMonteCarloResults) {
629 if results.is_null() {
630 return;
631 }
632
633 unsafe {
634 let results = Box::from_raw(results);
635 let num = results.num_results as usize;
636
637 if !results.ranges.is_null() {
639 std::alloc::dealloc(
640 results.ranges as *mut u8,
641 std::alloc::Layout::array::<c_double>(num).unwrap(),
642 );
643 }
644
645 if !results.impact_velocities.is_null() {
646 std::alloc::dealloc(
647 results.impact_velocities as *mut u8,
648 std::alloc::Layout::array::<c_double>(num).unwrap(),
649 );
650 }
651
652 if !results.impact_positions_x.is_null() {
653 std::alloc::dealloc(
654 results.impact_positions_x as *mut u8,
655 std::alloc::Layout::array::<c_double>(num).unwrap(),
656 );
657 }
658
659 if !results.impact_positions_y.is_null() {
660 std::alloc::dealloc(
661 results.impact_positions_y as *mut u8,
662 std::alloc::Layout::array::<c_double>(num).unwrap(),
663 );
664 }
665
666 if !results.impact_positions_z.is_null() {
667 std::alloc::dealloc(
668 results.impact_positions_z as *mut u8,
669 std::alloc::Layout::array::<c_double>(num).unwrap(),
670 );
671 }
672
673 }
675}
676
677#[no_mangle]
679pub extern "C" fn ballistics_get_version() -> *const c_char {
680 let version = CString::new("0.3.0").unwrap();
681 let ptr = version.as_ptr();
682 std::mem::forget(version);
683 ptr
684}