From b92c12c5ff2af34b1f8c06783083a98fd9e65597 Mon Sep 17 00:00:00 2001 From: Steve Date: Sun, 29 Sep 2019 23:14:56 +0100 Subject: [PATCH] Replace ByteBuf impl with VirtualAlloc for Windows --- Cargo.toml | 8 +- src/lib.rs | 3 +- src/memory/mod.rs | 6 +- src/memory/virtualalloc_bytebuf.rs | 258 +++++++++++++++++++++++++++++ 4 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 src/memory/virtualalloc_bytebuf.rs diff --git a/Cargo.toml b/Cargo.toml index 0c86dc6..76151f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ libm = { version = "0.1.2", optional = true } num-rational = "0.2.2" num-traits = "0.2.8" libc = "0.2.58" +winapi = "0.3.8" [dev-dependencies] assert_matches = "1.1" @@ -25,7 +26,10 @@ rand = "0.4.2" wabt = "0.9" [features] -default = ["std"] +default = [ + "std", + "winapi/memoryapi" +] # Disable for no_std support std = [ "parity-wasm/std", @@ -41,7 +45,7 @@ core = [ "libm" ] # Enforce using the linear memory implementation based on `Vec` instead of -# mmap on unix systems. +# `mmap` on unix systems and `VirtualAlloc` on Windows systems. # # Useful for tests and if you need to minimize unsafe usage at the cost of performance on some # workloads. diff --git a/src/lib.rs b/src/lib.rs index 610723f..14181eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,7 +93,6 @@ //! ); //! } //! ``` - #![warn(missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] @@ -112,6 +111,8 @@ extern crate assert_matches; #[cfg(test)] extern crate wabt; +extern crate winapi; + extern crate memory_units as memory_units_crate; extern crate parity_wasm; diff --git a/src/memory/mod.rs b/src/memory/mod.rs index f438234..3324303 100644 --- a/src/memory/mod.rs +++ b/src/memory/mod.rs @@ -14,7 +14,11 @@ use Error; #[path = "mmap_bytebuf.rs"] mod bytebuf; -#[cfg(any(not(unix), feature = "vec_memory"))] +#[cfg(all(windows, not(feature = "vec_memory")))] +#[path = "virtualalloc_bytebuf.rs"] +mod bytebuf; + +#[cfg(any(all(not(windows), not(unix)), feature = "vec_memory"))] #[path = "vec_bytebuf.rs"] mod bytebuf; diff --git a/src/memory/virtualalloc_bytebuf.rs b/src/memory/virtualalloc_bytebuf.rs new file mode 100644 index 0000000..3ec83e6 --- /dev/null +++ b/src/memory/virtualalloc_bytebuf.rs @@ -0,0 +1,258 @@ +//! An implementation of a `ByteBuf` based on virtual memory. +//! +//! This implementation uses `VirtualAlloc` and `VirtualFree` on Windows systems. +//! +//! It reserves virtual memory up to `WASM_MAX_PAGES` for efficiency. +//! +//! This implementation is several orders of magnitudes faster than `vec_memory` implementation. +//! +//! NOTE: Pages in this source file refer to wasm32 pages which are defined by the spec as 64KiB in size. + +use winapi::shared::ntdef::NULL; +use winapi::shared::minwindef::LPVOID; +use winapi::shared::basetsd::SIZE_T; +use winapi::um::winnt::{MEM_RESERVE, MEM_RELEASE, MEM_COMMIT, PAGE_READWRITE}; +use winapi::um::memoryapi::{VirtualAlloc, VirtualFree}; + +use std::ptr::{NonNull}; +use std::slice; + +struct VAlloc { + /// The pointer that points to the start of the mapping. + /// + /// This value doesn't change after creation. + ptr: NonNull, + /// The length of this mapping. + /// + /// Cannot be more than `isize::max_value()`. This value doesn't change after creation. + len: usize, +} + +const WASM_PAGE_SIZE: SIZE_T = 65536; // Size of a wasm32 page in bytes +const WASM_MAX_PAGES: SIZE_T = 65536 * WASM_PAGE_SIZE; // The maximum number of pages of a wasm32 program in bytes. + +impl VAlloc { + /// Reserves new pages up to `65536 * 64KiB` and commits initial pages in the range [0, initial). + /// + /// Returns `Err` if: + /// - `len` should not exceed `isize::max_value()` + /// - `len` should be greater than 0. + /// - `VirtualAlloc` returns an error (almost certainly means out of memory). + /// - `VirtualAlloc` returns an error when committing pages. + /// - `initial pages` cannot be committed. + fn new(len: usize) -> Result { + println!("windows vm new = {:?}", len); + + if len > isize::max_value() as usize { + return Err("`len` should not exceed `isize::max_value()`"); + } + + if len == 0 { + return Err("`len` should be greater than 0"); + } + + let ptr = unsafe { + // Safety Proof: + // There are not specific safety proofs are required for this call, since the call + // by itself can't invoke any safety problems (however, misusing its result can). + // + // VirtualAlloc zeroes out allocated pages. + VirtualAlloc( + // `lpAddress` - let the system to choose the address at which to create the mapping. + NULL, + // `dwSize` - allocate the maximum number of wasm32 pages to bypass the overhead of rezising. + WASM_MAX_PAGES, + // `flAllocationType` - reserve pages so that they are not committed to memory immediately. + MEM_RESERVE, + // `flProtect` - apply READ WRITE !EXECUTE protection. + PAGE_READWRITE, + ) + }; + + // Checking if there is an error with allocating memory pages. + let base_ptr = match ptr { + NULL => return Err("VirtualAlloc returned an error"), + _ => ptr as *mut u8, + }; + + // Commit initial pages. + let ptr = unsafe { + // Even though we are committing, actual physical pages are not allocated until + // they are accessed. + // + // Safety proof: + // This should work once pages are successfully reserved. + // Issue arise only with passing the wrong arguments or resource exhaustion. + VirtualAlloc( + // `lpAddress` - set the base address of pages to commit. + base_ptr as LPVOID, + // `dwSize` - set the length of pages to commit. This is the same as the initial or minimum. + len, + // `flAllocationType` - commit pages so that it can be read/written to. + MEM_COMMIT, + // `flProtect` - apply READ WRITE !EXECUTE protection. + PAGE_READWRITE, + ) + }; + + // Checking if there is an error with allocating memory pages. + if ptr == NULL { + return Err("VirtualAlloc couldn't commit initial pages") + } + + let base_ptr = NonNull::new(base_ptr).ok_or("VirtualAlloc returned an error")?; + + Ok( + Self { ptr: base_ptr, len } + ) + } + + /// Commits more pages `new_len` to be used by + /// + fn grow(&mut self, new_len: usize) -> Result<(), &'static str> { + // Pointer to memory base + let base_ptr = self.ptr.as_ptr() as LPVOID; + + // Commit initial pages. + let ptr = unsafe { + // Even though we are committing, actual physical pages are not allocated until + // they are accessed. + // + // Safety proof: + // This should work once pages are successfully reserved. + // Issue arise only with passing the wrong arguments or resource exhaustion. + VirtualAlloc( + // `lpAddress` - set the base address of pages to commit. + // Overlapping committed pages are ignored. + base_ptr, + // `dwSize` - set the length of pages to commit. This is the same as the initial or minimum. + new_len, + // `flAllocationType` - commit pages so that it can be read/written to. + MEM_COMMIT, + // `flProtect` - apply READ WRITE !EXECUTE protection. + PAGE_READWRITE, + ) + }; + + // Checking if there is an error with allocating memory pages. + if ptr == NULL { + return Err("VirtualAlloc couldn't commit pages on grow") + } + + Ok(()) + } + + fn as_slice(&self) -> &[u8] { + unsafe { + // Safety Proof: + // - Aliasing guarantees of `self.ptr` are not violated since `self` is the only owner. + // - This pointer was allocated for `self.len` bytes and thus is a valid slice. + // - `self.len` doesn't change throughout the lifetime of `self`. + // - The value is returned valid for the duration of lifetime of `self`. + // `self` cannot be destroyed while the returned slice is alive. + // - `self.ptr` is of `NonNull` type and thus `.as_ptr()` can never return NULL. + // - `self.len` cannot be larger than `isize::max_value()`. + slice::from_raw_parts(self.ptr.as_ptr(), self.len) + } + } + + fn as_slice_mut(&mut self) -> &mut [u8] { + unsafe { + // Safety Proof: + // - See the proof for `Self::as_slice` + // - Additionally, it is not possible to obtain two mutable references for `self.ptr` + slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) + } + } +} + +impl Drop for VAlloc { + fn drop(&mut self) { + let ret_val = unsafe { + // Safety proof: + // - `self.ptr` was allocated by a call to `VirtualAlloc`. + VirtualFree( + // lpAddress - base address of pages to deallocate. + self.ptr.as_ptr() as LPVOID, + // dwSize - since address was provided by VirtualAlloc, we can provide 0 to deallocate pages allocated. + 0, + // dwFreeType - frees pages allocated by VirtualAlloc. + MEM_RELEASE, + ) + }; + + // `VirtualFree` can fail if the wrong arguments are passed, for example using MEM_RELEASE `dwFreeType` + // with a non-zero `dwSize`. + // + // Asserting here to make sure we do not fail silently and leak memory when we can't free pages. + // + // VirtualFree is successful if it returns a non-zero value. + assert_ne!(ret_val, 0, "VirtualFree failed"); + } +} + +pub struct ByteBuf { + region: Option, +} + +impl ByteBuf { + pub fn new(len: usize) -> Result { + let region = Some(VAlloc::new(len)?); + + Ok(Self { region }) + } + + /// WebAssembly memory only grows and there is currently no shrink operator. + /// This implementation, although named `realloc` (to remain compatible with existing code) + /// really only grows based to the WebAssembly spec. + /// + /// This is intentional because `VAlloc` implementation reserves 2^32 bytes ahead of time for efficiency. + /// And dropping and reallocating those pages defeats the purpose of reserving them ahead of time. + /// + /// With the above, any `new_len` lesser than the old `len` returns an error. + pub fn realloc(&mut self, new_len: usize) -> Result<(), &'static str> { + if let Some(ref mut region) = self.region { + // When `new_len` is lesser or equal to current `region.len`, do nothing. + // Even though that is currently not likely to happen because of prior validations + // notably in `MemoryInstnce::grow`. + // + // Also `MemoryInstnce::grow` already makes sure `new_len` is not greater than + // specified maximum or `WASM_MAX_PAGES`. + if new_len > region.len { + region.grow(new_len)?; + } + } + + Ok(()) + } + + pub fn len(&self) -> usize { + self.region.as_ref().map(|m| m.len).unwrap_or(0) + } + + pub fn as_slice(&self) -> &[u8] { + self.region.as_ref().map(|m| m.as_slice()).unwrap_or(&[]) + } + + pub fn as_slice_mut(&mut self) -> &mut [u8] { + self.region + .as_mut() + .map(|m| m.as_slice_mut()) + .unwrap_or(&mut []) + } + + pub fn erase(&mut self) -> Result<(), &'static str> { + let len = self.len(); + if len > 0 { + // The order is important. + // + // 1. First we clear, and thus drop, the current region if any. + // 2. And then we create a new one. + // + // Otherwise we double the peak memory consumption. + self.region = None; + self.region = Some(VAlloc::new(len)?); + } + Ok(()) + } +}