wasm_interp: support WASI argc/argv, and writing to stdout/stderr

This commit is contained in:
Brian Carroll 2022-12-03 12:28:20 +00:00
parent 736630b53f
commit 284dc6fa51
No known key found for this signature in database
GPG key ID: 5C7B2EC4101703C0
4 changed files with 386 additions and 69 deletions

View file

@ -145,15 +145,12 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
self.call_export_help(module, arg_type_bytes) self.call_export_help(module, arg_type_bytes)
} }
pub fn call_export_from_cli<'arg, A>( pub fn call_export_from_cli(
&mut self, &mut self,
module: &WasmModule<'a>, module: &WasmModule<'a>,
fn_name: &str, fn_name: &str,
arg_strings: A, arg_strings: &'a [&'a String],
) -> Result<Option<Value>, String> ) -> Result<Option<Value>, String> {
where
A: IntoIterator<Item = &'arg str>,
{
let arg_type_bytes = self.prepare_to_call_export(module, fn_name)?; let arg_type_bytes = self.prepare_to_call_export(module, fn_name)?;
for (value_str, type_byte) in arg_strings.into_iter().zip(arg_type_bytes.iter().copied()) { for (value_str, type_byte) in arg_strings.into_iter().zip(arg_type_bytes.iter().copied()) {
@ -246,7 +243,6 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
// We just popped the stack frame for the entry function. Terminate the program. // We just popped the stack frame for the entry function. Terminate the program.
Action::Break Action::Break
} else { } else {
dbg!(return_addr, block_depth);
self.program_counter = return_addr as usize; self.program_counter = return_addr as usize;
self.outermost_block = block_depth; self.outermost_block = block_depth;
Action::Continue Action::Continue
@ -374,9 +370,13 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
if let Some(return_val) = optional_return_val { if let Some(return_val) = optional_return_val {
self.value_stack.push(return_val); self.value_stack.push(return_val);
} }
if let Some(debug_string) = self.debug_string.as_mut() {
std::write!(debug_string, " {}.{}", import.module, import.name).unwrap();
}
} else { } else {
let return_addr = self.program_counter as u32; let return_addr = self.program_counter as u32;
self.program_counter = module.code.function_offsets[fn_index] as usize; let internal_fn_index = fn_index - n_import_fns;
self.program_counter = module.code.function_offsets[internal_fn_index] as usize;
let return_block_depth = self.outermost_block; let return_block_depth = self.outermost_block;
self.outermost_block = self.block_loop_addrs.len() as u32; self.outermost_block = self.block_loop_addrs.len() as u32;
@ -407,6 +407,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
} }
let mut action = Action::Continue; let mut action = Action::Continue;
let mut implicit_return = false;
match op_code { match op_code {
UNREACHABLE => { UNREACHABLE => {
@ -461,6 +462,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
if self.block_loop_addrs.len() == self.outermost_block as usize { if self.block_loop_addrs.len() == self.outermost_block as usize {
// implicit RETURN at end of function // implicit RETURN at end of function
action = self.do_return(); action = self.do_return();
implicit_return = true;
} else { } else {
self.block_loop_addrs.pop().unwrap(); self.block_loop_addrs.pop().unwrap();
} }
@ -1457,6 +1459,18 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
let base = self.call_stack.value_stack_base(); let base = self.call_stack.value_stack_base();
let slice = self.value_stack.get_slice(base as usize); let slice = self.value_stack.get_slice(base as usize);
eprintln!("{:#07x} {:17} {:?}", file_offset, debug_string, slice); eprintln!("{:#07x} {:17} {:?}", file_offset, debug_string, slice);
if op_code == RETURN || (op_code == END && implicit_return) {
let next_code_section_index = module
.code
.function_offsets
.iter()
.position(|o| *o as usize > self.program_counter)
.unwrap_or(module.code.function_offsets.len());
let fn_index = module.import.imports.len() + next_code_section_index - 1;
eprintln!("returning to function {}\n", fn_index);
} else if op_code == CALL || op_code == CALLINDIRECT {
eprintln!("");
}
} }
action action

View file

@ -9,6 +9,7 @@ pub use instance::Instance;
use roc_wasm_module::Value; use roc_wasm_module::Value;
use value_stack::ValueStack; use value_stack::ValueStack;
use wasi::WasiDispatcher;
pub trait ImportDispatcher { pub trait ImportDispatcher {
/// Dispatch a call from WebAssembly to your own code, based on module and function name. /// Dispatch a call from WebAssembly to your own code, based on module and function name.
@ -21,11 +22,23 @@ pub trait ImportDispatcher {
) -> Option<Value>; ) -> Option<Value>;
} }
pub const DEFAULT_IMPORTS: DefaultImportDispatcher = DefaultImportDispatcher {}; pub const DEFAULT_IMPORTS: DefaultImportDispatcher = DefaultImportDispatcher {
wasi: WasiDispatcher { args: &[] },
};
pub struct DefaultImportDispatcher {} pub struct DefaultImportDispatcher<'a> {
wasi: WasiDispatcher<'a>,
}
impl ImportDispatcher for DefaultImportDispatcher { impl<'a> DefaultImportDispatcher<'a> {
pub fn new(args: &'a [&'a String]) -> Self {
DefaultImportDispatcher {
wasi: WasiDispatcher { args },
}
}
}
impl<'a> ImportDispatcher for DefaultImportDispatcher<'a> {
fn dispatch( fn dispatch(
&mut self, &mut self,
module_name: &str, module_name: &str,
@ -34,7 +47,7 @@ impl ImportDispatcher for DefaultImportDispatcher {
memory: &mut [u8], memory: &mut [u8],
) -> Option<Value> { ) -> Option<Value> {
if module_name == wasi::MODULE_NAME { if module_name == wasi::MODULE_NAME {
wasi::dispatch(function_name, arguments, memory) self.wasi.dispatch(function_name, arguments, memory)
} else { } else {
panic!( panic!(
"DefaultImportDispatcher does not implement {}.{}", "DefaultImportDispatcher does not implement {}.{}",

View file

@ -1,4 +1,4 @@
use bumpalo::Bump; use bumpalo::{collections::Vec, Bump};
use clap::ArgAction; use clap::ArgAction;
use clap::{Arg, Command}; use clap::{Arg, Command};
use std::ffi::OsString; use std::ffi::OsString;
@ -6,7 +6,7 @@ use std::fs;
use std::io; use std::io;
use std::process; use std::process;
use roc_wasm_interp::{Instance, DEFAULT_IMPORTS}; use roc_wasm_interp::{DefaultImportDispatcher, Instance};
use roc_wasm_module::WasmModule; use roc_wasm_module::WasmModule;
pub const FLAG_FUNCTION: &str = "function"; pub const FLAG_FUNCTION: &str = "function";
@ -61,10 +61,7 @@ fn main() -> io::Result<()> {
let start_fn_name = matches.get_one::<String>(FLAG_FUNCTION).unwrap(); let start_fn_name = matches.get_one::<String>(FLAG_FUNCTION).unwrap();
let is_debug_mode = matches.get_flag(FLAG_DEBUG); let is_debug_mode = matches.get_flag(FLAG_DEBUG);
let is_hex_format = matches.get_flag(FLAG_HEX); let is_hex_format = matches.get_flag(FLAG_HEX);
let start_arg_strings = matches let start_arg_strings = matches.get_many::<String>(ARGS_FOR_APP).unwrap_or_default();
.get_many::<String>(ARGS_FOR_APP)
.unwrap_or_default()
.map(|s| s.as_str());
// Load the WebAssembly binary file // Load the WebAssembly binary file
@ -87,15 +84,17 @@ fn main() -> io::Result<()> {
// Create an execution instance // Create an execution instance
let mut inst = Instance::for_module(&arena, &module, DEFAULT_IMPORTS, is_debug_mode) let args = Vec::from_iter_in(start_arg_strings, &arena);
.unwrap_or_else(|e| { let dispatcher = DefaultImportDispatcher::new(&args);
let mut inst =
Instance::for_module(&arena, &module, dispatcher, is_debug_mode).unwrap_or_else(|e| {
eprintln!("{}", e); eprintln!("{}", e);
process::exit(2); process::exit(2);
}); });
// Run // Run
let result = inst.call_export_from_cli(&module, start_fn_name, start_arg_strings); let result = inst.call_export_from_cli(&module, start_fn_name, &args);
// Print out return value, if any // Print out return value, if any

View file

@ -1,54 +1,345 @@
use roc_wasm_module::Value; use roc_wasm_module::Value;
use std::io::{self, Write};
pub const MODULE_NAME: &str = "wasi_snapshot_preview1"; pub const MODULE_NAME: &str = "wasi_snapshot_preview1";
pub fn dispatch(function_name: &str, arguments: &[Value], _memory: &mut [u8]) -> Option<Value> { pub struct WasiDispatcher<'a> {
match function_name { pub args: &'a [&'a String],
"args_get" => todo!("WASI {}({:?})", function_name, arguments), }
"args_sizes_get" => todo!("WASI {}({:?})", function_name, arguments),
"environ_get" => todo!("WASI {}({:?})", function_name, arguments), /// Implementation of WASI syscalls
"environ_sizes_get" => todo!("WASI {}({:?})", function_name, arguments), /// References for other engines:
"clock_res_get" => todo!("WASI {}({:?})", function_name, arguments), /// https://github.com/wasmerio/wasmer/blob/ef8d2f651ed29b4b06fdc2070eb8189922c54d82/lib/wasi/src/syscalls/mod.rs
"clock_time_get" => todo!("WASI {}({:?})", function_name, arguments), /// https://github.com/wasm3/wasm3/blob/045040a97345e636b8be4f3086e6db59cdcc785f/source/extra/wasi_core.h
"fd_advise" => todo!("WASI {}({:?})", function_name, arguments), impl<'a> WasiDispatcher<'a> {
"fd_allocate" => todo!("WASI {}({:?})", function_name, arguments), pub fn new(args: &'a [&'a String]) -> Self {
"fd_close" => todo!("WASI {}({:?})", function_name, arguments), WasiDispatcher { args }
"fd_datasync" => todo!("WASI {}({:?})", function_name, arguments), }
"fd_fdstat_get" => todo!("WASI {}({:?})", function_name, arguments),
"fd_fdstat_set_flags" => todo!("WASI {}({:?})", function_name, arguments), pub fn dispatch(
"fd_fdstat_set_rights" => todo!("WASI {}({:?})", function_name, arguments), &mut self,
"fd_filestat_get" => todo!("WASI {}({:?})", function_name, arguments), function_name: &str,
"fd_filestat_set_size" => todo!("WASI {}({:?})", function_name, arguments), arguments: &[Value],
"fd_filestat_set_times" => todo!("WASI {}({:?})", function_name, arguments), memory: &mut [u8],
"fd_pread" => todo!("WASI {}({:?})", function_name, arguments), ) -> Option<Value> {
"fd_prestat_get" => todo!("WASI {}({:?})", function_name, arguments), let success_code = Some(Value::I32(Errno::Success as i32));
"fd_prestat_dir_name" => todo!("WASI {}({:?})", function_name, arguments),
"fd_pwrite" => todo!("WASI {}({:?})", function_name, arguments), match function_name {
"fd_read" => todo!("WASI {}({:?})", function_name, arguments), "args_get" => {
"fd_readdir" => todo!("WASI {}({:?})", function_name, arguments), // uint8_t ** argv,
"fd_renumber" => todo!("WASI {}({:?})", function_name, arguments), let mut ptr_ptr_argv = arguments[0].unwrap_i32() as usize;
"fd_seek" => todo!("WASI {}({:?})", function_name, arguments), // uint8_t * argv_buf
"fd_sync" => todo!("WASI {}({:?})", function_name, arguments), let mut ptr_argv_buf = arguments[1].unwrap_i32() as usize;
"fd_tell" => todo!("WASI {}({:?})", function_name, arguments),
"fd_write" => todo!("WASI {}({:?})", function_name, arguments), for arg in self.args {
"path_create_directory" => todo!("WASI {}({:?})", function_name, arguments), write_u32(memory, ptr_ptr_argv, ptr_argv_buf as u32);
"path_filestat_get" => todo!("WASI {}({:?})", function_name, arguments), let bytes_target = &mut memory[ptr_argv_buf..][..arg.len()];
"path_filestat_set_times" => todo!("WASI {}({:?})", function_name, arguments), bytes_target.copy_from_slice(arg.as_bytes());
"path_link" => todo!("WASI {}({:?})", function_name, arguments), memory[ptr_argv_buf + arg.len()] = 0; // C string zero termination
"path_open" => todo!("WASI {}({:?})", function_name, arguments), ptr_argv_buf += arg.len() + 1;
"path_readlink" => todo!("WASI {}({:?})", function_name, arguments), ptr_ptr_argv += 4;
"path_remove_directory" => todo!("WASI {}({:?})", function_name, arguments), }
"path_rename" => todo!("WASI {}({:?})", function_name, arguments),
"path_symlink" => todo!("WASI {}({:?})", function_name, arguments), success_code
"path_unlink_file" => todo!("WASI {}({:?})", function_name, arguments), }
"poll_oneoff" => todo!("WASI {}({:?})", function_name, arguments), "args_sizes_get" => {
"proc_exit" => todo!("WASI {}({:?})", function_name, arguments), // (i32, i32) -> i32
"proc_raise" => todo!("WASI {}({:?})", function_name, arguments),
"sched_yield" => todo!("WASI {}({:?})", function_name, arguments), // number of string arguments
"random_get" => todo!("WASI {}({:?})", function_name, arguments), let ptr_argc = arguments[0].unwrap_i32() as usize;
"sock_recv" => todo!("WASI {}({:?})", function_name, arguments), // size of string arguments buffer
"sock_send" => todo!("WASI {}({:?})", function_name, arguments), let ptr_argv_buf_size = arguments[1].unwrap_i32() as usize;
"sock_shutdown" => todo!("WASI {}({:?})", function_name, arguments),
_ => panic!("Unknown WASI function {}({:?})", function_name, arguments), let argc = self.args.len() as u32;
write_u32(memory, ptr_argc, argc);
let argv_buf_size: u32 = self.args.iter().map(|a| 1 + a.len() as u32).sum();
write_u32(memory, ptr_argv_buf_size, argv_buf_size);
success_code
}
"environ_get" => todo!("WASI {}({:?})", function_name, arguments),
"environ_sizes_get" => todo!("WASI {}({:?})", function_name, arguments),
"clock_res_get" => todo!("WASI {}({:?})", function_name, arguments),
"clock_time_get" => todo!("WASI {}({:?})", function_name, arguments),
"fd_advise" => todo!("WASI {}({:?})", function_name, arguments),
"fd_allocate" => todo!("WASI {}({:?})", function_name, arguments),
"fd_close" => todo!("WASI {}({:?})", function_name, arguments),
"fd_datasync" => todo!("WASI {}({:?})", function_name, arguments),
"fd_fdstat_get" => todo!("WASI {}({:?})", function_name, arguments),
"fd_fdstat_set_flags" => todo!("WASI {}({:?})", function_name, arguments),
"fd_fdstat_set_rights" => todo!("WASI {}({:?})", function_name, arguments),
"fd_filestat_get" => todo!("WASI {}({:?})", function_name, arguments),
"fd_filestat_set_size" => todo!("WASI {}({:?})", function_name, arguments),
"fd_filestat_set_times" => todo!("WASI {}({:?})", function_name, arguments),
"fd_pread" => todo!("WASI {}({:?})", function_name, arguments),
"fd_prestat_get" => todo!("WASI {}({:?})", function_name, arguments),
"fd_prestat_dir_name" => todo!("WASI {}({:?})", function_name, arguments),
"fd_pwrite" => todo!("WASI {}({:?})", function_name, arguments),
"fd_read" => todo!("WASI {}({:?})", function_name, arguments),
"fd_readdir" => todo!("WASI {}({:?})", function_name, arguments),
"fd_renumber" => todo!("WASI {}({:?})", function_name, arguments),
"fd_seek" => todo!("WASI {}({:?})", function_name, arguments),
"fd_sync" => todo!("WASI {}({:?})", function_name, arguments),
"fd_tell" => todo!("WASI {}({:?})", function_name, arguments),
"fd_write" => {
// file descriptor
let fd = arguments[0].unwrap_i32();
// Array of IO vectors
let ptr_iovs = arguments[1].unwrap_i32() as usize;
// Length of array
let iovs_len = arguments[2].unwrap_i32();
// Out param: number of bytes written
let ptr_nwritten = arguments[3].unwrap_i32() as usize;
let mut write_lock = match fd {
1 => Ok(io::stdout().lock()),
2 => Err(io::stderr().lock()),
_ => return Some(Value::I32(Errno::Inval as i32)),
};
let mut n_written: i32 = 0;
let mut negative_length_count = 0;
for _ in 0..iovs_len {
// https://man7.org/linux/man-pages/man2/readv.2.html
// struct iovec {
// void *iov_base; /* Starting address */
// size_t iov_len; /* Number of bytes to transfer */
// };
let iov_base = read_u32(memory, ptr_iovs) as usize;
let iov_len = read_i32(memory, ptr_iovs + 4);
if iov_len < 0 {
// I found negative-length iov's when I implemented this in JS for the web REPL (see wasi.js)
// I'm not sure why, but this solution worked, and it's the same WASI libc - there's only one.
n_written += iov_len;
negative_length_count += 1;
continue;
}
let bytes = &memory[iov_base..][..iov_len as usize];
match &mut write_lock {
Ok(stdout) => {
n_written += stdout.write(bytes).unwrap() as i32;
}
Err(stderr) => {
n_written += stderr.write(bytes).unwrap() as i32;
}
}
}
write_i32(memory, ptr_nwritten, n_written);
if negative_length_count > 0 {
// Let's see if we ever get this message. If not, we can remove this negative-length stuff.
eprintln!(
"WASI DEV INFO: found {} negative-length iovecs.",
negative_length_count
);
}
success_code
}
"path_create_directory" => todo!("WASI {}({:?})", function_name, arguments),
"path_filestat_get" => todo!("WASI {}({:?})", function_name, arguments),
"path_filestat_set_times" => todo!("WASI {}({:?})", function_name, arguments),
"path_link" => todo!("WASI {}({:?})", function_name, arguments),
"path_open" => todo!("WASI {}({:?})", function_name, arguments),
"path_readlink" => todo!("WASI {}({:?})", function_name, arguments),
"path_remove_directory" => todo!("WASI {}({:?})", function_name, arguments),
"path_rename" => todo!("WASI {}({:?})", function_name, arguments),
"path_symlink" => todo!("WASI {}({:?})", function_name, arguments),
"path_unlink_file" => todo!("WASI {}({:?})", function_name, arguments),
"poll_oneoff" => todo!("WASI {}({:?})", function_name, arguments),
"proc_exit" => todo!("WASI {}({:?})", function_name, arguments),
"proc_raise" => todo!("WASI {}({:?})", function_name, arguments),
"sched_yield" => todo!("WASI {}({:?})", function_name, arguments),
"random_get" => todo!("WASI {}({:?})", function_name, arguments),
"sock_recv" => todo!("WASI {}({:?})", function_name, arguments),
"sock_send" => todo!("WASI {}({:?})", function_name, arguments),
"sock_shutdown" => todo!("WASI {}({:?})", function_name, arguments),
_ => panic!("Unknown WASI function {}({:?})", function_name, arguments),
}
} }
} }
fn read_u32(memory: &mut [u8], addr: usize) -> u32 {
let mut bytes = [0; 4];
bytes.copy_from_slice(&memory[addr..][..4]);
u32::from_le_bytes(bytes)
}
fn read_i32(memory: &mut [u8], addr: usize) -> i32 {
let mut bytes = [0; 4];
bytes.copy_from_slice(&memory[addr..][..4]);
i32::from_le_bytes(bytes)
}
fn write_u32(memory: &mut [u8], addr: usize, value: u32) {
memory[addr..][..4].copy_from_slice(&value.to_le_bytes());
}
fn write_i32(memory: &mut [u8], addr: usize, value: i32) {
memory[addr..][..4].copy_from_slice(&value.to_le_bytes());
}
/// Error codes returned by functions.
/// Not all of these error codes are returned by the functions provided by this
/// API; some are used in higher-level library layers, and others are provided
/// merely for alignment with POSIX.
#[repr(u8)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Errno {
/// No error occurred. System call completed successfully.
Success,
/// Argument list too long.
Toobig,
/// Permission denied.
Access,
/// Address in use.
Addrinuse,
/// Address not available.
Addrnotavail,
/// Address family not supported.
Afnosupport,
/// Resource unavailable, or operation would block.
Again,
/// Connection already in progress.
Already,
/// Bad file descriptor.
Badf,
/// Bad message.
Badmsg,
/// Device or resource busy.
Busy,
/// Operation canceled.
Canceled,
/// No child processes.
Child,
/// Connection aborted.
Connaborted,
/// Connection refused.
Connrefused,
/// Connection reset.
Connreset,
/// Resource deadlock would occur.
Deadlk,
/// Destination address required.
Destaddrreq,
/// Mathematics argument out of domain of function.
Dom,
/// Reserved.
Dquot,
/// File exists.
Exist,
/// Bad address.
Fault,
/// File too large.
Fbig,
/// Host is unreachable.
Hostunreach,
/// Identifier removed.
Idrm,
/// Illegal byte sequence.
Ilseq,
/// Operation in progress.
Inprogress,
/// Interrupted function.
Intr,
/// Invalid argument.
Inval,
/// I/O error.
Io,
/// Socket is connected.
Isconn,
/// Is a directory.
Isdir,
/// Too many levels of symbolic links.
Loop,
/// File descriptor value too large.
Mfile,
/// Too many links.
Mlink,
/// Message too large.
Msgsize,
/// Reserved.
Multihop,
/// Filename too long.
Nametoolong,
/// Network is down.
Netdown,
/// Connection aborted by network.
Netreset,
/// Network unreachable.
Netunreach,
/// Too many files open in system.
Nfile,
/// No buffer space available.
Nobufs,
/// No such device.
Nodev,
/// No such file or directory.
Noent,
/// Executable file format error.
Noexec,
/// No locks available.
Nolck,
/// Reserved.
Nolink,
/// Not enough space.
Nomem,
/// No message of the desired type.
Nomsg,
/// Protocol not available.
Noprotoopt,
/// No space left on device.
Nospc,
/// Function not supported.
Nosys,
/// The socket is not connected.
Notconn,
/// Not a directory or a symbolic link to a directory.
Notdir,
/// Directory not empty.
Notempty,
/// State not recoverable.
Notrecoverable,
/// Not a socket.
Notsock,
/// Not supported, or operation not supported on socket.
Notsup,
/// Inappropriate I/O control operation.
Notty,
/// No such device or address.
Nxio,
/// Value too large to be stored in data type.
Overflow,
/// Previous owner died.
Ownerdead,
/// Operation not permitted.
Perm,
/// Broken pipe.
Pipe,
/// Protocol error.
Proto,
/// Protocol not supported.
Protonosupport,
/// Protocol wrong type for socket.
Prototype,
/// Result too large.
Range,
/// Read-only file system.
Rofs,
/// Invalid seek.
Spipe,
/// No such process.
Srch,
/// Reserved.
Stale,
/// Connection timed out.
Timedout,
/// Text file busy.
Txtbsy,
/// Cross-device link.
Xdev,
/// Extension: Capabilities insufficient.
Notcapable,
}