Skip to content

Commit 22d93bc

Browse files
feat: initial wasi p1 implementation
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent 741955a commit 22d93bc

30 files changed

Lines changed: 2970 additions & 46 deletions

‎Cargo.lock‎

Lines changed: 291 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ publish = false
88
[workspace]
99
resolver = "3"
1010
members = ["crates/*"]
11-
default-members = [".", "crates/parser", "crates/tinywasm", "crates/types"]
11+
default-members = [".", "crates/parser", "crates/tinywasm", "crates/types", "crates/wasi"]
1212

1313
[workspace.package]
1414
version = "0.11.0-pre.0"
@@ -23,6 +23,7 @@ categories = ["compilers", "embedded", "no-std", "virtualization", "wasm"]
2323
tinywasm = { path = "crates/tinywasm", version = "0.11.0-pre.0", default-features = false }
2424
tinywasm-parser = { path = "crates/parser", version = "0.11.0-pre.0", default-features = false }
2525
tinywasm-types = { path = "crates/types", version = "0.11.0-pre.0", default-features = false }
26+
tinywasm-wasi = { path = "crates/wasi", version = "0.11.0-pre.0" }
2627

2728
anyhow = "1.0"
2829
log = "0.4"
@@ -37,6 +38,12 @@ wat = "1.259"
3738

3839
criterion = { version = "0.8", default-features = false, features = ["cargo_bench_support", "rayon"] }
3940

41+
[workspace.lints.cargo]
42+
unused_dependencies = "allow" # false-positive on libm
43+
44+
[workspace.lints.rust]
45+
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(rust_analyzer)"] }
46+
4047
[[example]]
4148
name = "rust"
4249
test = false
@@ -61,12 +68,13 @@ name = "tinywasm_modes"
6168
anyhow.workspace = true
6269
criterion.workspace = true
6370
pretty_env_logger.workspace = true
64-
tinywasm = { path = "crates/tinywasm" }
71+
tinywasm = { path = "crates/tinywasm", features = ["state"] }
6572
tinywasm-parser.workspace = true
73+
tinywasm-wasi.workspace = true
6674
wat.workspace = true
6775

68-
[lints.cargo]
69-
unused_dependencies = "allow" # false-positive on libm
76+
[lints]
77+
workspace = true
7078

7179
[profile.bench]
7280
opt-level = 3

‎crates/cli/Cargo.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ tinywasm = { workspace = true, features = [
3838
"log",
3939
"std",
4040
] }
41+
tinywasm-wasi.workspace = true
4142
wasm-testsuite = { workspace = true, optional = true }
4243
wast = { workspace = true, optional = true }
4344
wat = { workspace = true, optional = true }
@@ -52,3 +53,6 @@ default = ["wast", "wat"]
5253
tests = ["dep:serde", "dep:serde_json", "dep:wasm-testsuite", "wast"]
5354
wast = ["dep:wast"]
5455
wat = ["dep:wat"]
56+
57+
[lints]
58+
workspace = true

