diff --git a/Cargo.toml b/Cargo.toml index c668fc9..2f794dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ exclude = [ "res/*", "spec/*" ] [dependencies] parity-wasm = "0.23" byteorder = "1.0" +memory_units = { git = "https://github.com/pepyakin/memory_units.git", rev = "e09093e" } [dev-dependencies] wabt = "0.1.2" diff --git a/spec/src/run.rs b/spec/src/run.rs index d49504d..cd8c0c7 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 b21745a..f87f156 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; #[cfg(all(not(feature = "32bit_opt_in"), target_pointer_width = "32"))] compile_error! {"32-bit targets are not supported at the moment. @@ -360,6 +361,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..1071e55 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() } } @@ -92,12 +93,12 @@ impl<'a, B: 'a> CheckedRegion<'a, B> where B: ::std::ops::Deref> impl MemoryInstance { /// Allocate a memory instance. /// - /// The memory allocated with initial number of pages specified by `initial_pages`. - /// Minimal possible value for `initial_pages` is 0 and maximum possible is `65536`. + /// The memory allocated with initial number of pages specified by `initial`. + /// Minimal possible value for `initial` is 0 and maximum possible is `65536`. /// (Since maximum addressible memory is 232 = 4GiB = 65536 * [64KiB][`LINEAR_MEMORY_PAGE_SIZE`]). /// /// It is possible to limit maximum number of pages this memory instance can have by specifying - /// `maximum_page`. If not specified, this memory instance would be able to allocate up to 4GiB. + /// `maximum`. If not specified, this memory instance would be able to allocate up to 4GiB. /// /// Allocated memory is always zeroed. /// @@ -105,39 +106,28 @@ impl MemoryInstance { /// /// Returns `Err` if: /// - /// - `initial_pages` is greater than `maximum_pages` - /// - either `initial_pages` or `maximum_pages` is greater than `65536`. + /// - `initial` is greater than `maximum` + /// - either `initial` or `maximum` 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)?; + fn new(initial: Pages, maximum: Option) -> Self { + let limits = ResizableLimits::new(initial.0 as u32, maximum.map(|p| p.0 as u32)); - 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); - - let memory = MemoryInstance { + let initial_size: Bytes = initial.into(); + MemoryInstance { limits: limits, - buffer: RefCell::new(vec![0; initial_size as usize]), - maximum_size: maximum_size, - }; - - Ok(memory) + buffer: RefCell::new(vec![0; initial_size.0]), + initial: initial, + maximum: maximum, + } } /// Return linear memory limits. @@ -146,24 +136,47 @@ 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. + /// + /// # Example + /// + /// To convert number of pages to number of bytes you can use the following code: + /// + /// ```rust + /// use wasmi::MemoryInstance; + /// use wasmi::memory_units::*; + /// + /// let memory = MemoryInstance::alloc(Pages(1), None).unwrap(); + /// let byte_size: Bytes = memory.current_size().into(); + /// assert_eq!( + /// byte_size, + /// Bytes(65536), + /// ); + /// ``` + pub fn current_size(&self) -> Pages { + Bytes(self.buffer.borrow().len()).round_up_to() } /// Copy data from memory at given offset. + /// + /// This will allocate vector for you. + /// If you can provide a mutable slice you can use [`get_into`]. + /// + /// [`get_into`]: #method.get_into pub fn get(&self, offset: u32, size: usize) -> Result, Error> { let buffer = self.buffer.borrow(); let region = self.checked_region(&buffer, offset as usize, size)?; @@ -196,27 +209,42 @@ impl MemoryInstance { } /// Increases the size of the linear memory by given number of pages. - /// Returns previous memory size (in pages) if succeeds. + /// Returns previous memory size if succeeds. /// /// # 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,21 +341,32 @@ 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)] mod tests { - use super::{MemoryInstance, LINEAR_MEMORY_MAX_PAGES}; + use super::{MemoryInstance, LINEAR_MEMORY_PAGE_SIZE}; use Error; - use parity_wasm::elements::ResizableLimits; + use memory_units::Pages; #[test] fn alloc() { @@ -338,16 +377,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, @@ -357,9 +399,14 @@ mod tests { } } + #[test] + fn ensure_page_size() { + use memory_units::ByteSize; + assert_eq!(LINEAR_MEMORY_PAGE_SIZE, Pages::byte_size()); + } + 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 +465,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 9aa46dc..b37055a 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 8c87393..e96c3b5 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; @@ -267,7 +268,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() ))); @@ -277,7 +278,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> {