Merge pull request #4767 from roc-lang/wasm_interp_repl_test

Replace Wasmer with roc_wasm_interp
This commit is contained in:
Folkert de Vries 2022-12-18 20:44:58 +01:00 committed by GitHub
commit a18197347b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 531 additions and 1411 deletions

View file

@ -31,7 +31,7 @@ jobs:
run: cargo run --locked --release format --check crates/compiler/builtins/roc run: cargo run --locked --release format --check crates/compiler/builtins/roc
- name: zig wasm tests - name: zig wasm tests
run: cargo build --release -p roc_wasm_interp && cd crates/compiler/builtins/bitcode && ./run-wasm-tests.sh run: cd crates/compiler/builtins/bitcode && ./run-wasm-tests.sh
- name: regular rust tests - name: regular rust tests
run: cargo test --locked --release && sccache --show-stats run: cargo test --locked --release && sccache --show-stats
@ -54,7 +54,6 @@ jobs:
- name: run `roc test` on Dict builtins - name: run `roc test` on Dict builtins
run: cargo run --locked --release -- test crates/compiler/builtins/roc/Dict.roc && sccache --show-stats run: cargo run --locked --release -- test crates/compiler/builtins/roc/Dict.roc && sccache --show-stats
#TODO pass --locked into the script here as well, this avoids rebuilding dependencies unnecessarily
- name: wasm repl test - name: wasm repl test
run: crates/repl_test/test_wasm.sh && sccache --show-stats run: crates/repl_test/test_wasm.sh && sccache --show-stats

895
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -22,7 +22,7 @@ i386-cli-run = ["target-x86"]
editor = ["roc_editor"] editor = ["roc_editor"]
run-wasm32 = ["wasmer", "wasmer-wasi"] run-wasm32 = ["roc_wasm_interp"]
# Compiling for a different target than the current machine can cause linker errors. # Compiling for a different target than the current machine can cause linker errors.
target-arm = ["roc_build/target-arm", "roc_repl_cli/target-arm"] target-arm = ["roc_build/target-arm", "roc_repl_cli/target-arm"]
@ -65,11 +65,10 @@ roc_repl_cli = { path = "../repl_cli", optional = true }
roc_tracing = { path = "../tracing" } roc_tracing = { path = "../tracing" }
roc_intern = { path = "../compiler/intern" } roc_intern = { path = "../compiler/intern" }
roc_gen_llvm = {path = "../compiler/gen_llvm"} roc_gen_llvm = {path = "../compiler/gen_llvm"}
roc_wasm_interp = { path = "../wasm_interp", optional = true }
ven_pretty = { path = "../vendor/pretty" } ven_pretty = { path = "../vendor/pretty" }
wasmer-wasi = { version = "2.2.1", optional = true }
clap.workspace = true clap.workspace = true
const_format.workspace = true const_format.workspace = true
mimalloc.workspace = true mimalloc.workspace = true
@ -88,15 +87,8 @@ inkwell.workspace = true
[target.'cfg(not(windows))'.dependencies] [target.'cfg(not(windows))'.dependencies]
roc_repl_expect = { path = "../repl_expect" } roc_repl_expect = { path = "../repl_expect" }
# Wasmer singlepass compiler only works on x86_64.
[target.'cfg(target_arch = "x86_64")'.dependencies]
wasmer = { version = "2.2.1", optional = true, default-features = false, features = ["singlepass", "universal"] }
[target.'cfg(not(target_arch = "x86_64"))'.dependencies]
wasmer = { version = "2.2.1", optional = true, default-features = false, features = ["cranelift", "universal"] }
[dev-dependencies] [dev-dependencies]
wasmer-wasi = "2.2.1"
pretty_assertions = "1.3.0" pretty_assertions = "1.3.0"
roc_test_utils = { path = "../test_utils" } roc_test_utils = { path = "../test_utils" }
roc_utils = { path = "../utils" } roc_utils = { path = "../utils" }
@ -107,13 +99,6 @@ cli_utils = { path = "../cli_utils" }
once_cell = "1.15.0" once_cell = "1.15.0"
parking_lot = "0.12" parking_lot = "0.12"
# Wasmer singlepass compiler only works on x86_64.
[target.'cfg(target_arch = "x86_64")'.dev-dependencies]
wasmer = { version = "2.2.1", default-features = false, features = ["singlepass", "universal"] }
[target.'cfg(not(target_arch = "x86_64"))'.dev-dependencies]
wasmer = { version = "2.2.1", default-features = false, features = ["cranelift", "universal"] }
[[bench]] [[bench]]
name = "time_bench" name = "time_bench"
harness = false harness = false

View file

