From 0d5d63fdb1b7d34eca0fa1d7d7daadeef5333698 Mon Sep 17 00:00:00 2001 From: Peter Huene Date: Fri, 20 Mar 2020 16:47:25 -0700 Subject: [PATCH] Support WASI snapshot0 in the C API. This commit adds support for snapshot0 in the WASI C API. A name parameter was added to `wasi_instance_new` to accept which WASI module is being instantiated. Additionally, the C# API now supports constructing a WASI instance based on the WASI module name. Fixes #1221. --- crates/c-api/include/wasi.h | 1 + crates/c-api/src/ext.rs | 2 +- crates/c-api/src/wasi.rs | 207 ++++++++++++--- crates/misc/dotnet/src/Interop.cs | 1 + crates/misc/dotnet/src/Wasi.cs | 16 +- crates/misc/dotnet/src/WasiBuilder.cs | 16 +- .../dotnet/tests/Modules/WasiSnapshot0.wat | 66 +++++ .../misc/dotnet/tests/WasiSnapshot0Tests.cs | 246 ++++++++++++++++++ crates/misc/dotnet/tests/WasiTests.cs | 18 +- crates/wasi-common/src/old/snapshot_0/ctx.rs | 134 ++++++---- crates/wasi-common/wig/src/wasi.rs | 17 +- src/commands/run.rs | 9 +- 12 files changed, 613 insertions(+), 120 deletions(-) create mode 100644 crates/misc/dotnet/tests/Modules/WasiSnapshot0.wat create mode 100644 crates/misc/dotnet/tests/WasiSnapshot0Tests.cs diff --git a/crates/c-api/include/wasi.h b/crates/c-api/include/wasi.h index e1a7ffc5dc..3a5176925c 100644 --- a/crates/c-api/include/wasi.h +++ b/crates/c-api/include/wasi.h @@ -52,6 +52,7 @@ WASI_DECLARE_OWN(instance) WASI_API_EXTERN own wasi_instance_t* wasi_instance_new( wasm_store_t* store, + const char* name, own wasi_config_t* config, own wasm_trap_t** trap ); diff --git a/crates/c-api/src/ext.rs b/crates/c-api/src/ext.rs index 88688c8af7..6d0fe082cd 100644 --- a/crates/c-api/src/ext.rs +++ b/crates/c-api/src/ext.rs @@ -185,7 +185,7 @@ pub unsafe extern "C" fn wasmtime_linker_define_wasi( instance: *const wasi_instance_t, ) -> bool { let linker = &mut (*linker).linker; - (*instance).wasi.add_to_linker(linker).is_ok() + (*instance).add_to_linker(linker).is_ok() } #[no_mangle] diff --git a/crates/c-api/src/wasi.rs b/crates/c-api/src/wasi.rs index 8905d023fb..c05ff90a6c 100644 --- a/crates/c-api/src/wasi.rs +++ b/crates/c-api/src/wasi.rs @@ -1,14 +1,18 @@ //! The WASI embedding API definitions for Wasmtime. use crate::{wasm_extern_t, wasm_importtype_t, wasm_store_t, wasm_trap_t, ExternHost, ExternType}; +use anyhow::Result; use std::collections::HashMap; use std::ffi::CStr; use std::fs::File; use std::os::raw::{c_char, c_int}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::slice; -use wasi_common::{preopen_dir, WasiCtxBuilder}; -use wasmtime::{HostRef, Trap}; -use wasmtime_wasi::Wasi; +use wasi_common::{ + old::snapshot_0::WasiCtxBuilder as WasiSnapshot0CtxBuilder, preopen_dir, + WasiCtxBuilder as WasiPreview1CtxBuilder, +}; +use wasmtime::{HostRef, Linker, Store, Trap}; +use wasmtime_wasi::{old::snapshot_0::Wasi as WasiSnapshot0, Wasi as WasiPreview1}; unsafe fn cstr_to_path<'a>(path: *const c_char) -> Option<&'a Path> { CStr::from_ptr(path).to_str().map(Path::new).ok() @@ -22,18 +26,32 @@ unsafe fn create_file(path: *const c_char) -> Option { File::create(cstr_to_path(path)?).ok() } -#[repr(C)] -pub struct wasi_config_t { - builder: WasiCtxBuilder, +pub enum WasiModule { + Snapshot0(WasiSnapshot0), + Preview1(WasiPreview1), } -impl wasi_config_t {} +impl WasiModule {} + +#[repr(C)] +#[derive(Default)] +pub struct wasi_config_t { + args: Vec>, + env: Vec<(Vec, Vec)>, + stdin: Option, + stdout: Option, + stderr: Option, + preopens: Vec<(File, PathBuf)>, + inherit_args: bool, + inherit_env: bool, + inherit_stdin: bool, + inherit_stdout: bool, + inherit_stderr: bool, +} #[no_mangle] pub unsafe extern "C" fn wasi_config_new() -> *mut wasi_config_t { - Box::into_raw(Box::new(wasi_config_t { - builder: WasiCtxBuilder::new(), - })) + Box::into_raw(Box::new(wasi_config_t::default())) } #[no_mangle] @@ -47,16 +65,17 @@ pub unsafe extern "C" fn wasi_config_set_argv( argc: c_int, argv: *const *const c_char, ) { - (*config).builder.args( - slice::from_raw_parts(argv, argc as usize) - .iter() - .map(|a| slice::from_raw_parts(*a as *const u8, CStr::from_ptr(*a).to_bytes().len())), - ); + (*config).args = slice::from_raw_parts(argv, argc as usize) + .iter() + .map(|p| CStr::from_ptr(*p).to_bytes().to_owned()) + .collect(); + (*config).inherit_args = false; } #[no_mangle] pub unsafe extern "C" fn wasi_config_inherit_argv(config: *mut wasi_config_t) { - (*config).builder.inherit_args(); + (*config).args.clear(); + (*config).inherit_args = true; } #[no_mangle] @@ -69,17 +88,22 @@ pub unsafe extern "C" fn wasi_config_set_env( let names = slice::from_raw_parts(names, envc as usize); let values = slice::from_raw_parts(values, envc as usize); - (*config).builder.envs( - names - .iter() - .map(|p| CStr::from_ptr(*p).to_bytes()) - .zip(values.iter().map(|p| CStr::from_ptr(*p).to_bytes())), - ); + (*config).env = names + .iter() + .map(|p| CStr::from_ptr(*p).to_bytes().to_owned()) + .zip( + values + .iter() + .map(|p| CStr::from_ptr(*p).to_bytes().to_owned()), + ) + .collect(); + (*config).inherit_env = false; } #[no_mangle] pub unsafe extern "C" fn wasi_config_inherit_env(config: *mut wasi_config_t) { - (*config).builder.inherit_env(); + (*config).env.clear(); + (*config).inherit_env = true; } #[no_mangle] @@ -92,14 +116,16 @@ pub unsafe extern "C" fn wasi_config_set_stdin_file( None => return false, }; - (*config).builder.stdin(file); + (*config).stdin = Some(file); + (*config).inherit_stdin = false; true } #[no_mangle] pub unsafe extern "C" fn wasi_config_inherit_stdin(config: *mut wasi_config_t) { - (*config).builder.inherit_stdin(); + (*config).stdin = None; + (*config).inherit_stdin = true; } #[no_mangle] @@ -112,14 +138,16 @@ pub unsafe extern "C" fn wasi_config_set_stdout_file( None => return false, }; - (*config).builder.stdout(file); + (*config).stdout = Some(file); + (*config).inherit_stdout = false; true } #[no_mangle] pub unsafe extern "C" fn wasi_config_inherit_stdout(config: *mut wasi_config_t) { - (*config).builder.inherit_stdout(); + (*config).stdout = None; + (*config).inherit_stdout = true; } #[no_mangle] @@ -132,14 +160,16 @@ pub unsafe extern "C" fn wasi_config_set_stderr_file( None => return false, }; - (*config).builder.stderr(file); + (*config).stderr = Some(file); + (*config).inherit_stderr = false; true } #[no_mangle] pub unsafe extern "C" fn wasi_config_inherit_stderr(config: *mut wasi_config_t) { - (*config).builder.inherit_stderr(); + (*config).stderr = None; + (*config).inherit_stderr = true; } #[no_mangle] @@ -161,29 +191,110 @@ pub unsafe extern "C" fn wasi_config_preopen_dir( None => return false, }; - (*config).builder.preopened_dir(dir, guest_path); + (*config).preopens.push((dir, guest_path.to_owned())); true } +enum WasiInstance { + Preview1(WasiPreview1), + Snapshot0(WasiSnapshot0), +} + +macro_rules! config_to_builder { + ($builder:ident, $config:ident) => {{ + let mut builder = $builder::new(); + + if $config.inherit_args { + builder.inherit_args(); + } else if !$config.args.is_empty() { + builder.args($config.args); + } + + if $config.inherit_env { + builder.inherit_env(); + } else if !$config.env.is_empty() { + builder.envs($config.env); + } + + if $config.inherit_stdin { + builder.inherit_stdin(); + } else if let Some(file) = $config.stdin { + builder.stdin(file); + } + + if $config.inherit_stdout { + builder.inherit_stdout(); + } else if let Some(file) = $config.stdout { + builder.stdout(file); + } + + if $config.inherit_stderr { + builder.inherit_stderr(); + } else if let Some(file) = $config.stderr { + builder.stderr(file); + } + + for preopen in $config.preopens { + builder.preopened_dir(preopen.0, preopen.1); + } + + builder + }}; +} + +fn create_snapshot0_instance(store: &Store, config: wasi_config_t) -> Result { + Ok(WasiInstance::Snapshot0(WasiSnapshot0::new( + store, + config_to_builder!(WasiSnapshot0CtxBuilder, config) + .build() + .map_err(|e| e.to_string())?, + ))) +} + +fn create_preview1_instance(store: &Store, config: wasi_config_t) -> Result { + Ok(WasiInstance::Preview1(WasiPreview1::new( + store, + config_to_builder!(WasiPreview1CtxBuilder, config) + .build() + .map_err(|e| e.to_string())?, + ))) +} + #[repr(C)] pub struct wasi_instance_t { - pub wasi: Wasi, + wasi: WasiInstance, export_cache: HashMap>, } +impl wasi_instance_t { + pub fn add_to_linker(&self, linker: &mut Linker) -> Result<()> { + match &self.wasi { + WasiInstance::Snapshot0(w) => w.add_to_linker(linker), + WasiInstance::Preview1(w) => w.add_to_linker(linker), + } + } +} + #[no_mangle] pub unsafe extern "C" fn wasi_instance_new( store: *mut wasm_store_t, + name: *const c_char, config: *mut wasi_config_t, trap: *mut *mut wasm_trap_t, ) -> *mut wasi_instance_t { let store = &(*store).store.borrow(); - let mut config = Box::from_raw(config); + let config = Box::from_raw(config); - match config.builder.build() { - Ok(ctx) => Box::into_raw(Box::new(wasi_instance_t { - wasi: Wasi::new(store, ctx), + let result = match CStr::from_ptr(name).to_str().unwrap_or("") { + "wasi_snapshot_preview1" => create_preview1_instance(store, *config), + "wasi_unstable" => create_snapshot0_instance(store, *config), + _ => Err("unsupported WASI version".into()), + }; + + match result { + Ok(wasi) => Box::into_raw(Box::new(wasi_instance_t { + wasi, export_cache: HashMap::new(), })), Err(e) => { @@ -206,20 +317,32 @@ pub unsafe extern "C" fn wasi_instance_bind_import( instance: *mut wasi_instance_t, import: *const wasm_importtype_t, ) -> *const wasm_extern_t { - // TODO: support previous versions? - if (*import).ty.module() != "wasi_snapshot_preview1" { - return std::ptr::null_mut(); - } - // The import should be a function (WASI only exports functions) let func_type = match (*import).ty.ty() { ExternType::Func(f) => f, _ => return std::ptr::null_mut(), }; + let module = (*import).ty.module(); let name = (*import).ty.name(); - match (*instance).wasi.get_export(name) { + let import = match &(*instance).wasi { + WasiInstance::Preview1(wasi) => { + if module != "wasi_snapshot_preview1" { + return std::ptr::null(); + } + wasi.get_export(name) + } + WasiInstance::Snapshot0(wasi) => { + if module != "wasi_unstable" { + return std::ptr::null(); + } + + wasi.get_export(name) + } + }; + + match import { Some(export) => { if export.ty() != func_type { return std::ptr::null_mut(); diff --git a/crates/misc/dotnet/src/Interop.cs b/crates/misc/dotnet/src/Interop.cs index 877b5efd88..81f29887a4 100644 --- a/crates/misc/dotnet/src/Interop.cs +++ b/crates/misc/dotnet/src/Interop.cs @@ -936,6 +936,7 @@ namespace Wasmtime [DllImport(LibraryName)] public static extern WasiInstanceHandle wasi_instance_new( StoreHandle store, + [MarshalAs(UnmanagedType.LPUTF8Str)] string name, WasiConfigHandle config, out IntPtr trap ); diff --git a/crates/misc/dotnet/src/Wasi.cs b/crates/misc/dotnet/src/Wasi.cs index e16feaf58b..a0cd1ef367 100644 --- a/crates/misc/dotnet/src/Wasi.cs +++ b/crates/misc/dotnet/src/Wasi.cs @@ -9,18 +9,26 @@ namespace Wasmtime /// /// Creates a default instance. /// - public Wasi(Store store) : + /// The store to use for the new WASI instance. + /// The name of the WASI module to create. + public Wasi(Store store, string name) : this( (store ?? throw new ArgumentNullException(nameof(store))).Handle, - Interop.wasi_config_new() + Interop.wasi_config_new(), + name ) { } - internal Wasi(Interop.StoreHandle store, Interop.WasiConfigHandle config) + internal Wasi(Interop.StoreHandle store, Interop.WasiConfigHandle config, string name) { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("Name cannot be null or empty.", nameof(name)); + } + IntPtr trap; - Handle = Interop.wasi_instance_new(store, config, out trap); + Handle = Interop.wasi_instance_new(store, name, config, out trap); config.SetHandleAsInvalid(); if (trap != IntPtr.Zero) diff --git a/crates/misc/dotnet/src/WasiBuilder.cs b/crates/misc/dotnet/src/WasiBuilder.cs index d1a363f3ea..fd5b445874 100644 --- a/crates/misc/dotnet/src/WasiBuilder.cs +++ b/crates/misc/dotnet/src/WasiBuilder.cs @@ -12,10 +12,22 @@ namespace Wasmtime /// /// Constructs a new . /// - public WasiBuilder() + public WasiBuilder(string name) { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("Name cannot be null or empty.", nameof(name)); + } + + Name = name; } + /// + /// Gets the name of the WASI module being built. + /// + /// The name of the WASI module. + public string Name { get; private set; } + /// /// Adds a command line argument to the builder. /// @@ -271,7 +283,7 @@ namespace Wasmtime SetStandardError(config); SetPreopenDirectories(config); - return new Wasi(store.Handle, config); + return new Wasi(store.Handle, config, Name); } private unsafe void SetConfigArgs(Interop.WasiConfigHandle config) diff --git a/crates/misc/dotnet/tests/Modules/WasiSnapshot0.wat b/crates/misc/dotnet/tests/Modules/WasiSnapshot0.wat new file mode 100644 index 0000000000..2ebca6b9b3 --- /dev/null +++ b/crates/misc/dotnet/tests/Modules/WasiSnapshot0.wat @@ -0,0 +1,66 @@ +(module + (type $t0 (func (param i32 i32) (result i32))) + (type $t1 (func (param i32 i32 i32 i32) (result i32))) + (type $t2 (func (param i32) (result i32))) + (type $t3 (func (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32))) + (import "wasi_unstable" "environ_sizes_get" (func $wasi_unstable.environ_sizes_get (type $t0))) + (import "wasi_unstable" "environ_get" (func $wasi_unstable.environ_get (type $t0))) + (import "wasi_unstable" "args_sizes_get" (func $wasi_unstable.args_sizes_get (type $t0))) + (import "wasi_unstable" "args_get" (func $wasi_unstable.args_get (type $t0))) + (import "wasi_unstable" "fd_write" (func $wasi_unstable.fd_write (type $t1))) + (import "wasi_unstable" "fd_read" (func $wasi_unstable.fd_read (type $t1))) + (import "wasi_unstable" "fd_close" (func $wasi_unstable.fd_close (type $t2))) + (import "wasi_unstable" "path_open" (func $wasi_unstable.path_open (type $t3))) + (memory $memory 1) + (export "memory" (memory 0)) + (func $call_environ_sizes_get (type $t0) (param $p0 i32) (param $p1 i32) (result i32) + local.get $p0 + local.get $p1 + call $wasi_unstable.environ_sizes_get) + (func $call_environ_get (type $t0) (param $p0 i32) (param $p1 i32) (result i32) + local.get $p0 + local.get $p1 + call $wasi_unstable.environ_get) + (func $call_args_sizes_get (type $t0) (param $p0 i32) (param $p1 i32) (result i32) + local.get $p0 + local.get $p1 + call $wasi_unstable.args_sizes_get) + (func $call_args_get (type $t0) (param $p0 i32) (param $p1 i32) (result i32) + local.get $p0 + local.get $p1 + call $wasi_unstable.args_get) + (func $call_fd_write (type $t1) (param $p0 i32) (param $p1 i32) (param $p2 i32) (param $p3 i32) (result i32) + local.get $p0 + local.get $p1 + local.get $p2 + local.get $p3 + call $wasi_unstable.fd_write) + (func $call_fd_read (type $t1) (param $p0 i32) (param $p1 i32) (param $p2 i32) (param $p3 i32) (result i32) + local.get $p0 + local.get $p1 + local.get $p2 + local.get $p3 + call $wasi_unstable.fd_read) + (func $call_fd_close (type $t2) (param $p0 i32) (result i32) + local.get $p0 + call $wasi_unstable.fd_close) + (func $call_path_open (type $t3) (param $p0 i32) (param $p1 i32) (param $p2 i32) (param $p3 i32) (param $p4 i32) (param $p5 i64) (param $p6 i64) (param $p7 i32) (param $p8 i32) (result i32) + local.get $p0 + local.get $p1 + local.get $p2 + local.get $p3 + local.get $p4 + local.get $p5 + local.get $p6 + local.get $p7 + local.get $p8 + call $wasi_unstable.path_open) + (export "call_environ_sizes_get" (func $call_environ_sizes_get)) + (export "call_environ_get" (func $call_environ_get)) + (export "call_args_sizes_get" (func $call_args_sizes_get)) + (export "call_args_get" (func $call_args_get)) + (export "call_fd_write" (func $call_fd_write)) + (export "call_fd_read" (func $call_fd_read)) + (export "call_fd_close" (func $call_fd_close)) + (export "call_path_open" (func $call_path_open)) +) diff --git a/crates/misc/dotnet/tests/WasiSnapshot0Tests.cs b/crates/misc/dotnet/tests/WasiSnapshot0Tests.cs new file mode 100644 index 0000000000..6fae2f6aa8 --- /dev/null +++ b/crates/misc/dotnet/tests/WasiSnapshot0Tests.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using FluentAssertions; +using Xunit; + +namespace Wasmtime.Tests +{ + public class WasiSnapshot0Fixture : ModuleFixture + { + protected override string ModuleFileName => "WasiSnapshot0.wat"; + } + + public class WasiSnapshot0Tests : IClassFixture + { + public WasiSnapshot0Tests(WasiSnapshot0Fixture fixture) + { + Fixture = fixture; + } + + private WasiSnapshot0Fixture Fixture { get; set; } + + [Fact] + public void ItHasNoEnvironmentByDefault() + { + using var instance = Fixture.Module.Instantiate(new Wasi(Fixture.Module.Store, "wasi_unstable")); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + + Assert.Equal(0, inst.call_environ_sizes_get(0, 4)); + Assert.Equal(0, memory.ReadInt32(0)); + Assert.Equal(0, memory.ReadInt32(4)); + } + + [Fact] + public void ItHasSpecifiedEnvironment() + { + var env = new Dictionary() { + {"FOO", "BAR"}, + {"WASM", "IS"}, + {"VERY", "COOL"}, + }; + + var wasi = new WasiBuilder("wasi_unstable") + .WithEnvironmentVariables(env.Select(kvp => (kvp.Key, kvp.Value))) + .Build(Fixture.Module.Store); + + using var instance = Fixture.Module.Instantiate(wasi); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + + Assert.Equal(0, inst.call_environ_sizes_get(0, 4)); + Assert.Equal(env.Count, memory.ReadInt32(0)); + Assert.Equal(env.Sum(kvp => kvp.Key.Length + kvp.Value.Length + 2), memory.ReadInt32(4)); + Assert.Equal(0, inst.call_environ_get(0, 4 * env.Count)); + + for (int i = 0; i < env.Count; ++i) + { + var kvp = memory.ReadNullTerminatedString(memory.ReadInt32(i * 4)).Split("="); + Assert.Equal(env[kvp[0]], kvp[1]); + } + } + + [Fact] + public void ItInheritsEnvironment() + { + var wasi = new WasiBuilder("wasi_unstable") + .WithInheritedEnvironment() + .Build(Fixture.Module.Store); + + using var instance = Fixture.Module.Instantiate(wasi); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + + Assert.Equal(0, inst.call_environ_sizes_get(0, 4)); + Assert.Equal(Environment.GetEnvironmentVariables().Keys.Count, memory.ReadInt32(0)); + } + + [Fact] + public void ItHasNoArgumentsByDefault() + { + using var instance = Fixture.Module.Instantiate(new Wasi(Fixture.Module.Store, "wasi_unstable")); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + + Assert.Equal(0, inst.call_args_sizes_get(0, 4)); + Assert.Equal(0, memory.ReadInt32(0)); + Assert.Equal(0, memory.ReadInt32(4)); + } + + [Fact] + public void ItHasSpecifiedArguments() + { + var args = new List() { + "WASM", + "IS", + "VERY", + "COOL" + }; + + var wasi = new WasiBuilder("wasi_unstable") + .WithArgs(args) + .Build(Fixture.Module.Store); + + using var instance = Fixture.Module.Instantiate(wasi); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + + Assert.Equal(0, inst.call_args_sizes_get(0, 4)); + Assert.Equal(args.Count, memory.ReadInt32(0)); + Assert.Equal(args.Sum(a => a.Length + 1), memory.ReadInt32(4)); + Assert.Equal(0, inst.call_args_get(0, 4 * args.Count)); + + for (int i = 0; i < args.Count; ++i) + { + var arg = memory.ReadNullTerminatedString(memory.ReadInt32(i * 4)); + Assert.Equal(args[i], arg); + } + } + + [Fact] + public void ItInheritsArguments() + { + var wasi = new WasiBuilder("wasi_unstable") + .WithInheritedArgs() + .Build(Fixture.Module.Store); + + using var instance = Fixture.Module.Instantiate(wasi); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + + Assert.Equal(0, inst.call_args_sizes_get(0, 4)); + Assert.Equal(Environment.GetCommandLineArgs().Length, memory.ReadInt32(0)); + } + + [Fact] + public void ItSetsStdIn() + { + const string MESSAGE = "WASM IS VERY COOL"; + + using var file = new TempFile(); + File.WriteAllText(file.Path, MESSAGE); + + var wasi = new WasiBuilder("wasi_unstable") + .WithStandardInput(file.Path) + .Build(Fixture.Module.Store); + + using var instance = Fixture.Module.Instantiate(wasi); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + memory.WriteInt32(0, 8); + memory.WriteInt32(4, MESSAGE.Length); + + Assert.Equal(0, inst.call_fd_read(0, 0, 1, 32)); + Assert.Equal(MESSAGE.Length, memory.ReadInt32(32)); + Assert.Equal(MESSAGE, memory.ReadString(8, MESSAGE.Length)); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + public void ItSetsStdOutAndStdErr(int fd) + { + const string MESSAGE = "WASM IS VERY COOL"; + + using var file = new TempFile(); + + var builder = new WasiBuilder("wasi_unstable"); + if (fd == 1) + { + builder.WithStandardOutput(file.Path); + } + else if (fd == 2) + { + builder.WithStandardError(file.Path); + } + + var wasi = builder.Build(Fixture.Module.Store); + + using var instance = Fixture.Module.Instantiate(wasi); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + memory.WriteInt32(0, 8); + memory.WriteInt32(4, MESSAGE.Length); + memory.WriteString(8, MESSAGE); + + Assert.Equal(0, inst.call_fd_write(fd, 0, 1, 32)); + Assert.Equal(MESSAGE.Length, memory.ReadInt32(32)); + Assert.Equal(0, inst.call_fd_close(fd)); + Assert.Equal(MESSAGE, File.ReadAllText(file.Path)); + } + + [Fact] + public void ItSetsPreopenDirectories() + { + const string MESSAGE = "WASM IS VERY COOL"; + + using var file = new TempFile(); + + var wasi = new WasiBuilder("wasi_unstable") + .WithPreopenedDirectory(Path.GetDirectoryName(file.Path), "/foo") + .Build(Fixture.Module.Store); + + using var instance = Fixture.Module.Instantiate(wasi); + dynamic inst = instance; + + var memory = instance.Externs.Memories[0]; + var fileName = Path.GetFileName(file.Path); + memory.WriteString(0, fileName); + + Assert.Equal(0, inst.call_path_open( + 3, + 0, + 0, + fileName.Length, + 0, + 0x40 /* RIGHTS_FD_WRITE */, + 0, + 0, + 64 + ) + ); + + var fileFd = (int) memory.ReadInt32(64); + Assert.True(fileFd > 3); + + memory.WriteInt32(0, 8); + memory.WriteInt32(4, MESSAGE.Length); + memory.WriteString(8, MESSAGE); + + Assert.Equal(0, inst.call_fd_write(fileFd, 0, 1, 64)); + Assert.Equal(MESSAGE.Length, memory.ReadInt32(64)); + Assert.Equal(0, inst.call_fd_close(fileFd)); + Assert.Equal(MESSAGE, File.ReadAllText(file.Path)); + } + } +} diff --git a/crates/misc/dotnet/tests/WasiTests.cs b/crates/misc/dotnet/tests/WasiTests.cs index 44696ee9dc..e5f5c0117e 100644 --- a/crates/misc/dotnet/tests/WasiTests.cs +++ b/crates/misc/dotnet/tests/WasiTests.cs @@ -24,7 +24,7 @@ namespace Wasmtime.Tests [Fact] public void ItHasNoEnvironmentByDefault() { - using var instance = Fixture.Module.Instantiate(new Wasi(Fixture.Module.Store)); + using var instance = Fixture.Module.Instantiate(new Wasi(Fixture.Module.Store, "wasi_snapshot_preview1")); dynamic inst = instance; var memory = instance.Externs.Memories[0]; @@ -43,7 +43,7 @@ namespace Wasmtime.Tests {"VERY", "COOL"}, }; - var wasi = new WasiBuilder() + var wasi = new WasiBuilder("wasi_snapshot_preview1") .WithEnvironmentVariables(env.Select(kvp => (kvp.Key, kvp.Value))) .Build(Fixture.Module.Store); @@ -67,7 +67,7 @@ namespace Wasmtime.Tests [Fact] public void ItInheritsEnvironment() { - var wasi = new WasiBuilder() + var wasi = new WasiBuilder("wasi_snapshot_preview1") .WithInheritedEnvironment() .Build(Fixture.Module.Store); @@ -83,7 +83,7 @@ namespace Wasmtime.Tests [Fact] public void ItHasNoArgumentsByDefault() { - using var instance = Fixture.Module.Instantiate(new Wasi(Fixture.Module.Store)); + using var instance = Fixture.Module.Instantiate(new Wasi(Fixture.Module.Store, "wasi_snapshot_preview1")); dynamic inst = instance; var memory = instance.Externs.Memories[0]; @@ -103,7 +103,7 @@ namespace Wasmtime.Tests "COOL" }; - var wasi = new WasiBuilder() + var wasi = new WasiBuilder("wasi_snapshot_preview1") .WithArgs(args) .Build(Fixture.Module.Store); @@ -127,7 +127,7 @@ namespace Wasmtime.Tests [Fact] public void ItInheritsArguments() { - var wasi = new WasiBuilder() + var wasi = new WasiBuilder("wasi_snapshot_preview1") .WithInheritedArgs() .Build(Fixture.Module.Store); @@ -148,7 +148,7 @@ namespace Wasmtime.Tests using var file = new TempFile(); File.WriteAllText(file.Path, MESSAGE); - var wasi = new WasiBuilder() + var wasi = new WasiBuilder("wasi_snapshot_preview1") .WithStandardInput(file.Path) .Build(Fixture.Module.Store); @@ -173,7 +173,7 @@ namespace Wasmtime.Tests using var file = new TempFile(); - var builder = new WasiBuilder(); + var builder = new WasiBuilder("wasi_snapshot_preview1"); if (fd == 1) { builder.WithStandardOutput(file.Path); @@ -206,7 +206,7 @@ namespace Wasmtime.Tests using var file = new TempFile(); - var wasi = new WasiBuilder() + var wasi = new WasiBuilder("wasi_snapshot_preview1") .WithPreopenedDirectory(Path.GetDirectoryName(file.Path), "/foo") .Build(Fixture.Module.Store); diff --git a/crates/wasi-common/src/old/snapshot_0/ctx.rs b/crates/wasi-common/src/old/snapshot_0/ctx.rs index 5b74ede010..28c1cac673 100644 --- a/crates/wasi-common/src/old/snapshot_0/ctx.rs +++ b/crates/wasi-common/src/old/snapshot_0/ctx.rs @@ -101,46 +101,46 @@ impl PendingCString { /// A builder allowing customizable construction of `WasiCtx` instances. pub struct WasiCtxBuilder { - stdin: PendingEntry, - stdout: PendingEntry, - stderr: PendingEntry, - preopens: Vec<(PathBuf, File)>, - args: Vec, - env: HashMap, + stdin: Option, + stdout: Option, + stderr: Option, + preopens: Option>, + args: Option>, + env: Option>, } impl WasiCtxBuilder { /// Builder for a new `WasiCtx`. pub fn new() -> Self { - let stdin = PendingEntry::Thunk(Entry::null); - let stdout = PendingEntry::Thunk(Entry::null); - let stderr = PendingEntry::Thunk(Entry::null); Self { - stdin, - stdout, - stderr, - preopens: Vec::new(), - args: vec![], - env: HashMap::new(), + stdin: Some(PendingEntry::Thunk(Entry::null)), + stdout: Some(PendingEntry::Thunk(Entry::null)), + stderr: Some(PendingEntry::Thunk(Entry::null)), + preopens: Some(Vec::new()), + args: Some(Vec::new()), + env: Some(HashMap::new()), } } /// Add arguments to the command-line arguments list. /// /// Arguments must be valid UTF-8 with no NUL bytes, or else `WasiCtxBuilder::build()` will fail. - pub fn args>(mut self, args: impl IntoIterator) -> Self { - self.args = args - .into_iter() - .map(|arg| arg.as_ref().to_vec().into()) - .collect(); + pub fn args>(&mut self, args: impl IntoIterator) -> &mut Self { + self.args + .as_mut() + .unwrap() + .extend(args.into_iter().map(|a| a.as_ref().to_vec().into())); self } /// Add an argument to the command-line arguments list. /// /// Arguments must be valid UTF-8 with no NUL bytes, or else `WasiCtxBuilder::build()` will fail. - pub fn arg>(mut self, arg: S) -> Self { - self.args.push(arg.as_ref().to_vec().into()); + pub fn arg>(&mut self, arg: S) -> &mut Self { + self.args + .as_mut() + .unwrap() + .push(arg.as_ref().to_vec().into()); self } @@ -148,16 +148,36 @@ impl WasiCtxBuilder { /// /// If any arguments from the host process contain invalid UTF-8, `WasiCtxBuilder::build()` will /// fail. - pub fn inherit_args(mut self) -> Self { - self.args = env::args_os().map(PendingCString::OsString).collect(); + pub fn inherit_args(&mut self) -> &mut Self { + let args = self.args.as_mut().unwrap(); + args.clear(); + args.extend(env::args_os().map(PendingCString::OsString)); + self + } + + /// Inherit stdin from the host process. + pub fn inherit_stdin(&mut self) -> &mut Self { + self.stdin = Some(PendingEntry::Thunk(Entry::duplicate_stdin)); + self + } + + /// Inherit stdout from the host process. + pub fn inherit_stdout(&mut self) -> &mut Self { + self.stdout = Some(PendingEntry::Thunk(Entry::duplicate_stdout)); + self + } + + /// Inherit stdout from the host process. + pub fn inherit_stderr(&mut self) -> &mut Self { + self.stderr = Some(PendingEntry::Thunk(Entry::duplicate_stderr)); self } /// Inherit the stdin, stdout, and stderr streams from the host process. - pub fn inherit_stdio(mut self) -> Self { - self.stdin = PendingEntry::Thunk(Entry::duplicate_stdin); - self.stdout = PendingEntry::Thunk(Entry::duplicate_stdout); - self.stderr = PendingEntry::Thunk(Entry::duplicate_stderr); + pub fn inherit_stdio(&mut self) -> &mut Self { + self.stdin = Some(PendingEntry::Thunk(Entry::duplicate_stdin)); + self.stdout = Some(PendingEntry::Thunk(Entry::duplicate_stdout)); + self.stderr = Some(PendingEntry::Thunk(Entry::duplicate_stderr)); self } @@ -165,10 +185,10 @@ impl WasiCtxBuilder { /// /// If any environment variables from the host process contain invalid Unicode (UTF-16 for /// Windows, UTF-8 for other platforms), `WasiCtxBuilder::build()` will fail. - pub fn inherit_env(mut self) -> Self { - self.env = std::env::vars_os() - .map(|(k, v)| (k.into(), v.into())) - .collect(); + pub fn inherit_env(&mut self) -> &mut Self { + let env = self.env.as_mut().unwrap(); + env.clear(); + env.extend(std::env::vars_os().map(|(k, v)| (k.into(), v.into()))); self } @@ -176,8 +196,10 @@ impl WasiCtxBuilder { /// /// Environment variable keys and values must be valid UTF-8 with no NUL bytes, or else /// `WasiCtxBuilder::build()` will fail. - pub fn env>(mut self, k: S, v: S) -> Self { + pub fn env>(&mut self, k: S, v: S) -> &mut Self { self.env + .as_mut() + .unwrap() .insert(k.as_ref().to_vec().into(), v.as_ref().to_vec().into()); self } @@ -187,40 +209,40 @@ impl WasiCtxBuilder { /// Environment variable keys and values must be valid UTF-8 with no NUL bytes, or else /// `WasiCtxBuilder::build()` will fail. pub fn envs, T: Borrow<(S, S)>>( - mut self, + &mut self, envs: impl IntoIterator, - ) -> Self { - self.env = envs - .into_iter() - .map(|t| { - let (k, v) = t.borrow(); - (k.as_ref().to_vec().into(), v.as_ref().to_vec().into()) - }) - .collect(); + ) -> &mut Self { + self.env.as_mut().unwrap().extend(envs.into_iter().map(|t| { + let (k, v) = t.borrow(); + (k.as_ref().to_vec().into(), v.as_ref().to_vec().into()) + })); self } /// Provide a File to use as stdin - pub fn stdin(mut self, file: File) -> Self { - self.stdin = PendingEntry::File(file); + pub fn stdin(&mut self, file: File) -> &mut Self { + self.stdin = Some(PendingEntry::File(file)); self } /// Provide a File to use as stdout - pub fn stdout(mut self, file: File) -> Self { - self.stdout = PendingEntry::File(file); + pub fn stdout(&mut self, file: File) -> &mut Self { + self.stdout = Some(PendingEntry::File(file)); self } /// Provide a File to use as stderr - pub fn stderr(mut self, file: File) -> Self { - self.stderr = PendingEntry::File(file); + pub fn stderr(&mut self, file: File) -> &mut Self { + self.stderr = Some(PendingEntry::File(file)); self } /// Add a preopened directory. - pub fn preopened_dir>(mut self, dir: File, guest_path: P) -> Self { - self.preopens.push((guest_path.as_ref().to_owned(), dir)); + pub fn preopened_dir>(&mut self, dir: File, guest_path: P) -> &mut Self { + self.preopens + .as_mut() + .unwrap() + .push((guest_path.as_ref().to_owned(), dir)); self } @@ -228,17 +250,21 @@ impl WasiCtxBuilder { /// /// If any of the arguments or environment variables in this builder cannot be converted into /// `CString`s, either due to NUL bytes or Unicode conversions, this returns an error. - pub fn build(self) -> WasiCtxBuilderResult { + pub fn build(&mut self) -> WasiCtxBuilderResult { // Process arguments and environment variables into `CString`s, failing quickly if they // contain any NUL bytes, or if conversion from `OsString` fails. let args = self .args + .take() + .unwrap() .into_iter() .map(|arg| arg.into_utf8_cstring()) .collect::>>()?; let env = self .env + .take() + .unwrap() .into_iter() .map(|(k, v)| { k.into_string().and_then(|mut pair| { @@ -257,12 +283,12 @@ impl WasiCtxBuilder { let mut fd_pool = FdPool::new(); let mut entries: HashMap = HashMap::new(); // Populate the non-preopen fds. - for pending in vec![self.stdin, self.stdout, self.stderr] { + for pending in &mut [&mut self.stdin, &mut self.stdout, &mut self.stderr] { let fd = fd_pool .allocate() .ok_or(WasiCtxBuilderError::TooManyFilesOpen)?; log::debug!("WasiCtx inserting ({:?}, {:?})", fd, pending); - match pending { + match pending.take().unwrap() { PendingEntry::Thunk(f) => { entries.insert(fd, f()?); } @@ -272,7 +298,7 @@ impl WasiCtxBuilder { } } // Then add the preopen fds. - for (guest_path, dir) in self.preopens { + for (guest_path, dir) in self.preopens.take().unwrap() { // We do the increment at the beginning of the loop body, so that we don't overflow // unnecessarily if we have exactly the maximum number of file descriptors. let preopen_fd = fd_pool diff --git a/crates/wasi-common/wig/src/wasi.rs b/crates/wasi-common/wig/src/wasi.rs index 2c1fcdcc1a..001bb708fb 100644 --- a/crates/wasi-common/wig/src/wasi.rs +++ b/crates/wasi-common/wig/src/wasi.rs @@ -33,14 +33,19 @@ pub fn define_struct(args: TokenStream) -> TokenStream { let mut get_exports = Vec::new(); let mut ctor_externs = Vec::new(); let mut ctor_fields = Vec::new(); + let mut linker_add = Vec::new(); for module in doc.modules() { + let module_name = module.name.as_str(); for func in module.funcs() { let name = func.name.as_str(); - let name_ident = Ident::new(func.name.as_str(), Span::call_site()); + let name_ident = Ident::new(name, Span::call_site()); fields.push(quote! { pub #name_ident: wasmtime::Func }); get_exports.push(quote! { #name => Some(&self.#name_ident) }); ctor_fields.push(name_ident.clone()); + linker_add.push(quote! { + linker.define(#module_name, #name, self.#name_ident.clone())?; + }); let mut shim_arg_decls = Vec::new(); let mut params = Vec::new(); @@ -249,6 +254,12 @@ pub fn define_struct(args: TokenStream) -> TokenStream { _ => None, } } + + /// Adds all wasi items to the specified `Linker`. + pub fn add_to_linker(&self, linker: &mut wasmtime::Linker) -> anyhow::Result<()> { + #(#linker_add)* + Ok(()) + } } } } @@ -270,10 +281,10 @@ pub fn define_struct_for_wiggle(args: TokenStream) -> TokenStream { for module in doc.modules() { let module_name = module.name.as_str(); - let module_id = Ident::new(module.name.as_str(), Span::call_site()); + let module_id = Ident::new(module_name, Span::call_site()); for func in module.funcs() { let name = func.name.as_str(); - let name_ident = Ident::new(func.name.as_str(), Span::call_site()); + let name_ident = Ident::new(name, Span::call_site()); fields.push(quote! { pub #name_ident: wasmtime::Func }); get_exports.push(quote! { #name => Some(&self.#name_ident) }); ctor_fields.push(name_ident.clone()); diff --git a/src/commands/run.rs b/src/commands/run.rs index 769491e6a6..5908215ed2 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -336,13 +336,12 @@ impl ModuleRegistry { let cx1 = cx1.build()?; - let mut cx2 = wasi_common::old::snapshot_0::WasiCtxBuilder::new() - .inherit_stdio() - .args(argv) - .envs(vars); + let mut cx2 = wasi_common::old::snapshot_0::WasiCtxBuilder::new(); + + cx2.inherit_stdio().args(argv).envs(vars); for (name, file) in preopen_dirs { - cx2 = cx2.preopened_dir(file.try_clone()?, name); + cx2.preopened_dir(file.try_clone()?, name); } let cx2 = cx2.build()?;