diff options
Diffstat (limited to 'examples')
| -rw-r--r-- | examples/doom/Cargo.toml | 21 | ||||
| -rw-r--r-- | examples/doom/README.md | 19 | ||||
| -rwxr-xr-x | examples/doom/build.sh | 55 | ||||
| -rw-r--r-- | examples/doom/guest/tinywasm_puredoom.c | 203 | ||||
| -rw-r--r-- | examples/doom/out/.gitkeep | 0 | ||||
| -rw-r--r-- | examples/doom/src/main.rs | 202 | ||||
| -rw-r--r-- | examples/doom/src/runtime.rs | 348 |
7 files changed, 848 insertions, 0 deletions
diff --git a/examples/doom/Cargo.toml b/examples/doom/Cargo.toml new file mode 100644 index 0000000..c1d0af4 --- /dev/null +++ b/examples/doom/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name="tinywasm-doom" +publish=false +edition.workspace=true +rust-version.workspace=true + +[dependencies] +eyre.workspace=true +pretty_env_logger.workspace=true +tinywasm={workspace=true, features=[ + "std", + "parser", + "log", + "archive", + "canonicalize-nans", + "debug", + "parallel-parser", +]} +log.workspace=true +softbuffer="0.4" +winit="0.30" diff --git a/examples/doom/README.md b/examples/doom/README.md new file mode 100644 index 0000000..f3bc60e --- /dev/null +++ b/examples/doom/README.md @@ -0,0 +1,19 @@ +# Doom Example + +This example builds a WebAssembly guest from [`PureDOOM`](https://github.com/Daivuk/PureDOOM) and runs it inside `tinywasm`. The host uses `winit` and `softbuffer` to present the framebuffer. + +## Prerequisites + +- `clang` +- `git` +- a Doom WAD that you provide yourself + +## Running the example + +```sh +# download & build `PureDOOM` +./examples/doom/build.sh + +# start doom with the WAD you want to use +cargo run -p tinywasm-doom --release -- /path/to/DOOM1.WAD +``` diff --git a/examples/doom/build.sh b/examples/doom/build.sh new file mode 100755 index 0000000..a6b6c01 --- /dev/null +++ b/examples/doom/build.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +upstream_dir="upstream/PureDOOM" +out_dir="out" +wrapper_src="guest/tinywasm_puredoom.c" +output="$out_dir/puredoom.wasm" + +if ! command -v git >/dev/null 2>&1; then + printf 'missing required tool: git\n' >&2 + exit 1 +fi + +if ! command -v clang >/dev/null 2>&1; then + printf 'missing required tool: clang\n' >&2 + exit 1 +fi + +mkdir -p "upstream" "$out_dir" + +if [[ ! -d "$upstream_dir/.git" ]]; then + git clone --depth 1 https://github.com/Daivuk/PureDOOM.git "$upstream_dir" +else + git -C "$upstream_dir" pull --ff-only +fi + +clang \ + --target=wasm32-unknown-unknown \ + -O2 \ + -nostdlib \ + -fno-builtin \ + -w \ + -Wl,--no-entry \ + -Wl,--allow-undefined \ + -Wl,--export=tinywasm_doom_init \ + -Wl,--export=tinywasm_doom_update \ + -Wl,--export=tinywasm_doom_framebuffer \ + -Wl,--export=tinywasm_doom_sound_buffer \ + -Wl,--export=tinywasm_doom_tick_midi \ + -Wl,--export=tinywasm_doom_wad_path_buf \ + -Wl,--export=tinywasm_doom_key_down \ + -Wl,--export=tinywasm_doom_key_up \ + -Wl,--export=memory \ + -Wl,--export=__heap_base \ + -Wl,--export=__data_end \ + -Wl,--initial-memory=16777216 \ + -Wl,--max-memory=268435456 \ + -Wl,--stack-first \ + -I"$upstream_dir" \ + "$wrapper_src" \ + -o "$output" + +printf 'built %s\n' "$output" diff --git a/examples/doom/guest/tinywasm_puredoom.c b/examples/doom/guest/tinywasm_puredoom.c new file mode 100644 index 0000000..1e3e6bd --- /dev/null +++ b/examples/doom/guest/tinywasm_puredoom.c @@ -0,0 +1,203 @@ +#define DOOM_IMPLEMENTATION +#include "PureDOOM.h" + +#if defined(__clang__) +#define IMPORT(name) __attribute__((import_module("env"), import_name(name))) +#else +#define IMPORT(name) +#endif + +extern int host_open(const char *filename, const char *mode) IMPORT("host_open"); +extern void host_close(int handle) IMPORT("host_close"); +extern int host_read(int handle, void *buf, int count) IMPORT("host_read"); +extern int host_write(int handle, const void *buf, int count) IMPORT("host_write"); +extern int host_seek(int handle, int offset, int origin) IMPORT("host_seek"); +extern int host_tell(int handle) IMPORT("host_tell"); +extern int host_eof(int handle) IMPORT("host_eof"); +extern void host_gettime(int *sec, int *usec) IMPORT("host_gettime"); +extern void host_exit(int code) IMPORT("host_exit"); +extern void host_print(const char *text) IMPORT("host_print"); + +extern unsigned char __heap_base; + +static char g_wad_path[1024]; +static char g_home_dir[] = "."; +static char g_program_name[] = "puredoom"; +static char g_iwad_flag[] = "-iwad"; +static char *g_argv[] = {g_program_name, g_iwad_flag, g_wad_path, 0}; +static unsigned int g_heap_ptr; + +static unsigned int align_up(unsigned int value, unsigned int alignment) +{ + return (value + alignment - 1u) & ~(alignment - 1u); +} + +unsigned int strlen(const char *str) +{ + unsigned int len = 0; + while (str[len] != '\0') + ++len; + return len; +} + +static int str_eq(const char *left, const char *right) +{ + unsigned int i = 0; + for (;;) + { + if (left[i] != right[i]) + return 0; + if (left[i] == '\0') + return 1; + ++i; + } +} + +static void *guest_malloc(int size) +{ + unsigned int alloc_size; + unsigned int end; + unsigned int capacity; + unsigned int extra_pages; + void *result; + + if (size <= 0) + return 0; + + if (g_heap_ptr == 0) + g_heap_ptr = align_up((unsigned int)(unsigned long)&__heap_base, 16u); + + alloc_size = align_up((unsigned int)size, 16u); + end = g_heap_ptr + alloc_size; + capacity = __builtin_wasm_memory_size(0) * 65536u; + + if (end > capacity) + { + extra_pages = (end - capacity + 65535u) / 65536u; + if (__builtin_wasm_memory_grow(0, extra_pages) == (unsigned int)-1) + return 0; + } + + result = (void *)(unsigned long)g_heap_ptr; + g_heap_ptr = end; + return result; +} + +static void guest_free(void *ptr) +{ + (void)ptr; +} + +static void *guest_open(const char *filename, const char *mode) +{ + int handle = host_open(filename, mode); + if (handle < 0) + return 0; + return (void *)(unsigned long)(handle + 1); +} + +static int guest_handle(void *handle) +{ + return (int)(unsigned long)handle - 1; +} + +static void guest_close(void *handle) +{ + host_close(guest_handle(handle)); +} + +static int guest_read(void *handle, void *buf, int count) +{ + return host_read(guest_handle(handle), buf, count); +} + +static int guest_write(void *handle, const void *buf, int count) +{ + return host_write(guest_handle(handle), buf, count); +} + +static int guest_seek(void *handle, int offset, doom_seek_t origin) +{ + return host_seek(guest_handle(handle), offset, (int)origin); +} + +static int guest_tell(void *handle) +{ + return host_tell(guest_handle(handle)); +} + +static int guest_eof(void *handle) +{ + return host_eof(guest_handle(handle)); +} + +static void guest_gettime(int *sec, int *usec) +{ + host_gettime(sec, usec); +} + +static void guest_exit(int code) +{ + host_exit(code); +} + +static char *guest_getenv(const char *var) +{ + if (str_eq(var, "HOME")) + return g_home_dir; + return 0; +} + +unsigned int tinywasm_doom_wad_path_buf(void) +{ + return (unsigned int)(unsigned long)g_wad_path; +} + +void tinywasm_doom_init(void) +{ + doom_set_print(host_print); + doom_set_malloc(guest_malloc, guest_free); + doom_set_file_io(guest_open, guest_close, guest_read, guest_write, guest_seek, guest_tell, guest_eof); + doom_set_gettime(guest_gettime); + doom_set_exit(guest_exit); + doom_set_getenv(guest_getenv); + + doom_set_default_int("key_up", DOOM_KEY_W); + doom_set_default_int("key_down", DOOM_KEY_S); + doom_set_default_int("key_strafeleft", DOOM_KEY_A); + doom_set_default_int("key_straferight", DOOM_KEY_D); + doom_set_default_int("key_use", DOOM_KEY_E); + doom_set_resolution(320, 200); + + doom_init(3, g_argv, DOOM_FLAG_HIDE_MOUSE_OPTIONS | DOOM_FLAG_HIDE_SOUND_OPTIONS | DOOM_FLAG_HIDE_MUSIC_OPTIONS); +} + +void tinywasm_doom_update(void) +{ + doom_update(); +} + +unsigned int tinywasm_doom_framebuffer(void) +{ + return (unsigned int)(unsigned long)doom_get_framebuffer(4); +} + +unsigned int tinywasm_doom_sound_buffer(void) +{ + return (unsigned int)(unsigned long)doom_get_sound_buffer(); +} + +unsigned long tinywasm_doom_tick_midi(void) +{ + return doom_tick_midi(); +} + +void tinywasm_doom_key_down(int key) +{ + doom_key_down((doom_key_t)key); +} + +void tinywasm_doom_key_up(int key) +{ + doom_key_up((doom_key_t)key); +} diff --git a/examples/doom/out/.gitkeep b/examples/doom/out/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/examples/doom/out/.gitkeep diff --git a/examples/doom/src/main.rs b/examples/doom/src/main.rs new file mode 100644 index 0000000..095183d --- /dev/null +++ b/examples/doom/src/main.rs @@ -0,0 +1,202 @@ +mod runtime; + +use std::num::NonZeroU32; +use std::path::{Path, PathBuf}; +use std::rc::Rc; + +use eyre::{ContextCompat, Result, bail, eyre}; +use runtime::{Runtime, SCREEN_HEIGHT, SCREEN_WIDTH}; +use softbuffer::{Context as SoftbufferContext, Surface}; +use winit::application::ApplicationHandler; +use winit::dpi::{LogicalSize, Size}; +use winit::event::{ElementState, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; +use winit::keyboard::{KeyCode, PhysicalKey}; +use winit::window::{Window, WindowAttributes, WindowLevel}; + +const GUEST_MODULE: &str = "examples/doom/out/puredoom.wasm"; +const DEFAULT_TITLE: &str = "tinywasm doom"; + +fn main() -> Result<()> { + pretty_env_logger::init(); + + let wad_path = std::env::args().nth(1).map(PathBuf::from).context("usage: cargo run -p tinywasm-doom -- <wad>")?; + if !wad_path.exists() { + bail!("WAD not found: {}", wad_path.display()); + } + + let guest_path = Path::new(GUEST_MODULE); + if !guest_path.exists() { + bail!("guest module missing: {}. Run ./examples/doom/build.sh first", guest_path.display()); + } + + let event_loop = EventLoop::new()?; + let mut app = DoomApp::new(wad_path, guest_path.to_path_buf())?; + event_loop.run_app(&mut app)?; + Ok(()) +} + +struct DoomApp { + runtime: Runtime, + window: Option<Rc<Window>>, + softbuffer_context: Option<SoftbufferContext<Rc<Window>>>, + softbuffer_surface: Option<Surface<Rc<Window>, Rc<Window>>>, +} + +impl DoomApp { + fn new(wad_path: PathBuf, guest_path: PathBuf) -> Result<Self> { + Ok(Self { + runtime: Runtime::new(wad_path, guest_path)?, + window: None, + softbuffer_context: None, + softbuffer_surface: None, + }) + } + + fn present(&mut self) -> Result<()> { + let Some(surface) = self.softbuffer_surface.as_mut() else { + return Ok(()); + }; + + surface + .resize(NonZeroU32::new(SCREEN_WIDTH as u32).unwrap(), NonZeroU32::new(SCREEN_HEIGHT as u32).unwrap()) + .map_err(|err| eyre!(err.to_string()))?; + let mut buffer = surface.buffer_mut().map_err(|err| eyre!(err.to_string()))?; + self.runtime.write_framebuffer(&mut buffer)?; + buffer.present().map_err(|err| eyre!(err.to_string()))?; + Ok(()) + } +} + +impl ApplicationHandler for DoomApp { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + event_loop.set_control_flow(ControlFlow::Poll); + + let size = LogicalSize::new((SCREEN_WIDTH * 2) as f64, (SCREEN_HEIGHT * 2) as f64); + + let attributes = WindowAttributes::default() + .with_title(DEFAULT_TITLE) + .with_inner_size(Size::Logical(size)) + .with_resizable(false) + .with_window_level(if cfg!(target_os = "linux") { WindowLevel::AlwaysOnTop } else { WindowLevel::Normal }); + + let window = Rc::new(event_loop.create_window(attributes).expect("create window")); + let context = SoftbufferContext::new(window.clone()).expect("create softbuffer context"); + let surface = Surface::new(&context, window.clone()).expect("create softbuffer surface"); + + self.softbuffer_context = Some(context); + self.softbuffer_surface = Some(surface); + self.window = Some(window.clone()); + window.set_cursor_visible(false); + window.request_redraw(); + } + + fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: winit::window::WindowId, event: WindowEvent) { + match event { + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::RedrawRequested => { + if let Err(err) = self.present() { + log::error!("present failed: {err:?}"); + event_loop.exit(); + } + } + WindowEvent::KeyboardInput { event, .. } => { + if let Some(key) = doom_key(&event.physical_key) { + let result = if matches!(event.state, ElementState::Pressed) { + self.runtime.key_down(i32::from(key)) + } else { + self.runtime.key_up(i32::from(key)) + }; + + if let Err(err) = result { + log::error!("keyboard event failed: {err:?}"); + event_loop.exit(); + } + } + } + _ => {} + } + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if let Err(err) = self.runtime.tick() { + log::error!("doom tick failed: {err:?}"); + event_loop.exit(); + return; + } + + if let Some(window) = &self.window { + window.request_redraw(); + } + + if self.runtime.host_state.borrow().exit_code.is_some() { + event_loop.exit(); + } + } +} + +fn doom_key(key: &PhysicalKey) -> Option<u8> { + let code = match key { + PhysicalKey::Code(KeyCode::Enter) => 13, + PhysicalKey::Code(KeyCode::Escape) => 27, + PhysicalKey::Code(KeyCode::ArrowLeft) => 0xac, + PhysicalKey::Code(KeyCode::ArrowRight) => 0xae, + PhysicalKey::Code(KeyCode::ArrowUp) => 0xad, + PhysicalKey::Code(KeyCode::ArrowDown) => 0xaf, + PhysicalKey::Code(KeyCode::ControlLeft | KeyCode::ControlRight) => 0x80 + 0x1d, + PhysicalKey::Code(KeyCode::ShiftLeft | KeyCode::ShiftRight) => 0xb6, + PhysicalKey::Code(KeyCode::AltLeft | KeyCode::AltRight) => 0xb8, + PhysicalKey::Code(KeyCode::Space) => b' ', + PhysicalKey::Code(KeyCode::F2) => 0x80 + 0x3c, + PhysicalKey::Code(KeyCode::F3) => 0x80 + 0x3d, + PhysicalKey::Code(KeyCode::F4) => 0x80 + 0x3e, + PhysicalKey::Code(KeyCode::F5) => 0x80 + 0x3f, + PhysicalKey::Code(KeyCode::F6) => 0x80 + 0x40, + PhysicalKey::Code(KeyCode::F7) => 0x80 + 0x41, + PhysicalKey::Code(KeyCode::F8) => 0x80 + 0x42, + PhysicalKey::Code(KeyCode::F9) => 0x80 + 0x43, + PhysicalKey::Code(KeyCode::F10) => 0x80 + 0x44, + PhysicalKey::Code(KeyCode::F11) => 0x80 + 0x57, + PhysicalKey::Code(KeyCode::Equal) => b'=', + PhysicalKey::Code(KeyCode::Minus) => b'-', + PhysicalKey::Code(KeyCode::KeyA) => b'a', + PhysicalKey::Code(KeyCode::KeyB) => b'b', + PhysicalKey::Code(KeyCode::KeyC) => b'c', + PhysicalKey::Code(KeyCode::KeyD) => b'd', + PhysicalKey::Code(KeyCode::KeyE) => b'e', + PhysicalKey::Code(KeyCode::KeyF) => b'f', + PhysicalKey::Code(KeyCode::KeyG) => b'g', + PhysicalKey::Code(KeyCode::KeyH) => b'h', + PhysicalKey::Code(KeyCode::KeyI) => b'i', + PhysicalKey::Code(KeyCode::KeyJ) => b'j', + PhysicalKey::Code(KeyCode::KeyK) => b'k', + PhysicalKey::Code(KeyCode::KeyL) => b'l', + PhysicalKey::Code(KeyCode::KeyM) => b'm', + PhysicalKey::Code(KeyCode::KeyN) => b'n', + PhysicalKey::Code(KeyCode::KeyO) => b'o', + PhysicalKey::Code(KeyCode::KeyP) => b'p', + PhysicalKey::Code(KeyCode::KeyQ) => b'q', + PhysicalKey::Code(KeyCode::KeyR) => b'r', + PhysicalKey::Code(KeyCode::KeyS) => b's', + PhysicalKey::Code(KeyCode::KeyT) => b't', + PhysicalKey::Code(KeyCode::KeyU) => b'u', + PhysicalKey::Code(KeyCode::KeyV) => b'v', + PhysicalKey::Code(KeyCode::KeyW) => b'w', + PhysicalKey::Code(KeyCode::KeyX) => b'x', + PhysicalKey::Code(KeyCode::KeyY) => b'y', + PhysicalKey::Code(KeyCode::KeyZ) => b'z', + PhysicalKey::Code(KeyCode::Digit0) => b'0', + PhysicalKey::Code(KeyCode::Digit1) => b'1', + PhysicalKey::Code(KeyCode::Digit2) => b'2', + PhysicalKey::Code(KeyCode::Digit3) => b'3', + PhysicalKey::Code(KeyCode::Digit4) => b'4', + PhysicalKey::Code(KeyCode::Digit5) => b'5', + PhysicalKey::Code(KeyCode::Digit6) => b'6', + PhysicalKey::Code(KeyCode::Digit7) => b'7', + PhysicalKey::Code(KeyCode::Digit8) => b'8', + PhysicalKey::Code(KeyCode::Digit9) => b'9', + _ => return None, + }; + + Some(code) +} diff --git a/examples/doom/src/runtime.rs b/examples/doom/src/runtime.rs new file mode 100644 index 0000000..980dc21 --- /dev/null +++ b/examples/doom/src/runtime.rs @@ -0,0 +1,348 @@ +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::fs::{File, OpenOptions, create_dir_all}; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::time::Instant; + +use eyre::Result; +use tinywasm::{FuncContext, HostFunction, Imports, ModuleInstance, Store}; + +const IMPORT_MODULE: &str = "env"; +pub const SCREEN_WIDTH: usize = 320; +pub const SCREEN_HEIGHT: usize = 200; + +pub struct Runtime { + store: Store, + update: tinywasm::FunctionTyped<(), ()>, + framebuffer: tinywasm::FunctionTyped<(), i32>, + key_down: tinywasm::FunctionTyped<i32, ()>, + key_up: tinywasm::FunctionTyped<i32, ()>, + memory: tinywasm::Memory, + framebuffer_bytes: Vec<u8>, + pub host_state: Rc<RefCell<HostState>>, +} + +impl Runtime { + pub fn new(wad_path: PathBuf, guest_path: PathBuf) -> Result<Self> { + let module = tinywasm::parse_file(&guest_path)?; + let mut store = Store::default(); + let host_state = Rc::new(RefCell::new(HostState::new(wad_path))); + let imports = build_imports(&mut store, host_state.clone()); + let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?; + + let wad_path_buf = instance.func::<(), i32>(&store, "tinywasm_doom_wad_path_buf")?; + let init = instance.func::<(), ()>(&store, "tinywasm_doom_init")?; + let update = instance.func::<(), ()>(&store, "tinywasm_doom_update")?; + let framebuffer = instance.func::<(), i32>(&store, "tinywasm_doom_framebuffer")?; + let key_down = instance.func::<i32, ()>(&store, "tinywasm_doom_key_down")?; + let key_up = instance.func::<i32, ()>(&store, "tinywasm_doom_key_up")?; + let memory = instance.memory("memory")?; + + let buf_ptr = wad_path_buf.call(&mut store, ())? as usize; + let wad_path_string = host_state.borrow().wad_path.to_string_lossy().into_owned(); + memory.write_cstring_bytes(&mut store, buf_ptr, &wad_path_string)?; + init.call(&mut store, ())?; + + let width = SCREEN_WIDTH; + let height = SCREEN_HEIGHT; + + Ok(Self { + store, + update, + framebuffer, + key_down, + key_up, + memory, + framebuffer_bytes: vec![0; width * height * 4], + host_state, + }) + } + + pub fn tick(&mut self) -> Result<()> { + self.update.call(&mut self.store, ())?; + Ok(()) + } + + pub fn write_framebuffer(&mut self, dst: &mut [u32]) -> Result<()> { + let ptr = self.framebuffer.call(&mut self.store, ())? as usize; + self.memory.read_exact(&self.store, ptr, &mut self.framebuffer_bytes)?; + for (index, pixel) in dst.iter_mut().enumerate() { + let byte_index = index * 4; + let chunk = &self.framebuffer_bytes[byte_index..byte_index + 4]; + *pixel = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32; + } + Ok(()) + } + + pub fn key_down(&mut self, key: i32) -> Result<()> { + self.key_down.call(&mut self.store, key)?; + Ok(()) + } + + pub fn key_up(&mut self, key: i32) -> Result<()> { + self.key_up.call(&mut self.store, key)?; + Ok(()) + } +} + +pub struct HostState { + pub wad_path: PathBuf, + runtime_dir: PathBuf, + start: Instant, + files: BTreeMap<i32, File>, + next_file: i32, + pub exit_code: Option<i32>, +} + +impl HostState { + fn new(wad_path: PathBuf) -> Self { + let runtime_dir = PathBuf::from("examples/doom/out/runtime"); + let _ = create_dir_all(&runtime_dir); + Self { wad_path, runtime_dir, start: Instant::now(), files: BTreeMap::new(), next_file: 3, exit_code: None } + } + + fn resolve_path(&self, path: &str) -> PathBuf { + let candidate = Path::new(path); + if candidate.is_absolute() { candidate.to_path_buf() } else { self.runtime_dir.join(candidate) } + } + + fn should_redirect_to_wad(&self, requested: &str) -> bool { + let Some(file_name) = Path::new(requested).file_name().and_then(|name| name.to_str()) else { + return false; + }; + + let requested = file_name.to_ascii_lowercase(); + let provided = self.wad_path.file_name().and_then(|name| name.to_str()).map(|name| name.to_ascii_lowercase()); + + match provided.as_deref() { + Some("doom1.wad") => requested == "doom1.wad", + Some("doom.wad") => requested == "doom.wad", + Some("doomu.wad") => requested == "doomu.wad", + Some("doom2.wad") => requested == "doom2.wad", + Some("doom2f.wad") => requested == "doom2f.wad", + Some("plutonia.wad") => requested == "plutonia.wad", + Some("tnt.wad") => requested == "tnt.wad", + Some(provided_name) => requested == provided_name, + None => false, + } + } + + fn open_mode_options(mode: &str) -> OpenOptions { + let mut options = OpenOptions::new(); + let plus = mode.as_bytes().contains(&b'+'); + + match mode.as_bytes().first().copied() { + Some(b'r') => { + options.read(true); + if plus { + options.write(true); + } + } + Some(b'w') => { + options.write(true).create(true).truncate(true); + if plus { + options.read(true); + } + } + Some(b'a') => { + options.write(true).create(true).append(true); + if plus { + options.read(true); + } + } + _ => { + options.read(true); + } + } + + options + } +} + +fn build_imports(store: &mut Store, state: Rc<RefCell<HostState>>) -> Imports { + let mut imports = Imports::new(); + + { + let state = state.clone(); + imports.define( + IMPORT_MODULE, + "host_open", + HostFunction::from(store, move |ctx: FuncContext<'_>, (filename_ptr, mode_ptr): (i32, i32)| { + let memory = ctx.memory("memory")?; + let filename = memory.read_cstring_until_null(ctx.store(), filename_ptr as usize, 1024)?; + let mode = memory.read_cstring_until_null(ctx.store(), mode_ptr as usize, 16)?; + let filename = filename.to_string_lossy(); + let mode = mode.to_string_lossy(); + let mut state = state.borrow_mut(); + let path = if filename == state.wad_path.to_string_lossy() || state.should_redirect_to_wad(&filename) { + state.wad_path.clone() + } else { + state.resolve_path(&filename) + }; + + if path.is_dir() { + log::debug!("guest open rejected directory: path={} mode={}", path.display(), mode); + return Ok(-1); + } + + let file = match HostState::open_mode_options(&mode).open(&path) { + Ok(file) => file, + Err(err) => { + log::debug!("guest open failed: path={} mode={} err={err}", path.display(), mode); + return Ok(-1); + } + }; + + let handle = state.next_file; + state.next_file += 1; + state.files.insert(handle, file); + Ok(handle) + }), + ); + } + + { + let state = state.clone(); + imports.define( + IMPORT_MODULE, + "host_close", + HostFunction::from(store, move |_ctx: FuncContext<'_>, handle: i32| { + state.borrow_mut().files.remove(&handle); + Ok(()) + }), + ); + } + + { + let state = state.clone(); + imports.define( + IMPORT_MODULE, + "host_read", + HostFunction::from(store, move |mut ctx: FuncContext<'_>, (handle, buf_ptr, count): (i32, i32, i32)| { + let mut state = state.borrow_mut(); + let Some(file) = state.files.get_mut(&handle) else { + return Ok(0); + }; + let mut buffer = vec![0; count.max(0) as usize]; + let read = file.read(&mut buffer).map_err(|err| tinywasm::Error::Other(err.to_string()))?; + ctx.memory("memory")?.copy_from_slice(ctx.store_mut(), buf_ptr as usize, &buffer[..read])?; + Ok(read as i32) + }), + ); + } + + { + let state = state.clone(); + imports.define( + IMPORT_MODULE, + "host_write", + HostFunction::from(store, move |ctx: FuncContext<'_>, (handle, buf_ptr, count): (i32, i32, i32)| { + let data = ctx.memory("memory")?.read_vec(ctx.store(), buf_ptr as usize, count.max(0) as usize)?; + let mut state = state.borrow_mut(); + let Some(file) = state.files.get_mut(&handle) else { + return Ok(-1); + }; + let written = file.write(&data).map_err(|err| tinywasm::Error::Other(err.to_string()))?; + Ok(written as i32) + }), + ); + } + + { + let state = state.clone(); + imports.define( + IMPORT_MODULE, + "host_seek", + HostFunction::from(store, move |_ctx: FuncContext<'_>, (handle, offset, origin): (i32, i32, i32)| { + let seek_from = match origin { + 0 => SeekFrom::Start(offset.max(0) as u64), + 1 => SeekFrom::Current(offset as i64), + 2 => SeekFrom::End(offset as i64), + _ => return Err(tinywasm::Error::Other(format!("invalid seek origin: {origin}"))), + }; + let mut state = state.borrow_mut(); + let Some(file) = state.files.get_mut(&handle) else { + return Ok(-1); + }; + let pos = file.seek(seek_from).map_err(|err| tinywasm::Error::Other(err.to_string()))?; + Ok(pos.min(i32::MAX as u64) as i32) + }), + ); + } + + { + let state = state.clone(); + imports.define( + IMPORT_MODULE, + "host_tell", + HostFunction::from(store, move |_ctx: FuncContext<'_>, handle: i32| { + let mut state = state.borrow_mut(); + let Some(file) = state.files.get_mut(&handle) else { + return Ok(-1); + }; + let pos = file.stream_position().map_err(|err| tinywasm::Error::Other(err.to_string()))?; + Ok(pos.min(i32::MAX as u64) as i32) + }), + ); + } + + { + let state = state.clone(); + imports.define( + IMPORT_MODULE, + "host_eof", + HostFunction::from(store, move |_ctx: FuncContext<'_>, handle: i32| { + let mut state = state.borrow_mut(); + let Some(file) = state.files.get_mut(&handle) else { + return Ok(1); + }; + let pos = file.stream_position().map_err(|err| tinywasm::Error::Other(err.to_string()))?; + let len = file.metadata().map_err(|err| tinywasm::Error::Other(err.to_string()))?.len(); + Ok((pos >= len) as i32) + }), + ); + } + + { + let state = state.clone(); + imports.define( + IMPORT_MODULE, + "host_gettime", + HostFunction::from(store, move |mut ctx: FuncContext<'_>, (sec_ptr, usec_ptr): (i32, i32)| { + let elapsed = state.borrow().start.elapsed(); + let sec = elapsed.as_secs().min(i32::MAX as u64) as i32; + let usec = elapsed.subsec_micros() as i32; + let memory = ctx.memory("memory")?; + memory.copy_from_slice(ctx.store_mut(), sec_ptr as usize, &sec.to_le_bytes())?; + memory.copy_from_slice(ctx.store_mut(), usec_ptr as usize, &usec.to_le_bytes())?; + Ok(()) + }), + ); + } + + { + let state = state.clone(); + imports.define( + IMPORT_MODULE, + "host_exit", + HostFunction::from(store, move |_ctx: FuncContext<'_>, code: i32| { + state.borrow_mut().exit_code = Some(code); + Ok(()) + }), + ); + } + + imports.define( + IMPORT_MODULE, + "host_print", + HostFunction::from(store, move |ctx: FuncContext<'_>, ptr: i32| { + let text = ctx.memory("memory")?.read_cstring_until_null(ctx.store(), ptr as usize, 4096)?; + log::info!("guest: {}", text.to_string_lossy()); + Ok(()) + }), + ); + + imports +} |