@ -853,7 +853,7 @@ fn roc_run<'a, I: IntoIterator<Item = &'a OsStr>>(
{ {
use std::os::unix::ffi::OsStrExt; use std::os::unix::ffi::OsStrExt;
run_with_wasmer( run_wasm(
generated_filename, generated_filename,
args.into_iter().map(|os_str| os_str.as_bytes()), args.into_iter().map(|os_str| os_str.as_bytes()),
); );
@ -861,11 +861,11 @@ fn roc_run<'a, I: IntoIterator<Item = &'a OsStr>>(
#[cfg(not(target_family = "unix"))] #[cfg(not(target_family = "unix"))]
{ {
run_with_wasmer( run_wasm(
generated_filename, generated_filename,
args.into_iter().map(|os_str| { args.into_iter().map(|os_str| {
os_str.to_str().expect( os_str.to_str().expect(
"Roc does not currently support passing non-UTF8 arguments to Wasmer.", "Roc does not currently support passing non-UTF8 arguments to Wasm.",
) )
}), }),
); );
@ -1239,38 +1239,33 @@ fn roc_run_native<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
} }
#[cfg(feature = "run-wasm32")] #[cfg(feature = "run-wasm32")]
fn run_with_wasmer<I: Iterator<Item = S>, S: AsRef<[u8]>>(wasm_path: &std::path::Path, args: I) { fn run_wasm<I: Iterator<Item = S>, S: AsRef<[u8]>>(wasm_path: &std::path::Path, args: I) {
use wasmer::{Instance, Module, Store}; use bumpalo::collections::Vec;
use roc_wasm_interp::{DefaultImportDispatcher, Instance};
let store = Store::default(); let bytes = std::fs::read(wasm_path).unwrap();
let module = Module::from_file(&store, &wasm_path).unwrap(); let arena = Bump::new();
// First, we create the `WasiEnv` let mut argv = Vec::<&[u8]>::new_in(&arena);
use wasmer_wasi::WasiState; for arg in args {
let mut wasi_env = WasiState::new("hello").args(args).finalize().unwrap(); let mut arg_copy = Vec::<u8>::new_in(&arena);
arg_copy.extend_from_slice(arg.as_ref());
// Then, we get the import object related to our WASI argv.push(arg_copy.into_bump_slice());
// and attach it to the Wasm instance.
let import_object = wasi_env.import_object(&module).unwrap();
let instance = Instance::new(&module, &import_object).unwrap();
let start = instance.exports.get_function("_start").unwrap();
use wasmer_wasi::WasiError;
match start.call(&[]) {
Ok(_) => {}
Err(e) => match e.downcast::<WasiError>() {
Ok(WasiError::Exit(0)) => {
// we run the `_start` function, so exit(0) is expected
}
other => panic!("Wasmer error: {:?}", other),
},
} }
let import_dispatcher = DefaultImportDispatcher::new(&argv);
let mut instance = Instance::from_bytes(&arena, &bytes, import_dispatcher, false).unwrap();
instance
.call_export("_start", [])
.unwrap()
.unwrap()
.expect_i32()
.unwrap();
} }
#[cfg(not(feature = "run-wasm32"))] #[cfg(not(feature = "run-wasm32"))]
fn run_with_wasmer<I: Iterator<Item = S>, S: AsRef<[u8]>>(_wasm_path: &std::path::Path, _args: I) { fn run_wasm<I: Iterator<Item = S>, S: AsRef<[u8]>>(_wasm_path: &std::path::Path, _args: I) {
println!("Running wasm files is not supported on this target."); println!("Running wasm files is not supported on this target.");
} }

View file

@ -1014,7 +1014,7 @@ mod cli_run {
let mut path = file.with_file_name(executable_filename); let mut path = file.with_file_name(executable_filename);
path.set_extension("wasm"); path.set_extension("wasm");
let stdout = crate::run_with_wasmer(&path, stdin); let stdout = crate::run_wasm(&path, stdin);
if !stdout.ends_with(expected_ending) { if !stdout.ends_with(expected_ending) {
panic!( panic!(
@ -1363,75 +1363,49 @@ mod cli_run {
} }
} }
#[allow(dead_code)] #[cfg(feature = "wasm32-cli-run")]
fn run_with_wasmer(wasm_path: &std::path::Path, stdin: &[&str]) -> String { fn run_wasm(wasm_path: &std::path::Path, stdin: &[&str]) -> String {
use std::io::Write; use bumpalo::Bump;
use wasmer::{Instance, Module, Store}; use roc_wasm_interp::{DefaultImportDispatcher, Instance, Value, WasiFile};
// std::process::Command::new("cp") let wasm_bytes = std::fs::read(wasm_path).unwrap();
// .args(&[ let arena = Bump::new();
// wasm_path.to_str().unwrap(),
// "/home/folkertdev/roc/wasm/nqueens.wasm",
// ])
// .output()
// .unwrap();
let store = Store::default(); let mut instance = {
let module = Module::from_file(&store, wasm_path).unwrap(); let mut fake_stdin = vec![];
let fake_stdout = vec![];
let fake_stderr = vec![];
for s in stdin {
fake_stdin.extend_from_slice(s.as_bytes())
}
let mut fake_stdin = wasmer_wasi::Pipe::new(); let mut dispatcher = DefaultImportDispatcher::default();
let fake_stdout = wasmer_wasi::Pipe::new(); dispatcher.wasi.files = vec![
let fake_stderr = wasmer_wasi::Pipe::new(); WasiFile::ReadOnly(fake_stdin),
WasiFile::WriteOnly(fake_stdout),
WasiFile::WriteOnly(fake_stderr),
];
for line in stdin { Instance::from_bytes(&arena, &wasm_bytes, dispatcher, false).unwrap()
write!(fake_stdin, "{}", line).unwrap(); };
}
// First, we create the `WasiEnv` let result = instance.call_export("_start", []);
use wasmer_wasi::WasiState;
let mut wasi_env = WasiState::new("hello")
.stdin(Box::new(fake_stdin))
.stdout(Box::new(fake_stdout))
.stderr(Box::new(fake_stderr))
.finalize()
.unwrap();
// Then, we get the import object related to our WASI match result {
// and attach it to the Wasm instance. Ok(Some(Value::I32(0))) => match &instance.import_dispatcher.wasi.files[1] {
let import_object = wasi_env WasiFile::WriteOnly(fake_stdout) => String::from_utf8(fake_stdout.clone())
.import_object(&module) .unwrap_or_else(|_| "Wasm test printed invalid UTF-8".into()),
.unwrap_or_else(|_| wasmer::imports!()); _ => unreachable!(),
},
let instance = Instance::new(&module, &import_object).unwrap(); Ok(Some(Value::I32(exit_code))) => {
format!("WASI app exit code {}", exit_code)
let start = instance.exports.get_function("_start").unwrap(); }
Ok(Some(val)) => {
match start.call(&[]) { format!("WASI _start returned an unexpected number type {:?}", val)
Ok(_) => read_wasi_stdout(wasi_env), }
Ok(None) => "WASI _start returned no value".into(),
Err(e) => { Err(e) => {
use wasmer_wasi::WasiError; format!("WASI error {}", e)
match e.downcast::<WasiError>() {
Ok(WasiError::Exit(0)) => {
// we run the `_start` function, so exit(0) is expected
read_wasi_stdout(wasi_env)
}
other => format!("Something went wrong running a wasm test: {:?}", other),
}
} }
} }
} }
#[allow(dead_code)]
fn read_wasi_stdout(wasi_env: wasmer_wasi::WasiEnv) -> String {
let mut state = wasi_env.state.lock().unwrap();
match state.fs.stdout_mut() {
Ok(Some(stdout)) => {
let mut buf = String::new();
stdout.read_to_string(&mut buf).unwrap();
buf
}
_ => todo!(),
}
}

View file

@ -3,9 +3,6 @@
# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
set -euxo pipefail set -euxo pipefail
# Test failures will always point at the _start function # For non-native binaries, Zig test needs a "test command" it can use
# Make sure to look at the rest of the stack trace! cargo build --locked --release -p roc_wasm_interp
# Zig will try to run the test binary it produced, but since your OS doesn't know how to
# run Wasm binaries natively, we need to provide a Wasm interpreter as a "test command".
zig test -target wasm32-wasi-musl -O ReleaseFast src/main.zig --test-cmd ../../../../target/release/roc_wasm_interp --test-cmd-bin zig test -target wasm32-wasi-musl -O ReleaseFast src/main.zig --test-cmd ../../../../target/release/roc_wasm_interp --test-cmd-bin

View file

@ -237,11 +237,11 @@ where
T: FromWasm32Memory + Wasm32Result, T: FromWasm32Memory + Wasm32Result,
{ {
let dispatcher = TestDispatcher { let dispatcher = TestDispatcher {
wasi: wasi::WasiDispatcher { args: &[] }, wasi: wasi::WasiDispatcher::default(),
}; };
let is_debug_mode = roc_debug_flags::dbg_set!(roc_debug_flags::ROC_LOG_WASM_INTERP); let is_debug_mode = roc_debug_flags::dbg_set!(roc_debug_flags::ROC_LOG_WASM_INTERP);
let mut inst = Instance::for_module(&arena, &module, dispatcher, is_debug_mode)?; let mut inst = Instance::for_module(&arena, &module, dispatcher, is_debug_mode)?;
let opt_value = inst.call_export(module, test_wrapper_name, [])?; let opt_value = inst.call_export(test_wrapper_name, [])?;
let addr_value = opt_value.ok_or("No return address from Wasm test")?; let addr_value = opt_value.ok_or("No return address from Wasm test")?;
let addr = addr_value.expect_i32().map_err(|e| format!("{:?}", e))?; let addr = addr_value.expect_i32().map_err(|e| format!("{:?}", e))?;
let output = <T as FromWasm32Memory>::decode(&inst.memory, addr as u32); let output = <T as FromWasm32Memory>::decode(&inst.memory, addr as u32);
@ -266,25 +266,21 @@ where
.map_err(|e| format!("{:?}", e))?; .map_err(|e| format!("{:?}", e))?;
let dispatcher = TestDispatcher { let dispatcher = TestDispatcher {
wasi: wasi::WasiDispatcher { args: &[] }, wasi: wasi::WasiDispatcher::default(),
}; };
let is_debug_mode = roc_debug_flags::dbg_set!(roc_debug_flags::ROC_LOG_WASM_INTERP); let is_debug_mode = roc_debug_flags::dbg_set!(roc_debug_flags::ROC_LOG_WASM_INTERP);
let mut inst = Instance::for_module(&arena, &module, dispatcher, is_debug_mode)?; let mut inst = Instance::for_module(&arena, &module, dispatcher, is_debug_mode)?;
// Allocate a vector in the test host that refcounts will be copied into // Allocate a vector in the test host that refcounts will be copied into
let mut refcount_vector_addr: i32 = inst let mut refcount_vector_addr: i32 = inst
.call_export( .call_export(INIT_REFCOUNT_NAME, [Value::I32(num_refcounts as i32)])?
&module,
INIT_REFCOUNT_NAME,
[Value::I32(num_refcounts as i32)],
)?
.ok_or_else(|| format!("No return address from {}", INIT_REFCOUNT_NAME))? .ok_or_else(|| format!("No return address from {}", INIT_REFCOUNT_NAME))?
.expect_i32() .expect_i32()
.map_err(|type_err| format!("{:?}", type_err))?; .map_err(|type_err| format!("{:?}", type_err))?;
// Run the test, ignoring the result // Run the test, ignoring the result
let _result_addr: i32 = inst let _result_addr: i32 = inst
.call_export(&module, TEST_WRAPPER_NAME, [])? .call_export(TEST_WRAPPER_NAME, [])?
.ok_or_else(|| format!("No return address from {}", TEST_WRAPPER_NAME))? .ok_or_else(|| format!("No return address from {}", TEST_WRAPPER_NAME))?
.expect_i32() .expect_i32()
.map_err(|type_err| format!("{:?}", type_err))?; .map_err(|type_err| format!("{:?}", type_err))?;

View file

@ -225,22 +225,22 @@ fn execute_wasm_module<'a>(arena: &'a Bump, orig_module: WasmModule<'a>) -> Resu
}; };
let dispatcher = TestDispatcher { let dispatcher = TestDispatcher {
wasi: wasi::WasiDispatcher { args: &[] }, wasi: wasi::WasiDispatcher::default(),
}; };
let is_debug_mode = true; let is_debug_mode = false;
let mut inst = Instance::for_module(&arena, &module, dispatcher, is_debug_mode)?; let mut inst = Instance::for_module(&arena, &module, dispatcher, is_debug_mode)?;
// In Zig, main can only return u8 or void, but our result is too wide for that. // In Zig, main can only return u8 or void, but our result is too wide for that.
// But I want to use main so that I can test that _start is created for it! // But I want to use main so that I can test that _start is created for it!
// So return void from main, and call another function to get the result. // So return void from main, and call another function to get the result.
inst.call_export(&module, "_start", [])?; inst.call_export("_start", [])?;
// FIXME: read_host_result does not actually appear as an export! // FIXME: read_host_result does not actually appear as an export!
// The interpreter has to look it up in debug info! (Apparently Wasm3 did this!) // The interpreter has to look it up in debug info! (Apparently Wasm3 did this!)
// If we change gen_wasm to export it, then it does the same for js_unused, // If we change gen_wasm to export it, then it does the same for js_unused,
// so we can't test import elimination and function reordering. // so we can't test import elimination and function reordering.
// We should to come back to this and fix it. // We should to come back to this and fix it.
inst.call_export(&module, "read_host_result", [])? inst.call_export("read_host_result", [])?
.ok_or(String::from("expected a return value"))? .ok_or(String::from("expected a return value"))?
.expect_i32() .expect_i32()
.map_err(|type_err| format!("{:?}", type_err)) .map_err(|type_err| format!("{:?}", type_err))

View file

@ -9,24 +9,15 @@ description = "Tests the roc REPL."
[build-dependencies] [build-dependencies]
roc_cli = {path = "../cli"} roc_cli = {path = "../cli"}
[dependencies]
lazy_static = "1.4.0"
[dev-dependencies] [dev-dependencies]
indoc = "1.0.7" indoc = "1.0.7"
strip-ansi-escapes = "0.1.1" strip-ansi-escapes = "0.1.1"
wasmer-wasi = "2.2.1" bumpalo.workspace = true
roc_build = { path = "../compiler/build" } roc_build = { path = "../compiler/build" }
roc_repl_cli = {path = "../repl_cli"} roc_repl_cli = {path = "../repl_cli"}
roc_test_utils = {path = "../test_utils"} roc_test_utils = {path = "../test_utils"}
roc_wasm_interp = {path = "../wasm_interp"}
# Wasmer singlepass compiler only works on x86_64.
[target.'cfg(target_arch = "x86_64")'.dev-dependencies]
wasmer = { version = "2.2.1", default-features = false, features = ["singlepass", "universal"] }
[target.'cfg(not(target_arch = "x86_64"))'.dev-dependencies]
wasmer = { version = "2.2.1", default-features = false, features = ["cranelift", "universal"] }
[features] [features]
default = ["target-aarch64", "target-x86_64", "target-wasm32"] default = ["target-aarch64", "target-x86_64", "target-wasm32"]

View file

@ -1,8 +1,3 @@
//! Tests the roc REPL.
#[allow(unused_imports)]
#[macro_use]
extern crate lazy_static;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;

View file

@ -1,289 +1,166 @@
use std::{ use bumpalo::Bump;
cell::RefCell, use roc_wasm_interp::{
fs, wasi, DefaultImportDispatcher, ImportDispatcher, Instance, Value, WasiDispatcher,
ops::{Deref, DerefMut},
path::Path,
sync::Mutex,
thread_local,
}; };
use wasmer::{
imports, ChainableNamedResolver, Function, ImportObject, Instance, Module, Store, Value,
};
use wasmer_wasi::WasiState;
const WASM_REPL_COMPILER_PATH: &str = "../../target/wasm32-wasi/release/roc_repl_wasm.wasm"; const COMPILER_BYTES: &[u8] =
include_bytes!("../../../target/wasm32-wasi/release/roc_repl_wasm.wasm");
thread_local! { struct CompilerDispatcher<'a> {
static REPL_STATE: RefCell<Option<ReplState>> = RefCell::new(None) arena: &'a Bump,
src: &'a str,
answer: String,
wasi: WasiDispatcher<'a>,
app: Option<Instance<'a, DefaultImportDispatcher<'a>>>,
result_addr: Option<i32>,
} }
// The compiler Wasm instance. impl<'a> ImportDispatcher for CompilerDispatcher<'a> {
// This takes several *seconds* to initialise, so we only want to do it once for all tests. fn dispatch(
// Every test mutates compiler memory in `unsafe` ways, so we run them sequentially using a Mutex. &mut self,
// Even if Cargo uses many threads, these tests won't go any faster. But that's fine, they're quick. module_name: &str,
lazy_static! { function_name: &str,
static ref COMPILER: Instance = init_compiler(); arguments: &[Value],
} compiler_memory: &mut [u8],
) -> Option<Value> {
static TEST_MUTEX: Mutex<()> = Mutex::new(()); let unknown = || {
panic!(
/// Load the compiler .wasm file and get it ready to execute "I could not find an implementation for import {}.{}",
/// THIS FUNCTION TAKES 4 SECONDS TO RUN module_name, function_name
fn init_compiler() -> Instance { )
let path = Path::new(WASM_REPL_COMPILER_PATH);
let wasm_module_bytes = match fs::read(&path) {
Ok(bytes) => bytes,
Err(e) => panic!("{}", format_compiler_load_error(e)),
};
println!("loaded Roc compiler bytes");
let store = Store::default();
// This is the slow line. Skipping validation checks reduces module compilation time from 5s to 4s.
// Safety: We trust rustc to produce a valid module.
let wasmer_module =
unsafe { Module::from_binary_unchecked(&store, &wasm_module_bytes).unwrap() };
// Specify the external functions the Wasm module needs to link to
// We only use WASI so that we can debug test failures more easily with println!(), dbg!(), etc.
let mut wasi_env = WasiState::new("compiler").finalize().unwrap();
let wasi_import_obj = wasi_env
.import_object(&wasmer_module)
.unwrap_or_else(|_| ImportObject::new());
let repl_import_obj = imports! {
"env" => {
"wasmer_create_app" => Function::new_native(&store, wasmer_create_app),
"wasmer_run_app" => Function::new_native(&store, wasmer_run_app),
"wasmer_get_result_and_memory" => Function::new_native(&store, wasmer_get_result_and_memory),
"wasmer_copy_input_string" => Function::new_native(&store, wasmer_copy_input_string),
"wasmer_copy_output_string" => Function::new_native(&store, wasmer_copy_output_string),
"now" => Function::new_native(&store, dummy_system_time_now),
}
};
// "Chain" the import objects together. Wasmer will look up the REPL object first, then the WASI object
let import_object = wasi_import_obj.chain_front(repl_import_obj);
println!("Instantiating Roc compiler");
// Make a fully-linked instance with its own block of memory
let inst = Instance::new(&wasmer_module, &import_object).unwrap();
println!("Instantiated Roc compiler");
inst
}
struct ReplState {
src: &'static str,
app: Option<Instance>,
result_addr: Option<u32>,
output: Option<String>,
}
fn wasmer_create_app(app_bytes_ptr: u32, app_bytes_len: u32) -> u32 {
let app: Instance = {
let memory = COMPILER.exports.get_memory("memory").unwrap();
let memory_bytes: &[u8] = unsafe { memory.data_unchecked() };
// Find the slice of bytes for the compiled Roc app
let ptr = app_bytes_ptr as usize;
let len = app_bytes_len as usize;
let app_module_bytes: &[u8] = &memory_bytes[ptr..][..len];
// Parse the bytes into a Wasmer module
let store = Store::default();
let wasmer_module = match Module::new(&store, app_module_bytes) {
Ok(m) => m,
Err(e) => {
println!("Failed to create Wasm module\n{:?}", e);
if false {
let path = std::env::temp_dir().join("roc_repl_test_invalid_app.wasm");
fs::write(&path, app_module_bytes).unwrap();
println!("Wrote invalid wasm to {:?}", path);
}
return false.into();
}
}; };
// Get the WASI imports for the app if module_name == wasi::MODULE_NAME {
let mut wasi_env = WasiState::new("app").finalize().unwrap(); self.wasi
let import_object = wasi_env .dispatch(function_name, arguments, compiler_memory)
.import_object(&wasmer_module) } else if module_name == "env" {
.unwrap_or_else(|_| imports!()); match function_name {
"test_create_app" => {
// Get some bytes from the compiler Wasm instance and create the app Wasm instance
// fn test_create_app(app_bytes_ptr: *const u8, app_bytes_len: usize) -> u32;
assert_eq!(arguments.len(), 2);
let app_bytes_ptr = arguments[0].expect_i32().unwrap() as usize;
let app_bytes_len = arguments[1].expect_i32().unwrap() as usize;
let app_bytes = &compiler_memory[app_bytes_ptr..][..app_bytes_len];
// Create an executable instance let is_debug_mode = false;
match Instance::new(&wasmer_module, &import_object) { let instance = Instance::from_bytes(
Ok(instance) => instance, self.arena,
Err(e) => { app_bytes,
println!("Failed to create Wasm instance {:?}", e); DefaultImportDispatcher::default(),
return false.into(); is_debug_mode,
} )
} .unwrap();
};
REPL_STATE.with(|f| { self.app = Some(instance);
if let Some(state) = f.borrow_mut().deref_mut() { let ok = Value::I32(true as i32);
state.app = Some(app) Some(ok)
} else { }
unreachable!() "test_run_app" => {
} // fn test_run_app() -> usize;
}); assert_eq!(arguments.len(), 0);
match &mut self.app {
return true.into(); Some(instance) => {
} let result_addr = instance
.call_export("wrapper", [])
fn wasmer_run_app() -> u32 { .unwrap()
REPL_STATE.with(|f| { .expect("No return address from wrapper")
if let Some(state) = f.borrow_mut().deref_mut() { .expect_i32()
if let Some(app) = &state.app { .unwrap();
let wrapper = app.exports.get_function("wrapper").unwrap(); self.result_addr = Some(result_addr);
let memory_size = instance.memory.len();
let result_addr: i32 = match wrapper.call(&[]) { Some(Value::I32(memory_size as i32))
Err(e) => panic!("{:?}", e), }
Ok(result) => result[0].unwrap_i32(), None => panic!("Trying to run the app but it hasn't been created"),
}; }
state.result_addr = Some(result_addr as u32); }
"test_get_result_and_memory" => {
let memory = app.exports.get_memory("memory").unwrap(); // Copy the app's entire memory buffer into the compiler's memory,
memory.size().bytes().0 as u32 // and return the location in that buffer where we can find the app result.
} else { // fn test_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize;
unreachable!() assert_eq!(arguments.len(), 1);
let buffer_alloc_addr = arguments[0].expect_i32().unwrap() as usize;
match &self.app {
Some(instance) => {
let len = instance.memory.len();
compiler_memory[buffer_alloc_addr..][..len]
.copy_from_slice(&instance.memory);
self.result_addr.map(Value::I32)
}
None => panic!("Trying to get result and memory but there is no app"),
}
}
"test_copy_input_string" => {
// Copy the Roc source code from the test into the compiler Wasm instance
// fn test_copy_input_string(src_buffer_addr: *mut u8);
assert_eq!(arguments.len(), 1);
let src_buffer_addr = arguments[0].expect_i32().unwrap() as usize;
let len = self.src.len();
compiler_memory[src_buffer_addr..][..len].copy_from_slice(self.src.as_bytes());
None
}
"test_copy_output_string" => {
// The REPL now has a string representing the answer. Make it available to the test code.
// fn test_copy_output_string(output_ptr: *const u8, output_len: usize);
assert_eq!(arguments.len(), 2);
let output_ptr = arguments[0].expect_i32().unwrap() as usize;
let output_len = arguments[1].expect_i32().unwrap() as usize;
match std::str::from_utf8(&compiler_memory[output_ptr..][..output_len]) {
Ok(answer) => {
self.answer = answer.into();
}
Err(e) => panic!("{:?}", e),
}
None
}
"now" => Some(Value::F64(0.0)),
_ => unknown(),
} }
} else { } else {
unreachable!() unknown()
} }
})
}
fn wasmer_get_result_and_memory(buffer_alloc_addr: u32) -> u32 {
REPL_STATE.with(|f| {
if let Some(state) = f.borrow().deref() {
if let Some(app) = &state.app {
let app_memory = app.exports.get_memory("memory").unwrap();
let result_addr = state.result_addr.unwrap();
let app_memory_bytes: &[u8] = unsafe { app_memory.data_unchecked() };
let buf_addr = buffer_alloc_addr as usize;
let len = app_memory_bytes.len();
let memory = COMPILER.exports.get_memory("memory").unwrap();
let compiler_memory_bytes: &mut [u8] = unsafe { memory.data_unchecked_mut() };
compiler_memory_bytes[buf_addr..][..len].copy_from_slice(app_memory_bytes);
result_addr
} else {
panic!("REPL app not found")
}
} else {
panic!("REPL state not found")
}
})
}
fn wasmer_copy_input_string(src_buffer_addr: u32) {
let src = REPL_STATE.with(|rs| {
if let Some(state) = rs.borrow_mut().deref_mut() {
std::mem::take(&mut state.src)
} else {
unreachable!()
}
});
let memory = COMPILER.exports.get_memory("memory").unwrap();
let memory_bytes: &mut [u8] = unsafe { memory.data_unchecked_mut() };
let buf_addr = src_buffer_addr as usize;
let len = src.len();
memory_bytes[buf_addr..][..len].copy_from_slice(src.as_bytes());
}
fn wasmer_copy_output_string(output_ptr: u32, output_len: u32) {
let output: String = {
let memory = COMPILER.exports.get_memory("memory").unwrap();
let memory_bytes: &[u8] = unsafe { memory.data_unchecked() };
// Find the slice of bytes for the output string
let ptr = output_ptr as usize;
let len = output_len as usize;
let output_bytes: &[u8] = &memory_bytes[ptr..][..len];
// Copy it out of the Wasm module
let copied_bytes = output_bytes.to_vec();
unsafe { String::from_utf8_unchecked(copied_bytes) }
};
REPL_STATE.with(|f| {
if let Some(state) = f.borrow_mut().deref_mut() {
state.output = Some(output)
}
})
}
fn format_compiler_load_error(e: std::io::Error) -> String {
if matches!(e.kind(), std::io::ErrorKind::NotFound) {
format!(
"\n\n {}\n\n",
[
"ROC COMPILER WASM BINARY NOT FOUND",
"Please run these tests using repl_test/run_wasm.sh!",
"It will build a .wasm binary for the compiler, and a native binary for the tests themselves",
]
.join("\n ")
)
} else {
format!("{:?}", e)
} }
} }
fn dummy_system_time_now() -> f64 { fn run(src: &'static str) -> Result<String, String> {
0.0 let arena = Bump::new();
}
fn run(src: &'static str) -> (bool, String) { let mut instance = {
println!("run"); let dispatcher = CompilerDispatcher {
REPL_STATE.with(|rs| { arena: &arena,
*rs.borrow_mut().deref_mut() = Some(ReplState {
src, src,
answer: String::new(),
wasi: WasiDispatcher::default(),
app: None, app: None,
result_addr: None, result_addr: None,
output: None, };
});
});
let ok = if let Ok(_guard) = TEST_MUTEX.lock() { let is_debug_mode = false; // logs every instruction!
let entrypoint = COMPILER Instance::from_bytes(&arena, COMPILER_BYTES, dispatcher, is_debug_mode).unwrap()
.exports
.get_function("entrypoint_from_test")
.unwrap();
let src_len = Value::I32(src.len() as i32);
let wasm_ok: i32 = entrypoint.call(&[src_len]).unwrap().deref()[0].unwrap_i32();
wasm_ok != 0
} else {
panic!(
"Failed to acquire test mutex! A previous test must have panicked while holding it, running Wasm"
)
}; };
let final_state: ReplState = REPL_STATE.with(|rs| rs.take()).unwrap(); let len = Value::I32(src.len() as i32);
let output: String = final_state.output.unwrap(); let wasm_ok: i32 = instance
.call_export("entrypoint_from_test", [len])
.unwrap()
.unwrap()
.expect_i32()
.unwrap();
let answer_str = instance.import_dispatcher.answer.to_owned();
(ok, output) if wasm_ok == 0 {
Err(answer_str)
} else {
Ok(answer_str)
}
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn expect_success(input: &'static str, expected: &str) { pub fn expect_success(input: &'static str, expected: &str) {
let (ok, output) = run(input); assert_eq!(run(input), Ok(expected.into()));
if !ok {
panic!("\n{}\n", output);
}
assert_eq!(output, expected);
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn expect_failure(input: &'static str, expected: &str) { pub fn expect_failure(input: &'static str, expected: &str) {
let (ok, output) = run(input); assert_eq!(run(input), Err(expected.into()));
assert_eq!(ok, false);
assert_eq!(output, expected);
} }

View file

@ -7,7 +7,7 @@ set -euxo pipefail
# We need to clear RUSTFLAGS for this command, as CI sets normally some flags that are specific to CPU targets. # We need to clear RUSTFLAGS for this command, as CI sets normally some flags that are specific to CPU targets.
# Tests target wasm32-wasi instead of wasm32-unknown-unknown, so that we can debug with println! and dbg! # Tests target wasm32-wasi instead of wasm32-unknown-unknown, so that we can debug with println! and dbg!
RUSTFLAGS="" cargo build --target wasm32-wasi -p roc_repl_wasm --no-default-features --features wasmer --release RUSTFLAGS="" cargo build --locked --release --target wasm32-wasi -p roc_repl_wasm --no-default-features --features wasi_test
# Build & run the test code on *native* target, not WebAssembly # Build & run the test code on *native* target, not WebAssembly
cargo test -p repl_test --features wasm -- --test-threads=1 cargo test --locked --release -p repl_test --features wasm

View file

@ -33,7 +33,7 @@ roc_target = {path = "../compiler/roc_target"}
roc_types = {path = "../compiler/types"} roc_types = {path = "../compiler/types"}
[features] [features]
wasmer = ["futures"] wasi_test = ["futures"]
# Tell wasm-pack not to run wasm-opt automatically. We run it explicitly when we need to. # Tell wasm-pack not to run wasm-opt automatically. We run it explicitly when we need to.
# (Workaround for a CI install issue with wasm-pack https://github.com/rustwasm/wasm-pack/issues/864) # (Workaround for a CI install issue with wasm-pack https://github.com/rustwasm/wasm-pack/issues/864)

View file

@ -20,7 +20,7 @@ extern "C" {
// To debug in the browser, start up the web REPL as per instructions in repl_www/README.md // To debug in the browser, start up the web REPL as per instructions in repl_www/README.md
// and sprinkle your code with console_log!("{:?}", my_value); // and sprinkle your code with console_log!("{:?}", my_value);
// (Or if you're running the unit tests in Wasmer, you can just use println! or dbg!) // (Or if you're running the unit tests with WASI, you can just use println! or dbg!)
#[macro_export] #[macro_export]
macro_rules! console_log { macro_rules! console_log {
($($t:tt)*) => (log(&format_args!($($t)*).to_string())) ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))

View file

@ -1,16 +1,16 @@
use futures::executor; use futures::executor;
extern "C" { extern "C" {
fn wasmer_create_app(app_bytes_ptr: *const u8, app_bytes_len: usize) -> u32; fn test_create_app(app_bytes_ptr: *const u8, app_bytes_len: usize) -> u32;
fn wasmer_run_app() -> usize; fn test_run_app() -> usize;
fn wasmer_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize; fn test_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize;
fn wasmer_copy_input_string(src_buffer_addr: *mut u8); fn test_copy_input_string(src_buffer_addr: *mut u8);
fn wasmer_copy_output_string(output_ptr: *const u8, output_len: usize); fn test_copy_output_string(output_ptr: *const u8, output_len: usize);
} }
/// Async wrapper to match the equivalent JS function /// Async wrapper to match the equivalent JS function
pub async fn js_create_app(wasm_module_bytes: &[u8]) -> Result<(), String> { pub async fn js_create_app(wasm_module_bytes: &[u8]) -> Result<(), String> {
let ok = unsafe { wasmer_create_app(wasm_module_bytes.as_ptr(), wasm_module_bytes.len()) } != 0; let ok = unsafe { test_create_app(wasm_module_bytes.as_ptr(), wasm_module_bytes.len()) } != 0;
if ok { if ok {
Ok(()) Ok(())
} else { } else {
@ -19,22 +19,21 @@ pub async fn js_create_app(wasm_module_bytes: &[u8]) -> Result<(), String> {
} }
pub fn js_run_app() -> usize { pub fn js_run_app() -> usize {
unsafe { wasmer_run_app() } unsafe { test_run_app() }
} }
pub fn js_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize { pub fn js_get_result_and_memory(buffer_alloc_addr: *mut u8) -> usize {
unsafe { wasmer_get_result_and_memory(buffer_alloc_addr) } unsafe { test_get_result_and_memory(buffer_alloc_addr) }
} }
/// Entrypoint for Wasmer tests /// Entrypoint for tests using WASI and a CLI interpreter
/// - Synchronous API, to avoid the need to run an async executor across the Wasm/native boundary. /// - Synchronous API, to avoid the need to run an async executor across the Wasm/native boundary.
/// (wasmer has a sync API for creating an Instance, whereas browsers don't) /// - Uses an extra callback to allocate & copy the input string (in the browser version, wasm_bindgen does this)
/// - Uses an extra callback to allocate & copy the input string (wasm_bindgen does this for JS)
#[no_mangle] #[no_mangle]
pub extern "C" fn entrypoint_from_test(src_len: usize) -> bool { pub extern "C" fn entrypoint_from_test(src_len: usize) -> bool {
let mut src_buffer = std::vec![0; src_len]; let mut src_buffer = std::vec![0; src_len];
let src = unsafe { let src = unsafe {
wasmer_copy_input_string(src_buffer.as_mut_ptr()); test_copy_input_string(src_buffer.as_mut_ptr());
String::from_utf8_unchecked(src_buffer) String::from_utf8_unchecked(src_buffer)
}; };
let result_async = crate::repl::entrypoint_from_js(src); let result_async = crate::repl::entrypoint_from_js(src);
@ -43,7 +42,7 @@ pub extern "C" fn entrypoint_from_test(src_len: usize) -> bool {
let output = result.unwrap_or_else(|s| s); let output = result.unwrap_or_else(|s| s);
unsafe { wasmer_copy_output_string(output.as_ptr(), output.len()) } unsafe { test_copy_output_string(output.as_ptr(), output.len()) }
ok ok
} }

View file

@ -6,15 +6,15 @@ mod repl;
// //
#[cfg(feature = "console_error_panic_hook")] #[cfg(feature = "console_error_panic_hook")]
extern crate console_error_panic_hook; extern crate console_error_panic_hook;
#[cfg(not(feature = "wasmer"))] #[cfg(not(feature = "wasi_test"))]
mod externs_js; mod externs_js;
#[cfg(not(feature = "wasmer"))] #[cfg(not(feature = "wasi_test"))]
pub use externs_js::{entrypoint_from_js, js_create_app, js_get_result_and_memory, js_run_app}; pub use externs_js::{entrypoint_from_js, js_create_app, js_get_result_and_memory, js_run_app};
// //
// Interface with test code outside the Wasm module // Interface with test code outside the Wasm module
// //
#[cfg(feature = "wasmer")] #[cfg(feature = "wasi_test")]
mod externs_test; mod externs_test;
#[cfg(feature = "wasmer")] #[cfg(feature = "wasi_test")]
pub use externs_test::{entrypoint_from_test, js_create_app, js_get_result_and_memory, js_run_app}; pub use externs_test::{entrypoint_from_test, js_create_app, js_get_result_and_memory, js_run_app};

View file

@ -12,7 +12,7 @@ path = "src/main.rs"
[dependencies] [dependencies]
roc_wasm_module = { path = "../wasm_module" } roc_wasm_module = { path = "../wasm_module" }
rand = "0.8.4"
bitvec.workspace = true bitvec.workspace = true
bumpalo.workspace = true bumpalo.workspace = true
clap.workspace = true clap.workspace = true

View file

@ -24,7 +24,7 @@ enum Block {
Normal { vstack: usize }, Normal { vstack: usize },
} }
#[derive(Debug)] #[derive(Debug, Clone)]
struct BranchCacheEntry { struct BranchCacheEntry {
addr: u32, addr: u32,
argument: u32, argument: u32,
@ -33,6 +33,7 @@ struct BranchCacheEntry {
#[derive(Debug)] #[derive(Debug)]
pub struct Instance<'a, I: ImportDispatcher> { pub struct Instance<'a, I: ImportDispatcher> {
module: &'a WasmModule<'a>,
/// Contents of the WebAssembly instance's memory /// Contents of the WebAssembly instance's memory
pub memory: Vec<'a, u8>, pub memory: Vec<'a, u8>,
/// Metadata for every currently-active function call /// Metadata for every currently-active function call
@ -47,10 +48,14 @@ pub struct Instance<'a, I: ImportDispatcher> {
blocks: Vec<'a, Block>, blocks: Vec<'a, Block>,
/// Outermost block depth for the currently-executing function. /// Outermost block depth for the currently-executing function.
outermost_block: u32, outermost_block: u32,
/// Cache for branching instructions /// Current function index
branch_cache: Vec<'a, BranchCacheEntry>, current_function: usize,
/// Cache for branching instructions, split into buckets for each function.
branch_cache: Vec<'a, Vec<'a, BranchCacheEntry>>,
/// Number of imports in the module
import_count: usize,
/// Import dispatcher from user code /// Import dispatcher from user code
import_dispatcher: I, pub import_dispatcher: I,
/// Temporary storage for import arguments /// Temporary storage for import arguments
import_arguments: Vec<'a, Value>, import_arguments: Vec<'a, Value>,
/// temporary storage for output using the --debug option /// temporary storage for output using the --debug option
@ -58,7 +63,8 @@ pub struct Instance<'a, I: ImportDispatcher> {
} }
impl<'a, I: ImportDispatcher> Instance<'a, I> { impl<'a, I: ImportDispatcher> Instance<'a, I> {
pub fn new<G>( #[cfg(test)]
pub(crate) fn new<G>(
arena: &'a Bump, arena: &'a Bump,
memory_pages: u32, memory_pages: u32,
program_counter: usize, program_counter: usize,
@ -70,6 +76,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
{ {
let mem_bytes = memory_pages * MemorySection::PAGE_SIZE; let mem_bytes = memory_pages * MemorySection::PAGE_SIZE;
Instance { Instance {
module: arena.alloc(WasmModule::new(arena)),
memory: Vec::from_iter_in(iter::repeat(0).take(mem_bytes as usize), arena), memory: Vec::from_iter_in(iter::repeat(0).take(mem_bytes as usize), arena),
call_stack: CallStack::new(arena), call_stack: CallStack::new(arena),
value_stack: ValueStack::new(arena), value_stack: ValueStack::new(arena),
@ -77,16 +84,29 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
program_counter, program_counter,
blocks: Vec::new_in(arena), blocks: Vec::new_in(arena),
outermost_block: 0, outermost_block: 0,
branch_cache: Vec::new_in(arena), branch_cache: bumpalo::vec![in arena; bumpalo::vec![in arena]],
current_function: 0,
import_count: 0,
import_dispatcher, import_dispatcher,
import_arguments: Vec::new_in(arena), import_arguments: Vec::new_in(arena),
debug_string: Some(String::new()), debug_string: Some(String::new()),
} }
} }
pub fn from_bytes(
arena: &'a Bump,
module_bytes: &[u8],
import_dispatcher: I,
is_debug_mode: bool,
) -> Result<Self, std::string::String> {
let module =
WasmModule::preload(arena, module_bytes, false).map_err(|e| format!("{:?}", e))?;
Self::for_module(arena, arena.alloc(module), import_dispatcher, is_debug_mode)
}
pub fn for_module( pub fn for_module(
arena: &'a Bump, arena: &'a Bump,
module: &WasmModule<'a>, module: &'a WasmModule<'a>,
import_dispatcher: I, import_dispatcher: I,
is_debug_mode: bool, is_debug_mode: bool,
) -> Result<Self, std::string::String> { ) -> Result<Self, std::string::String> {
@ -118,7 +138,15 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
None None
}; };
let import_count = module.import.imports.len();
let branch_cache = {
let num_functions = import_count + module.code.function_count as usize;
let empty_caches_iter = iter::repeat(Vec::new_in(arena)).take(num_functions);
Vec::from_iter_in(empty_caches_iter, arena)
};
Ok(Instance { Ok(Instance {
module,
memory, memory,
call_stack, call_stack,
value_stack, value_stack,
@ -126,23 +154,20 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
program_counter: usize::MAX, program_counter: usize::MAX,
blocks: Vec::new_in(arena), blocks: Vec::new_in(arena),
outermost_block: 0, outermost_block: 0,
branch_cache: Vec::new_in(arena), current_function: usize::MAX,
branch_cache,
import_count,
import_dispatcher, import_dispatcher,
import_arguments: Vec::new_in(arena), import_arguments: Vec::new_in(arena),
debug_string, debug_string,
}) })
} }
pub fn call_export<A>( pub fn call_export<A>(&mut self, fn_name: &str, arg_values: A) -> Result<Option<Value>, String>
&mut self,
module: &WasmModule<'a>,
fn_name: &str,
arg_values: A,
) -> Result<Option<Value>, String>
where where
A: IntoIterator<Item = Value>, A: IntoIterator<Item = Value>,
{ {
let arg_type_bytes = self.prepare_to_call_export(module, fn_name)?; let arg_type_bytes = self.prepare_to_call_export(self.module, fn_name)?;
for (i, (value, type_byte)) in arg_values for (i, (value, type_byte)) in arg_values
.into_iter() .into_iter()
@ -160,14 +185,14 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
self.value_stack.push(value); self.value_stack.push(value);
} }
self.call_export_help(module, arg_type_bytes) self.call_export_help(self.module, arg_type_bytes)
} }
pub fn call_export_from_cli( 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 [&'a String], arg_strings: &'a [&'a [u8]],
) -> Result<Option<Value>, String> { ) -> Result<Option<Value>, String> {
// We have two different mechanisms for handling CLI arguments! // We have two different mechanisms for handling CLI arguments!
// 1. Basic numbers: // 1. Basic numbers:
@ -183,12 +208,13 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
// Implement the "basic numbers" CLI // Implement the "basic numbers" CLI
// Check if the called Wasm function takes numeric arguments, and if so, try to parse them from the CLI. // Check if the called Wasm function takes numeric arguments, and if so, try to parse them from the CLI.
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 for (value_bytes, type_byte) in arg_strings
.iter() .iter()
.skip(1) // first string is the .wasm filename .skip(1) // first string is the .wasm filename
.zip(arg_type_bytes.iter().copied()) .zip(arg_type_bytes.iter().copied())
{ {
use ValueType::*; use ValueType::*;
let value_str = String::from_utf8_lossy(value_bytes);
let value = match ValueType::from(type_byte) { let value = match ValueType::from(type_byte) {
I32 => Value::I32(value_str.parse::<i32>().map_err(|e| e.to_string())?), I32 => Value::I32(value_str.parse::<i32>().map_err(|e| e.to_string())?),
I64 => Value::I64(value_str.parse::<i64>().map_err(|e| e.to_string())?), I64 => Value::I64(value_str.parse::<i64>().map_err(|e| e.to_string())?),
@ -206,7 +232,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
module: &'m WasmModule<'a>, module: &'m WasmModule<'a>,
fn_name: &str, fn_name: &str,
) -> Result<&'m [u8], String> { ) -> Result<&'m [u8], String> {
let fn_index = { self.current_function = {
let mut export_iter = module.export.exports.iter(); let mut export_iter = module.export.exports.iter();
export_iter export_iter
// First look up the name in exports // First look up the name in exports
@ -237,18 +263,18 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
"I couldn't find a function '{}' in this WebAssembly module", "I couldn't find a function '{}' in this WebAssembly module",
fn_name fn_name
) )
})? })? as usize
}; };
let internal_fn_index = self.current_function - self.import_count;
self.program_counter = { self.program_counter = {
let internal_fn_index = fn_index as usize - module.import.function_count();
let mut cursor = module.code.function_offsets[internal_fn_index] as usize; let mut cursor = module.code.function_offsets[internal_fn_index] as usize;
let _start_fn_byte_length = u32::parse((), &module.code.bytes, &mut cursor); let _start_fn_byte_length = u32::parse((), &module.code.bytes, &mut cursor);
cursor cursor
}; };
let arg_type_bytes = { let arg_type_bytes = {
let internal_fn_index = fn_index as usize - module.import.imports.len();
let signature_index = module.function.signatures[internal_fn_index]; let signature_index = module.function.signatures[internal_fn_index];
module.types.look_up_arg_type_bytes(signature_index) module.types.look_up_arg_type_bytes(signature_index)
}; };
@ -256,7 +282,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
if self.debug_string.is_some() { if self.debug_string.is_some() {
println!( println!(
"Calling export func[{}] '{}' at address {:#x}", "Calling export func[{}] '{}' at address {:#x}",
fn_index, self.current_function,
fn_name, fn_name,
self.program_counter + module.code.section_offset as usize self.program_counter + module.code.section_offset as usize
); );
@ -385,8 +411,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
use OpCode::*; use OpCode::*;
let addr = self.program_counter as u32; let addr = self.program_counter as u32;
let cache_result = self let cache_result = self.branch_cache[self.current_function]
.branch_cache
.iter() .iter()
.find(|entry| entry.addr == addr && entry.argument == relative_blocks_outward); .find(|entry| entry.addr == addr && entry.argument == relative_blocks_outward);
@ -412,7 +437,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
_ => {} _ => {}
} }
} }
self.branch_cache.push(BranchCacheEntry { self.branch_cache[self.current_function].push(BranchCacheEntry {
addr, addr,
argument: relative_blocks_outward, argument: relative_blocks_outward,
target: self.program_counter as u32, target: self.program_counter as u32,
@ -427,9 +452,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
fn_index: usize, fn_index: usize,
module: &WasmModule<'a>, module: &WasmModule<'a>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let n_import_fns = module.import.imports.len(); let (signature_index, opt_import) = if fn_index < self.import_count {
let (signature_index, opt_import) = if fn_index < n_import_fns {
// Imported non-Wasm function // Imported non-Wasm function
let import = &module.import.imports[fn_index]; let import = &module.import.imports[fn_index];
let sig = match import.description { let sig = match import.description {
@ -439,7 +462,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
(sig, Some(import)) (sig, Some(import))
} else { } else {
// Wasm function // Wasm function
let sig = module.function.signatures[fn_index - n_import_fns]; let sig = module.function.signatures[fn_index - self.import_count];
(sig, None) (sig, None)
}; };
@ -477,7 +500,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
} }
} else { } else {
let return_addr = self.program_counter as u32; let return_addr = self.program_counter as u32;
let internal_fn_index = fn_index - n_import_fns; let internal_fn_index = fn_index - self.import_count;
self.program_counter = module.code.function_offsets[internal_fn_index] as usize; 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;
@ -494,6 +517,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
&mut self.program_counter, &mut self.program_counter,
)?; )?;
} }
self.current_function = fn_index;
Ok(()) Ok(())
} }
@ -541,7 +565,9 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
}); });
if condition == 0 { if condition == 0 {
let addr = self.program_counter as u32; let addr = self.program_counter as u32;
let cache_result = self.branch_cache.iter().find(|entry| entry.addr == addr); let cache_result = self.branch_cache[self.current_function]
.iter()
.find(|entry| entry.addr == addr);
if let Some(entry) = cache_result { if let Some(entry) = cache_result {
self.program_counter = entry.target as usize; self.program_counter = entry.target as usize;
} else { } else {
@ -572,7 +598,7 @@ impl<'a, I: ImportDispatcher> Instance<'a, I> {
_ => {} _ => {}
} }
} }
self.branch_cache.push(BranchCacheEntry { self.branch_cache[self.current_function].push(BranchCacheEntry {
addr, addr,
argument: 0, argument: 0,
target: self.program_counter as u32, target: self.program_counter as u32,

View file

@ -6,9 +6,10 @@ pub mod wasi;
// Main external interface // Main external interface
pub use instance::Instance; pub use instance::Instance;
pub use wasi::WasiDispatcher; pub use wasi::{WasiDispatcher, WasiFile};
use roc_wasm_module::{Value, ValueType, WasmModule}; pub use roc_wasm_module::Value;
use roc_wasm_module::{ValueType, WasmModule};
use value_stack::ValueStack; use value_stack::ValueStack;
pub trait ImportDispatcher { pub trait ImportDispatcher {
@ -22,18 +23,22 @@ pub trait ImportDispatcher {
) -> Option<Value>; ) -> Option<Value>;
} }
pub const DEFAULT_IMPORTS: DefaultImportDispatcher = DefaultImportDispatcher { impl Default for DefaultImportDispatcher<'_> {
wasi: WasiDispatcher { args: &[] }, fn default() -> Self {
}; DefaultImportDispatcher {
wasi: WasiDispatcher::new(&[]),
}
}
}
pub struct DefaultImportDispatcher<'a> { pub struct DefaultImportDispatcher<'a> {
wasi: WasiDispatcher<'a>, pub wasi: WasiDispatcher<'a>,
} }
impl<'a> DefaultImportDispatcher<'a> { impl<'a> DefaultImportDispatcher<'a> {
pub fn new(args: &'a [&'a String]) -> Self { pub fn new(args: &'a [&'a [u8]]) -> Self {
DefaultImportDispatcher { DefaultImportDispatcher {
wasi: WasiDispatcher { args }, wasi: WasiDispatcher::new(args),
} }
} }
} }

