pub struct LinearMemory<const PAGE_SIZE: usize = { crate::Limits::MEM_PAGE_SIZE as usize }> {
inner_data: RwSpinLock<Vec<AtomicU8>>,
}Expand description
Implementation of the linear memory suitable for concurrent access
Implements the base for the instructions described in https://webassembly.github.io/spec/core/exec/instructions.html#memory-instructions.
This linear memory implementation internally relies on a Vec<AtomicU8>. Thus, the atomic unit
of information for it is a byte (u8). All access to the linear memory internally occur through
AtomicU8::load and AtomicU8::store, avoiding the creation of shared and mut refs to
the internal data completely. This avoids undefined behavior. Racy multibyte writes to the same
data however may tear (e.g. for any number of concurrent writes to a given byte, only one is
effectively written). Because of this, the LinearMemory::store function does not require
&mut self – &self suffices.
The implementation of atomic stores to multibyte values requires a global write lock. Rust’s memory model considers partially overlapping atomic operations involving a write as undefined behavior. As there is no way to predict if an atomic multibyte store operation might overlap with another store or load operation, only a lock at runtime can avoid this cause of undefined behavior.
§Notes on overflowing
All operations that rely on accessing n bytes starting at index in the linear memory have to
perform bounds checking. Thus, they always have to ensure that n + index < linear_memory.len()
holds true (e.g. n + index - 1 must be a valid index into linear_memory). However,
writing that check as is bears the danger of an overflow, assuming that n, index and
linear_memory.len() are the same given integer type, n + index can overflow, resulting in
the check passing despite the access being out of bounds!
To avoid this, the bounds checks are carefully ordered to avoid any overflows:
- First we check, that
n <= linear_memory.len()holds true, ensuring that the amount of bytes to be accessed is indeed smaller than or equal to the linear memory’s size. If this does not hold true, continuation of the operation will yield out of bounds access in any case. - Then, as a second check, we verify that
index <= linear_memory.len() - n. This way we avoid the overflow, as there is no addition. The subtraction in the left hand can not underflow, due to the previous check (which asserts thatnis smaller than or equal tolinear_memory.len()).
Combined in the given order, these two checks enable bounds checking without risking any
overflow or underflow, provided that n, index and linear_memory.len() are of the same
integer type.
In addition, the Wasm specification requires a certain order of checks. For example, when a
copy instruction is emitted with a count of zero (i.e. no bytes to be copied), an out of
bounds index still has to cause a trap. To control the order of checks manually, use of slice
indexing is avoided altogether.
§Notes on locking
The internal data vector of the LinearMemory is wrapped in a RwSpinLock. Despite the
name, writes to the linear memory do not require an acquisition of a write lock. Non-atomic
or atomic single-byte writes are implemented through a shared ref to the internal vector, with
AtomicU8 to achieve interior mutability without undefined behavior.
However, linear memory can grow. As the linear memory is implemented via a Vec, a grow
can result in the vector’s internal data buffer to be copied over to a bigger, fresh allocation.
The old buffer is then freed. Combined with concurrent access, this can cause use-after-free.
To avoid this, a grow operation of the linear memory acquires a write lock, blocking all
read/write to the linear memory in between.
§Unsafe Note
As the manual index checking assures all indices to be valid, there is no need to re-check.
Therefore slice::get_unchecked is used access the internal AtomicU8 in the vector
backing a LinearMemory, implicating the use of unsafe.
To gain some confidence in the correctness of the unsafe code in this module, run miri:
cargo miri test --test memory # quick
cargo miri test # thoroughFields§
§inner_data: RwSpinLock<Vec<AtomicU8>>Implementations§
Source§impl<const PAGE_SIZE: usize> LinearMemory<PAGE_SIZE>
impl<const PAGE_SIZE: usize> LinearMemory<PAGE_SIZE>
Sourceconst PAGE_SIZE: usize = PAGE_SIZE
const PAGE_SIZE: usize = PAGE_SIZE
Size of a page in the linear memory, measured in bytes
The WASM specification demands a page size of 64 KiB, that is 65536 bytes:
https://webassembly.github.io/spec/core/exec/runtime.html?highlight=page#memory-instances
Sourcepub fn new() -> Self
pub fn new() -> Self
Create a new, empty LinearMemory
Sourcepub fn new_with_initial_pages(pages: u16) -> Self
pub fn new_with_initial_pages(pages: u16) -> Self
Create a new, empty LinearMemory
Sourcepub fn grow(&self, pages_to_add: u16)
pub fn grow(&self, pages_to_add: u16)
Grow the LinearMemory by a number of pages
Sourcepub fn pages(&self) -> u16
pub fn pages(&self) -> u16
Get the number of pages currently allocated to this LinearMemory
Sourcepub fn len(&self) -> usize
pub fn len(&self) -> usize
Get the length in bytes currently allocated to this LinearMemory
Sourcepub fn store<const N: usize, T: LittleEndianBytes<N>>(
&self,
index: usize,
value: T,
) -> Result<(), RuntimeError>
pub fn store<const N: usize, T: LittleEndianBytes<N>>( &self, index: usize, value: T, ) -> Result<(), RuntimeError>
At a given index, store a datum in the LinearMemory
Sourcepub fn store_bytes<const N: usize>(
&self,
index: usize,
bytes: [u8; N],
) -> Result<(), RuntimeError>
pub fn store_bytes<const N: usize>( &self, index: usize, bytes: [u8; N], ) -> Result<(), RuntimeError>
At a given index, store a number of bytes N in the LinearMemory
Sourcepub fn load<const N: usize, T: LittleEndianBytes<N>>(
&self,
index: usize,
) -> Result<T, RuntimeError>
pub fn load<const N: usize, T: LittleEndianBytes<N>>( &self, index: usize, ) -> Result<T, RuntimeError>
From a given index, load a datum from the LinearMemory
Sourcepub fn load_bytes<const N: usize>(
&self,
index: usize,
) -> Result<[u8; N], RuntimeError>
pub fn load_bytes<const N: usize>( &self, index: usize, ) -> Result<[u8; N], RuntimeError>
From a given index, load a number of bytes N from the LinearMemory
Sourcepub fn fill(
&self,
index: usize,
data_byte: u8,
count: usize,
) -> Result<(), RuntimeError>
pub fn fill( &self, index: usize, data_byte: u8, count: usize, ) -> Result<(), RuntimeError>
Implementation of the behavior described in
https://webassembly.github.io/spec/core/exec/instructions.html#xref-syntax-instructions-syntax-instr-memory-mathsf-memory-fill.
Note, that the WASM spec defines the behavior by recursion, while our implementation uses
the memset like core::ptr::write_bytes.
Sourcepub fn copy(
&self,
destination_index: usize,
source_mem: &Self,
source_index: usize,
count: usize,
) -> Result<(), RuntimeError>
pub fn copy( &self, destination_index: usize, source_mem: &Self, source_index: usize, count: usize, ) -> Result<(), RuntimeError>
Copy count bytes from one region in the linear memory to another region in the same or a
different linear memory
- Both regions may overlap
- Copies the
countbytes starting fromsource_index, overwriting thecountbytes starting fromdestination_index
pub fn init( &self, destination_index: usize, source_data: &[u8], source_index: usize, count: usize, ) -> Result<(), RuntimeError>
Sourcepub fn access_mut_slice<R>(&self, accessor: impl FnOnce(&mut [u8]) -> R) -> R
pub fn access_mut_slice<R>(&self, accessor: impl FnOnce(&mut [u8]) -> R) -> R
Allows a given closure to temporarily access the entire memory as a
&mut [u8].
§Note on locking
This operation exclusively locks the entire linear memory for the duration of this function call. To acquire the lock, this function may also block until the lock is available.