‎crates/cli/README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The crate also exposes reusable helpers such as `tinywasm_cli::wast_runner::Wast
1111
$ cargo install tinywasm-cli --version 0.9.0 --bin tinywasm
1212
$ tinywasm --help
1313
$ tinywasm ./module.wasm
14+
$ tinywasm run --env MODE=test ./command.wasm first second
1415
$ tinywasm run --invoke add ./module.wasm 1 2
1516
$ tinywasm compile ./module.wat -o ./module.twasm
1617
$ tinywasm dump ./module.twasm
@@ -23,6 +24,11 @@ Notes:
2324
- `run`, `dump`, and `inspect` accept `.wasm`, `.wat`, and `.twasm` inputs.
2425
- Use `-` as the input path to read a module from stdin.
2526
- Without `--invoke`, `tinywasm` expects the module to have a start function or `_start` export.
27+
- WASI Preview 1 imports are provided automatically. Arguments after the module path become guest
28+
arguments, while repeatable `--env NAME=VALUE` options configure the guest environment.
29+
- WASI commands inherit stdin, stdout, and stderr. Filesystem access and host environment inheritance
30+
are not enabled.
31+
- `proc_exit` determines the CLI process status without being printed as a trap.
2632
- `compile` writes TinyWasm's `twasm` archive format.
2733
- Function invocation arguments are parsed from the export signature, so `tinywasm run --invoke add ./module.wasm 1 2` works without repeating Wasm types on the command line.
2834
- `inspect` uses ANSI colors automatically when writing to a terminal; set `NO_COLOR=1` to disable them.

‎crates/cli/src/bin.rs‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
use anyhow::Result;
21
use clap::Parser;
3-
use tinywasm_cli::{Cli, run_cli};
2+
use std::process::ExitCode;
3+
use tinywasm_cli::{Cli, WasiExit, run_cli};
44

5-
fn main() -> Result<()> {
5+
fn main() -> ExitCode {
66
let cli = Cli::parse();
77
pretty_env_logger::formatted_builder().filter_level(cli.log_level.into()).init();
8-
run_cli(cli)
8+
match run_cli(cli) {
9+
Ok(()) => ExitCode::SUCCESS,
10+
Err(error) => match error.downcast_ref::<WasiExit>() {
11+
Some(exit) => std::process::exit(exit.0 as i32),
12+
None => {
13+
eprintln!("Error: {error:?}");
14+
ExitCode::FAILURE
15+
}
16+
},
17+
}
918
}

‎crates/cli/src/cli.rs‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,18 @@ pub struct RunArgs {
6161
#[arg(long)]
6262
pub invoke: Option<String>,
6363

64+
/// Add a WASI environment variable
65+
#[arg(long = "env", value_name = "NAME=VALUE")]
66+
pub env: Vec<String>,
67+
68+
/// Preopen a host directory for WASI as HOST or HOST::GUEST
69+
#[arg(long = "dir", value_name = "HOST[::GUEST]")]
70+
pub dirs: Vec<String>,
71+
6472
#[command(flatten)]
6573
pub engine: EngineFlags,
6674

67-
/// Arguments passed to the invoked Wasm function
75+
/// Arguments passed to the WASI command or invoked Wasm function
6876
#[arg(trailing_var_arg = true)]
6977
pub args: Vec<String>,
7078
}

‎crates/cli/src/cmd/run.rs‎

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use anyhow::Result;
2+
use std::path::Path;
23
use tinywasm::types::ExportType;
34
use tinywasm::{ModuleInstance, Store};
5+
use tinywasm_wasi::p1::WasiCtx;
46

7+
use crate::WasiExit;
58
use crate::cli::RunArgs;
69
use crate::load::load_module;
710
use crate::output::print_results;
@@ -10,15 +13,45 @@ use crate::value_parse::parse_invocation_args;
1013
pub fn run(args: RunArgs) -> Result<()> {
1114
let module_path = args.module.as_deref().ok_or_else(|| anyhow::anyhow!("missing module path"))?;
1215
let loaded = load_module(module_path)?;
13-
let mut store = Store::new(args.engine.build_engine()?);
14-
let instance = ModuleInstance::instantiate_no_start(&mut store, &loaded.module, None)?;
16+
let argv0 = Path::new(module_path).file_name().and_then(|name| name.to_str()).unwrap_or(module_path);
17+
let guest_args = if args.invoke.is_none() {
18+
std::iter::once(argv0.to_owned()).chain(args.args.iter().cloned()).collect()
19+
} else {
20+
vec![argv0.to_owned()]
21+
};
22+
let env = args
23+
.env
24+
.iter()
25+
.map(|value| {
26+
value
27+
.split_once('=')
28+
.filter(|(name, _)| !name.is_empty())
29+
.map(|(name, value)| (name.to_owned(), value.to_owned()))
30+
.ok_or_else(|| anyhow::anyhow!("invalid --env value `{value}`, expected NAME=VALUE"))
31+
})
32+
.collect::<Result<Vec<_>>>()?;
33+
let mut wasi = WasiCtx::new().with_args(guest_args)?.with_env(env)?.inherit_stdio();
34+
for mapping in &args.dirs {
35+
let (host, guest) = mapping.split_once("::").unwrap_or((mapping, mapping));
36+
if host.is_empty() || guest.is_empty() {
37+
return Err(anyhow::anyhow!("invalid --dir value `{mapping}`, expected HOST or HOST::GUEST"));
38+
}
39+
wasi = wasi.preopen_dir(host, guest)?;
40+
}
41+
let mut store = Store::new(args.engine.build_engine()?).with_state(wasi);
42+
let imports = tinywasm_wasi::p1::imports();
43+
let instance = ModuleInstance::instantiate_no_start(&mut store, &loaded.module, Some(&imports))?;
44+
45+
let ran_core_start = if loaded.module.start_func.is_some() {
46+
let result = instance.start(&mut store);
47+
finish_wasi_call(result, &mut store)?;
48+
true
49+
} else {
50+
false
51+
};
1552

1653
match args.invoke.as_deref() {
1754
Some(export) => {
18-
if loaded.module.start_func.is_some() {
19-
let _ = instance.start(&mut store)?;
20-
}
21-
2255
let func_ty = loaded
2356
.module
2457
.exports()
@@ -30,23 +63,38 @@ pub fn run(args: RunArgs) -> Result<()> {
3063
let func = instance.func_untyped(&store, export)?;
3164
let params = parse_invocation_args(func_ty, &args.args)?;
3265
let mut results = vec![tinywasm::WasmValue::I32(0); func.ty(&store)?.results().len()];
33-
func.call(&mut store, &params, &mut results)?;
66+
let result = func.call(&mut store, &params, &mut results);
67+
finish_wasi_call(result, &mut store)?;
3468
print_results(&results);
3569
Ok(())
3670
}
3771
None => {
38-
if instance.start_func(&store)?.is_some() {
39-
let _ = instance.start(&mut store)?;
40-
return Ok(());
41-
}
42-
43-
let start = instance.func_untyped(&store, "_start").map_err(|_| {
44-
anyhow::anyhow!(
45-
"module has no start function or `_start` export. Use `tinywasm inspect {module_path}` or `tinywasm run --invoke <export> {module_path}`"
46-
)
47-
})?;
48-
start.call(&mut store, &[], &mut [])?;
72+
let start = match instance.func_untyped(&store, "_start") {
73+
Ok(start) => start,
74+
Err(_) if ran_core_start => return Ok(()),
75+
Err(_) => {
76+
return Err(anyhow::anyhow!(
77+
"module has no start function or `_start` export. Use `tinywasm inspect {module_path}` or `tinywasm run --invoke <export> {module_path}`"
78+
));
79+
}
80+
};
81+
let result = start.call(&mut store, &[], &mut []);
82+
finish_wasi_call(result, &mut store)?;
4983
Ok(())
5084
}
5185
}
5286
}
87+
88+
fn finish_wasi_call<T>(result: tinywasm::Result<T>, store: &mut Store) -> Result<T> {
89+
match result {
90+
Ok(value) => Ok(value),
91+
Err(error) => {
92+
if let Some(wasi) = store.state_mut::<WasiCtx>()
93+
&& let Some(status) = wasi.take_exit_status()
94+
{
95+
return Err(WasiExit(status).into());
96+
}
97+
Err(error.into())
98+
}
99+
}
100+
}

‎crates/cli/src/lib.rs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,22 @@ pub mod wast_runner;
1111

1212
use anyhow::Result;
1313
use clap::CommandFactory;
14+
use std::fmt::{Display, Formatter};
1415

1516
pub use cli::{Cli, Commands};
1617

18+
/// A WASI command exit requested through `proc_exit`.
19+
#[derive(Debug)]
20+
pub struct WasiExit(pub u32);
21+
22+
impl Display for WasiExit {
23+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
24+
write!(f, "WASI command exited with status {}", self.0)
25+
}
26+
}
27+
28+
impl std::error::Error for WasiExit {}
29+
1730
pub fn run_cli(cli: Cli) -> Result<()> {
1831
match cli.command {
1932
Some(Commands::Run(args)) => cmd::run::run(args),

‎crates/cli/tests/cli.rs‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,116 @@ fn bare_run_requires_default_entrypoint() {
7272
.stderr(predicate::str::contains("no start function or `_start` export"));
7373
}
7474

75+
#[test]
76+
fn run_supplies_wasi_arguments() {
77+
let dir = tempdir().unwrap();
78+
let module = write_module(
79+
&dir,
80+
"args.wat",
81+
r#"(module
82+
(import "wasi_snapshot_preview1" "args_sizes_get" (func $args_sizes_get (param i32 i32) (result i32)))
83+
(import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32)))
84+
(memory (export "memory") 1)
85+
(func (export "_start")
86+
i32.const 0
87+
i32.const 4
88+
call $args_sizes_get
89+
drop
90+
i32.const 0
91+
i32.load
92+
call $proc_exit))"#,
93+
);
94+
95+
Command::cargo_bin("tinywasm").unwrap().args(["run", &module, "one", "two"]).assert().code(3).stderr("");
96+
}
97+
98+
#[test]
99+
fn run_supplies_explicit_wasi_environment() {
100+
let dir = tempdir().unwrap();
101+
let module = write_module(
102+
&dir,
103+
"env.wat",
104+
r#"(module
105+
(import "wasi_snapshot_preview1" "environ_sizes_get" (func $environ_sizes_get (param i32 i32) (result i32)))
106+
(import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32)))
107+
(memory (export "memory") 1)
108+
(func (export "_start")
109+
i32.const 0
110+
i32.const 4
111+
call $environ_sizes_get
112+
drop
113+
i32.const 0
114+
i32.load
115+
call $proc_exit))"#,
116+
);
117+
118+
Command::cargo_bin("tinywasm")
119+
.unwrap()
120+
.args(["run", "--env", "MODE=test", "--env", "COLOR=off", &module])
121+
.assert()
122+
.code(2)
123+
.stderr("");
124+
}
125+
126+
#[test]
127+
fn run_inherits_wasi_stdout() {
128+
let dir = tempdir().unwrap();
129+
let module = write_module(
130+
&dir,
131+
"stdout.wat",
132+
r#"(module
133+
(import "wasi_snapshot_preview1" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
134+
(memory (export "memory") 1)
135+
(data (i32.const 16) "hello wasi\n")
136+
(func (export "_start")
137+
i32.const 0 i32.const 16 i32.store
138+
i32.const 4 i32.const 11 i32.store
139+
i32.const 1 i32.const 0 i32.const 1 i32.const 8
140+
call $fd_write
141+
drop))"#,
142+
);
143+
144+
Command::cargo_bin("tinywasm").unwrap().args(["run", &module]).assert().success().stdout("hello wasi\n");
145+
}
146+
147+
#[test]
148+
fn run_preopens_wasi_directory() {
149+
let dir = tempdir().unwrap();
150+
let module = write_module(
151+
&dir,
152+
"preopen.wat",
153+
r#"(module
154+
(import "wasi_snapshot_preview1" "fd_prestat_get" (func $prestat (param i32 i32) (result i32)))
155+
(import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32)))
156+
(memory (export "memory") 1)
157+
(func (export "_start")
158+
i32.const 3
159+
i32.const 0
160+
call $prestat
161+
call $proc_exit))"#,
162+
);
163+
let mapping = format!("{}::/data", dir.path().display());
164+
165+
Command::cargo_bin("tinywasm").unwrap().args(["run", "--dir", &mapping, &module]).assert().success().stderr("");
166+
}
167+
168+
#[test]
169+
fn run_executes_core_start_before_wasi_start() {
170+
let dir = tempdir().unwrap();
171+
let module = write_module(
172+
&dir,
173+
"start-order.wat",
174+
r#"(module
175+
(import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32)))
176+
(global $status (mut i32) (i32.const 1))
177+
(func $initialize i32.const 7 global.set $status)
178+
(start $initialize)
179+
(func (export "_start") global.get $status call $proc_exit))"#,
180+
);
181+
182+
Command::cargo_bin("tinywasm").unwrap().args(["run", &module]).assert().code(7).stderr("");
183+
}
184+
75185
#[test]
76186
fn inspect_lists_exports() {
77187
let dir = tempdir().unwrap();

‎crates/parser/Cargo.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@ std = ["tinywasm-types/std", "wasmparser/std"]
3232
# validate WebAssembly while parsing
3333
validate = ["wasmparser/features", "wasmparser/validate"]
3434

35-
[lints.rust]
36-
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(rust_analyzer)"] }
35+
[lints]
36+
workspace = true

0 commit comments

Comments
 (0)