View file

@ -65,7 +65,10 @@ fn main() -> io::Result<()> {
let start_arg_strings = matches.get_many::<String>(ARGS_FOR_APP).unwrap_or_default(); let start_arg_strings = matches.get_many::<String>(ARGS_FOR_APP).unwrap_or_default();
let wasm_path = matches.get_one::<String>(WASM_FILE).unwrap(); let wasm_path = matches.get_one::<String>(WASM_FILE).unwrap();
// WASI expects the .wasm file to be argv[0] // WASI expects the .wasm file to be argv[0]
let wasi_argv = Vec::from_iter_in(once(wasm_path).chain(start_arg_strings), &arena); let wasi_argv_iter = once(wasm_path)
.chain(start_arg_strings)
.map(|s| s.as_bytes());
let wasi_argv = Vec::from_iter_in(wasi_argv_iter, &arena);
// Load the WebAssembly binary file // Load the WebAssembly binary file

View file

@ -8,7 +8,7 @@ mod test_i32;
mod test_i64; mod test_i64;
mod test_mem; mod test_mem;
use crate::{DefaultImportDispatcher, Instance, DEFAULT_IMPORTS}; use crate::{DefaultImportDispatcher, Instance};
use bumpalo::{collections::Vec, Bump}; use bumpalo::{collections::Vec, Bump};
use roc_wasm_module::{ use roc_wasm_module::{
opcodes::OpCode, Export, ExportType, SerialBuffer, Signature, Value, ValueType, WasmModule, opcodes::OpCode, Export, ExportType, SerialBuffer, Signature, Value, ValueType, WasmModule,
@ -18,7 +18,13 @@ pub fn default_state(arena: &Bump) -> Instance<DefaultImportDispatcher> {
let pages = 1; let pages = 1;
let program_counter = 0; let program_counter = 0;
let globals = []; let globals = [];
Instance::new(arena, pages, program_counter, globals, DEFAULT_IMPORTS) Instance::new(
arena,
pages,
program_counter,
globals,
DefaultImportDispatcher::default(),
)
} }
pub fn const_value(buf: &mut Vec<'_, u8>, value: Value) { pub fn const_value(buf: &mut Vec<'_, u8>, value: Value) {
@ -85,9 +91,10 @@ where
std::fs::write(&filename, outfile_buf).unwrap(); std::fs::write(&filename, outfile_buf).unwrap();
} }
let mut inst = Instance::for_module(&arena, &module, DEFAULT_IMPORTS, true).unwrap(); let mut inst =
Instance::for_module(&arena, &module, DefaultImportDispatcher::default(), false).unwrap();
let return_val = inst.call_export(&module, "test", []).unwrap().unwrap(); let return_val = inst.call_export("test", []).unwrap().unwrap();
assert_eq!(return_val, expected); assert_eq!(return_val, expected);
} }

