use crate::error::{Error, Result};
use boxdd_sys::ffi;
mod sealed {
pub trait Sealed {}
}
pub(crate) unsafe trait FfiOutput: sealed::Sealed + Copy {}
macro_rules! impl_ffi_output {
($($ty:ty),+ $(,)?) => {
$(
impl sealed::Sealed for $ty {}
unsafe impl FfiOutput for $ty {}
)+
};
}
impl_ffi_output!(ffi::b2ContactData, ffi::b2JointId, ffi::b2ShapeId);
pub(crate) unsafe fn fill_from_ffi<T: FfiOutput>(
out: &mut Vec<T>,
requested: i32,
fill: impl FnOnce(*mut T, i32) -> i32,
) -> Result<()> {
out.clear();
let requested_usize =
usize::try_from(requested).map_err(|_| Error::NegativeFfiOutputCapacity {
capacity: requested,
})?;
if requested_usize == 0 {
return Ok(());
}
out.try_reserve(requested_usize)
.map_err(|_| Error::FfiOutputAllocationFailed)?;
let output = out.spare_capacity_mut();
debug_assert!(output.len() >= requested_usize);
let initialized = fill(output.as_mut_ptr().cast::<T>(), requested);
let initialized_usize = usize::try_from(initialized)
.map_err(|_| Error::NegativeFfiOutputCount { count: initialized })?;
if initialized_usize > requested_usize {
return Err(Error::FfiOutputCountExceedsCapacity {
count: initialized,
capacity: requested,
});
}
unsafe { out.set_len(initialized_usize) };
Ok(())
}
pub(crate) unsafe fn read_from_ffi<T: FfiOutput>(
requested: i32,
fill: impl FnOnce(*mut T, i32) -> i32,
) -> Result<Vec<T>> {
let mut out = Vec::new();
unsafe { fill_from_ffi(&mut out, requested, fill)? };
Ok(out)
}
pub(crate) unsafe fn try_read_mapped_from_ffi<Raw: FfiOutput, Safe>(
requested: i32,
fill: impl FnOnce(*mut Raw, i32) -> i32,
mut map: impl FnMut(Raw) -> Result<Safe>,
) -> Result<Vec<Safe>> {
let raw = unsafe { read_from_ffi(requested, fill)? };
let mut out = Vec::new();
out.try_reserve(raw.len())
.map_err(|_| Error::FfiOutputAllocationFailed)?;
for raw in raw {
out.push(map(raw)?);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::{fill_from_ffi, try_read_mapped_from_ffi};
use crate::Error;
use boxdd_sys::ffi;
use std::panic::{AssertUnwindSafe, catch_unwind};
fn raw_shape(index: i32) -> ffi::b2ShapeId {
ffi::b2ShapeId {
index1: index,
world0: 2,
generation: 3,
}
}
#[test]
fn grows_from_existing_capacity_to_the_full_request() {
let mut out = Vec::<ffi::b2ShapeId>::with_capacity(8);
unsafe {
fill_from_ffi(&mut out, 10, |ptr, capacity| {
for index in 0..capacity {
ptr.add(index as usize).write(raw_shape(index));
}
capacity
})
.unwrap();
}
assert!(out.capacity() >= 10);
assert_eq!(out.len(), 10);
assert_eq!(out[9].index1, 9);
}
#[test]
fn zero_request_clears_without_calling_native_fill() {
let mut out = vec![raw_shape(7)];
unsafe {
fill_from_ffi(&mut out, 0, |_ptr, _capacity| {
panic!("zero-capacity fill must not run")
})
.unwrap();
}
assert!(out.is_empty());
}
#[test]
fn exact_and_excess_capacity_expose_only_initialized_values() {
for existing_capacity in [2, 8] {
let mut out = Vec::<ffi::b2ShapeId>::with_capacity(existing_capacity);
let original_ptr = out.as_ptr();
unsafe {
fill_from_ffi(&mut out, 2, |ptr, _capacity| {
ptr.write(raw_shape(11));
ptr.add(1).write(raw_shape(12));
2
})
.unwrap();
}
assert_eq!(out.len(), 2);
assert_eq!(out[0].index1, 11);
assert_eq!(out[1].index1, 12);
assert_eq!(out.as_ptr(), original_ptr);
}
}
#[test]
fn repeated_fill_reuses_the_allocation() {
let mut out = Vec::<ffi::b2ShapeId>::with_capacity(4);
unsafe {
fill_from_ffi(&mut out, 4, |ptr, _capacity| {
ptr.write(raw_shape(1));
1
})
.unwrap();
}
let original_ptr = out.as_ptr();
unsafe {
fill_from_ffi(&mut out, 4, |ptr, _capacity| {
ptr.write(raw_shape(2));
1
})
.unwrap();
}
assert_eq!(out.as_ptr(), original_ptr);
assert_eq!(out.len(), 1);
assert_eq!(out[0].index1, 2);
}
#[test]
fn negative_request_is_rejected_after_clearing() {
let mut out = vec![raw_shape(7)];
let error = unsafe { fill_from_ffi(&mut out, -1, |_ptr, _capacity| 0) }.unwrap_err();
assert_eq!(error, Error::NegativeFfiOutputCapacity { capacity: -1 });
assert!(out.is_empty());
}
#[test]
fn negative_native_count_is_rejected_without_committing_length() {
let mut out = vec![raw_shape(7)];
let error = unsafe { fill_from_ffi(&mut out, 2, |_ptr, _capacity| -1) }.unwrap_err();
assert_eq!(error, Error::NegativeFfiOutputCount { count: -1 });
assert!(out.is_empty());
}
#[test]
fn excessive_native_count_is_rejected_without_committing_length() {
let mut out = vec![raw_shape(7)];
let error = unsafe { fill_from_ffi(&mut out, 2, |_ptr, _capacity| 3) }.unwrap_err();
assert_eq!(
error,
Error::FfiOutputCountExceedsCapacity {
count: 3,
capacity: 2,
}
);
assert!(out.is_empty());
}
#[test]
fn fill_panic_leaves_the_visible_length_at_zero() {
let mut out = vec![raw_shape(7)];
let panic = catch_unwind(AssertUnwindSafe(|| unsafe {
let _ = fill_from_ffi(&mut out, 2, |ptr, _capacity| {
ptr.write(raw_shape(1));
panic!("synthetic native fill panic");
});
}));
assert!(panic.is_err());
assert!(out.is_empty());
}
#[test]
fn mapped_read_discards_partial_output_on_error() {
let error = unsafe {
try_read_mapped_from_ffi(
2,
|ptr: *mut ffi::b2ShapeId, _capacity| {
ptr.write(raw_shape(1));
ptr.add(1).write(raw_shape(2));
2
},
|raw| {
if raw.index1 == 2 {
Err(Error::WrongWorld)
} else {
Ok(i64::from(raw.index1))
}
},
)
}
.unwrap_err();
assert_eq!(error, Error::WrongWorld);
}
#[test]
fn mapped_read_supports_a_different_safe_layout() {
let out = unsafe {
try_read_mapped_from_ffi(
2,
|ptr: *mut ffi::b2ShapeId, capacity| {
for index in 0..capacity {
ptr.add(index as usize).write(raw_shape(index));
}
capacity
},
|raw| Ok([i64::from(raw.index1), i64::from(raw.world0), 17]),
)
.unwrap()
};
assert_eq!(out, [[0, 2, 17], [1, 2, 17]]);
}
}