Skip to content

feat: memory64 support #38

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@

- **Tiny**: TinyWasm is designed to be as small as possible without significantly compromising performance or functionality (< 4000 LLOC).
- **Portable**: TinyWasm runs on any platform that Rust can target, including `no_std`, with minimal external dependencies.
- **Safe**: No unsafe code is used in the runtime (`rkyv`, which uses unsafe code, can be used for serialization but is optional).
- **Safe**: No unsafe code is used in the runtime

## Status
## Current Status

TinyWasm passes all WebAssembly MVP tests from the [WebAssembly core testsuite](https://github.com/WebAssembly/testsuite) and is able to run most WebAssembly programs. Additionally, the current 2.0 Draft is mostly supported, with the exception of Fixed-Width SIMD and Memory64/Multiple Memories. See the [Supported Proposals](#supported-proposals) section for more information.
TinyWasm passes all WebAssembly MVP tests from the [WebAssembly core testsuite](https://github.com/WebAssembly/testsuite) and is able to run most WebAssembly programs. Additionally, the current 2.0 WebAssembly is mostly supported, with the exception of the SIMD and Memory64 proposals. See the [Supported Proposals](#supported-proposals) section for more information.

## Safety

Safety wise, TinyWasm doesn't use any unsafe code and is designed to be completly memory-safe. Untrusted WebAssembly code should not be able to crash the runtime or access memory outside of its sandbox, however currently there is no protection against infinite loops or excessive memory usage. Unvalidated Wasm and untrusted, precompilled twasm bytecode is safe to run too but can crash the runtime.

## Supported Proposals

Expand All @@ -38,7 +42,7 @@ TinyWasm passes all WebAssembly MVP tests from the [WebAssembly core testsuite](
| [**Multiple Memories**](https://github.com/WebAssembly/multi-memory/blob/master/proposals/multi-memory/Overview.md) | 🟢 | 0.8.0 |
| [**Custom Page Sizes**](https://github.com/WebAssembly/custom-page-sizes/blob/main/proposals/custom-page-sizes/Overview.md) | 🟢 | `next` |
| [**Tail Call**](https://github.com/WebAssembly/tail-call/blob/main/proposals/tail-call/Overview.md) | 🟢 | `next` |
| [**Memory64**](https://github.com/WebAssembly/memory64/blob/master/proposals/memory64/Overview.md) | 🚧 | N/A |
| [**Memory64**](https://github.com/WebAssembly/memory64/blob/master/proposals/memory64/Overview.md) | 🟢 | `next` |
| [**Fixed-Width SIMD**](https://github.com/webassembly/simd) | 🚧 | N/A |

## Usage
Expand Down
2 changes: 1 addition & 1 deletion crates/tinywasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ tinywasm-types={version="0.9.0-alpha.0", path="../types", default-features=false
libm={version="0.2", default-features=false}

[dev-dependencies]
wasm-testsuite={version="0.4.4"}
wasm-testsuite={version="0.5.0"}
indexmap="2.7"
wast={workspace=true}
wat={workspace=true}
Expand Down
55 changes: 40 additions & 15 deletions crates/tinywasm/src/interpreter/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,16 +518,31 @@ impl<'store, 'stack> Executor<'store, 'stack> {

fn exec_memory_size(&mut self, addr: u32) {
let mem = self.store.get_mem(self.module.resolve_mem_addr(addr));
self.stack.values.push::<i32>(mem.page_count as i32);

match mem.is_64bit() {
true => self.stack.values.push::<i64>(mem.page_count as i64),
false => self.stack.values.push::<i32>(mem.page_count as i32),
}
}
fn exec_memory_grow(&mut self, addr: u32) {
let mem = self.store.get_mem_mut(self.module.resolve_mem_addr(addr));
let prev_size = mem.page_count as i32;
let pages_delta = self.stack.values.pop::<i32>();
self.stack.values.push::<i32>(match mem.grow(pages_delta) {
Some(_) => prev_size,
None => -1,
});
let prev_size = mem.page_count;

let pages_delta = match mem.is_64bit() {
true => self.stack.values.pop::<i64>(),
false => self.stack.values.pop::<i32>() as i64,
};

match (
mem.is_64bit(),
match mem.grow(pages_delta) {
Some(_) => prev_size as i64,
None => -1_i64,
},
) {
(true, size) => self.stack.values.push::<i64>(size),
(false, size) => self.stack.values.push::<i32>(size as i32),
};
}

fn exec_memory_copy(&mut self, from: u32, to: u32) -> Result<()> {
Expand Down Expand Up @@ -605,14 +620,13 @@ impl<'store, 'stack> Executor<'store, 'stack> {
dst as usize,
src as usize,
size as usize,
)?;
)
} else {
// copy between two memories
let (table_from, table_to) =
self.store.get_tables_mut(self.module.resolve_table_addr(from), self.module.resolve_table_addr(to))?;
table_to.copy_from_slice(dst as usize, table_from.load(src as usize, size as usize)?)?;
table_to.copy_from_slice(dst as usize, table_from.load(src as usize, size as usize)?)
}
Ok(())
}

fn exec_mem_load<LOAD: MemLoadable<LOAD_SIZE>, const LOAD_SIZE: usize, TARGET: InternalValue>(
Expand All @@ -622,11 +636,16 @@ impl<'store, 'stack> Executor<'store, 'stack> {
cast: fn(LOAD) -> TARGET,
) -> ControlFlow<Option<Error>> {
let mem = self.store.get_mem(self.module.resolve_mem_addr(mem_addr));
let val = self.stack.values.pop::<i32>() as u64;
let Some(Ok(addr)) = offset.checked_add(val).map(TryInto::try_into) else {

let addr = match mem.is_64bit() {
true => self.stack.values.pop::<i64>() as u64,
false => self.stack.values.pop::<i32>() as u32 as u64,
};

let Some(Ok(addr)) = offset.checked_add(addr).map(TryInto::try_into) else {
cold();
return ControlFlow::Break(Some(Error::Trap(Trap::MemoryOutOfBounds {
offset: val as usize,
offset: addr as usize,
len: LOAD_SIZE,
max: 0,
})));
Expand All @@ -644,10 +663,16 @@ impl<'store, 'stack> Executor<'store, 'stack> {
let mem = self.store.get_mem_mut(self.module.resolve_mem_addr(mem_addr));
let val = self.stack.values.pop::<T>();
let val = (cast(val)).to_mem_bytes();
let addr = self.stack.values.pop::<i32>() as u64;

let addr = match mem.is_64bit() {
true => self.stack.values.pop::<i64>() as u64,
false => self.stack.values.pop::<i32>() as u32 as u64,
};

if let Err(e) = mem.store((offset + addr) as usize, val.len(), &val) {
return ControlFlow::Break(Some(e));
}

ControlFlow::Continue(())
}

Expand Down Expand Up @@ -707,7 +732,7 @@ impl<'store, 'stack> Executor<'store, 'stack> {
return Err(Trap::TableOutOfBounds { offset: 0, len: 0, max: 0 }.into());
};

table.init(dst, &items[offset as usize..(offset + size) as usize])
table.init(dst as i64, &items[offset as usize..(offset + size) as usize])
}
fn exec_table_grow(&mut self, table_index: u32) -> Result<()> {
let table = self.store.get_table_mut(self.module.resolve_table_addr(table_index));
Expand Down
2 changes: 1 addition & 1 deletion crates/tinywasm/src/reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl MemoryRefMut<'_> {
}

/// Grow the memory by the given number of pages
pub fn grow(&mut self, delta_pages: i32) -> Option<i32> {
pub fn grow(&mut self, delta_pages: i64) -> Option<i64> {
self.0.grow(delta_pages)
}

Expand Down
19 changes: 11 additions & 8 deletions crates/tinywasm/src/store/memory.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use alloc::vec;
use alloc::vec::Vec;
use tinywasm_types::{MemoryType, ModuleInstanceAddr};
use tinywasm_types::{MemoryArch, MemoryType, ModuleInstanceAddr};

use crate::{Error, Result, cold, log};

Expand Down Expand Up @@ -28,6 +28,11 @@ impl MemoryInstance {
}
}

#[inline]
pub(crate) fn is_64bit(&self) -> bool {
matches!(self.kind.arch(), MemoryArch::I64)
}

#[inline(always)]
pub(crate) fn len(&self) -> usize {
self.data.len()
Expand Down Expand Up @@ -124,15 +129,13 @@ impl MemoryInstance {
}

#[inline]
pub(crate) fn grow(&mut self, pages_delta: i32) -> Option<i32> {
pub(crate) fn grow(&mut self, pages_delta: i64) -> Option<i64> {
let current_pages = self.page_count;
let new_pages = current_pages as i64 + pages_delta as i64;
debug_assert!(new_pages <= i32::MAX as i64, "page count should never be greater than i32::MAX");
let new_pages = current_pages as i64 + pages_delta;

if new_pages < 0 || new_pages as usize > self.max_pages() {
log::debug!("memory.grow failed: new_pages={}, max_pages={}", new_pages, self.max_pages());
log::debug!("{} {}", self.kind.page_count_max(), self.kind.page_size());

return None;
}

Expand All @@ -145,7 +148,7 @@ impl MemoryInstance {
self.data.reserve_exact(new_size);
self.data.resize_with(new_size, Default::default);
self.page_count = new_pages as usize;
Some(current_pages as i32)
Some(current_pages as i64)
}
}

Expand Down Expand Up @@ -241,14 +244,14 @@ mod memory_instance_tests {
fn test_memory_grow() {
let mut memory = create_test_memory();
let original_pages = memory.page_count;
assert_eq!(memory.grow(1), Some(original_pages as i32));
assert_eq!(memory.grow(1), Some(original_pages as i64));
assert_eq!(memory.page_count, original_pages + 1);
}

#[test]
fn test_memory_grow_out_of_bounds() {
let mut memory = create_test_memory();
assert!(memory.grow(memory.kind.max_size() as i32 + 1).is_none());
assert!(memory.grow(memory.kind.max_size() as i64 + 1).is_none());
}

#[test]
Expand Down
28 changes: 14 additions & 14 deletions crates/tinywasm/src/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,6 @@ impl Store {
let mem_count = self.data.memories.len();
let mut mem_addrs = Vec::with_capacity(mem_count);
for (i, mem) in memories.into_iter().enumerate() {
if let MemoryArch::I64 = mem.arch() {
return Err(Error::UnsupportedFeature("64-bit memories".to_string()));
}
self.data.memories.push(MemoryInstance::new(mem, idx));
mem_addrs.push((i + mem_count) as MemAddr);
}
Expand Down Expand Up @@ -325,7 +322,7 @@ impl Store {

// this one is active, so we need to initialize it (essentially a `table.init` instruction)
ElementKind::Active { offset, table } => {
let offset = self.eval_i32_const(offset)?;
let offset = self.eval_size_const(offset)?;
let table_addr = table_addrs
.get(table as usize)
.copied()
Expand Down Expand Up @@ -373,7 +370,7 @@ impl Store {
return Err(Error::Other(format!("memory {mem_addr} not found for data segment {i}")));
};

let offset = self.eval_i32_const(offset)?;
let offset = self.eval_size_const(offset)?;
let Some(mem) = self.data.memories.get_mut(*mem_addr as usize) else {
return Err(Error::Other(format!("memory {mem_addr} not found for data segment {i}")));
};
Expand Down Expand Up @@ -418,15 +415,18 @@ impl Store {
Ok(self.data.funcs.len() as FuncAddr - 1)
}

/// Evaluate a constant expression, only supporting i32 globals and i32.const
pub(crate) fn eval_i32_const(&self, const_instr: tinywasm_types::ConstInstruction) -> Result<i32> {
use tinywasm_types::ConstInstruction::*;
let val = match const_instr {
I32Const(i) => i,
GlobalGet(addr) => self.data.globals[addr as usize].value.get().unwrap_32() as i32,
_ => return Err(Error::Other("expected i32".to_string())),
};
Ok(val)
/// Evaluate a constant expression that's either a i32 or a i64 as a global or a const instruction
pub(crate) fn eval_size_const(&self, const_instr: tinywasm_types::ConstInstruction) -> Result<i64> {
Ok(match const_instr {
ConstInstruction::I32Const(i) => i as i64,
ConstInstruction::I64Const(i) => i,
ConstInstruction::GlobalGet(addr) => match self.data.globals[addr as usize].value.get() {
TinyWasmValue::Value32(i) => i as i64,
TinyWasmValue::Value64(i) => i as i64,
o => return Err(Error::Other(format!("expected i32 or i64, got {o:?}"))),
},
o => return Err(Error::Other(format!("expected i32, got {o:?}"))),
})
}

/// Evaluate a constant expression
Expand Down
2 changes: 1 addition & 1 deletion crates/tinywasm/src/store/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ impl TableInstance {
.expect("error initializing table: function not found. This should have been caught by the validator")
}

pub(crate) fn init(&mut self, offset: i32, init: &[TableElement]) -> Result<()> {
pub(crate) fn init(&mut self, offset: i64, init: &[TableElement]) -> Result<()> {
let offset = offset as usize;
let end = offset.checked_add(init.len()).ok_or({
Error::Trap(crate::Trap::TableOutOfBounds { offset, len: init.len(), max: self.elements.len() })
Expand Down
Loading
Loading