use crate::{
calculate_zero_angle_with_conditions, run_monte_carlo, AtmosphericConditions, BallisticInputs,
DragModel, MonteCarloParams, TrajectorySolver, WindConditions,
};
use std::ffi::CString;
use std::os::raw::{c_char, c_double, c_int};
use std::ptr;
#[repr(C)]
pub struct FFIBallisticInputs {
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, }
#[repr(C)]
pub struct FFIWindConditions {
pub speed: c_double, pub direction: c_double, }
#[repr(C)]
pub struct FFIAtmosphericConditions {
pub temperature: c_double, pub pressure: c_double, pub humidity: c_double, pub altitude: c_double, }
#[repr(C)]
pub struct FFITrajectorySample {
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, }
#[repr(C)]
pub struct FFITrajectoryPoint {
pub time: c_double,
pub position_x: c_double,
pub position_y: c_double,
pub position_z: c_double,
pub velocity_magnitude: c_double,
pub kinetic_energy: c_double,
}
#[repr(C)]
pub struct FFITrajectoryResult {
pub max_range: c_double,
pub max_height: c_double,
pub time_of_flight: c_double,
pub impact_velocity: c_double,
pub impact_energy: c_double,
pub points: *mut FFITrajectoryPoint,
pub point_count: c_int,
pub sampled_points: *mut FFITrajectorySample,
pub sampled_point_count: c_int,
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, }
#[repr(C)]
pub struct FFIMonteCarloParams {
pub num_simulations: c_int,
pub velocity_std_dev: c_double,
pub angle_std_dev: c_double,
pub bc_std_dev: c_double,
pub wind_speed_std_dev: c_double,
pub target_distance: c_double, pub base_wind_speed: c_double, pub base_wind_direction: c_double, pub azimuth_std_dev: c_double, }
#[repr(C)]
pub struct FFIMonteCarloResults {
pub ranges: *mut c_double,
pub impact_velocities: *mut c_double,
pub impact_positions_x: *mut c_double,
pub impact_positions_y: *mut c_double,
pub impact_positions_z: *mut c_double,
pub num_results: c_int,
pub mean_range: c_double,
pub std_dev_range: c_double,
pub mean_impact_velocity: c_double,
pub std_dev_impact_velocity: c_double,
pub hit_probability: c_double, }
fn convert_inputs(inputs: &FFIBallisticInputs) -> BallisticInputs {
let mut ballistic_inputs = BallisticInputs::default();
ballistic_inputs.muzzle_velocity = inputs.muzzle_velocity;
ballistic_inputs.muzzle_angle = inputs.muzzle_angle;
ballistic_inputs.azimuth_angle = inputs.azimuth_angle;
ballistic_inputs.use_rk4 = inputs.use_rk4 != 0;
ballistic_inputs.use_adaptive_rk45 = inputs.use_adaptive_rk45 != 0;
ballistic_inputs.bc_value = inputs.bc_value;
ballistic_inputs.bullet_mass = inputs.bullet_mass;
ballistic_inputs.bullet_diameter = inputs.bullet_diameter;
ballistic_inputs.bc_type = match inputs.bc_type {
1 => DragModel::G7,
2 => DragModel::G2,
3 => DragModel::G5,
4 => DragModel::G6,
5 => DragModel::G8,
6 => DragModel::GI,
7 => DragModel::GS,
_ => DragModel::G1,
};
ballistic_inputs.sight_height = inputs.sight_height;
ballistic_inputs.target_distance = inputs.target_distance;
ballistic_inputs.temperature = inputs.temperature;
ballistic_inputs.twist_rate = inputs.twist_rate;
ballistic_inputs.is_twist_right = inputs.is_twist_right != 0;
ballistic_inputs.shooting_angle = inputs.shooting_angle;
ballistic_inputs.altitude = inputs.altitude;
if !inputs.latitude.is_nan() {
ballistic_inputs.latitude = Some(inputs.latitude);
}
ballistic_inputs.caliber_inches = inputs.bullet_diameter / 0.0254;
ballistic_inputs.weight_grains = inputs.bullet_mass / 0.00006479891;
ballistic_inputs.bullet_length = inputs.bullet_diameter * 4.0;
ballistic_inputs.enable_wind_shear = inputs.enable_wind_shear != 0;
ballistic_inputs.enable_trajectory_sampling = inputs.enable_trajectory_sampling != 0;
ballistic_inputs.sample_interval = inputs.sample_interval;
ballistic_inputs.enable_pitch_damping = inputs.enable_pitch_damping != 0;
ballistic_inputs.enable_precession_nutation = inputs.enable_precession_nutation != 0;
ballistic_inputs.use_enhanced_spin_drift = inputs.enable_spin_drift != 0;
ballistic_inputs.enable_advanced_effects =
inputs.enable_magnus != 0 || inputs.enable_coriolis != 0;
ballistic_inputs
}
#[no_mangle]
pub extern "C" fn ballistics_calculate_trajectory(
inputs: *const FFIBallisticInputs,
wind: *const FFIWindConditions,
atmosphere: *const FFIAtmosphericConditions,
max_range: c_double,
step_size: c_double,
) -> *mut FFITrajectoryResult {
if inputs.is_null() {
return ptr::null_mut();
}
let inputs = unsafe { &*inputs };
let ballistic_inputs = convert_inputs(inputs);
let wind_conditions = if wind.is_null() {
WindConditions::default()
} else {
let wind = unsafe { &*wind };
WindConditions {
speed: wind.speed,
direction: wind.direction,
}
};
let atmospheric_conditions = if atmosphere.is_null() {
AtmosphericConditions::default()
} else {
let atmo = unsafe { &*atmosphere };
AtmosphericConditions {
temperature: atmo.temperature,
pressure: atmo.pressure,
humidity: atmo.humidity,
altitude: atmo.altitude,
}
};
let mut solver =
TrajectorySolver::new(ballistic_inputs, wind_conditions, atmospheric_conditions);
solver.set_max_range(max_range);
solver.set_time_step(step_size / 1000.0);
match solver.solve() {
Ok(result) => {
let point_count = result.points.len();
let points = if point_count > 0 {
let mut ffi_points = Vec::with_capacity(point_count);
for (i, point) in result.points.iter().enumerate() {
ffi_points.push(FFITrajectoryPoint {
time: point.time,
position_x: point.position[0],
position_y: point.position[1],
position_z: point.position[2],
velocity_magnitude: point.velocity_magnitude,
kinetic_energy: point.kinetic_energy,
});
#[cfg(debug_assertions)]
if i == 0 || i == result.points.len() - 1 || i % 100 == 0 {
eprintln!(
"FFI point {}: lateral={:.2}m, vertical={:.2}m, downrange={:.2}m",
i, point.position[0], point.position[1], point.position[2]
);
}
}
let points_ptr = ffi_points.as_mut_ptr();
std::mem::forget(ffi_points); points_ptr
} else {
ptr::null_mut()
};
let (sampled_points, sampled_point_count) =
if let Some(ref samples) = result.sampled_points {
let mut ffi_samples = Vec::with_capacity(samples.len());
for sample in samples {
ffi_samples.push(FFITrajectorySample {
distance: sample.distance_m,
time: sample.time_s,
velocity_mps: sample.velocity_mps,
energy_joules: sample.energy_j,
drop_meters: sample.drop_m,
windage_meters: sample.wind_drift_m,
mach: 0.0, spin_rate_rps: 0.0, });
}
let count = ffi_samples.len() as c_int;
let samples_ptr = ffi_samples.as_mut_ptr();
std::mem::forget(ffi_samples);
(samples_ptr, count)
} else {
(ptr::null_mut(), 0)
};
let (final_pitch, final_yaw, max_yaw, max_prec) =
if let Some(ref angular) = result.angular_state {
(
angular.pitch_angle,
angular.yaw_angle,
result.max_yaw_angle.unwrap_or(std::f64::NAN),
result.max_precession_angle.unwrap_or(std::f64::NAN),
)
} else {
(std::f64::NAN, std::f64::NAN, std::f64::NAN, std::f64::NAN)
};
let ffi_result = Box::new(FFITrajectoryResult {
max_range: result.max_range,
max_height: result.max_height,
time_of_flight: result.time_of_flight,
impact_velocity: result.impact_velocity,
impact_energy: result.impact_energy,
points,
point_count: point_count as c_int,
sampled_points,
sampled_point_count,
min_pitch_damping: result.min_pitch_damping.unwrap_or(std::f64::NAN),
transonic_mach: result.transonic_mach.unwrap_or(std::f64::NAN),
final_pitch_angle: final_pitch,
final_yaw_angle: final_yaw,
max_yaw_angle: max_yaw,
max_precession_angle: max_prec,
});
Box::into_raw(ffi_result)
}
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
pub extern "C" fn ballistics_free_trajectory_result(result: *mut FFITrajectoryResult) {
if !result.is_null() {
unsafe {
let result = Box::from_raw(result);
if !result.points.is_null() && result.point_count > 0 {
let points = Vec::from_raw_parts(
result.points,
result.point_count as usize,
result.point_count as usize,
);
drop(points);
}
if !result.sampled_points.is_null() && result.sampled_point_count > 0 {
let samples = Vec::from_raw_parts(
result.sampled_points,
result.sampled_point_count as usize,
result.sampled_point_count as usize,
);
drop(samples);
}
drop(result);
}
}
}
#[no_mangle]
pub extern "C" fn ballistics_calculate_zero_angle(
inputs: *const FFIBallisticInputs,
wind: *const FFIWindConditions,
atmosphere: *const FFIAtmosphericConditions,
zero_distance: c_double,
) -> c_double {
if inputs.is_null() {
return f64::NAN;
}
let inputs = unsafe { &*inputs };
let ballistic_inputs = convert_inputs(inputs);
let wind_conditions = if wind.is_null() {
WindConditions::default()
} else {
let wind = unsafe { &*wind };
WindConditions {
speed: wind.speed,
direction: wind.direction,
}
};
let atmospheric_conditions = if atmosphere.is_null() {
AtmosphericConditions::default()
} else {
let atmo = unsafe { &*atmosphere };
AtmosphericConditions {
temperature: atmo.temperature,
pressure: atmo.pressure,
humidity: atmo.humidity,
altitude: atmo.altitude,
}
};
let target_height = ballistic_inputs.sight_height;
#[cfg(debug_assertions)]
{
eprintln!("FFI: Calculating zero angle for:");
eprintln!(" Zero distance: {} m", zero_distance);
eprintln!(" Target height: {} m", target_height);
eprintln!(" Sight height: {} m", ballistic_inputs.sight_height);
eprintln!(" Wind speed: {} m/s", wind_conditions.speed);
eprintln!(" Temperature: {} C", atmospheric_conditions.temperature);
}
match calculate_zero_angle_with_conditions(
ballistic_inputs,
zero_distance,
target_height,
wind_conditions,
atmospheric_conditions,
) {
Ok(angle) => {
#[cfg(debug_assertions)]
eprintln!(
" Calculated angle: {} rad ({} deg)",
angle,
angle * 180.0 / std::f64::consts::PI
);
angle
}
Err(e) => {
#[cfg(debug_assertions)]
eprintln!(" Error: {:?}", e);
f64::NAN
}
}
}
#[no_mangle]
pub extern "C" fn ballistics_quick_trajectory(
muzzle_velocity: c_double,
bc: c_double,
sight_height: c_double,
zero_distance: c_double,
target_distance: c_double,
) -> c_double {
let mut inputs = BallisticInputs::default();
inputs.muzzle_velocity = muzzle_velocity;
inputs.bc_value = bc;
inputs.sight_height = sight_height;
inputs.target_distance = target_distance;
let wind = WindConditions::default();
let atmo = AtmosphericConditions::default();
let zero_angle = match calculate_zero_angle_with_conditions(
inputs.clone(),
zero_distance,
sight_height,
wind.clone(),
atmo.clone(),
) {
Ok(angle) => angle,
Err(_) => return f64::NAN,
};
inputs.muzzle_angle = zero_angle;
let mut solver = TrajectorySolver::new(inputs, wind, atmo);
solver.set_max_range(target_distance * 1.1);
match solver.solve() {
Ok(result) => {
for point in result.points {
if point.position[0] >= target_distance {
return -point.position[1]; }
}
f64::NAN
}
Err(_) => f64::NAN,
}
}
#[no_mangle]
pub extern "C" fn ballistics_monte_carlo(
inputs: *const FFIBallisticInputs,
_atmosphere: *const FFIAtmosphericConditions,
params: *const FFIMonteCarloParams,
) -> *mut FFIMonteCarloResults {
if inputs.is_null() || params.is_null() {
return ptr::null_mut();
}
let inputs = unsafe { &*inputs };
let params = unsafe { &*params };
let ballistic_inputs = convert_inputs(inputs);
let mc_params = MonteCarloParams {
num_simulations: params.num_simulations as usize,
velocity_std_dev: params.velocity_std_dev,
angle_std_dev: params.angle_std_dev,
bc_std_dev: params.bc_std_dev,
wind_speed_std_dev: params.wind_speed_std_dev,
target_distance: if params.target_distance.is_nan() {
None
} else {
Some(params.target_distance)
},
base_wind_speed: params.base_wind_speed,
base_wind_direction: params.base_wind_direction,
azimuth_std_dev: params.azimuth_std_dev,
};
match run_monte_carlo(ballistic_inputs, mc_params) {
Ok(results) => {
let num_results = results.ranges.len() as c_int;
let mean_range: f64 = results.ranges.iter().sum::<f64>() / num_results as f64;
let variance_range: f64 = results
.ranges
.iter()
.map(|r| (r - mean_range).powi(2))
.sum::<f64>()
/ num_results as f64;
let std_dev_range = variance_range.sqrt();
let mean_velocity: f64 =
results.impact_velocities.iter().sum::<f64>() / num_results as f64;
let variance_velocity: f64 = results
.impact_velocities
.iter()
.map(|v| (v - mean_velocity).powi(2))
.sum::<f64>()
/ num_results as f64;
let std_dev_velocity = variance_velocity.sqrt();
let hit_probability = if params.target_distance.is_nan() {
0.0
} else {
let target = params.target_distance;
let hit_radius = 0.3; let hits = results
.impact_positions
.iter()
.filter(|pos| {
let distance = (pos.x.powi(2) + pos.y.powi(2)).sqrt();
distance < target && pos.norm() < hit_radius
})
.count();
hits as f64 / num_results as f64
};
let ranges_ptr = unsafe {
let ptr = std::alloc::alloc(
std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
) as *mut c_double;
for (i, &range) in results.ranges.iter().enumerate() {
*ptr.add(i) = range;
}
ptr
};
let velocities_ptr = unsafe {
let ptr = std::alloc::alloc(
std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
) as *mut c_double;
for (i, &vel) in results.impact_velocities.iter().enumerate() {
*ptr.add(i) = vel;
}
ptr
};
let pos_x_ptr = unsafe {
let ptr = std::alloc::alloc(
std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
) as *mut c_double;
for (i, pos) in results.impact_positions.iter().enumerate() {
*ptr.add(i) = pos.x;
}
ptr
};
let pos_y_ptr = unsafe {
let ptr = std::alloc::alloc(
std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
) as *mut c_double;
for (i, pos) in results.impact_positions.iter().enumerate() {
*ptr.add(i) = pos.y;
}
ptr
};
let pos_z_ptr = unsafe {
let ptr = std::alloc::alloc(
std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
) as *mut c_double;
for (i, pos) in results.impact_positions.iter().enumerate() {
*ptr.add(i) = pos.z;
}
ptr
};
let result = Box::new(FFIMonteCarloResults {
ranges: ranges_ptr,
impact_velocities: velocities_ptr,
impact_positions_x: pos_x_ptr,
impact_positions_y: pos_y_ptr,
impact_positions_z: pos_z_ptr,
num_results,
mean_range,
std_dev_range,
mean_impact_velocity: mean_velocity,
std_dev_impact_velocity: std_dev_velocity,
hit_probability,
});
Box::into_raw(result)
}
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
pub extern "C" fn ballistics_free_monte_carlo_results(results: *mut FFIMonteCarloResults) {
if results.is_null() {
return;
}
unsafe {
let results = Box::from_raw(results);
let num = results.num_results as usize;
if !results.ranges.is_null() {
std::alloc::dealloc(
results.ranges as *mut u8,
std::alloc::Layout::array::<c_double>(num).unwrap(),
);
}
if !results.impact_velocities.is_null() {
std::alloc::dealloc(
results.impact_velocities as *mut u8,
std::alloc::Layout::array::<c_double>(num).unwrap(),
);
}
if !results.impact_positions_x.is_null() {
std::alloc::dealloc(
results.impact_positions_x as *mut u8,
std::alloc::Layout::array::<c_double>(num).unwrap(),
);
}
if !results.impact_positions_y.is_null() {
std::alloc::dealloc(
results.impact_positions_y as *mut u8,
std::alloc::Layout::array::<c_double>(num).unwrap(),
);
}
if !results.impact_positions_z.is_null() {
std::alloc::dealloc(
results.impact_positions_z as *mut u8,
std::alloc::Layout::array::<c_double>(num).unwrap(),
);
}
}
}
#[no_mangle]
pub extern "C" fn ballistics_get_version() -> *const c_char {
let version = CString::new("0.3.0").unwrap();
let ptr = version.as_ptr();
std::mem::forget(version);
ptr
}