From 8adce672578683dcc7ab62e5492e0f5dbc07a0b4 Mon Sep 17 00:00:00 2001 From: Henry Gressmann Date: Fri, 26 Jan 2024 18:03:59 +0100 Subject: [PATCH] docs: string example Signed-off-by: Henry Gressmann --- README.md | 1 + crates/tinywasm/src/instance.rs | 4 +- crates/tinywasm/src/reference.rs | 128 +++++++++++++++++- .../src/runtime/interpreter/macros.rs | 2 +- crates/tinywasm/src/store.rs | 61 ++++++++- examples/rust/Cargo.toml | 4 + examples/rust/build.sh | 4 +- examples/rust/src/hello.rs | 30 ++-- examples/rust/src/print.rs | 18 +++ examples/rust/src/tinywasm.rs | 2 +- examples/wasm-rust.rs | 35 ++++- 11 files changed, 261 insertions(+), 28 deletions(-) create mode 100644 examples/rust/src/print.rs diff --git a/README.md b/README.md index 5055889..57728a4 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ There are a couple of low-hanging fruits on the performance side, but they are n - [**Mutable Globals**](https://github.com/WebAssembly/mutable-global/blob/master/proposals/mutable-global/Overview.md) - **Fully implemented** - [**Multi-value**](https://github.com/WebAssembly/spec/blob/master/proposals/multi-value/Overview.md) - **Fully implemented** - [**Sign-extension operators**](https://github.com/WebAssembly/spec/blob/master/proposals/sign-extension-ops/Overview.md) - **Fully implemented** +- [**Bulk Memory Operations**](https://github.com/WebAssembly/spec/blob/master/proposals/bulk-memory-operations/Overview.md) - **_Partially implemented_** - [**Reference Types**](https://github.com/WebAssembly/reference-types/blob/master/proposals/reference-types/Overview.md) - **_Partially implemented_** - [**Multiple Memories**](https://github.com/WebAssembly/multi-memory/blob/master/proposals/multi-memory/Overview.md) - **_Partially implemented_** (not tested yet) - [**Memory64**](https://github.com/WebAssembly/memory64/blob/master/proposals/memory64/Overview.md) - **_Partially implemented_** (only 32-bit addressing is supported at the moment, but larger memories can be created) diff --git a/crates/tinywasm/src/instance.rs b/crates/tinywasm/src/instance.rs index 5346ac3..35b54d6 100644 --- a/crates/tinywasm/src/instance.rs +++ b/crates/tinywasm/src/instance.rs @@ -199,14 +199,14 @@ impl ModuleInstance { pub fn memory(&self, store: &Store, addr: MemAddr) -> Result { let addr = self.resolve_mem_addr(addr); let mem = store.get_mem(addr as usize)?; - Ok(MemoryRef { _instance: mem.clone() }) + Ok(MemoryRef { instance: mem.clone() }) } /// Get the start function of the module /// /// Returns None if the module has no start function /// If no start function is specified, also checks for a _start function in the exports - /// (which is not part of the spec, but used by llvm) + /// (which is not part of the spec, but used by some compilers) /// /// See pub fn start_func(&self, store: &Store) -> Result> { diff --git a/crates/tinywasm/src/reference.rs b/crates/tinywasm/src/reference.rs index bc02338..fdaae18 100644 --- a/crates/tinywasm/src/reference.rs +++ b/crates/tinywasm/src/reference.rs @@ -1,17 +1,135 @@ -use core::cell::RefCell; +use core::{ + cell::{Ref, RefCell}, + ffi::CStr, +}; + +use crate::{GlobalInstance, MemoryInstance, Result}; +use alloc::{ + ffi::CString, + rc::Rc, + string::{String, ToString}, + vec::Vec, +}; +use tinywasm_types::WasmValue; -use crate::{GlobalInstance, MemoryInstance}; -use alloc::rc::Rc; // This module essentially contains the public APIs to interact with the data stored in the store /// A reference to a memory instance #[derive(Debug, Clone)] pub struct MemoryRef { - pub(crate) _instance: Rc>, + pub(crate) instance: Rc>, +} + +/// A borrowed reference to a memory instance +#[derive(Debug)] +pub struct BorrowedMemory<'a> { + pub(crate) instance: Ref<'a, MemoryInstance>, +} + +impl<'a> BorrowedMemory<'a> { + /// Load a slice of memory + pub fn load(&self, offset: usize, len: usize) -> Result<&[u8]> { + self.instance.load(offset, 0, len) + } + + /// Load a C-style string from memory + pub fn load_cstr(&self, offset: usize, len: usize) -> Result<&CStr> { + let bytes = self.load(offset, len)?; + CStr::from_bytes_with_nul(bytes).map_err(|_| crate::Error::Other("Invalid C-style string".to_string())) + } + + /// Load a C-style string from memory, stopping at the first nul byte + pub fn load_cstr_until_nul(&self, offset: usize, max_len: usize) -> Result<&CStr> { + let bytes = self.load(offset, max_len)?; + CStr::from_bytes_until_nul(bytes).map_err(|_| crate::Error::Other("Invalid C-style string".to_string())) + } +} + +impl MemoryRef { + /// Borrow the memory instance + /// + /// This is useful for when you want to load only a reference to a slice of memory + /// without copying the data. The borrow should be dropped before any other memory + /// operations are performed. + pub fn borrow(&self) -> BorrowedMemory<'_> { + BorrowedMemory { instance: self.instance.borrow() } + } + + /// Load a slice of memory + pub fn load_vec(&self, offset: usize, len: usize) -> Result> { + self.instance.borrow().load(offset, 0, len).map(|x| x.to_vec()) + } + + /// Grow the memory by the given number of pages + pub fn grow(&self, delta_pages: i32) -> Option { + self.instance.borrow_mut().grow(delta_pages) + } + + /// Get the current size of the memory in pages + pub fn page_count(&self) -> usize { + self.instance.borrow().page_count() + } + + /// Copy a slice of memory to another place in memory + pub fn copy_within(&self, src: usize, dst: usize, len: usize) -> Result<()> { + self.instance.borrow_mut().copy_within(src, dst, len) + } + + /// Fill a slice of memory with a value + pub fn fill(&self, offset: usize, len: usize, val: u8) -> Result<()> { + self.instance.borrow_mut().fill(offset, len, val) + } + + /// Load a UTF-8 string from memory + pub fn load_string(&self, offset: usize, len: usize) -> Result { + let bytes = self.load_vec(offset, len)?; + Ok(String::from_utf8(bytes).map_err(|_| crate::Error::Other("Invalid UTF-8 string".to_string()))?) + } + + /// Load a C-style string from memory + pub fn load_cstring(&self, offset: usize, len: usize) -> Result { + Ok(CString::from(self.borrow().load_cstr(offset, len)?)) + } + + /// Load a C-style string from memory, stopping at the first nul byte + pub fn load_cstring_until_nul(&self, offset: usize, max_len: usize) -> Result { + Ok(CString::from(self.borrow().load_cstr_until_nul(offset, max_len)?)) + } + + /// Load a JavaScript-style utf-16 string from memory + pub fn load_js_string(&self, offset: usize, len: usize) -> Result { + let memref = self.borrow(); + let bytes = memref.load(offset, len)?; + let mut string = String::new(); + for i in 0..(len / 2) { + let c = u16::from_le_bytes([bytes[i * 2], bytes[i * 2 + 1]]); + string.push( + char::from_u32(c as u32).ok_or_else(|| crate::Error::Other("Invalid UTF-16 string".to_string()))?, + ); + } + Ok(string) + } + + /// Store a slice of memory + pub fn store(&self, offset: usize, len: usize, data: &[u8]) -> Result<()> { + self.instance.borrow_mut().store(offset, 0, data, len) + } } /// A reference to a global instance #[derive(Debug, Clone)] pub struct GlobalRef { - pub(crate) _instance: Rc>, + pub(crate) instance: Rc>, +} + +impl GlobalRef { + /// Get the value of the global + pub fn get(&self) -> WasmValue { + self.instance.borrow().get() + } + + /// Set the value of the global + pub fn set(&self, val: WasmValue) -> Result<()> { + self.instance.borrow_mut().set(val) + } } diff --git a/crates/tinywasm/src/runtime/interpreter/macros.rs b/crates/tinywasm/src/runtime/interpreter/macros.rs index 909acb3..cc65812 100644 --- a/crates/tinywasm/src/runtime/interpreter/macros.rs +++ b/crates/tinywasm/src/runtime/interpreter/macros.rs @@ -63,7 +63,7 @@ macro_rules! mem_store { let val = val as $store_type; let val = val.to_le_bytes(); - mem.borrow_mut().store(($arg.offset + addr) as usize, $arg.align as usize, &val)?; + mem.borrow_mut().store(($arg.offset + addr) as usize, $arg.align as usize, &val, val.len())?; }}; } diff --git a/crates/tinywasm/src/store.rs b/crates/tinywasm/src/store.rs index d281b5c..1c0d260 100644 --- a/crates/tinywasm/src/store.rs +++ b/crates/tinywasm/src/store.rs @@ -306,7 +306,9 @@ impl Store { })?; // See comment for active element sections in the function above why we need to do this here - if let Err(Error::Trap(trap)) = mem.borrow_mut().store(offset as usize, 0, &data.data) { + if let Err(Error::Trap(trap)) = + mem.borrow_mut().store(offset as usize, 0, &data.data, data.data.len()) + { return Ok((data_addrs.into_boxed_slice(), Some(trap))); } @@ -607,8 +609,8 @@ impl MemoryInstance { } } - pub(crate) fn store(&mut self, addr: usize, _align: usize, data: &[u8]) -> Result<()> { - let end = addr.checked_add(data.len()).ok_or_else(|| { + pub(crate) fn store(&mut self, addr: usize, _align: usize, data: &[u8], len: usize) -> Result<()> { + let end = addr.checked_add(len).ok_or_else(|| { Error::Trap(crate::Trap::MemoryOutOfBounds { offset: addr, len: data.len(), max: self.data.len() }) })?; @@ -646,9 +648,37 @@ impl MemoryInstance { self.page_count } - pub(crate) fn grow(&mut self, delta: i32) -> Option { + pub(crate) fn fill(&mut self, addr: usize, len: usize, val: u8) -> Result<()> { + let end = addr + .checked_add(len) + .ok_or_else(|| Error::Trap(crate::Trap::MemoryOutOfBounds { offset: addr, len, max: self.data.len() }))?; + if end > self.data.len() { + return Err(Error::Trap(crate::Trap::MemoryOutOfBounds { offset: addr, len, max: self.data.len() })); + } + self.data[addr..end].fill(val); + Ok(()) + } + + pub(crate) fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> Result<()> { + let end = src + .checked_add(len) + .ok_or_else(|| Error::Trap(crate::Trap::MemoryOutOfBounds { offset: src, len, max: self.data.len() }))?; + if end > self.data.len() || end < src { + return Err(Error::Trap(crate::Trap::MemoryOutOfBounds { offset: src, len, max: self.data.len() })); + } + let end = dst + .checked_add(len) + .ok_or_else(|| Error::Trap(crate::Trap::MemoryOutOfBounds { offset: dst, len, max: self.data.len() }))?; + if end > self.data.len() || end < dst { + return Err(Error::Trap(crate::Trap::MemoryOutOfBounds { offset: dst, len, max: self.data.len() })); + } + self.data[dst..end].copy_within(src..end, len); + Ok(()) + } + + pub(crate) fn grow(&mut self, pages_delta: i32) -> Option { let current_pages = self.page_count(); - let new_pages = current_pages as i64 + delta as i64; + let new_pages = current_pages as i64 + pages_delta as i64; if new_pages < 0 || new_pages > MAX_PAGES as i64 { return None; @@ -669,7 +699,7 @@ impl MemoryInstance { self.page_count = new_pages as usize; log::debug!("memory was {} pages", current_pages); - log::debug!("memory grown by {} pages", delta); + log::debug!("memory grown by {} pages", pages_delta); log::debug!("memory grown to {} pages", self.page_count); Some(current_pages.try_into().expect("memory size out of bounds, this should have been caught earlier")) @@ -690,6 +720,25 @@ impl GlobalInstance { pub(crate) fn new(ty: GlobalType, value: RawWasmValue, owner: ModuleInstanceAddr) -> Self { Self { ty, value, _owner: owner } } + + pub(crate) fn get(&self) -> WasmValue { + self.value.attach_type(self.ty.ty) + } + + pub(crate) fn set(&mut self, val: WasmValue) -> Result<()> { + if val.val_type() != self.ty.ty { + return Err(Error::Other(format!( + "global type mismatch: expected {:?}, got {:?}", + self.ty.ty, + val.val_type() + ))); + } + if !self.ty.mutable { + return Err(Error::Other("global is immutable".to_string())); + } + self.value = val.into(); + Ok(()) + } } /// A WebAssembly Element Instance diff --git a/examples/rust/Cargo.toml b/examples/rust/Cargo.toml index d557392..837f7f3 100644 --- a/examples/rust/Cargo.toml +++ b/examples/rust/Cargo.toml @@ -16,6 +16,10 @@ tinywasm={path="../../crates/tinywasm", features=["parser", "std"]} name="hello" path="src/hello.rs" +[[bin]] +name="print" +path="src/print.rs" + [[bin]] name="tinywasm" path="src/tinywasm.rs" diff --git a/examples/rust/build.sh b/examples/rust/build.sh index a8c587a..a13357d 100755 --- a/examples/rust/build.sh +++ b/examples/rust/build.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash cd "$(dirname "$0")" -bins=("hello" "tinywasm" "fibonacci") +bins=("hello" "fibonacci" "print" "tinywasm") exclude_wat=("tinywasm") out_dir="./target/wasm32-unknown-unknown/wasm" dest_dir="out" @@ -10,7 +10,7 @@ dest_dir="out" mkdir -p "$dest_dir" for bin in "${bins[@]}"; do - cargo build --target wasm32-unknown-unknown --package rust-wasm-examples --profile=wasm --bin "$bin" + RUSTFLAGS="-C target-feature=+reference-types -C panic=abort" cargo build --target wasm32-unknown-unknown --package rust-wasm-examples --profile=wasm --bin "$bin" cp "$out_dir/$bin.wasm" "$dest_dir/" wasm-opt "$dest_dir/$bin.wasm" -o "$dest_dir/$bin.wasm" -O --intrinsic-lowering -O diff --git a/examples/rust/src/hello.rs b/examples/rust/src/hello.rs index 34f3c7f..6bb6d97 100644 --- a/examples/rust/src/hello.rs +++ b/examples/rust/src/hello.rs @@ -1,18 +1,28 @@ -#![no_std] #![no_main] -#[cfg(not(test))] -#[panic_handler] -fn panic(_info: &core::panic::PanicInfo) -> ! { - core::arch::wasm32::unreachable() -} - #[link(wasm_import_module = "env")] extern "C" { - fn printi32(x: i32); + fn print_utf8(location: i32, len: i32); +} + +const ARG: &[u8] = &[0u8; 100]; + +#[no_mangle] +pub unsafe extern "C" fn arg_ptr() -> i32 { + ARG.as_ptr() as i32 } #[no_mangle] -pub unsafe extern "C" fn add_and_print(lh: i32, rh: i32) { - printi32(lh + rh); +pub unsafe extern "C" fn arg_size() -> i32 { + ARG.len() as i32 +} + +#[no_mangle] +pub unsafe extern "C" fn hello(len: i32) { + let arg = core::str::from_utf8(&ARG[0..len as usize]).unwrap(); + let res = format!("Hello, {}!", arg).as_bytes().to_vec(); + + let len = res.len() as i32; + let ptr = res.leak().as_ptr() as i32; + print_utf8(ptr, len); } diff --git a/examples/rust/src/print.rs b/examples/rust/src/print.rs new file mode 100644 index 0000000..34f3c7f --- /dev/null +++ b/examples/rust/src/print.rs @@ -0,0 +1,18 @@ +#![no_std] +#![no_main] + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + core::arch::wasm32::unreachable() +} + +#[link(wasm_import_module = "env")] +extern "C" { + fn printi32(x: i32); +} + +#[no_mangle] +pub unsafe extern "C" fn add_and_print(lh: i32, rh: i32) { + printi32(lh + rh); +} diff --git a/examples/rust/src/tinywasm.rs b/examples/rust/src/tinywasm.rs index 0fb9261..08c8135 100644 --- a/examples/rust/src/tinywasm.rs +++ b/examples/rust/src/tinywasm.rs @@ -12,7 +12,7 @@ pub extern "C" fn hello() { } fn run() -> tinywasm::Result<()> { - let module = tinywasm::Module::parse_bytes(include_bytes!("../out/hello.wasm"))?; + let module = tinywasm::Module::parse_bytes(include_bytes!("../out/print.wasm"))?; let mut store = tinywasm::Store::default(); let mut imports = tinywasm::Imports::new(); diff --git a/examples/wasm-rust.rs b/examples/wasm-rust.rs index 3b26863..6580bd5 100644 --- a/examples/wasm-rust.rs +++ b/examples/wasm-rust.rs @@ -7,12 +7,15 @@ fn main() -> Result<()> { println!("Usage: cargo run --example wasm-rust "); println!("Available examples:"); println!(" hello"); - println!(" tinywasm"); + println!(" printi32"); + println!(" fibonacci - calculate fibonacci(30)"); + println!(" tinywasm - run printi32 inside of tinywasm inside of itself"); return Ok(()); } match args[1].as_str() { "hello" => hello()?, + "printi32" => printi32()?, "fibonacci" => fibonacci()?, "tinywasm" => tinywasm()?, _ => {} @@ -48,6 +51,36 @@ fn hello() -> Result<()> { let module = Module::parse_bytes(&HELLO_WASM)?; let mut store = Store::default(); + let mut imports = Imports::new(); + imports.define( + "env", + "print_utf8", + Extern::typed_func(|mut ctx: FuncContext<'_>, args: (i32, i32)| { + let mem = ctx.memory("memory")?; + let ptr = args.1 as usize; + let len = args.0 as usize; + let string = mem.load_string(ptr, len)?; + println!("{}", string); + Ok(()) + }), + )?; + + let instance = module.instantiate(&mut store, Some(imports))?; + let arg_ptr = instance.exported_func::<(), i32>(&mut store, "arg_ptr")?.call(&mut store, ())?; + let arg = b"world"; + + instance.exported_memory(&mut store, "memory")?.store(arg_ptr as usize, arg.len(), arg)?; + let hello = instance.exported_func::(&mut store, "hello")?; + hello.call(&mut store, arg.len() as i32)?; + + Ok(()) +} + +fn printi32() -> Result<()> { + const HELLO_WASM: &[u8] = include_bytes!("./rust/out/print.wasm"); + let module = Module::parse_bytes(&HELLO_WASM)?; + let mut store = Store::default(); + let mut imports = Imports::new(); imports.define( "env",