diff --git a/Cargo.toml b/Cargo.toml index d5d321f..a30a643 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ exclude = [ "res/*", "spec/*" ] [dependencies] parity-wasm = "0.23" byteorder = "1.0" +memory_units = "0.2" [dev-dependencies] wabt = "0.1.2" diff --git a/spec/src/run.rs b/spec/src/run.rs index 7f56ce7..f2be345 100644 --- a/spec/src/run.rs +++ b/spec/src/run.rs @@ -16,6 +16,7 @@ use wasmi::{ Module, Signature, MemoryDescriptor, Trap, TableDescriptor, GlobalDescriptor, FuncInstance, RuntimeArgs, }; +use wasmi::memory_units::Pages; #[derive(Debug)] enum Error { @@ -43,7 +44,7 @@ impl SpecModule { fn new() -> Self { SpecModule { table: TableInstance::alloc(10, Some(20)).unwrap(), - memory: MemoryInstance::alloc(1, Some(2)).unwrap(), + memory: MemoryInstance::alloc(Pages(1), Some(Pages(2))).unwrap(), global_i32: GlobalInstance::alloc(RuntimeValue::I32(666), false), global_i64: GlobalInstance::alloc(RuntimeValue::I64(666), false), global_f32: GlobalInstance::alloc(RuntimeValue::F32(666.0), false), diff --git a/src/lib.rs b/src/lib.rs index 7d69caf..1cc481a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,6 +100,7 @@ extern crate wabt; extern crate parity_wasm; extern crate byteorder; +extern crate memory_units as memory_units_crate; use std::fmt; use std::error; @@ -127,9 +128,9 @@ impl Trap { } /// Error type which can thrown by wasm code or by host environment. -/// +/// /// See [`Trap`] for details. -/// +/// /// [`Trap`]: struct.Trap.html #[derive(Debug)] pub enum TrapKind { @@ -231,9 +232,9 @@ pub enum Error { impl Error { /// Returns [`HostError`] if this `Error` represents some host error. - /// + /// /// I.e. if this error have variant [`Host`] or [`Trap`][`Trap`] with [host][`TrapKind::Host`] error. - /// + /// /// [`HostError`]: trait.HostError.html /// [`Host`]: enum.Error.html#variant.Host /// [`Trap`]: enum.Error.html#variant.Trap @@ -354,6 +355,12 @@ pub use self::global::{GlobalInstance, GlobalRef}; pub use self::func::{FuncInstance, FuncRef}; pub use self::types::{Signature, ValueType, GlobalDescriptor, TableDescriptor, MemoryDescriptor}; +/// WebAssembly-specific sizes and units. +pub mod memory_units { + pub use memory_units_crate::wasm32::*; + pub use memory_units_crate::{Bytes, ByteSize, RoundUpTo, size_of}; +} + /// Deserialized module prepared for instantiation. pub struct Module { labels: HashMap>, diff --git a/src/memory.rs b/src/memory.rs index 6efc438..82b88ca 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -6,17 +6,17 @@ use std::rc::Rc; use std::cell::RefCell; use parity_wasm::elements::ResizableLimits; use Error; -use module::check_limits; +use memory_units::{RoundUpTo, Pages, Bytes}; /// Size of a page of [linear memory][`MemoryInstance`] - 64KiB. /// /// The size of a memory is always a integer multiple of a page size. /// /// [`MemoryInstance`]: struct.MemoryInstance.html -pub const LINEAR_MEMORY_PAGE_SIZE: u32 = 65536; +pub const LINEAR_MEMORY_PAGE_SIZE: Bytes = Bytes(65536); /// Maximal number of pages. -const LINEAR_MEMORY_MAX_PAGES: u32 = 65536; +const LINEAR_MEMORY_MAX_PAGES: Pages = Pages(65536); /// Reference to a memory (See [`MemoryInstance`] for details). /// @@ -52,8 +52,8 @@ pub struct MemoryInstance { limits: ResizableLimits, /// Linear memory buffer. buffer: RefCell>, - /// Maximum buffer size. - maximum_size: u32, + initial: Pages, + maximum: Option, } impl fmt::Debug for MemoryInstance { @@ -61,7 +61,8 @@ impl fmt::Debug for MemoryInstance { f.debug_struct("MemoryInstance") .field("limits", &self.limits) .field("buffer.len", &self.buffer.borrow().len()) - .field("maximum_size", &self.maximum_size) + .field("maximum", &self.maximum) + .field("initial", &self.initial) .finish() } } @@ -109,35 +110,26 @@ impl MemoryInstance { /// - either `initial_pages` or `maximum_pages` is greater than `65536`. /// /// [`LINEAR_MEMORY_PAGE_SIZE`]: constant.LINEAR_MEMORY_PAGE_SIZE.html - pub fn alloc(initial_pages: u32, maximum_pages: Option) -> Result { - let memory = MemoryInstance::new(ResizableLimits::new(initial_pages, maximum_pages))?; + pub fn alloc(initial: Pages, maximum: Option) -> Result { + validate_memory(initial, maximum).map_err(Error::Memory)?; + + let memory = MemoryInstance::new(initial, maximum); Ok(MemoryRef(Rc::new(memory))) } /// Create new linear memory instance. - fn new(limits: ResizableLimits) -> Result { - check_limits(&limits)?; - - let initial_pages = limits.initial(); - let maximum_pages = limits.maximum().unwrap_or(LINEAR_MEMORY_MAX_PAGES); - - if initial_pages > LINEAR_MEMORY_MAX_PAGES { - return Err(Error::Memory(format!("initial memory size must be at most {} pages", LINEAR_MEMORY_MAX_PAGES))); - } - if maximum_pages > LINEAR_MEMORY_MAX_PAGES { - return Err(Error::Memory(format!("maximum memory size must be at most {} pages", LINEAR_MEMORY_MAX_PAGES))); - } - - let maximum_size = maximum_pages.saturating_mul(LINEAR_MEMORY_PAGE_SIZE); - let initial_size = initial_pages.saturating_mul(LINEAR_MEMORY_PAGE_SIZE); - + fn new(initial: Pages, maximum: Option) -> Self { + let limits = ResizableLimits::new(initial.0 as u32, maximum.map(|p| p.0 as u32)); let memory = MemoryInstance { limits: limits, - buffer: RefCell::new(vec![0; initial_size as usize]), - maximum_size: maximum_size, + buffer: RefCell::new(vec![]), + initial: initial, + maximum: maximum, }; - Ok(memory) + memory.grow(initial).expect("Initial grow should always succeed"); + + memory } /// Return linear memory limits. @@ -146,21 +138,23 @@ impl MemoryInstance { } /// Returns number of pages this `MemoryInstance` was created with. - pub fn initial_pages(&self) -> u32 { - self.limits.initial() + pub fn initial(&self) -> Pages { + self.initial } /// Returns maximum amount of pages this `MemoryInstance` can grow to. /// /// Returns `None` if there is no limit set. - /// This means that memory can grow up to 4GiB. - pub fn maximum_pages(&self) -> Option { - self.limits.maximum() + /// Maximum memory size cannot exceed `65536` pages or 4GiB. + pub fn maximum(&self) -> Option { + self.maximum } - /// Return linear memory size (in pages). - pub fn size(&self) -> u32 { - self.buffer.borrow().len() as u32 / LINEAR_MEMORY_PAGE_SIZE + /// Returns current linear memory size. + /// + /// Maximum memory size cannot exceed `65536` pages or 4GiB. + pub fn current_size(&self) -> Pages { + Bytes(self.buffer.borrow().len()).round_up_to() } /// Copy data from memory at given offset. @@ -201,22 +195,37 @@ impl MemoryInstance { /// # Errors /// /// Returns `Err` if attempted to allocate more memory than permited by the limit. - pub fn grow(&self, pages: u32) -> Result { - let mut buffer = self.buffer.borrow_mut(); - let old_size = buffer.len() as u32; - match calculate_memory_size(old_size, pages, self.maximum_size) { - None => Err(Error::Memory( - format!( - "Trying to grow memory by {} pages when already have {}", - pages, - old_size / LINEAR_MEMORY_PAGE_SIZE, - ) - )), - Some(new_size) => { - buffer.resize(new_size as usize, 0); - Ok(old_size / LINEAR_MEMORY_PAGE_SIZE) - }, + pub fn grow(&self, additional: Pages) -> Result { + let size_before_grow: Pages = self.current_size(); + println!("grow({:?}) = {:?}", additional, size_before_grow); + + if additional == Pages(0) { + return Ok(size_before_grow); } + if additional > Pages(65536) { + return Err(Error::Memory(format!( + "Trying to grow memory by more than 65536 pages" + ))); + } + + let new_size: Pages = size_before_grow + additional; + let maximum = self.maximum.unwrap_or(LINEAR_MEMORY_MAX_PAGES); + if new_size > maximum { + return Err(Error::Memory(format!( + "Trying to grow memory by {} pages when already have {}", + additional.0, size_before_grow.0, + ))); + } + + // Resize underlying buffer up to a new size filling newly allocated space with zeroes. + // This size is guaranteed to be larger than current size. + let new_buffer_length: Bytes = new_size.into(); + { + let mut buffer = self.buffer.borrow_mut(); + debug_assert!(new_buffer_length.0 > buffer.len()); + buffer.resize(new_buffer_length.0, 0); + } + Ok(size_before_grow) } fn checked_region<'a, B>(&self, buffer: &'a B, offset: usize, size: usize) -> Result, Error> @@ -313,13 +322,24 @@ impl MemoryInstance { } } -fn calculate_memory_size(old_size: u32, additional_pages: u32, maximum_size: u32) -> Option { - let size = additional_pages - .saturating_mul(LINEAR_MEMORY_PAGE_SIZE); - match size.checked_add(old_size) { - Some(size) if size <= maximum_size => Some(size), - _ => None, +pub fn validate_memory(initial: Pages, maximum: Option) -> Result<(), String> { + if initial > LINEAR_MEMORY_MAX_PAGES { + return Err(format!("initial memory size must be at most {} pages", LINEAR_MEMORY_MAX_PAGES.0)); } + if let Some(maximum) = maximum { + if initial > maximum { + return Err(format!( + "maximum limit {} is less than minimum {}", + maximum.0, + initial.0, + )); + } + + if maximum > LINEAR_MEMORY_MAX_PAGES { + return Err(format!("maximum memory size must be at most {} pages", LINEAR_MEMORY_MAX_PAGES.0)); + } + } + Ok(()) } #[cfg(test)] @@ -328,6 +348,7 @@ mod tests { use super::{MemoryInstance, LINEAR_MEMORY_MAX_PAGES}; use Error; use parity_wasm::elements::ResizableLimits; + use memory_units::Pages; #[test] fn alloc() { @@ -338,16 +359,19 @@ mod tests { (1, Some(1), true), (0, Some(1), true), (1, Some(0), false), - (0, Some(LINEAR_MEMORY_MAX_PAGES), true), - (LINEAR_MEMORY_MAX_PAGES, Some(LINEAR_MEMORY_MAX_PAGES), true), - (LINEAR_MEMORY_MAX_PAGES, Some(0), false), - (LINEAR_MEMORY_MAX_PAGES, None, true), + (0, Some(65536), true), + (65536, Some(65536), true), + (65536, Some(0), false), + (65536, None, true), ]; - for &(initial, maybe_max, expected_ok) in fixtures { - let result = MemoryInstance::alloc(initial, maybe_max); + for (index, &(initial, maybe_max, expected_ok)) in fixtures.iter().enumerate() { + let initial: Pages = Pages(initial); + let maximum: Option = maybe_max.map(|m| Pages(m)); + let result = MemoryInstance::alloc(initial, maximum); if result.is_ok() != expected_ok { panic!( - "unexpected error, initial={}, max={:?}, expected={}, result={:?}", + "unexpected error at {}, initial={:?}, max={:?}, expected={}, result={:?}", + index, initial, maybe_max, expected_ok, @@ -358,8 +382,7 @@ mod tests { } fn create_memory(initial_content: &[u8]) -> MemoryInstance { - let mem = MemoryInstance::new(ResizableLimits::new(1, Some(1))) - .expect("MemoryInstance created successfuly"); + let mem = MemoryInstance::new(Pages(1), Some(Pages(1))); mem.set(0, initial_content).expect("Successful initialize the memory"); mem } @@ -418,7 +441,7 @@ mod tests { #[test] fn get_into() { - let mem = MemoryInstance::new(ResizableLimits::new(1, None)).expect("memory instance creation should not fail"); + let mem = MemoryInstance::new(Pages(1), None); mem.set(6, &[13, 17, 129]).expect("memory set should not fail"); let mut data = [0u8; 2]; diff --git a/src/module.rs b/src/module.rs index e900c0e..155bd05 100644 --- a/src/module.rs +++ b/src/module.rs @@ -14,6 +14,7 @@ use memory::MemoryRef; use host::Externals; use common::{DEFAULT_MEMORY_INDEX, DEFAULT_TABLE_INDEX}; use types::{GlobalDescriptor, TableDescriptor, MemoryDescriptor}; +use memory_units::Pages; /// Reference to a [`ModuleInstance`]. /// @@ -325,10 +326,11 @@ impl ModuleInstance { &[], ) { - let memory = MemoryInstance::alloc( - memory_type.limits().initial(), - memory_type.limits().maximum() - )?; + let initial: Pages = Pages(memory_type.limits().initial() as usize); + let maximum: Option = memory_type.limits().maximum().map(|m| Pages(m as usize)); + + let memory = MemoryInstance::alloc(initial, maximum) + .expect("Due to validation `initial` and `maximum` should be valid"); instance.push_memory(memory); } diff --git a/src/runner.rs b/src/runner.rs index 127d976..105184b 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -16,6 +16,7 @@ use host::Externals; use common::{DEFAULT_MEMORY_INDEX, DEFAULT_TABLE_INDEX, BlockFrame, BlockFrameType}; use common::stack::StackWithLimit; use common::{DEFAULT_FRAME_STACK_LIMIT, DEFAULT_VALUE_STACK_LIMIT}; +use memory_units::Pages; /// Function interpreter. pub struct Interpreter<'a, E: Externals + 'a> { @@ -643,7 +644,7 @@ impl<'a, E: Externals> Interpreter<'a, E> { let m = context.module() .memory_by_index(DEFAULT_MEMORY_INDEX) .expect("Due to validation memory should exists"); - let s = m.size(); + let s = m.current_size().0; context .value_stack_mut() .push(RuntimeValue::I32(s as i32))?; @@ -657,9 +658,10 @@ impl<'a, E: Externals> Interpreter<'a, E> { let m = context.module() .memory_by_index(DEFAULT_MEMORY_INDEX) .expect("Due to validation memory should exists"); - // Pushes -1 if allocation fails or previous memory size, if succeeds. - let m = m.grow(pages) - .unwrap_or(u32::MAX); + let m = match m.grow(Pages(pages as usize)) { + Ok(Pages(new_size)) => new_size as u32, + Err(_) => u32::MAX, // Returns -1 (or 0xFFFFFFFF) in case of error. + }; context .value_stack_mut() .push(RuntimeValue::I32(m as i32))?; diff --git a/src/tests/host.rs b/src/tests/host.rs index a6546b9..9bbb284 100644 --- a/src/tests/host.rs +++ b/src/tests/host.rs @@ -4,6 +4,7 @@ use { RuntimeValue, RuntimeArgs, TableDescriptor, MemoryDescriptor, Trap, TrapKind, }; use types::ValueType; +use memory_units::Pages; use super::parse_wat; #[derive(Debug, Clone, PartialEq)] @@ -36,7 +37,7 @@ struct TestHost { impl TestHost { fn new() -> TestHost { TestHost { - memory: Some(MemoryInstance::alloc(1, Some(1)).unwrap()), + memory: Some(MemoryInstance::alloc(Pages(1), Some(Pages(1))).unwrap()), instance: None, } } @@ -483,7 +484,7 @@ fn defer_providing_externals() { // Create HostImportResolver with some initialized memory instance. // This memory instance will be provided as 'mem' export. let host_import_resolver = - HostImportResolver { mem: MemoryInstance::alloc(1, Some(1)).unwrap() }; + HostImportResolver { mem: MemoryInstance::alloc(Pages(1), Some(Pages(1))).unwrap() }; // Instantiate module with `host_import_resolver` as import resolver for "host" module. let instance = ModuleInstance::new( diff --git a/src/tests/wasm.rs b/src/tests/wasm.rs index 8b2573f..263fa1b 100644 --- a/src/tests/wasm.rs +++ b/src/tests/wasm.rs @@ -3,6 +3,7 @@ use { MemoryRef, ModuleImportResolver, ModuleInstance, NopExternals, RuntimeValue, TableInstance, TableRef, Module, GlobalDescriptor, TableDescriptor, MemoryDescriptor, }; +use memory_units::Pages; use std::fs::File; struct Env { @@ -17,7 +18,7 @@ impl Env { Env { table_base: GlobalInstance::alloc(RuntimeValue::I32(0), false), memory_base: GlobalInstance::alloc(RuntimeValue::I32(0), false), - memory: MemoryInstance::alloc(256, None).unwrap(), + memory: MemoryInstance::alloc(Pages(256), None).unwrap(), table: TableInstance::alloc(64, None).unwrap(), } } diff --git a/src/validation/mod.rs b/src/validation/mod.rs index 1836791..4cb5f5b 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -8,6 +8,7 @@ use parity_wasm::elements::{ use common::stack; use self::context::ModuleContextBuilder; use self::func::Validator; +use memory_units::Pages; mod context; mod func; @@ -259,7 +260,7 @@ fn validate_limits(limits: &ResizableLimits) -> Result<(), Error> { if let Some(maximum) = limits.maximum() { if limits.initial() > maximum { return Err(Error(format!( - "maximum limit {} is lesser than minimum {}", + "maximum limit {} is less than minimum {}", maximum, limits.initial() ))); @@ -269,7 +270,9 @@ fn validate_limits(limits: &ResizableLimits) -> Result<(), Error> { } fn validate_memory_type(memory_type: &MemoryType) -> Result<(), Error> { - validate_limits(memory_type.limits()) + let initial: Pages = Pages(memory_type.limits().initial() as usize); + let maximum: Option = memory_type.limits().maximum().map(|m| Pages(m as usize)); + ::memory::validate_memory(initial, maximum).map_err(Error) } fn validate_table_type(table_type: &TableType) -> Result<(), Error> {