use std::{
io::{self, Read, Write},
ptr, slice,
};
#[derive(Debug)]
pub struct FdMmap {
ptr: usize,
len: usize,
fd: FdGuard,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FdGuard(pub i32);
impl Default for FdGuard {
fn default() -> Self {
Self(-1)
}
}
impl Clone for FdGuard {
fn clone(&self) -> Self {
unsafe { Self(libc::dup(self.0)) }
}
}
impl Drop for FdGuard {
fn drop(&mut self) {
if self.0 >= 0 {
unsafe {
libc::close(self.0);
}
self.0 = -1;
}
}
}
impl FdMmap {
pub fn new() -> Self {
let empty = Vec::<u8>::new();
Self {
ptr: empty.as_ptr() as usize,
len: 0,
fd: FdGuard::default(),
}
}
pub fn accessible_reserved(
accessible_size: usize,
mapping_size: usize,
) -> Result<Self, String> {
let page_size = region::page::size();
assert!(accessible_size <= mapping_size);
assert_eq!(mapping_size & (page_size - 1), 0);
assert_eq!(accessible_size & (page_size - 1), 0);
if mapping_size == 0 {
return Ok(Self::new());
}
let fd = unsafe {
let file = libc::tmpfile();
if file.is_null() {
return Err(format!(
"failed to create temporary file - {}",
io::Error::last_os_error()
));
}
FdGuard(libc::fileno(file))
};
unsafe {
if libc::ftruncate(fd.0, mapping_size as libc::off_t) < 0 {
return Err("could not truncate tmpfile".to_string());
}
}
Ok(if accessible_size == mapping_size {
let ptr = unsafe {
libc::mmap(
ptr::null_mut(),
mapping_size,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_FILE | libc::MAP_SHARED,
fd.0,
0,
)
};
if ptr as isize == -1_isize {
return Err(io::Error::last_os_error().to_string());
}
Self {
ptr: ptr as usize,
len: mapping_size,
fd,
}
} else {
let ptr = unsafe {
libc::mmap(
ptr::null_mut(),
mapping_size,
libc::PROT_NONE,
libc::MAP_FILE | libc::MAP_SHARED,
fd.0,
0,
)
};
if ptr as isize == -1_isize {
return Err(io::Error::last_os_error().to_string());
}
let mut result = Self {
ptr: ptr as usize,
len: mapping_size,
fd,
};
if accessible_size != 0 {
result.make_accessible(0, accessible_size)?;
}
result
})
}
pub fn make_accessible(&mut self, start: usize, len: usize) -> Result<(), String> {
let page_size = region::page::size();
assert_eq!(start & (page_size - 1), 0);
assert_eq!(len & (page_size - 1), 0);
assert!(len < self.len);
assert!(start < self.len - len);
let ptr = self.ptr as *const u8;
unsafe { region::protect(ptr.add(start), len, region::Protection::READ_WRITE) }
.map_err(|e| e.to_string())
}
pub fn as_slice(&self) -> &[u8] {
unsafe { slice::from_raw_parts(self.ptr as *const u8, self.len) }
}
pub fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe { slice::from_raw_parts_mut(self.ptr as *mut u8, self.len) }
}
pub fn as_mut_ptr(&mut self) -> *mut u8 {
self.ptr as *mut u8
}
pub fn len(&self) -> usize {
self.len
}
pub fn duplicate(&self, hint_used: Option<usize>) -> Result<Self, String> {
use std::os::unix::prelude::FromRawFd;
if self.len == 0 {
return Ok(Self::new());
}
unsafe {
libc::fsync(self.fd.0);
}
let fd = unsafe {
let file = libc::tmpfile();
if file.is_null() {
return Err(format!(
"failed to create temporary file - {}",
io::Error::last_os_error()
));
}
FdGuard(libc::fileno(file))
};
unsafe {
if libc::ioctl(fd.0, 0x94, 9, self.fd.0) != 0
{
#[cfg(feature = "tracing")]
trace!("memory copy started");
let len = match hint_used {
Some(a) => a,
None => self.len,
};
let mut source = std::fs::File::from_raw_fd(self.fd.0);
let mut out = std::fs::File::from_raw_fd(fd.0);
copy_file_range(&mut source, 0, &mut out, 0, len)
.map_err(|err| format!("Could not copy memory: {err}"))?;
#[cfg(feature = "tracing")]
trace!("memory copy finished (size={})", len);
}
}
let flags = libc::MAP_FILE | libc::MAP_SHARED;
let ptr = unsafe {
libc::mmap(
ptr::null_mut(),
self.len,
libc::PROT_READ | libc::PROT_WRITE,
flags,
fd.0,
0,
)
};
if ptr as isize == -1_isize {
return Err(io::Error::last_os_error().to_string());
}
Ok(Self {
ptr: ptr as usize,
len: self.len,
fd,
})
}
}
impl Drop for FdMmap {
fn drop(&mut self) {
if self.len != 0 {
let r = unsafe { libc::munmap(self.ptr as *mut libc::c_void, self.len) };
assert_eq!(r, 0, "munmap failed: {}", io::Error::last_os_error());
}
}
}
#[cfg(target_family = "unix")]
fn copy_file_range(
source: &mut std::fs::File,
source_offset: u64,
out: &mut std::fs::File,
out_offset: u64,
len: usize,
) -> Result<(), std::io::Error> {
use std::io::{Seek, SeekFrom};
let source_original_pos = source.stream_position()?;
source.seek(SeekFrom::Start(source_offset))?;
let out_original_pos = out.stream_position()?;
out.seek(SeekFrom::Start(out_offset))?;
let mut buffer = vec![0u8; 4096];
let mut to_read = len;
while to_read > 0 {
let chunk_size = std::cmp::min(to_read, buffer.len());
let read = source.read(&mut buffer[0..chunk_size])?;
out.write_all(&buffer[0..read])?;
to_read -= read;
}
out.flush()?;
source.seek(SeekFrom::Start(source_original_pos))?;
out.flush()?;
out.sync_data()?;
out.seek(SeekFrom::Start(out_original_pos))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_family = "unix")]
#[test]
fn test_copy_file_range() -> Result<(), std::io::Error> {
use std::{fs::OpenOptions, io::Seek};
let dir = std::env::temp_dir().join("wasmer/copy_file_range");
if dir.is_dir() {
std::fs::remove_dir_all(&dir).unwrap()
}
std::fs::create_dir_all(&dir).unwrap();
let pa = dir.join("a");
let pb = dir.join("b");
let data: Vec<u8> = (0..100).collect();
let mut a = OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(pa)
.unwrap();
a.write_all(&data).unwrap();
let datb: Vec<u8> = (100..200).collect();
let mut b = OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(pb)
.unwrap();
b.write_all(&datb).unwrap();
a.seek(io::SeekFrom::Start(30)).unwrap();
b.seek(io::SeekFrom::Start(99)).unwrap();
copy_file_range(&mut a, 10, &mut b, 40, 15).unwrap();
assert_eq!(a.stream_position().unwrap(), 30);
assert_eq!(b.stream_position().unwrap(), 99);
b.seek(io::SeekFrom::Start(0)).unwrap();
let mut out = Vec::new();
let len = b.read_to_end(&mut out).unwrap();
assert_eq!(len, 100);
assert_eq!(out[0..40], datb[0..40]);
assert_eq!(out[40..55], data[10..25]);
assert_eq!(out[55..100], datb[55..100]);
Ok(())
}
}