use fuel_backend_contract::backend::HostStorage;
use fuel_ir::{DType, Error, HostBuffer, HostBufferRef, Result};
use memmap2::Mmap;
use std::fs::File;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct MmappedHostStorage {
mmap: Arc<Mmap>,
dtype: DType,
elem_offset: usize,
elem_count: usize,
}
impl MmappedHostStorage {
pub unsafe fn from_file(
file: &File,
dtype: DType,
byte_offset: usize,
elem_count: usize,
) -> Result<Self> {
let mmap =
unsafe { Mmap::map(file) }.map_err(|e| Error::Msg(format!("mmap failed: {e}")).bt())?;
unsafe { Self::from_shared_mmap(Arc::new(mmap), dtype, byte_offset, elem_count) }
}
pub unsafe fn from_shared_mmap(
mmap: Arc<Mmap>,
dtype: DType,
byte_offset: usize,
elem_count: usize,
) -> Result<Self> {
let elem_size = dtype.size_in_bytes();
if elem_size == 0 {
return Err(Error::Msg("dtype has zero size".into()).bt());
}
if !byte_offset.is_multiple_of(elem_size) {
return Err(Error::Msg(format!(
"mmap byte_offset {byte_offset} not aligned to {elem_size}-byte dtype {dtype:?}",
))
.bt());
}
let end = byte_offset
.checked_add(
elem_count
.checked_mul(elem_size)
.ok_or_else(|| Error::Msg("elem_count overflow".into()).bt())?,
)
.ok_or_else(|| Error::Msg("byte range overflow".into()).bt())?;
if end > mmap.len() {
return Err(Error::Msg(format!(
"mmap range [{byte_offset}, {end}) out of bounds (len = {})",
mmap.len()
))
.bt());
}
Ok(Self {
mmap,
dtype,
elem_offset: byte_offset / elem_size,
elem_count,
})
}
pub fn dtype(&self) -> DType {
self.dtype
}
pub fn len(&self) -> usize {
self.elem_count
}
pub fn is_empty(&self) -> bool {
self.elem_count == 0
}
unsafe fn typed_slice<T>(&self) -> &[T] {
let byte_offset = self.elem_offset * self.dtype.size_in_bytes();
let ptr = unsafe { self.mmap.as_ptr().add(byte_offset) } as *const T;
unsafe { std::slice::from_raw_parts(ptr, self.elem_count) }
}
}
impl HostStorage for MmappedHostStorage {
fn as_host_buffer_ref(&self) -> Result<HostBufferRef<'_>> {
let r = unsafe {
match self.dtype {
DType::U8 => HostBufferRef::U8(self.typed_slice::<u8>()),
DType::I8 => HostBufferRef::I8(self.typed_slice::<i8>()),
DType::U32 => HostBufferRef::U32(self.typed_slice::<u32>()),
DType::I16 => HostBufferRef::I16(self.typed_slice::<i16>()),
DType::I32 => HostBufferRef::I32(self.typed_slice::<i32>()),
DType::I64 => HostBufferRef::I64(self.typed_slice::<i64>()),
DType::BF16 => HostBufferRef::BF16(self.typed_slice::<half::bf16>()),
DType::F16 => HostBufferRef::F16(self.typed_slice::<half::f16>()),
DType::F32 => HostBufferRef::F32(self.typed_slice::<f32>()),
DType::F64 => HostBufferRef::F64(self.typed_slice::<f64>()),
DType::F8E4M3 => HostBufferRef::F8E4M3(self.typed_slice::<float8::F8E4M3>()),
DType::F6E2M3 => HostBufferRef::F6E2M3(self.typed_slice::<u8>()),
DType::F6E3M2 => HostBufferRef::F6E3M2(self.typed_slice::<u8>()),
DType::F4 => HostBufferRef::F4(self.typed_slice::<u8>()),
DType::F8E8M0 => HostBufferRef::F8E8M0(self.typed_slice::<u8>()),
DType::F8E5M2 => {
return Err(
Error::UnsupportedDTypeForOp(self.dtype, "as_host_buffer_ref").bt(),
);
}
DType::F8E6M2 => {
return Err(
Error::UnsupportedDTypeForOp(self.dtype, "as_host_buffer_ref").bt(),
);
}
DType::Bool => HostBufferRef::Bool(self.typed_slice::<u8>()),
}
};
Ok(r)
}
fn into_host_buffer(self) -> Result<HostBuffer> {
Ok(self.as_host_buffer_ref()?.to_owned())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn mmap_of(
bytes: &[u8],
dtype: DType,
elem_count: usize,
) -> (tempfile::NamedTempFile, MmappedHostStorage) {
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(bytes).unwrap();
f.flush().unwrap();
let storage =
unsafe { MmappedHostStorage::from_file(f.as_file(), dtype, 0, elem_count) }.unwrap();
(f, storage)
}
#[test]
fn mmap_f32_round_trip() {
let values = [1.0_f32, 2.0, 3.0, 4.0];
let bytes: &[u8] = bytemuck::cast_slice(&values);
let (_keep_file_alive, storage) = mmap_of(bytes, DType::F32, values.len());
let view = storage.as_host_buffer_ref().unwrap();
assert_eq!(view.dtype(), DType::F32);
assert_eq!(view.len(), 4);
match view {
HostBufferRef::F32(s) => assert_eq!(s, &values),
_ => panic!("unexpected dtype"),
}
}
#[test]
fn mmap_rejects_misaligned_offset() {
let bytes = vec![0u8; 16];
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(&bytes).unwrap();
f.flush().unwrap();
let err =
unsafe { MmappedHostStorage::from_file(f.as_file(), DType::F32, 1, 1) }.unwrap_err();
assert!(
format!("{err}").contains("not aligned"),
"expected alignment error, got: {err}"
);
}
#[test]
fn mmap_rejects_out_of_bounds() {
let bytes = vec![0u8; 8];
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(&bytes).unwrap();
f.flush().unwrap();
let err =
unsafe { MmappedHostStorage::from_file(f.as_file(), DType::F32, 0, 3) }.unwrap_err();
assert!(
format!("{err}").contains("out of bounds"),
"expected OOB error, got: {err}"
);
}
#[test]
fn mmap_zero_length_is_ok() {
let bytes = vec![0u8; 0];
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(&bytes).unwrap();
f.flush().unwrap();
let storage = match unsafe { MmappedHostStorage::from_file(f.as_file(), DType::F32, 0, 0) }
{
Ok(s) => s,
Err(e) if format!("{e}").contains("mmap failed") => return,
Err(e) => panic!("unexpected error: {e}"),
};
assert_eq!(storage.len(), 0);
assert!(storage.is_empty());
}
#[test]
fn mmap_into_host_buffer_copies() {
let values = [10.0_f32, 20.0, 30.0];
let bytes: &[u8] = bytemuck::cast_slice(&values);
let (_keep, storage) = mmap_of(bytes, DType::F32, values.len());
let owned = storage.into_host_buffer().unwrap();
match owned {
HostBuffer::F32(v) => assert_eq!(v, values),
_ => panic!("unexpected dtype"),
}
}
#[test]
fn mmap_shared_mmap_multi_view() {
let values = [1.0_f32, 2.0, 3.0, 4.0];
let bytes: &[u8] = bytemuck::cast_slice(&values);
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(bytes).unwrap();
f.flush().unwrap();
let mmap = Arc::new(unsafe { Mmap::map(f.as_file()) }.unwrap());
let head = unsafe { MmappedHostStorage::from_shared_mmap(mmap.clone(), DType::F32, 0, 2) }
.unwrap();
let tail = unsafe { MmappedHostStorage::from_shared_mmap(mmap, DType::F32, 8, 2) }.unwrap();
let (h, t) = (
head.as_host_buffer_ref().unwrap(),
tail.as_host_buffer_ref().unwrap(),
);
match (h, t) {
(HostBufferRef::F32(h), HostBufferRef::F32(t)) => {
assert_eq!(h, &[1.0, 2.0]);
assert_eq!(t, &[3.0, 4.0]);
}
_ => panic!("unexpected dtype"),
}
}
}