diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f1daae895a..8fc74fc095 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,9 +2,9 @@ name: Rust - Continuous Integration on: push: - branches: [ master ] + branches: [ "gear-v0.30.0" ] pull_request: - branches: [ master ] + branches: [ "gear-v0.30.0" ] jobs: check: @@ -62,8 +62,20 @@ jobs: # windows-latest was pinned to windows-2019 # because of https://github.com/paritytech/wasmi/runs/5021520759 os: [ubuntu-latest, windows-latest, macos-latest] + include: + # Include a new variable `rustc-args` with `-- --test-threads 1` + # for windows-latest to be used with virtual_memory crate feature + # enabled while testing. + - os: windows-latest + test-args: "--test-threads 1" runs-on: ${{ matrix.os }} steps: + - name: Configure Pagefile for Windows + if: matrix.os == 'windows-latest' + uses: al-cheb/configure-pagefile-action@v1.2 + with: + minimum-size: 6GB + maximum-size: 32GB - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: @@ -95,9 +107,10 @@ jobs: uses: actions-rs/cargo@v1 env: RUSTFLAGS: '--cfg debug_assertions' + TEST_FLAGS: ${{ matrix.test-args }} with: command: test - args: --workspace --release --all-features + args: --workspace --release --all-features -- $TEST_FLAGS fmt: name: Formatting @@ -188,7 +201,7 @@ jobs: run: | # Note: We use `|| true` because cargo install returns an error # if cargo-udeps was already installed on the CI runner. - cargo install --locked cargo-udeps || true + cargo install cargo-udeps || true - uses: actions-rs/cargo@v1 with: command: udeps @@ -223,7 +236,7 @@ jobs: run: | # Note: We use `|| true` because cargo install returns an error # if cargo-udeps was already installed on the CI runner. - cargo install --locked cargo-fuzz || true + cargo install cargo-fuzz || true - name: Fuzz Translate uses: actions-rs/cargo@v1 with: diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 8ad3302621..7b7f4e9b1f 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -16,6 +16,7 @@ libm = "0.2.1" num-traits = { version = "0.2.8", default-features = false } downcast-rs = { version = "1.2", default-features = false } paste = "1" +region = { version = "3.0.0", optional = true } [dev-dependencies] rand = "0.8.2" @@ -24,6 +25,16 @@ rand = "0.8.2" default = ["std"] # Use `no-default-features` for a `no_std` build. std = ["num-traits/std", "downcast-rs/std"] +# Enables OS supported virtual memory. +# +# Note +# +# - This feature is only supported on 64-bit platforms. +# For 32-bit platforms the linear memory will fallback to using the Vec +# based implementation. +# - The default is to fall back is an inefficient vector based implementation. +# - By nature this feature requires `region` and the Rust standard library. +virtual_memory = ["region", "std"] [package.metadata.cargo-udeps.ignore] # cargo-udeps cannot detect that libm is used for no_std targets only. diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 3a55b05d18..79c5dc6add 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -18,6 +18,9 @@ mod units; mod untyped; mod value; +#[cfg(feature = "virtual_memory")] +mod vmem; + #[cfg(not(feature = "std"))] extern crate alloc; @@ -43,3 +46,6 @@ pub use self::{ untyped::{DecodeUntypedSlice, EncodeUntypedSlice, UntypedError, UntypedValue}, value::ValueType, }; + +#[cfg(feature = "virtual_memory")] +pub use self::vmem::{VirtualMemory, VirtualMemoryError}; diff --git a/crates/core/src/vmem.rs b/crates/core/src/vmem.rs new file mode 100644 index 0000000000..ccdc364eb5 --- /dev/null +++ b/crates/core/src/vmem.rs @@ -0,0 +1,106 @@ +//! An implementation of a byte buffer based on virtual memory. +//! +//! This implementation uses `mmap` on POSIX systems (and should use `VirtualAlloc` on windows). +//! There are possibilities to improve the performance for the reallocating case by reserving +//! memory up to maximum. This might be a problem for systems that don't have a lot of virtual +//! memory (i.e. 32-bit platforms). + +use core::{ + fmt, + fmt::{Debug, Display}, + slice, +}; +use region::{Allocation, Protection}; + +/// Dummy error for fallible `Vec`-based virtual memory operations. +#[derive(Debug)] +pub enum VirtualMemoryError { + Region(region::Error), + AllocationOutOfBounds, +} + +impl From for VirtualMemoryError { + #[inline] + fn from(error: region::Error) -> Self { + Self::Region(error) + } +} + +impl Display for VirtualMemoryError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Region(error) => write!( + f, + "encountered failure while operating with virtual memory: {}", + error + ), + Self::AllocationOutOfBounds => write!(f, "virtual memory allocation is too big"), + } + } +} + +/// A virtual memory buffer. +pub struct VirtualMemory { + /// The virtual memory allocation. + allocation: Allocation, +} + +impl Debug for VirtualMemory { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("VirtualMemory") + .field("len", &self.allocation.len()) + .finish() + } +} + +unsafe impl Sync for VirtualMemory {} + +unsafe impl Send for VirtualMemory {} + +impl VirtualMemory { + /// The maximum allocation size for a `wasmi` virtual memory. + const MAX_ALLOCATION_SIZE: usize = u32::MAX as usize; + + /// Create a new virtual memory allocation. + /// + /// # Note + /// + /// The allocated virtual memory allows for read and write operations. + /// + /// # Errors + /// + /// - If `len` should not exceed `isize::max_value()` + /// - If `len` should be greater than 0. + /// - If the operating system returns an error upon virtual memory allocation. + pub fn new(len: usize) -> Result { + assert_ne!(len, 0, "cannot allocate empty virtual memory"); + if len > Self::MAX_ALLOCATION_SIZE { + return Err(VirtualMemoryError::AllocationOutOfBounds); + } + let allocation = region::alloc(len, Protection::READ_WRITE)?; + Ok(Self { allocation }) + } + + /// Returns a shared slice over the bytes of the virtual memory allocation. + #[inline] + pub fn data(&self) -> &[u8] { + // # SAFETY + // + // The operation is safe since we assume that the virtual memory allocation + // has been successful and allocated exactly `self.allocation.len()` bytes. + // Therefore creating a slice with `self.len` elements is valid. + // Aliasing guarantees are not violated since `self` is the only owner + // of the underlying virtual memory allocation. + unsafe { slice::from_raw_parts(self.allocation.as_ptr(), self.allocation.len()) } + } + + /// Returns an exclusive slice over the bytes of the virtual memory allocation. + #[inline] + pub fn data_mut(&mut self) -> &mut [u8] { + // # SAFETY + // + // See safety proof of the `as_slice` method. + // Additionally, it is not possible to obtain two mutable references for the same memory area. + unsafe { slice::from_raw_parts_mut(self.allocation.as_mut_ptr(), self.allocation.len()) } + } +} diff --git a/crates/wasmi/Cargo.toml b/crates/wasmi/Cargo.toml index 16854bea54..351f4be4a9 100644 --- a/crates/wasmi/Cargo.toml +++ b/crates/wasmi/Cargo.toml @@ -34,6 +34,16 @@ criterion = { version = "0.4", default-features = false } [features] default = ["std"] std = ["wasmi_core/std", "wasmi_arena/std", "wasmparser/std", "spin/std"] +# Enables OS supported virtual memory. +# +# Note +# +# - This feature is only supported on 64-bit platforms. +# For 32-bit platforms the linear memory will fallback to using the Vec +# based implementation. +# - The default is to fall back is an inefficient vector based implementation. +# - By nature this feature requires `region` and the Rust standard library. +virtual_memory = ["wasmi_core/virtual_memory", "std"] [[bench]] name = "benches" diff --git a/crates/wasmi/src/func/typed_func.rs b/crates/wasmi/src/func/typed_func.rs index b98fd3bdbf..3dcadd4d64 100644 --- a/crates/wasmi/src/func/typed_func.rs +++ b/crates/wasmi/src/func/typed_func.rs @@ -173,9 +173,7 @@ impl Default for CallResultsTuple { impl Copy for CallResultsTuple {} impl Clone for CallResultsTuple { fn clone(&self) -> Self { - Self { - _marker: PhantomData, - } + *self } } diff --git a/crates/wasmi/src/memory/buffer_vmem.rs b/crates/wasmi/src/memory/buffer_vmem.rs new file mode 100644 index 0000000000..bbfd3c2a75 --- /dev/null +++ b/crates/wasmi/src/memory/buffer_vmem.rs @@ -0,0 +1,70 @@ +use core::fmt::Debug; +use wasmi_core::VirtualMemory; + +/// A virtual memory based byte buffer implementation. +/// +/// # Note +/// +/// - This is a more efficient implementation of the byte buffer that +/// makes use of operating system provided virtual memory abstractions. +/// - This implementation allocates 4GB of virtual memory up front so +/// that grow operations later on are no-ops. The downside to this is +/// that this implementation is only supported on 64-bit systems. +/// 32-bit systems will fall back to the `Vec`-based implementation +/// even if the respective crate feature is enabled. +#[derive(Debug)] +pub struct ByteBuffer { + bytes: VirtualMemory, + len: usize, +} + +impl ByteBuffer { + /// Determines the initial size of the virtual memory allocation. + /// + /// # Note + /// + /// In this implementation we won't reallocate the virtually allocated + /// buffer and instead simply adjust the `len` field of the `ByteBuf` + /// wrapper in order to efficiently grow the virtual memory. + const ALLOCATION_SIZE: usize = u32::MAX as usize; + + /// Creates a new byte buffer with the given initial length. + /// + /// # Errors + /// + /// - If the initial length is 0. + /// - If the initial length exceeds the maximum supported limit. + pub fn new(initial_len: usize) -> Self { + let bytes = VirtualMemory::new(Self::ALLOCATION_SIZE) + .unwrap_or_else(|err| unreachable!("Failed to allocate memory: {err}")); + Self { + bytes, + len: initial_len, + } + } + + /// Grows the byte buffer by the given delta. + /// + /// # Errors + /// + /// If the new length of the byte buffer would exceed the maximum supported limit. + pub fn grow(&mut self, new_size: usize) { + assert!(new_size >= self.len()); + self.len = new_size; + } + + /// Returns the length of the byte buffer in bytes. + pub fn len(&self) -> usize { + self.len + } + + /// Returns a shared slice to the bytes underlying to the byte buffer. + pub fn data(&self) -> &[u8] { + &self.bytes.data()[..self.len] + } + + /// Returns an exclusive slice to the bytes underlying to the byte buffer. + pub fn data_mut(&mut self) -> &mut [u8] { + &mut self.bytes.data_mut()[..self.len] + } +} diff --git a/crates/wasmi/src/memory/mod.rs b/crates/wasmi/src/memory/mod.rs index 0a3a91cd4a..dc8a17bdab 100644 --- a/crates/wasmi/src/memory/mod.rs +++ b/crates/wasmi/src/memory/mod.rs @@ -1,11 +1,18 @@ -mod buffer; +#[cfg(all(feature = "virtual_memory", target_pointer_width = "64"))] +#[path = "buffer_vmem.rs"] +mod byte_buffer; + +#[cfg(not(all(feature = "virtual_memory", target_pointer_width = "64")))] +#[path = "buffer.rs"] +mod byte_buffer; + mod data; mod error; #[cfg(test)] mod tests; -use self::buffer::ByteBuffer; +use self::byte_buffer::ByteBuffer; pub use self::{ data::{DataSegment, DataSegmentEntity, DataSegmentIdx}, error::MemoryError,