use std::fmt;
use std::ptr::NonNull;
use region::{Allocation, Protection};
use crate::unified::{DeviceId, UnifiedError};
struct GuardedAllocation {
#[allow(dead_code)]
full_alloc: Allocation,
#[allow(dead_code)]
usable_offset: usize,
#[allow(dead_code)]
usable_size: usize,
}
pub struct GuardedHostBuffer {
ptr: NonNull<u8>,
size: usize,
device_id: DeviceId,
allocation: GuardedAllocation,
}
unsafe impl Send for GuardedHostBuffer {}
unsafe impl Sync for GuardedHostBuffer {}
impl GuardedHostBuffer {
pub fn new(size: usize) -> Result<Self, UnifiedError> {
Self::new_on(size, DeviceId::default())
}
pub fn new_on(size: usize, device_id: DeviceId) -> Result<Self, UnifiedError> {
if size == 0 {
return Err(UnifiedError::ZeroSize);
}
let page = region::page::size();
let usable = size.checked_next_multiple_of(page).ok_or_else(|| {
UnifiedError::Allocation(format!(
"size {size} overflows usable bytes when rounded to page size {page}"
))
})?;
let total = usable.checked_add(2 * page).ok_or_else(|| {
UnifiedError::Allocation(format!(
"usable {usable} overflows total bytes when adding 2 guard pages of {page}"
))
})?;
let alloc = region::alloc(total, Protection::NONE)
.map_err(|e| UnifiedError::Allocation(format!("region::alloc: {e}")))?;
let base = alloc.as_ptr::<u8>();
let usable_start = unsafe { base.add(page) };
unsafe {
region::protect(usable_start, usable, Protection::READ_WRITE)
.map_err(|e| UnifiedError::Allocation(format!("region::protect: {e}")))?;
}
let ptr = NonNull::new(usable_start as *mut u8)
.ok_or_else(|| UnifiedError::Allocation("region::alloc returned null".into()))?;
Ok(Self {
ptr,
size,
device_id,
allocation: GuardedAllocation {
full_alloc: alloc,
usable_offset: page,
usable_size: usable,
},
})
}
pub fn len(&self) -> usize {
self.size
}
pub fn is_empty(&self) -> bool {
self.size == 0
}
pub fn device_id(&self) -> DeviceId {
self.device_id
}
pub fn as_ptr(&self) -> *const u8 {
self.ptr.as_ptr() as *const u8
}
pub fn as_mut_ptr(&mut self) -> *mut u8 {
self.ptr.as_ptr()
}
pub fn as_slice(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.size) }
}
pub fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.size) }
}
#[cfg(test)]
pub(crate) fn usable_size(&self) -> usize {
self.allocation.usable_size
}
#[cfg(test)]
pub(crate) fn full_mapping_size(&self) -> usize {
self.allocation.full_alloc.len()
}
#[cfg(test)]
pub(crate) fn usable_offset(&self) -> usize {
self.allocation.usable_offset
}
pub fn prefetch_to_device(&self) -> Result<(), UnifiedError> {
Ok(())
}
pub fn prefetch_to_host(&self) -> Result<(), UnifiedError> {
Ok(())
}
}
impl fmt::Debug for GuardedHostBuffer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GuardedHostBuffer")
.field("ptr", &self.ptr.as_ptr())
.field("size", &self.size)
.field("usable_size", &self.allocation.usable_size)
.field("device_id", &self.device_id)
.finish()
}
}
#[deprecated(
since = "0.1.0",
note = "renamed to `GuardedHostBuffer`; this alias will be removed"
)]
pub type PinnedHostBuffer = GuardedHostBuffer;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip() {
let mut b = GuardedHostBuffer::new(128).unwrap();
b.as_mut_slice().fill(0xAB);
assert!(b.as_slice().iter().all(|&v| v == 0xAB));
assert_eq!(b.len(), 128);
assert!(!b.is_empty());
}
#[test]
fn zero_size_rejected() {
assert!(matches!(
GuardedHostBuffer::new(0).unwrap_err(),
UnifiedError::ZeroSize
));
}
#[test]
fn usize_max_size_rejected() {
let err = GuardedHostBuffer::new(usize::MAX).unwrap_err();
assert!(
matches!(err, UnifiedError::Allocation(_)),
"expected Allocation error, got {err:?}",
);
}
#[test]
fn device_id_carried() {
let b = GuardedHostBuffer::new_on(16, DeviceId(7)).unwrap();
assert_eq!(b.device_id(), DeviceId(7));
}
#[test]
fn prefetch_methods_are_no_ops() {
let b = GuardedHostBuffer::new(32).unwrap();
b.prefetch_to_device().expect("no-op should succeed");
b.prefetch_to_host().expect("no-op should succeed");
}
#[test]
fn len_reports_requested_not_rounded() {
let page = region::page::size();
let requested = 1234;
assert!(requested < page, "test assumes requested < page");
let b = GuardedHostBuffer::new(requested).unwrap();
assert_eq!(b.len(), requested);
assert_eq!(b.as_slice().len(), requested);
assert_eq!(b.usable_size(), page);
assert_eq!(b.full_mapping_size(), 3 * page);
assert_eq!(b.usable_offset(), page);
}
#[test]
fn full_range_round_trip() {
let page = region::page::size();
let requested = page + 17;
let mut b = GuardedHostBuffer::new(requested).unwrap();
for (i, byte) in b.as_mut_slice().iter_mut().enumerate() {
*byte = (i % 251) as u8;
}
for (i, &byte) in b.as_slice().iter().enumerate() {
assert_eq!(byte, (i % 251) as u8, "mismatch at {i}");
}
}
#[test]
fn guard_pages_are_inaccessible() {
let page = region::page::size();
let b = GuardedHostBuffer::new(1024).unwrap();
let usable_ptr = b.as_ptr();
let before = unsafe { usable_ptr.sub(page) };
let after = unsafe { usable_ptr.add(b.usable_size()) };
let prot_before = region::query(before).expect("query before-guard");
let prot_after = region::query(after).expect("query after-guard");
assert_eq!(
prot_before.protection(),
Protection::NONE,
"before-guard must be PROT_NONE/PAGE_NOACCESS",
);
assert_eq!(
prot_after.protection(),
Protection::NONE,
"after-guard must be PROT_NONE/PAGE_NOACCESS",
);
let prot_usable = region::query(usable_ptr).expect("query usable");
assert_eq!(
prot_usable.protection(),
Protection::READ_WRITE,
"usable region must be RW",
);
}
#[test]
fn drop_releases_mapping() {
for _ in 0..256 {
let b = GuardedHostBuffer::new(64 * 1024).unwrap();
drop(b);
}
}
}