View file

@ -1,7 +1,7 @@
#![cfg(test)] #![cfg(test)]
use super::{const_value, create_exported_function_no_locals, default_state}; use super::{const_value, create_exported_function_no_locals, default_state};
use crate::{instance::Action, ImportDispatcher, Instance, ValueStack, DEFAULT_IMPORTS}; use crate::{instance::Action, DefaultImportDispatcher, ImportDispatcher, Instance, ValueStack};
use bumpalo::{collections::Vec, Bump}; use bumpalo::{collections::Vec, Bump};
use roc_wasm_module::sections::{Import, ImportDesc}; use roc_wasm_module::sections::{Import, ImportDesc};
use roc_wasm_module::{ use roc_wasm_module::{
@ -554,10 +554,7 @@ fn test_call_import() {
let mut inst = Instance::for_module(&arena, &module, import_dispatcher, true).unwrap(); let mut inst = Instance::for_module(&arena, &module, import_dispatcher, true).unwrap();
let return_val = inst let return_val = inst.call_export(start_fn_name, []).unwrap().unwrap();
.call_export(&module, start_fn_name, [])
.unwrap()
.unwrap();
assert_eq!(return_val, Value::I32(234)); assert_eq!(return_val, Value::I32(234));
} }
@ -623,12 +620,10 @@ fn test_call_return_no_args() {
println!("Wrote to {}", filename); println!("Wrote to {}", filename);
} }
let mut inst = Instance::for_module(&arena, &module, DEFAULT_IMPORTS, true).unwrap(); let mut inst =
Instance::for_module(&arena, &module, DefaultImportDispatcher::default(), true).unwrap();
let return_val = inst let return_val = inst.call_export(start_fn_name, []).unwrap().unwrap();
.call_export(&module, start_fn_name, [])
.unwrap()
.unwrap();
assert_eq!(return_val, Value::I32(42)); assert_eq!(return_val, Value::I32(42));
} }
@ -762,10 +757,14 @@ fn test_call_indirect_help(table_index: u32, elem_index: u32) -> Value {
.unwrap(); .unwrap();
} }
let mut inst = Instance::for_module(&arena, &module, DEFAULT_IMPORTS, is_debug_mode).unwrap(); let mut inst = Instance::for_module(
inst.call_export(&module, start_fn_name, []) &arena,
.unwrap() &module,
.unwrap() DefaultImportDispatcher::default(),
is_debug_mode,
)
.unwrap();
inst.call_export(start_fn_name, []).unwrap().unwrap()
} }
// #[test] // #[test]

