Add a Module::deserialize_file method (#3266)
* Add a `Module::deserialize_file` method This commit adds a new method to the `wasmtime::Module` type, `deserialize_file`. This is intended to be the same as the `deserialize` method except for the serialized module is present as an on-disk file. This enables Wasmtime to internally use `mmap` to avoid copying bytes around and generally makes loading a module much faster. A C API is added in this commit as well for various bindings to use this accelerated path now as well. Another option perhaps for a Rust-based API is to have an API taking a `File` itself to allow for a custom file descriptor in one way or another, but for now that's left for a possible future refactoring if we find a use case. * Fix compat with main - handle readdonly mmap * wip * Try to fix Windows support
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
//! Low-level abstraction for allocating and managing zero-filled pages
|
||||
//! of memory.
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::anyhow;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use more_asserts::assert_le;
|
||||
use std::convert::TryFrom;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::ptr;
|
||||
use std::slice;
|
||||
|
||||
@@ -22,6 +27,7 @@ pub struct Mmap {
|
||||
// the coordination all happens at the OS layer.
|
||||
ptr: usize,
|
||||
len: usize,
|
||||
file: Option<File>,
|
||||
}
|
||||
|
||||
impl Mmap {
|
||||
@@ -34,6 +40,7 @@ impl Mmap {
|
||||
Self {
|
||||
ptr: empty.as_ptr() as usize,
|
||||
len: 0,
|
||||
file: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +51,117 @@ impl Mmap {
|
||||
Self::accessible_reserved(rounded_size, rounded_size)
|
||||
}
|
||||
|
||||
/// Creates a new `Mmap` by opening the file located at `path` and mapping
|
||||
/// it into memory.
|
||||
///
|
||||
/// The memory is mapped in read-only mode for the entire file. If portions
|
||||
/// of the file need to be modified then the `region` crate can be use to
|
||||
/// alter permissions of each page.
|
||||
///
|
||||
/// The memory mapping and the length of the file within the mapping are
|
||||
/// returned.
|
||||
pub fn from_file(path: &Path) -> Result<Self> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::prelude::*;
|
||||
|
||||
let file = File::open(path).context("failed to open file")?;
|
||||
let len = file
|
||||
.metadata()
|
||||
.context("failed to get file metadata")?
|
||||
.len();
|
||||
let len = usize::try_from(len).map_err(|_| anyhow!("file too large to map"))?;
|
||||
let ptr = unsafe {
|
||||
libc::mmap(
|
||||
ptr::null_mut(),
|
||||
len,
|
||||
libc::PROT_READ,
|
||||
libc::MAP_PRIVATE,
|
||||
file.as_raw_fd(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if ptr as isize == -1_isize {
|
||||
return Err(io::Error::last_os_error())
|
||||
.context(format!("mmap failed to allocate {:#x} bytes", len));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
ptr: ptr as usize,
|
||||
len,
|
||||
file: Some(file),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::fs::OpenOptions;
|
||||
use std::os::windows::prelude::*;
|
||||
use winapi::um::handleapi::*;
|
||||
use winapi::um::memoryapi::*;
|
||||
use winapi::um::winnt::*;
|
||||
unsafe {
|
||||
// Open the file with read/execute access and only share for
|
||||
// read. This will enable us to perform the proper mmap below
|
||||
// while also disallowing other processes modifying the file
|
||||
// and having those modifications show up in our address space.
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.access_mode(FILE_GENERIC_READ | FILE_GENERIC_EXECUTE)
|
||||
.share_mode(FILE_SHARE_READ)
|
||||
.open(path)
|
||||
.context("failed to open file")?;
|
||||
|
||||
let len = file
|
||||
.metadata()
|
||||
.context("failed to get file metadata")?
|
||||
.len();
|
||||
let len = usize::try_from(len).map_err(|_| anyhow!("file too large to map"))?;
|
||||
|
||||
// Create a file mapping that allows PAGE_EXECUTE_READ which
|
||||
// we'll be using for mapped text sections in ELF images later.
|
||||
let mapping = CreateFileMappingW(
|
||||
file.as_raw_handle().cast(),
|
||||
ptr::null_mut(),
|
||||
PAGE_EXECUTE_READ,
|
||||
0,
|
||||
0,
|
||||
ptr::null(),
|
||||
);
|
||||
if mapping.is_null() {
|
||||
return Err(io::Error::last_os_error())
|
||||
.context("failed to create file mapping");
|
||||
}
|
||||
|
||||
// Create a view for the entire file using `FILE_MAP_EXECUTE`
|
||||
// here so that we can later change the text section to execute.
|
||||
let ptr = MapViewOfFile(mapping, FILE_MAP_READ | FILE_MAP_EXECUTE, 0, 0, len);
|
||||
let err = io::Error::last_os_error();
|
||||
CloseHandle(mapping);
|
||||
if ptr.is_null() {
|
||||
return Err(err)
|
||||
.context(format!("failed to create map view of {:#x} bytes", len));
|
||||
}
|
||||
|
||||
let ret = Self {
|
||||
ptr: ptr as usize,
|
||||
len,
|
||||
file: Some(file),
|
||||
};
|
||||
|
||||
// Protect the entire file as PAGE_READONLY to start (i.e.
|
||||
// remove the execute bit)
|
||||
let mut old = 0;
|
||||
if VirtualProtect(ret.ptr as *mut _, ret.len, PAGE_READONLY, &mut old) == 0 {
|
||||
return Err(io::Error::last_os_error())
|
||||
.context("failed change pages to `PAGE_READONLY`");
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `Mmap` pointing to `accessible_size` bytes of page-aligned accessible memory,
|
||||
/// within a reserved mapping of `mapping_size` bytes. `accessible_size` and `mapping_size`
|
||||
/// must be native page-size multiples.
|
||||
@@ -83,6 +201,7 @@ impl Mmap {
|
||||
Self {
|
||||
ptr: ptr as usize,
|
||||
len: mapping_size,
|
||||
file: None,
|
||||
}
|
||||
} else {
|
||||
// Reserve the mapping size.
|
||||
@@ -107,6 +226,7 @@ impl Mmap {
|
||||
let mut result = Self {
|
||||
ptr: ptr as usize,
|
||||
len: mapping_size,
|
||||
file: None,
|
||||
};
|
||||
|
||||
if accessible_size != 0 {
|
||||
@@ -152,6 +272,7 @@ impl Mmap {
|
||||
Self {
|
||||
ptr: ptr as usize,
|
||||
len: mapping_size,
|
||||
file: None,
|
||||
}
|
||||
} else {
|
||||
// Reserve the mapping size.
|
||||
@@ -164,6 +285,7 @@ impl Mmap {
|
||||
let mut result = Self {
|
||||
ptr: ptr as usize,
|
||||
len: mapping_size,
|
||||
file: None,
|
||||
};
|
||||
|
||||
if accessible_size != 0 {
|
||||
@@ -234,6 +356,7 @@ impl Mmap {
|
||||
|
||||
/// Return the allocated memory as a mutable slice of u8.
|
||||
pub fn as_mut_slice(&mut self) -> &mut [u8] {
|
||||
debug_assert!(!self.is_readonly());
|
||||
unsafe { slice::from_raw_parts_mut(self.ptr as *mut u8, self.len) }
|
||||
}
|
||||
|
||||
@@ -257,9 +380,65 @@ impl Mmap {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) unsafe fn from_raw(ptr: usize, len: usize) -> Self {
|
||||
Self { ptr, len }
|
||||
/// Returns whether the underlying mapping is readonly, meaning that
|
||||
/// attempts to write will fault.
|
||||
pub fn is_readonly(&self) -> bool {
|
||||
self.file.is_some()
|
||||
}
|
||||
|
||||
/// Makes the specified `range` within this `Mmap` to be read/write.
|
||||
pub unsafe fn make_writable(&self, range: Range<usize>) -> Result<()> {
|
||||
assert!(range.start <= self.len());
|
||||
assert!(range.end <= self.len());
|
||||
assert!(range.start <= range.end);
|
||||
assert!(
|
||||
range.start % region::page::size() == 0,
|
||||
"changing of protections isn't page-aligned",
|
||||
);
|
||||
|
||||
let base = self.as_ptr().add(range.start);
|
||||
let len = range.end - range.start;
|
||||
|
||||
// On Windows when we have a file mapping we need to specifically use
|
||||
// `PAGE_WRITECOPY` to ensure that pages are COW'd into place because
|
||||
// we don't want our modifications to go back to the original file.
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use winapi::um::memoryapi::*;
|
||||
use winapi::um::winnt::*;
|
||||
|
||||
if self.file.is_some() {
|
||||
let mut old = 0;
|
||||
if VirtualProtect(base as *mut _, len, PAGE_WRITECOPY, &mut old) == 0 {
|
||||
return Err(io::Error::last_os_error())
|
||||
.context("failed to change pages to `PAGE_WRITECOPY`");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// If we're not on Windows or if we're on Windows with an anonymous
|
||||
// mapping then we can use the `region` crate.
|
||||
region::protect(base, len, region::Protection::READ_WRITE)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Makes the specified `range` within this `Mmap` to be read/execute.
|
||||
pub unsafe fn make_executable(&self, range: Range<usize>) -> Result<()> {
|
||||
assert!(range.start <= self.len());
|
||||
assert!(range.end <= self.len());
|
||||
assert!(range.start <= range.end);
|
||||
assert!(
|
||||
range.start % region::page::size() == 0,
|
||||
"changing of protections isn't page-aligned",
|
||||
);
|
||||
|
||||
region::protect(
|
||||
self.as_ptr().add(range.start),
|
||||
range.end - range.start,
|
||||
region::Protection::READ_EXECUTE,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,10 +455,15 @@ impl Drop for Mmap {
|
||||
fn drop(&mut self) {
|
||||
if self.len != 0 {
|
||||
use winapi::ctypes::c_void;
|
||||
use winapi::um::memoryapi::VirtualFree;
|
||||
use winapi::um::memoryapi::*;
|
||||
use winapi::um::winnt::MEM_RELEASE;
|
||||
let r = unsafe { VirtualFree(self.ptr as *mut c_void, 0, MEM_RELEASE) };
|
||||
assert_ne!(r, 0);
|
||||
if self.file.is_none() {
|
||||
let r = unsafe { VirtualFree(self.ptr as *mut c_void, 0, MEM_RELEASE) };
|
||||
assert_ne!(r, 0);
|
||||
} else {
|
||||
let r = unsafe { UnmapViewOfFile(self.ptr as *mut c_void) };
|
||||
assert_ne!(r, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user