View file

@ -1,5 +1,5 @@
use super::create_exported_function_no_locals; use super::create_exported_function_no_locals;
use crate::{Instance, DEFAULT_IMPORTS}; use crate::{DefaultImportDispatcher, Instance};
use bumpalo::{collections::Vec, Bump}; use bumpalo::{collections::Vec, Bump};
use roc_wasm_module::{ use roc_wasm_module::{
opcodes::OpCode, opcodes::OpCode,
@ -18,7 +18,7 @@ fn test_currentmemory() {
module.code.bytes.push(OpCode::CURRENTMEMORY as u8); module.code.bytes.push(OpCode::CURRENTMEMORY as u8);
module.code.bytes.encode_i32(0); module.code.bytes.encode_i32(0);
let mut state = Instance::new(&arena, pages, pc, [], DEFAULT_IMPORTS); let mut state = Instance::new(&arena, pages, pc, [], DefaultImportDispatcher::default());
state.execute_next_instruction(&module).unwrap(); state.execute_next_instruction(&module).unwrap();
assert_eq!(state.value_stack.pop(), Value::I32(3)) assert_eq!(state.value_stack.pop(), Value::I32(3))
} }
@ -37,7 +37,13 @@ fn test_growmemory() {
module.code.bytes.push(OpCode::GROWMEMORY as u8); module.code.bytes.push(OpCode::GROWMEMORY as u8);
module.code.bytes.encode_i32(0); module.code.bytes.encode_i32(0);
let mut state = Instance::new(&arena, existing_pages, pc, [], DEFAULT_IMPORTS); let mut state = Instance::new(
&arena,
existing_pages,
pc,
[],
DefaultImportDispatcher::default(),
);
state.execute_next_instruction(&module).unwrap(); state.execute_next_instruction(&module).unwrap();
state.execute_next_instruction(&module).unwrap(); state.execute_next_instruction(&module).unwrap();
assert_eq!(state.memory.len(), 5 * MemorySection::PAGE_SIZE as usize); assert_eq!(state.memory.len(), 5 * MemorySection::PAGE_SIZE as usize);
@ -79,10 +85,14 @@ fn test_load(load_op: OpCode, ty: ValueType, data: &[u8], addr: u32, offset: u32
std::fs::write("/tmp/roc/interp_load_test.wasm", outfile_buf).unwrap(); std::fs::write("/tmp/roc/interp_load_test.wasm", outfile_buf).unwrap();
} }
let mut inst = Instance::for_module(&arena, &module, DEFAULT_IMPORTS, is_debug_mode).unwrap(); let mut inst = Instance::for_module(
inst.call_export(&module, start_fn_name, []) &arena,
.unwrap() &module,
.unwrap() DefaultImportDispatcher::default(),
is_debug_mode,
)
.unwrap();
inst.call_export(start_fn_name, []).unwrap().unwrap()
} }
#[test] #[test]
@ -233,7 +243,7 @@ fn test_i64load32u() {
fn test_store<'a>( fn test_store<'a>(
arena: &'a Bump, arena: &'a Bump,
module: &mut WasmModule<'a>, module: &'a mut WasmModule<'a>,
addr: u32, addr: u32,
store_op: OpCode, store_op: OpCode,
offset: u32, offset: u32,
@ -276,8 +286,14 @@ fn test_store<'a>(
buf.append_u8(OpCode::END as u8); buf.append_u8(OpCode::END as u8);
}); });
let mut inst = Instance::for_module(arena, module, DEFAULT_IMPORTS, is_debug_mode).unwrap(); let mut inst = Instance::for_module(
inst.call_export(module, start_fn_name, []).unwrap(); arena,
module,
DefaultImportDispatcher::default(),
is_debug_mode,
)
.unwrap();
inst.call_export(start_fn_name, []).unwrap();
inst.memory inst.memory
} }
@ -285,13 +301,13 @@ fn test_store<'a>(
#[test] #[test]
fn test_i32store() { fn test_i32store() {
let arena = Bump::new(); let arena = Bump::new();
let mut module = WasmModule::new(&arena); let module = arena.alloc(WasmModule::new(&arena));
let addr: u32 = 0x11; let addr: u32 = 0x11;
let store_op = OpCode::I32STORE; let store_op = OpCode::I32STORE;
let offset = 1; let offset = 1;
let value = Value::I32(0x12345678); let value = Value::I32(0x12345678);
let memory = test_store(&arena, &mut module, addr, store_op, offset, value); let memory = test_store(&arena, module, addr, store_op, offset, value);
let index = (addr + offset) as usize; let index = (addr + offset) as usize;
assert_eq!(&memory[index..][..4], &[0x78, 0x56, 0x34, 0x12]); assert_eq!(&memory[index..][..4], &[0x78, 0x56, 0x34, 0x12]);
@ -300,13 +316,13 @@ fn test_i32store() {
#[test] #[test]
fn test_i64store() { fn test_i64store() {
let arena = Bump::new(); let arena = Bump::new();
let mut module = WasmModule::new(&arena); let module = arena.alloc(WasmModule::new(&arena));
let addr: u32 = 0x11; let addr: u32 = 0x11;
let store_op = OpCode::I64STORE; let store_op = OpCode::I64STORE;
let offset = 1; let offset = 1;
let value = Value::I64(0x123456789abcdef0); let value = Value::I64(0x123456789abcdef0);
let memory = test_store(&arena, &mut module, addr, store_op, offset, value); let memory = test_store(&arena, module, addr, store_op, offset, value);
let index = (addr + offset) as usize; let index = (addr + offset) as usize;
assert_eq!( assert_eq!(
@ -318,14 +334,14 @@ fn test_i64store() {
#[test] #[test]
fn test_f32store() { fn test_f32store() {
let arena = Bump::new(); let arena = Bump::new();
let mut module = WasmModule::new(&arena); let module = arena.alloc(WasmModule::new(&arena));
let addr: u32 = 0x11; let addr: u32 = 0x11;
let store_op = OpCode::F32STORE; let store_op = OpCode::F32STORE;
let offset = 1; let offset = 1;
let inner: f32 = 1.23456; let inner: f32 = 1.23456;
let value = Value::F32(inner); let value = Value::F32(inner);
let memory = test_store(&arena, &mut module, addr, store_op, offset, value); let memory = test_store(&arena, module, addr, store_op, offset, value);
let index = (addr + offset) as usize; let index = (addr + offset) as usize;
assert_eq!(&memory[index..][..4], &inner.to_le_bytes()); assert_eq!(&memory[index..][..4], &inner.to_le_bytes());
@ -334,14 +350,14 @@ fn test_f32store() {
#[test] #[test]
fn test_f64store() { fn test_f64store() {
let arena = Bump::new(); let arena = Bump::new();
let mut module = WasmModule::new(&arena); let module = arena.alloc(WasmModule::new(&arena));
let addr: u32 = 0x11; let addr: u32 = 0x11;
let store_op = OpCode::F64STORE; let store_op = OpCode::F64STORE;
let offset = 1; let offset = 1;
let inner: f64 = 1.23456; let inner: f64 = 1.23456;
let value = Value::F64(inner); let value = Value::F64(inner);
let memory = test_store(&arena, &mut module, addr, store_op, offset, value); let memory = test_store(&arena, module, addr, store_op, offset, value);
let index = (addr + offset) as usize; let index = (addr + offset) as usize;
assert_eq!(&memory[index..][..8], &inner.to_le_bytes()); assert_eq!(&memory[index..][..8], &inner.to_le_bytes());
@ -350,13 +366,13 @@ fn test_f64store() {
#[test] #[test]
fn test_i32store8() { fn test_i32store8() {
let arena = Bump::new(); let arena = Bump::new();
let mut module = WasmModule::new(&arena); let module = arena.alloc(WasmModule::new(&arena));
let addr: u32 = 0x11; let addr: u32 = 0x11;
let store_op = OpCode::I32STORE8; let store_op = OpCode::I32STORE8;
let offset = 1; let offset = 1;
let value = Value::I32(0x12345678); let value = Value::I32(0x12345678);
let memory = test_store(&arena, &mut module, addr, store_op, offset, value); let memory = test_store(&arena, module, addr, store_op, offset, value);
let index = (addr + offset) as usize; let index = (addr + offset) as usize;
assert_eq!(&memory[index..][..4], &[0x78, 0x00, 0x00, 0x00]); assert_eq!(&memory[index..][..4], &[0x78, 0x00, 0x00, 0x00]);
@ -365,13 +381,13 @@ fn test_i32store8() {
#[test] #[test]
fn test_i32store16() { fn test_i32store16() {
let arena = Bump::new(); let arena = Bump::new();
let mut module = WasmModule::new(&arena); let module = arena.alloc(WasmModule::new(&arena));
let addr: u32 = 0x11; let addr: u32 = 0x11;
let store_op = OpCode::I32STORE16; let store_op = OpCode::I32STORE16;
let offset = 1; let offset = 1;
let value = Value::I32(0x12345678); let value = Value::I32(0x12345678);
let memory = test_store(&arena, &mut module, addr, store_op, offset, value); let memory = test_store(&arena, module, addr, store_op, offset, value);
let index = (addr + offset) as usize; let index = (addr + offset) as usize;
assert_eq!(&memory[index..][..4], &[0x78, 0x56, 0x00, 0x00]); assert_eq!(&memory[index..][..4], &[0x78, 0x56, 0x00, 0x00]);
@ -380,13 +396,13 @@ fn test_i32store16() {
#[test] #[test]
fn test_i64store8() { fn test_i64store8() {
let arena = Bump::new(); let arena = Bump::new();
let mut module = WasmModule::new(&arena); let module = arena.alloc(WasmModule::new(&arena));
let addr: u32 = 0x11; let addr: u32 = 0x11;
let store_op = OpCode::I64STORE8; let store_op = OpCode::I64STORE8;
let offset = 1; let offset = 1;
let value = Value::I64(0x123456789abcdef0); let value = Value::I64(0x123456789abcdef0);
let memory = test_store(&arena, &mut module, addr, store_op, offset, value); let memory = test_store(&arena, module, addr, store_op, offset, value);
let index = (addr + offset) as usize; let index = (addr + offset) as usize;
assert_eq!( assert_eq!(
@ -398,13 +414,13 @@ fn test_i64store8() {
#[test] #[test]
fn test_i64store16() { fn test_i64store16() {
let arena = Bump::new(); let arena = Bump::new();
let mut module = WasmModule::new(&arena); let module = arena.alloc(WasmModule::new(&arena));
let addr: u32 = 0x11; let addr: u32 = 0x11;
let store_op = OpCode::I64STORE16; let store_op = OpCode::I64STORE16;
let offset = 1; let offset = 1;
let value = Value::I64(0x123456789abcdef0); let value = Value::I64(0x123456789abcdef0);
let memory = test_store(&arena, &mut module, addr, store_op, offset, value); let memory = test_store(&arena, module, addr, store_op, offset, value);
let index = (addr + offset) as usize; let index = (addr + offset) as usize;
assert_eq!( assert_eq!(
@ -416,13 +432,13 @@ fn test_i64store16() {
#[test] #[test]
fn test_i64store32() { fn test_i64store32() {
let arena = Bump::new(); let arena = Bump::new();
let mut module = WasmModule::new(&arena); let module = arena.alloc(WasmModule::new(&arena));
let addr: u32 = 0x11; let addr: u32 = 0x11;
let store_op = OpCode::I64STORE32; let store_op = OpCode::I64STORE32;
let offset = 1; let offset = 1;
let value = Value::I64(0x123456789abcdef0); let value = Value::I64(0x123456789abcdef0);
let memory = test_store(&arena, &mut module, addr, store_op, offset, value); let memory = test_store(&arena, module, addr, store_op, offset, value);
let index = (addr + offset) as usize; let index = (addr + offset) as usize;
assert_eq!( assert_eq!(

View file

@ -1,11 +1,33 @@
use rand::prelude::*;
use roc_wasm_module::Value; use roc_wasm_module::Value;
use std::io::{self, Write}; use std::io::{self, Read, StderrLock, StdoutLock, Write};
use std::process::exit; use std::process::exit;
pub const MODULE_NAME: &str = "wasi_snapshot_preview1"; pub const MODULE_NAME: &str = "wasi_snapshot_preview1";
pub struct WasiDispatcher<'a> { pub struct WasiDispatcher<'a> {
pub args: &'a [&'a String], pub args: &'a [&'a [u8]],
pub rng: ThreadRng,
pub files: Vec<WasiFile>,
}
impl Default for WasiDispatcher<'_> {
fn default() -> Self {
WasiDispatcher::new(&[])
}
}
pub enum WasiFile {
ReadOnly(Vec<u8>),
WriteOnly(Vec<u8>),
ReadWrite(Vec<u8>),
HostSystemFile,
}
enum WriteLock<'a> {
StdOut(StdoutLock<'a>),
Stderr(StderrLock<'a>),
RegularFile(&'a mut Vec<u8>),
} }
/// Implementation of WASI syscalls /// Implementation of WASI syscalls
@ -13,8 +35,16 @@ pub struct WasiDispatcher<'a> {
/// https://github.com/wasmerio/wasmer/blob/ef8d2f651ed29b4b06fdc2070eb8189922c54d82/lib/wasi/src/syscalls/mod.rs /// https://github.com/wasmerio/wasmer/blob/ef8d2f651ed29b4b06fdc2070eb8189922c54d82/lib/wasi/src/syscalls/mod.rs
/// https://github.com/wasm3/wasm3/blob/045040a97345e636b8be4f3086e6db59cdcc785f/source/extra/wasi_core.h /// https://github.com/wasm3/wasm3/blob/045040a97345e636b8be4f3086e6db59cdcc785f/source/extra/wasi_core.h
impl<'a> WasiDispatcher<'a> { impl<'a> WasiDispatcher<'a> {
pub fn new(args: &'a [&'a String]) -> Self { pub fn new(args: &'a [&'a [u8]]) -> Self {
WasiDispatcher { args } WasiDispatcher {
args,
rng: thread_rng(),
files: vec![
WasiFile::HostSystemFile,
WasiFile::HostSystemFile,
WasiFile::HostSystemFile,
],
}
} }
pub fn dispatch( pub fn dispatch(
@ -35,7 +65,7 @@ impl<'a> WasiDispatcher<'a> {
for arg in self.args { for arg in self.args {
write_u32(memory, ptr_ptr_argv, ptr_argv_buf as u32); write_u32(memory, ptr_ptr_argv, ptr_argv_buf as u32);
let bytes_target = &mut memory[ptr_argv_buf..][..arg.len()]; let bytes_target = &mut memory[ptr_argv_buf..][..arg.len()];
bytes_target.copy_from_slice(arg.as_bytes()); bytes_target.copy_from_slice(arg);
memory[ptr_argv_buf + arg.len()] = 0; // C string zero termination memory[ptr_argv_buf + arg.len()] = 0; // C string zero termination
ptr_argv_buf += arg.len() + 1; ptr_argv_buf += arg.len() + 1;
ptr_ptr_argv += 4; ptr_ptr_argv += 4;
@ -61,8 +91,8 @@ impl<'a> WasiDispatcher<'a> {
} }
"environ_get" => todo!("WASI {}({:?})", function_name, arguments), "environ_get" => todo!("WASI {}({:?})", function_name, arguments),
"environ_sizes_get" => todo!("WASI {}({:?})", function_name, arguments), "environ_sizes_get" => todo!("WASI {}({:?})", function_name, arguments),
"clock_res_get" => success_code, // this dummy implementation seems to be good enough "clock_res_get" => success_code, // this dummy implementation seems to be good enough for some functions
"clock_time_get" => success_code, // this dummy implementation seems to be good enough "clock_time_get" => success_code,
"fd_advise" => todo!("WASI {}({:?})", function_name, arguments), "fd_advise" => todo!("WASI {}({:?})", function_name, arguments),
"fd_allocate" => todo!("WASI {}({:?})", function_name, arguments), "fd_allocate" => todo!("WASI {}({:?})", function_name, arguments),
"fd_close" => todo!("WASI {}({:?})", function_name, arguments), "fd_close" => todo!("WASI {}({:?})", function_name, arguments),
@ -74,18 +104,90 @@ impl<'a> WasiDispatcher<'a> {
"fd_filestat_set_size" => todo!("WASI {}({:?})", function_name, arguments), "fd_filestat_set_size" => todo!("WASI {}({:?})", function_name, arguments),
"fd_filestat_set_times" => todo!("WASI {}({:?})", function_name, arguments), "fd_filestat_set_times" => todo!("WASI {}({:?})", function_name, arguments),
"fd_pread" => todo!("WASI {}({:?})", function_name, arguments), "fd_pread" => todo!("WASI {}({:?})", function_name, arguments),
"fd_prestat_get" => todo!("WASI {}({:?})", function_name, arguments), "fd_prestat_get" => {
"fd_prestat_dir_name" => todo!("WASI {}({:?})", function_name, arguments), // The preopened file descriptor to query
let fd = arguments[0].expect_i32().unwrap() as usize;
// ptr_buf: Where the metadata will be written
// preopen type: 4 bytes, where 0=dir is the only one supported, it seems
// preopen name length: 4 bytes
let ptr_buf = arguments[1].expect_i32().unwrap() as usize;
memory[ptr_buf..][..8].copy_from_slice(&0u64.to_le_bytes());
if fd < self.files.len() {
success_code
} else {
println!("WASI warning: file descriptor {} does not exist", fd);
Some(Value::I32(Errno::Badf as i32))
}
}
"fd_prestat_dir_name" => {
// We're not giving names to any of our files so just return success
success_code
}
"fd_pwrite" => todo!("WASI {}({:?})", function_name, arguments), "fd_pwrite" => todo!("WASI {}({:?})", function_name, arguments),
"fd_read" => todo!("WASI {}({:?})", function_name, arguments), "fd_read" => {
use WasiFile::*;
// file descriptor
let fd = arguments[0].expect_i32().unwrap() as usize;
// Array of IO vectors
let ptr_iovs = arguments[1].expect_i32().unwrap() as usize;
// Length of array
let iovs_len = arguments[2].expect_i32().unwrap();
// Out param: number of bytes read
let ptr_nread = arguments[3].expect_i32().unwrap() as usize;
// 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 mut n_read: usize = 0;
match self.files.get(fd) {
Some(ReadOnly(content) | ReadWrite(content)) => {
for _ in 0..iovs_len {
let iov_base = read_u32(memory, ptr_iovs) as usize;
let iov_len = read_i32(memory, ptr_iovs + 4) as usize;
let remaining = content.len() - n_read;
let len = remaining.min(iov_len);
if len == 0 {
break;
}
memory[iov_base..][..len].copy_from_slice(&content[n_read..][..len]);
n_read += len;
}
}
Some(HostSystemFile) if fd == 0 => {
let mut stdin = io::stdin();
for _ in 0..iovs_len {
let iov_base = read_u32(memory, ptr_iovs) as usize;
let iov_len = read_i32(memory, ptr_iovs + 4) as usize;
match stdin.read(&mut memory[iov_base..][..iov_len]) {
Ok(n) => {
n_read += n;
}
Err(_) => {
break;
}
}
}
}
_ => return Some(Value::I32(Errno::Badf as i32)),
};
memory[ptr_nread..][..4].copy_from_slice(&(n_read as u32).to_le_bytes());
success_code
}
"fd_readdir" => todo!("WASI {}({:?})", function_name, arguments), "fd_readdir" => todo!("WASI {}({:?})", function_name, arguments),
"fd_renumber" => todo!("WASI {}({:?})", function_name, arguments), "fd_renumber" => todo!("WASI {}({:?})", function_name, arguments),
"fd_seek" => todo!("WASI {}({:?})", function_name, arguments), "fd_seek" => todo!("WASI {}({:?})", function_name, arguments),
"fd_sync" => todo!("WASI {}({:?})", function_name, arguments), "fd_sync" => todo!("WASI {}({:?})", function_name, arguments),
"fd_tell" => todo!("WASI {}({:?})", function_name, arguments), "fd_tell" => todo!("WASI {}({:?})", function_name, arguments),
"fd_write" => { "fd_write" => {
use WasiFile::*;
// file descriptor // file descriptor
let fd = arguments[0].expect_i32().unwrap(); let fd = arguments[0].expect_i32().unwrap() as usize;
// Array of IO vectors // Array of IO vectors
let ptr_iovs = arguments[1].expect_i32().unwrap() as usize; let ptr_iovs = arguments[1].expect_i32().unwrap() as usize;
// Length of array // Length of array
@ -93,10 +195,18 @@ impl<'a> WasiDispatcher<'a> {
// Out param: number of bytes written // Out param: number of bytes written
let ptr_nwritten = arguments[3].expect_i32().unwrap() as usize; let ptr_nwritten = arguments[3].expect_i32().unwrap() as usize;
let mut write_lock = match fd { // Grab a lock for stdout/stderr before the loop rather than re-acquiring over and over.
1 => Ok(io::stdout().lock()), // Not really necessary for other files, but it's easier to use the same structure.
2 => Err(io::stderr().lock()), let mut write_lock = match self.files.get_mut(fd) {
_ => return Some(Value::I32(Errno::Inval as i32)), Some(HostSystemFile) => match fd {
1 => WriteLock::StdOut(io::stdout().lock()),
2 => WriteLock::Stderr(io::stderr().lock()),
_ => return Some(Value::I32(Errno::Inval as i32)),
},
Some(WriteOnly(content) | ReadWrite(content)) => {
WriteLock::RegularFile(content)
}
_ => return Some(Value::I32(Errno::Badf as i32)),
}; };
let mut n_written: i32 = 0; let mut n_written: i32 = 0;
@ -119,12 +229,16 @@ impl<'a> WasiDispatcher<'a> {
let bytes = &memory[iov_base..][..iov_len as usize]; let bytes = &memory[iov_base..][..iov_len as usize];
match &mut write_lock { match &mut write_lock {
Ok(stdout) => { WriteLock::StdOut(stdout) => {
n_written += stdout.write(bytes).unwrap() as i32; n_written += stdout.write(bytes).unwrap() as i32;
} }
Err(stderr) => { WriteLock::Stderr(stderr) => {
n_written += stderr.write(bytes).unwrap() as i32; n_written += stderr.write(bytes).unwrap() as i32;
} }
WriteLock::RegularFile(content) => {
content.extend_from_slice(bytes);
n_written += bytes.len() as i32;
}
} }
} }
@ -156,7 +270,16 @@ impl<'a> WasiDispatcher<'a> {
} }
"proc_raise" => todo!("WASI {}({:?})", function_name, arguments), "proc_raise" => todo!("WASI {}({:?})", function_name, arguments),
"sched_yield" => todo!("WASI {}({:?})", function_name, arguments), "sched_yield" => todo!("WASI {}({:?})", function_name, arguments),
"random_get" => todo!("WASI {}({:?})", function_name, arguments), "random_get" => {
// A pointer to a buffer where the random bytes will be written
let ptr_buf = arguments[0].expect_i32().unwrap() as usize;
// The number of bytes that will be written
let buf_len = arguments[1].expect_i32().unwrap() as usize;
for i in 0..buf_len {
memory[ptr_buf + i] = self.rng.gen();
}
success_code
}
"sock_recv" => todo!("WASI {}({:?})", function_name, arguments), "sock_recv" => todo!("WASI {}({:?})", function_name, arguments),
"sock_send" => todo!("WASI {}({:?})", function_name, arguments), "sock_send" => todo!("WASI {}({:?})", function_name, arguments),
"sock_shutdown" => todo!("WASI {}({:?})", function_name, arguments), "sock_shutdown" => todo!("WASI {}({:?})", function_name, arguments),