diff --git a/.gitignore b/.gitignore index 0ad7416262..daa279dbaf 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ rusty-tags.* *~ \#*\# docs/book +.vscode/ diff --git a/Cargo.lock b/Cargo.lock index c6979deb50..380ef58107 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1990,7 +1990,9 @@ dependencies = [ name = "wasmtime-c-api" version = "0.9.0" dependencies = [ + "wasi-common", "wasmtime", + "wasmtime-wasi", ] [[package]] diff --git a/crates/c-api/Cargo.toml b/crates/c-api/Cargo.toml index 9c1a820cb2..3b279681af 100644 --- a/crates/c-api/Cargo.toml +++ b/crates/c-api/Cargo.toml @@ -18,3 +18,5 @@ doctest = false [dependencies] wasmtime = { path = "../api" } +wasi-common = { path = "../wasi-common" } +wasmtime-wasi = { path = "../wasi" } diff --git a/crates/c-api/include/wasi.h b/crates/c-api/include/wasi.h new file mode 100644 index 0000000000..2df7df4412 --- /dev/null +++ b/crates/c-api/include/wasi.h @@ -0,0 +1,70 @@ +// WASI C API + +#ifndef WASI_H +#define WASI_H + +#include "wasm.h" + +#ifndef WASI_API_EXTERN +#ifdef _WIN32 +#define WASI_API_EXTERN __declspec(dllimport) +#else +#define WASI_API_EXTERN +#endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#define own + +#define WASI_DECLARE_OWN(name) \ + typedef struct wasi_##name##_t wasi_##name##_t; \ + WASI_API_EXTERN void wasi_##name##_delete(own wasi_##name##_t*); + +// WASI config + +WASI_DECLARE_OWN(config) + +WASI_API_EXTERN own wasi_config_t* wasi_config_new(); + +WASI_API_EXTERN void wasi_config_set_argv(wasi_config_t* config, int argc, const char* argv[]); +WASI_API_EXTERN void wasi_config_inherit_argv(wasi_config_t* config); + +WASI_API_EXTERN void wasi_config_set_env(wasi_config_t* config, int envc, const char* names[], const char* values[]); +WASI_API_EXTERN void wasi_config_inherit_env(wasi_config_t* config); + +WASI_API_EXTERN bool wasi_config_set_stdin(wasi_config_t* config, const char* path); +WASI_API_EXTERN void wasi_config_inherit_stdin(wasi_config_t* config); + +WASI_API_EXTERN bool wasi_config_set_stdout(wasi_config_t* config, const char* path); +WASI_API_EXTERN void wasi_config_inherit_stdout(wasi_config_t* config); + +WASI_API_EXTERN bool wasi_config_set_stderr(wasi_config_t* config, const char* path); +WASI_API_EXTERN void wasi_config_inherit_stderr(wasi_config_t* config); + +WASI_API_EXTERN bool wasi_config_preopen_dir(wasi_config_t* config, const char* path, const char* guest_path); + +// WASI instance + +WASI_DECLARE_OWN(instance) + +WASI_API_EXTERN own wasi_instance_t* wasi_instance_new( + wasm_store_t* store, + own wasi_config_t* config, + own wasm_trap_t** trap +); + +WASI_API_EXTERN const wasm_extern_t* wasi_instance_bind_import( + const wasi_instance_t* instance, + const wasm_importtype_t* import +); + +#undef own + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // #ifdef WASI_H \ No newline at end of file diff --git a/crates/c-api/src/lib.rs b/crates/c-api/src/lib.rs index 82c1f819d7..fdf07c435d 100644 --- a/crates/c-api/src/lib.rs +++ b/crates/c-api/src/lib.rs @@ -15,6 +15,12 @@ use wasmtime::{ Table, TableType, Trap, Val, ValType, }; +mod ext; +mod wasi; + +pub use crate::ext::*; +pub use crate::wasi::*; + macro_rules! declare_vec { ($name:ident, $elem_ty:path) => { #[repr(C)] @@ -1025,7 +1031,7 @@ pub unsafe extern "C" fn wasm_trap_new( if message[message.len() - 1] != 0 { panic!("wasm_trap_new message stringz expected"); } - let message = String::from_utf8_lossy(message); + let message = String::from_utf8_lossy(&message[..message.len() - 1]); let trap = Box::new(wasm_trap_t { trap: HostRef::new(Trap::new(message)), }); @@ -1777,7 +1783,3 @@ pub unsafe extern "C" fn wasm_valtype_vec_copy( let slice = slice::from_raw_parts((*src).data, (*src).size); (*out).set_from_slice(slice); } - -mod ext; - -pub use crate::ext::*; diff --git a/crates/c-api/src/wasi.rs b/crates/c-api/src/wasi.rs new file mode 100644 index 0000000000..b709750e8e --- /dev/null +++ b/crates/c-api/src/wasi.rs @@ -0,0 +1,274 @@ +//! The WASI embedding API definitions for Wasmtime. +use crate::{wasm_extern_t, wasm_importtype_t, wasm_store_t, wasm_trap_t, ExternHost, ExternType}; +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::slice; +use wasi_common::{preopen_dir, WasiCtxBuilder}; +use wasmtime::{HostRef, Trap}; +use wasmtime_wasi::Wasi; + +unsafe fn cstr_to_path<'a>(path: *const c_char) -> Option<&'a Path> { + CStr::from_ptr(path).to_str().map(Path::new).ok() +} + +unsafe fn open_file(path: *const c_char) -> Option { + File::open(cstr_to_path(path)?).ok() +} + +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, +} + +impl wasi_config_t {} + +#[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(), + })) +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_delete(config: *mut wasi_config_t) { + drop(Box::from_raw(config)); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_set_argv( + config: *mut wasi_config_t, + argc: c_int, + argv: *const *const c_char, +) { + let mut config = Box::from_raw(config); + config.builder = 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())), + ); + std::mem::forget(config); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_inherit_argv(config: *mut wasi_config_t) { + let mut config = Box::from_raw(config); + config.builder = config.builder.inherit_args(); + std::mem::forget(config); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_set_env( + config: *mut wasi_config_t, + envc: c_int, + names: *const *const c_char, + values: *const *const c_char, +) { + let names = slice::from_raw_parts(names, envc as usize); + let values = slice::from_raw_parts(values, envc as usize); + let mut config = Box::from_raw(config); + + for i in 0..envc as usize { + config.builder = config.builder.env( + CStr::from_ptr(names[i]).to_bytes(), + CStr::from_ptr(values[i]).to_bytes(), + ); + } + + std::mem::forget(config); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_inherit_env(config: *mut wasi_config_t) { + let mut config = Box::from_raw(config); + config.builder = config.builder.inherit_env(); + std::mem::forget(config); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_set_stdin( + config: *mut wasi_config_t, + path: *const c_char, +) -> bool { + let file = match open_file(path) { + Some(f) => f, + None => return false, + }; + + let mut config = Box::from_raw(config); + config.builder = config.builder.stdin(file); + std::mem::forget(config); + + true +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_inherit_stdin(config: *mut wasi_config_t) { + let mut config = Box::from_raw(config); + config.builder = config.builder.inherit_stdin(); + std::mem::forget(config); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_set_stdout( + config: *mut wasi_config_t, + path: *const c_char, +) -> bool { + let file = match create_file(path) { + Some(f) => f, + None => return false, + }; + + let mut config = Box::from_raw(config); + config.builder = config.builder.stdout(file); + std::mem::forget(config); + + true +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_inherit_stdout(config: *mut wasi_config_t) { + let mut config = Box::from_raw(config); + config.builder = config.builder.inherit_stdout(); + std::mem::forget(config); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_set_stderr( + config: *mut wasi_config_t, + path: *const c_char, +) -> bool { + let file = match create_file(path) { + Some(f) => f, + None => return false, + }; + + let mut config = Box::from_raw(config); + config.builder = config.builder.stderr(file); + std::mem::forget(config); + + true +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_inherit_stderr(config: *mut wasi_config_t) { + let mut config = Box::from_raw(config); + config.builder = config.builder.inherit_stderr(); + std::mem::forget(config); +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_config_preopen_dir( + config: *mut wasi_config_t, + path: *const c_char, + guest_path: *const c_char, +) -> bool { + let guest_path = match cstr_to_path(guest_path) { + Some(p) => p, + None => return false, + }; + + let dir = match cstr_to_path(path) { + Some(p) => match preopen_dir(p) { + Ok(d) => d, + Err(_) => return false, + }, + None => return false, + }; + + let mut config = Box::from_raw(config); + config.builder = config.builder.preopened_dir(dir, guest_path); + std::mem::forget(config); + + true +} + +#[repr(C)] +pub struct wasi_instance_t { + wasi: Wasi, + export_cache: HashMap, +} + +impl Drop for wasi_instance_t { + fn drop(&mut self) { + for v in self.export_cache.values() { + drop(unsafe { Box::from_raw(*v) }); + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_instance_new( + store: *mut wasm_store_t, + config: *mut wasi_config_t, + trap: *mut *mut wasm_trap_t, +) -> *mut wasi_instance_t { + let store = &(*store).store.borrow(); + 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), + export_cache: HashMap::new(), + })), + Err(e) => { + (*trap) = Box::into_raw(Box::new(wasm_trap_t { + trap: HostRef::new(Trap::new(e.to_string())), + })); + + std::ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn wasi_instance_delete(instance: *mut wasi_instance_t) { + drop(Box::from_raw(instance)); +} + +#[no_mangle] +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 name = (*import).ty.name(); + + match (*instance).wasi.get_export(name) { + Some(export) => { + if export.ty().params() != func_type.params() { + return std::ptr::null_mut(); + } + + if export.ty().results() != func_type.results() { + return std::ptr::null_mut(); + } + + *(*instance) + .export_cache + .entry(name.to_string()) + .or_insert_with(|| { + Box::into_raw(Box::new(wasm_extern_t { + which: ExternHost::Func(HostRef::new(export.clone())), + })) + }) + } + None => std::ptr::null_mut(), + } +} diff --git a/crates/misc/dotnet/docs/articles/intro.md b/crates/misc/dotnet/docs/articles/intro.md index 6b1d2b9552..f0e572adb3 100644 --- a/crates/misc/dotnet/docs/articles/intro.md +++ b/crates/misc/dotnet/docs/articles/intro.md @@ -142,13 +142,12 @@ namespace Tutorial { static void Main(string[] args) { - using (var engine = new Engine()) - using (var store = engine.CreateStore()) - using (var module = store.CreateModule("hello.wasm")) - using (dynamic instance = module.Instantiate(new Host())) - { - instance.run(); - } + using var engine = new Engine(); + using var store = engine.CreateStore(); + using var module = store.CreateModule("hello.wasm"); + using dynamic instance = module.Instantiate(new Host()); + + instance.run(); } } } diff --git a/crates/misc/dotnet/examples/global/Program.cs b/crates/misc/dotnet/examples/global/Program.cs index 4dcf32b131..b8e499485e 100644 --- a/crates/misc/dotnet/examples/global/Program.cs +++ b/crates/misc/dotnet/examples/global/Program.cs @@ -21,13 +21,12 @@ namespace HelloExample { static void Main(string[] args) { - using (var engine = new Engine()) - using (var store = engine.CreateStore()) - using (var module = store.CreateModule("global.wasm")) - using (dynamic instance = module.Instantiate(new Host())) - { - instance.run(20); - } + using var engine = new Engine(); + using var store = engine.CreateStore(); + using var module = store.CreateModule("global.wasm"); + using dynamic instance = module.Instantiate(new Host()); + + instance.run(20); } } } diff --git a/crates/misc/dotnet/examples/hello/Program.cs b/crates/misc/dotnet/examples/hello/Program.cs index c4d853386b..ad07f26e29 100644 --- a/crates/misc/dotnet/examples/hello/Program.cs +++ b/crates/misc/dotnet/examples/hello/Program.cs @@ -18,13 +18,12 @@ namespace HelloExample { static void Main(string[] args) { - using (var engine = new Engine()) - using (var store = engine.CreateStore()) - using (var module = store.CreateModule("hello.wasm")) - using (dynamic instance = module.Instantiate(new Host())) - { - instance.run(); - } + using var engine = new Engine(); + using var store = engine.CreateStore(); + using var module = store.CreateModule("hello.wasm"); + using dynamic instance = module.Instantiate(new Host()); + + instance.run(); } } } diff --git a/crates/misc/dotnet/examples/memory/Program.cs b/crates/misc/dotnet/examples/memory/Program.cs index f42b806fec..349899ca18 100644 --- a/crates/misc/dotnet/examples/memory/Program.cs +++ b/crates/misc/dotnet/examples/memory/Program.cs @@ -19,13 +19,12 @@ namespace HelloExample { static void Main(string[] args) { - using (var engine = new Engine()) - using (var store = engine.CreateStore()) - using (var module = store.CreateModule("memory.wasm")) - using (dynamic instance = module.Instantiate(new Host())) - { - instance.run(); - } + using var engine = new Engine(); + using var store = engine.CreateStore(); + using var module = store.CreateModule("memory.wasm"); + using dynamic instance = module.Instantiate(new Host()); + + instance.run(); } } } diff --git a/crates/misc/dotnet/src/Bindings/Binding.cs b/crates/misc/dotnet/src/Bindings/Binding.cs index f8d1a972a3..3ca964ec0c 100644 --- a/crates/misc/dotnet/src/Bindings/Binding.cs +++ b/crates/misc/dotnet/src/Bindings/Binding.cs @@ -10,35 +10,37 @@ namespace Wasmtime.Bindings /// /// Represents an abstract host binding. /// - public abstract class Binding + internal abstract class Binding { - internal abstract SafeHandle Bind(Store store, IHost host); + public abstract SafeHandle Bind(Store store, IHost host); - internal static void ThrowBindingException(Import import, MemberInfo member, string message) + public static WasmtimeException CreateBindingException(Import import, MemberInfo member, string message) { - throw new WasmtimeException($"Unable to bind '{member.DeclaringType.Name}.{member.Name}' to WebAssembly import '{import}': {message}."); + return new WasmtimeException($"Unable to bind '{member.DeclaringType.Name}.{member.Name}' to WebAssembly import '{import}': {message}."); } - internal static List GetImportBindings(IHost host, Module module) + public static List GetImportBindings(Module module, Wasi wasi = null, IHost host = null) { - if (host is null) - { - throw new ArgumentNullException(nameof(host)); - } - if (module is null) { throw new ArgumentNullException(nameof(module)); } - var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; - var type = host.GetType(); - var methods = type.GetMethods(flags).Where(m => !m.IsSpecialName && Attribute.IsDefined(m, typeof(ImportAttribute))); - var fields = type.GetFields(flags).Where(m => !m.IsSpecialName && Attribute.IsDefined(m, typeof(ImportAttribute))); - var bindings = new List(); + var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; + var type = host?.GetType(); + var methods = type?.GetMethods(flags).Where(m => !m.IsSpecialName && Attribute.IsDefined(m, typeof(ImportAttribute))); + var fields = type?.GetFields(flags).Where(m => !m.IsSpecialName && Attribute.IsDefined(m, typeof(ImportAttribute))); + foreach (var import in module.Imports.All) { + var wasiBinding = wasi?.Bind(import); + if (!(wasiBinding is null)) + { + bindings.Add(wasiBinding); + continue; + } + switch (import) { case FunctionImport func: @@ -63,7 +65,7 @@ namespace Wasmtime.Bindings private static FunctionBinding BindFunction(FunctionImport import, IEnumerable methods) { - var method = methods.Where(m => + var method = methods?.Where(m => { var attribute = (ImportAttribute)m.GetCustomAttribute(typeof(ImportAttribute)); if (attribute is null) @@ -88,7 +90,7 @@ namespace Wasmtime.Bindings private static GlobalBinding BindGlobal(GlobalImport import, IEnumerable fields) { - var field = fields.Where(f => + var field = fields?.Where(f => { var attribute = (ImportAttribute)f.GetCustomAttribute(typeof(ImportAttribute)); return attribute.Name == import.Name && @@ -108,7 +110,7 @@ namespace Wasmtime.Bindings private static MemoryBinding BindMemory(MemoryImport import, IEnumerable fields) { - var field = fields.Where(f => + var field = fields?.Where(f => { var attribute = (ImportAttribute)f.GetCustomAttribute(typeof(ImportAttribute)); return attribute.Name == import.Name && diff --git a/crates/misc/dotnet/src/Bindings/FunctionBinding.cs b/crates/misc/dotnet/src/Bindings/FunctionBinding.cs index 632fa733da..6d574f2412 100644 --- a/crates/misc/dotnet/src/Bindings/FunctionBinding.cs +++ b/crates/misc/dotnet/src/Bindings/FunctionBinding.cs @@ -11,7 +11,7 @@ namespace Wasmtime.Bindings /// /// Represents a host function binding. /// - public class FunctionBinding : Binding + internal class FunctionBinding : Binding { /// /// Constructs a new function binding. @@ -46,22 +46,19 @@ namespace Wasmtime.Bindings /// public MethodInfo Method { get; private set; } - internal override SafeHandle Bind(Store store, IHost host) + public override SafeHandle Bind(Store store, IHost host) { unsafe { - if (_callback != null) - { - throw new InvalidOperationException("Cannot bind more than once."); - } - - _callback = CreateCallback(store, host); - var parameters = Interop.ToValueTypeVec(Import.Parameters); var results = Interop.ToValueTypeVec(Import.Results); using (var funcType = Interop.wasm_functype_new(ref parameters, ref results)) { - return Interop.wasm_func_new(store.Handle, funcType, _callback); + var callback = CreateCallback(store, host); + var func = Interop.wasm_func_new(store.Handle, funcType, callback); + // Store the callback with the safe handle to keep the delegate GC reachable + func.Callback = callback; + return func; } } } @@ -70,17 +67,17 @@ namespace Wasmtime.Bindings { if (Method.IsStatic) { - ThrowBindingException(Import, Method, "method cannot be static"); + throw CreateBindingException(Import, Method, "method cannot be static"); } if (Method.IsGenericMethod) { - ThrowBindingException(Import, Method, "method cannot be generic"); + throw CreateBindingException(Import, Method, "method cannot be generic"); } if (Method.IsConstructor) { - ThrowBindingException(Import, Method, "method cannot be a constructor"); + throw CreateBindingException(Import, Method, "method cannot be a constructor"); } ValidateParameters(); @@ -93,7 +90,7 @@ namespace Wasmtime.Bindings var parameters = Method.GetParameters(); if (parameters.Length != Import.Parameters.Count) { - ThrowBindingException( + throw CreateBindingException( Import, Method, $"parameter mismatch: import requires {Import.Parameters.Count} but the method has {parameters.Length}"); @@ -106,18 +103,18 @@ namespace Wasmtime.Bindings { if (parameter.IsOut) { - ThrowBindingException(Import, Method, $"parameter '{parameter.Name}' cannot be an 'out' parameter"); + throw CreateBindingException(Import, Method, $"parameter '{parameter.Name}' cannot be an 'out' parameter"); } else { - ThrowBindingException(Import, Method, $"parameter '{parameter.Name}' cannot be a 'ref' parameter"); + throw CreateBindingException(Import, Method, $"parameter '{parameter.Name}' cannot be a 'ref' parameter"); } } var expected = Import.Parameters[i]; if (!Interop.TryGetValueKind(parameter.ParameterType, out var kind) || !Interop.IsMatchingKind(kind, expected)) { - ThrowBindingException(Import, Method, $"method parameter '{parameter.Name}' is expected to be of type '{Interop.ToString(expected)}'"); + throw CreateBindingException(Import, Method, $"method parameter '{parameter.Name}' is expected to be of type '{Interop.ToString(expected)}'"); } } } @@ -129,7 +126,7 @@ namespace Wasmtime.Bindings { if (Method.ReturnType != typeof(void)) { - ThrowBindingException(Import, Method, "method must return void"); + throw CreateBindingException(Import, Method, "method must return void"); } } else if (resultsCount == 1) @@ -137,14 +134,14 @@ namespace Wasmtime.Bindings var expected = Import.Results[0]; if (!Interop.TryGetValueKind(Method.ReturnType, out var kind) || !Interop.IsMatchingKind(kind, expected)) { - ThrowBindingException(Import, Method, $"return type is expected to be '{Interop.ToString(expected)}'"); + throw CreateBindingException(Import, Method, $"return type is expected to be '{Interop.ToString(expected)}'"); } } else { if (!IsTupleOfSize(Method.ReturnType, resultsCount)) { - ThrowBindingException(Import, Method, $"return type is expected to be a tuple of size {resultsCount}"); + throw CreateBindingException(Import, Method, $"return type is expected to be a tuple of size {resultsCount}"); } var typeArguments = @@ -163,7 +160,7 @@ namespace Wasmtime.Bindings var expected = Import.Results[i]; if (!Interop.TryGetValueKind(typeArgument, out var kind) || !Interop.IsMatchingKind(kind, expected)) { - ThrowBindingException(Import, Method, $"return tuple item #{i} is expected to be of type '{Interop.ToString(expected)}'"); + throw CreateBindingException(Import, Method, $"return tuple item #{i} is expected to be of type '{Interop.ToString(expected)}'"); } ++i; @@ -340,7 +337,5 @@ namespace Wasmtime.Bindings throw new NotSupportedException("Unsupported return value type."); } } - - private Interop.WasmFuncCallback _callback; } } diff --git a/crates/misc/dotnet/src/Bindings/GlobalBinding.cs b/crates/misc/dotnet/src/Bindings/GlobalBinding.cs index 33ebeb71c0..69df579868 100644 --- a/crates/misc/dotnet/src/Bindings/GlobalBinding.cs +++ b/crates/misc/dotnet/src/Bindings/GlobalBinding.cs @@ -8,7 +8,7 @@ namespace Wasmtime.Bindings /// /// Represents a host global binding. /// - public class GlobalBinding : Binding + internal class GlobalBinding : Binding { /// /// Constructs a new global binding. @@ -43,12 +43,12 @@ namespace Wasmtime.Bindings /// public FieldInfo Field { get; private set; } - internal override SafeHandle Bind(Store store, IHost host) + public override SafeHandle Bind(Store store, IHost host) { unsafe { dynamic global = Field.GetValue(host); - if (global.Handle != null) + if (!(global.Handle is null)) { throw new InvalidOperationException("Cannot bind more than once."); } @@ -74,17 +74,17 @@ namespace Wasmtime.Bindings { if (Field.IsStatic) { - ThrowBindingException(Import, Field, "field cannot be static"); + throw CreateBindingException(Import, Field, "field cannot be static"); } if (!Field.IsInitOnly) { - ThrowBindingException(Import, Field, "field must be readonly"); + throw CreateBindingException(Import, Field, "field must be readonly"); } if (!Field.FieldType.IsGenericType) { - ThrowBindingException(Import, Field, "field is expected to be of type 'Global'"); + throw CreateBindingException(Import, Field, "field is expected to be of type 'Global'"); } var definition = Field.FieldType.GetGenericTypeDefinition(); @@ -92,19 +92,19 @@ namespace Wasmtime.Bindings { if (Import.IsMutable) { - ThrowBindingException(Import, Field, "the import is mutable (use the 'MutableGlobal' type)"); + throw CreateBindingException(Import, Field, "the import is mutable (use the 'MutableGlobal' type)"); } } else if (definition == typeof(MutableGlobal<>)) { if (!Import.IsMutable) { - ThrowBindingException(Import, Field, "the import is constant (use the 'Global' type)"); + throw CreateBindingException(Import, Field, "the import is constant (use the 'Global' type)"); } } else { - ThrowBindingException(Import, Field, "field is expected to be of type 'Global' or 'MutableGlobal'"); + throw CreateBindingException(Import, Field, "field is expected to be of type 'Global' or 'MutableGlobal'"); } var arg = Field.FieldType.GetGenericArguments()[0]; @@ -113,12 +113,12 @@ namespace Wasmtime.Bindings { if (!Interop.IsMatchingKind(kind, Import.Kind)) { - ThrowBindingException(Import, Field, $"global type argument is expected to be of type '{Interop.ToString(Import.Kind)}'"); + throw CreateBindingException(Import, Field, $"global type argument is expected to be of type '{Interop.ToString(Import.Kind)}'"); } } else { - ThrowBindingException(Import, Field, $"'{arg}' is not a valid global type"); + throw CreateBindingException(Import, Field, $"'{arg}' is not a valid global type"); } } } diff --git a/crates/misc/dotnet/src/Bindings/MemoryBinding.cs b/crates/misc/dotnet/src/Bindings/MemoryBinding.cs index 790f3c5ddb..f2a10c7061 100644 --- a/crates/misc/dotnet/src/Bindings/MemoryBinding.cs +++ b/crates/misc/dotnet/src/Bindings/MemoryBinding.cs @@ -8,7 +8,7 @@ namespace Wasmtime.Bindings /// /// Represents a host memory binding. /// - public class MemoryBinding : Binding + internal class MemoryBinding : Binding { /// /// Constructs a new memory binding. @@ -43,10 +43,10 @@ namespace Wasmtime.Bindings /// public FieldInfo Field { get; private set; } - internal override SafeHandle Bind(Store store, IHost host) + public override SafeHandle Bind(Store store, IHost host) { Memory memory = (Memory)Field.GetValue(host); - if (memory.Handle != null) + if (!(memory.Handle is null)) { throw new InvalidOperationException("Cannot bind more than once."); } @@ -56,11 +56,11 @@ namespace Wasmtime.Bindings if (min != Import.Minimum) { - ThrowBindingException(Import, Field, $"Memory does not have the expected minimum of {Import.Minimum} page(s)"); + throw CreateBindingException(Import, Field, $"Memory does not have the expected minimum of {Import.Minimum} page(s)"); } if (max != Import.Maximum) { - ThrowBindingException(Import, Field, $"Memory does not have the expected maximum of {Import.Maximum} page(s)"); + throw CreateBindingException(Import, Field, $"Memory does not have the expected maximum of {Import.Maximum} page(s)"); } unsafe @@ -81,17 +81,17 @@ namespace Wasmtime.Bindings { if (Field.IsStatic) { - ThrowBindingException(Import, Field, "field cannot be static"); + throw CreateBindingException(Import, Field, "field cannot be static"); } if (!Field.IsInitOnly) { - ThrowBindingException(Import, Field, "field must be readonly"); + throw CreateBindingException(Import, Field, "field must be readonly"); } if (Field.FieldType != typeof(Memory)) { - ThrowBindingException(Import, Field, "field is expected to be of type 'Memory'"); + throw CreateBindingException(Import, Field, "field is expected to be of type 'Memory'"); } } } diff --git a/crates/misc/dotnet/src/Bindings/WasiBinding.cs b/crates/misc/dotnet/src/Bindings/WasiBinding.cs new file mode 100644 index 0000000000..4541f808c7 --- /dev/null +++ b/crates/misc/dotnet/src/Bindings/WasiBinding.cs @@ -0,0 +1,23 @@ +using System; +using System.Runtime.InteropServices; + +namespace Wasmtime.Bindings +{ + /// + /// Represents a binding to a WASI export. + /// + internal class WasiBinding : Binding + { + public WasiBinding(IntPtr handle) + { + _handle = handle; + } + + public override SafeHandle Bind(Store store, IHost host) + { + return new Interop.WasiExportHandle(_handle); + } + + private IntPtr _handle; + } +} diff --git a/crates/misc/dotnet/src/Externs/ExternMemory.cs b/crates/misc/dotnet/src/Externs/ExternMemory.cs index a9011aacb7..e86be4f341 100644 --- a/crates/misc/dotnet/src/Externs/ExternMemory.cs +++ b/crates/misc/dotnet/src/Externs/ExternMemory.cs @@ -63,6 +63,23 @@ namespace Wasmtime.Externs return Encoding.UTF8.GetString(Span.Slice(address, length)); } + /// + /// Reads a null-terminated UTF-8 string from memory. + /// + /// The zero-based address to read from. + /// Returns the string read from memory. + public string ReadNullTerminatedString(int address) + { + var slice = Span.Slice(address); + var terminator = slice.IndexOf((byte)0); + if (terminator == -1) + { + throw new InvalidOperationException("string is not null terminated"); + } + + return Encoding.UTF8.GetString(slice.Slice(0, terminator)); + } + /// /// Writes a UTF-8 string at the given address. /// diff --git a/crates/misc/dotnet/src/IHost.cs b/crates/misc/dotnet/src/IHost.cs index f8a3f4175f..1734c2d9f1 100644 --- a/crates/misc/dotnet/src/IHost.cs +++ b/crates/misc/dotnet/src/IHost.cs @@ -14,12 +14,5 @@ namespace Wasmtime /// /// A host can only bind to one module instance at a time. Instance Instance { get; set; } - - /// - /// Gets the import bindings of the host given a WebAssembly module. - /// - /// The WebAssembly module to get the import bindings for. - /// Returns the list of import bindings for the host. - List GetImportBindings(Module module) => Binding.GetImportBindings(this, module); } } diff --git a/crates/misc/dotnet/src/Imports/Import.cs b/crates/misc/dotnet/src/Imports/Import.cs index f2e8115027..09cf354fd0 100644 --- a/crates/misc/dotnet/src/Imports/Import.cs +++ b/crates/misc/dotnet/src/Imports/Import.cs @@ -12,10 +12,12 @@ namespace Wasmtime.Imports { unsafe { - var moduleName = Interop.wasm_importtype_module(importType); + Handle = importType; + + var moduleName = Interop.wasm_importtype_module(Handle); ModuleName = Marshal.PtrToStringUTF8((IntPtr)moduleName->data, (int)moduleName->size); - var name = Interop.wasm_importtype_name(importType); + var name = Interop.wasm_importtype_name(Handle); Name = Marshal.PtrToStringUTF8((IntPtr)name->data, (int)name->size); } } @@ -30,6 +32,8 @@ namespace Wasmtime.Imports /// public string Name { get; private set; } + internal IntPtr Handle { get; private set; } + /// public override string ToString() { diff --git a/crates/misc/dotnet/src/Imports/Imports.cs b/crates/misc/dotnet/src/Imports/Imports.cs index 153fee3363..bda7796e58 100644 --- a/crates/misc/dotnet/src/Imports/Imports.cs +++ b/crates/misc/dotnet/src/Imports/Imports.cs @@ -6,69 +6,72 @@ namespace Wasmtime.Imports /// /// Represents imported functions, globals, tables, and memories to a WebAssembly module. /// - public class Imports + public class Imports : IDisposable { internal Imports(Module module) { Interop.wasm_importtype_vec_t imports; Interop.wasm_module_imports(module.Handle, out imports); - try + var all = new List((int)imports.size); + var functions = new List(); + var globals = new List(); + var tables = new List(); + var memories = new List(); + + for (int i = 0; i < (int)imports.size; ++i) { - var all = new List((int)imports.size); - var functions = new List(); - var globals = new List(); - var tables = new List(); - var memories = new List(); - - for (int i = 0; i < (int)imports.size; ++i) + unsafe { - unsafe + var importType = imports.data[i]; + var externType = Interop.wasm_importtype_type(importType); + + switch (Interop.wasm_externtype_kind(externType)) { - var importType = imports.data[i]; - var externType = Interop.wasm_importtype_type(importType); + case Interop.wasm_externkind_t.WASM_EXTERN_FUNC: + var function = new FunctionImport(importType, externType); + functions.Add(function); + all.Add(function); + break; - switch (Interop.wasm_externtype_kind(externType)) - { - case Interop.wasm_externkind_t.WASM_EXTERN_FUNC: - var function = new FunctionImport(importType, externType); - functions.Add(function); - all.Add(function); - break; + case Interop.wasm_externkind_t.WASM_EXTERN_GLOBAL: + var global = new GlobalImport(importType, externType); + globals.Add(global); + all.Add(global); + break; - case Interop.wasm_externkind_t.WASM_EXTERN_GLOBAL: - var global = new GlobalImport(importType, externType); - globals.Add(global); - all.Add(global); - break; + case Interop.wasm_externkind_t.WASM_EXTERN_TABLE: + var table = new TableImport(importType, externType); + tables.Add(table); + all.Add(table); + break; - case Interop.wasm_externkind_t.WASM_EXTERN_TABLE: - var table = new TableImport(importType, externType); - tables.Add(table); - all.Add(table); - break; + case Interop.wasm_externkind_t.WASM_EXTERN_MEMORY: + var memory = new MemoryImport(importType, externType); + memories.Add(memory); + all.Add(memory); + break; - case Interop.wasm_externkind_t.WASM_EXTERN_MEMORY: - var memory = new MemoryImport(importType, externType); - memories.Add(memory); - all.Add(memory); - break; - - default: - throw new NotSupportedException("Unsupported import extern type."); - } + default: + throw new NotSupportedException("Unsupported import extern type."); } } - - Functions = functions; - Globals = globals; - Tables = tables; - Memories = memories; - All = all; } - finally + + Functions = functions; + Globals = globals; + Tables = tables; + Memories = memories; + All = all; + } + + /// + public unsafe void Dispose() + { + if (!(_imports.data is null)) { - Interop.wasm_importtype_vec_delete(ref imports); + Interop.wasm_importtype_vec_delete(ref _imports); + _imports.data = null; } } @@ -93,5 +96,7 @@ namespace Wasmtime.Imports public IReadOnlyList Memories { get; private set; } internal IReadOnlyList All { get; private set; } + + private Interop.wasm_importtype_vec_t _imports; } } diff --git a/crates/misc/dotnet/src/Instance.cs b/crates/misc/dotnet/src/Instance.cs index 86f0b3f825..f274101d08 100644 --- a/crates/misc/dotnet/src/Instance.cs +++ b/crates/misc/dotnet/src/Instance.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Runtime.InteropServices; using System.Dynamic; using Wasmtime.Externs; +using Wasmtime.Bindings; namespace Wasmtime { @@ -12,22 +13,23 @@ namespace Wasmtime /// public class Instance : DynamicObject, IDisposable { - internal Instance(Module module, IHost host) + internal Instance(Module module, Wasi wasi = null, IHost host = null) { Host = host; Module = module; - //Save the bindings to root the objects. - //Otherwise the GC may collect the delegates from ExternFunction for example. - _bindings = host.GetImportBindings(module); - var handles = _bindings.Select(b => b.Bind(module.Store, host)).ToList(); + // Save the bindings to root the objects. + // Otherwise the GC may collect the callback delegates from FunctionHandles for example. + _bindings = Binding.GetImportBindings(module, wasi, host) + .Select(b => b.Bind(module.Store, host)) + .ToArray(); unsafe { Handle = Interop.wasm_instance_new( Module.Store.Handle, Module.Handle, - handles.Select(h => ToExtern(h)).ToArray(), + _bindings.Select(h => ToExtern(h)).ToArray(), out var trap); if (trap != IntPtr.Zero) @@ -41,12 +43,6 @@ namespace Wasmtime throw new WasmtimeException($"Failed to instantiate module '{module.Name}'."); } - // Dispose of all function handles (not needed at runtime) - foreach (var h in handles.Where(h => h is Interop.FunctionHandle)) - { - h.Dispose(); - } - Interop.wasm_instance_exports(Handle, out _externs); Externs = new Wasmtime.Externs.Externs(Module.Exports, _externs); @@ -71,17 +67,27 @@ namespace Wasmtime public Wasmtime.Externs.Externs Externs { get; private set; } /// - public void Dispose() + public unsafe void Dispose() { if (!Handle.IsInvalid) { Handle.Dispose(); Handle.SetHandleAsInvalid(); } - if (_externs.size != UIntPtr.Zero) + + if (!(_bindings is null)) + { + foreach (var binding in _bindings) + { + binding.Dispose(); + } + _bindings = null; + } + + if (!(_externs.data is null)) { Interop.wasm_extern_vec_delete(ref _externs); - _externs.size = UIntPtr.Zero; + _externs.data = null; } } @@ -134,15 +140,18 @@ namespace Wasmtime case Interop.MemoryHandle m: return Interop.wasm_memory_as_extern(m); + case Interop.WasiExportHandle w: + return w.DangerousGetHandle(); + default: throw new NotSupportedException("Unexpected handle type."); } } internal Interop.InstanceHandle Handle { get; private set; } + private SafeHandle[] _bindings; private Interop.wasm_extern_vec_t _externs; private Dictionary _functions; private Dictionary _globals; - private List _bindings; } } diff --git a/crates/misc/dotnet/src/Interop.cs b/crates/misc/dotnet/src/Interop.cs index a7d9796175..2fd904f860 100644 --- a/crates/misc/dotnet/src/Interop.cs +++ b/crates/misc/dotnet/src/Interop.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; +using System.Text; namespace Wasmtime { @@ -10,6 +11,8 @@ namespace Wasmtime /// See https://github.com/WebAssembly/wasm-c-api/blob/master/include/wasm.h for the C API reference. internal static class Interop { + const string LibraryName = "wasmtime"; + internal class EngineHandle : SafeHandle { public EngineHandle() : base(IntPtr.Zero, true) @@ -61,6 +64,8 @@ namespace Wasmtime { } + public WasmFuncCallback Callback { get; set; } = null; + public override bool IsInvalid => handle == IntPtr.Zero; protected override bool ReleaseHandle() @@ -175,6 +180,51 @@ namespace Wasmtime } } + internal class WasiConfigHandle : SafeHandle + { + public WasiConfigHandle() : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.wasi_config_delete(handle); + return true; + } + } + + internal class WasiInstanceHandle : SafeHandle + { + public WasiInstanceHandle() : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.wasi_instance_delete(handle); + return true; + } + } + + internal class WasiExportHandle : SafeHandle + { + public WasiExportHandle(IntPtr handle) : base(IntPtr.Zero, false /* not owned */) + { + SetHandle(handle); + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + return true; + } + } + [StructLayout(LayoutKind.Sequential)] internal unsafe struct wasm_byte_vec_t { @@ -440,323 +490,419 @@ namespace Wasmtime return vec; } + internal static unsafe (byte*[], GCHandle[]) ToUTF8PtrArray(IList strings) + { + // Unfortunately .NET cannot currently marshal string[] as UTF-8 + // See: https://github.com/dotnet/runtime/issues/7315 + // Therefore, we need to marshal the strings manually + var handles = new GCHandle[strings.Count]; + for (int i = 0; i < strings.Count; ++i) + { + handles[i] = GCHandle.Alloc( + Encoding.UTF8.GetBytes(strings[i]), + GCHandleType.Pinned + ); + } + + var ptrs = new byte*[strings.Count]; + for (int i = 0; i < strings.Count; ++i) + { + ptrs[i] = (byte*)handles[i].AddrOfPinnedObject(); + } + + return (ptrs, handles); + } + // Engine imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern EngineHandle wasm_engine_new(); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_engine_delete(IntPtr engine); // Store imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern StoreHandle wasm_store_new(EngineHandle engine); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_store_delete(IntPtr engine); // Byte vec imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_byte_vec_new_empty(out wasm_byte_vec_t vec); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_byte_vec_new_uninitialized(out wasm_byte_vec_t vec, UIntPtr length); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_byte_vec_new(out wasm_byte_vec_t vec, UIntPtr length, byte[] data); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_byte_vec_copy(out wasm_byte_vec_t vec, ref wasm_byte_vec_t src); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_byte_vec_delete(ref wasm_byte_vec_t vec); // Value type vec imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_valtype_vec_new_empty(out wasm_valtype_vec_t vec); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_valtype_vec_new_uninitialized(out wasm_valtype_vec_t vec, UIntPtr length); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_valtype_vec_new(out wasm_valtype_vec_t vec, UIntPtr length, IntPtr[] data); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_valtype_vec_copy(out wasm_valtype_vec_t vec, ref wasm_valtype_vec_t src); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_valtype_vec_delete(ref wasm_valtype_vec_t vec); // Extern vec imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_extern_vec_new_empty(out wasm_extern_vec_t vec); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_extern_vec_new_uninitialized(out wasm_extern_vec_t vec, UIntPtr length); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_extern_vec_new(out wasm_extern_vec_t vec, UIntPtr length, IntPtr[] data); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_extern_vec_copy(out wasm_extern_vec_t vec, ref wasm_extern_vec_t src); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_extern_vec_delete(ref wasm_extern_vec_t vec); // Import type vec imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_importtype_vec_new_empty(out wasm_importtype_vec_t vec); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_importtype_vec_new_uninitialized(out wasm_importtype_vec_t vec, UIntPtr length); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_importtype_vec_new(out wasm_importtype_vec_t vec, UIntPtr length, IntPtr[] data); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_importtype_vec_copy(out wasm_importtype_vec_t vec, ref wasm_importtype_vec_t src); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_importtype_vec_delete(ref wasm_importtype_vec_t vec); // Export type vec imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_exporttype_vec_new_empty(out wasm_exporttype_vec_t vec); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_exporttype_vec_new_uninitialized(out wasm_exporttype_vec_t vec, UIntPtr length); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_exporttype_vec_new(out wasm_exporttype_vec_t vec, UIntPtr length, IntPtr[] data); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_exporttype_vec_copy(out wasm_exporttype_vec_t vec, ref wasm_exporttype_vec_t src); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_exporttype_vec_delete(ref wasm_exporttype_vec_t vec); // Import type imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern unsafe wasm_byte_vec_t* wasm_importtype_module(IntPtr importType); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern unsafe wasm_byte_vec_t* wasm_importtype_name(IntPtr importType); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern unsafe IntPtr wasm_importtype_type(IntPtr importType); // Export type imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern unsafe wasm_byte_vec_t* wasm_exporttype_name(IntPtr exportType); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern unsafe IntPtr wasm_exporttype_type(IntPtr exportType); // Module imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern ModuleHandle wasm_module_new(StoreHandle store, ref wasm_byte_vec_t bytes); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_module_imports(ModuleHandle module, out wasm_importtype_vec_t imports); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_module_exports(ModuleHandle module, out wasm_exporttype_vec_t exports); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_module_delete(IntPtr module); // Value type imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern ValueTypeHandle wasm_valtype_new(wasm_valkind_t kind); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_valtype_delete(IntPtr valueType); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern ValueKind wasm_valtype_kind(IntPtr valueType); // Extern imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern wasm_externkind_t wasm_extern_kind(IntPtr ext); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_extern_type(IntPtr ext); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_extern_as_func(IntPtr ext); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_extern_as_global(IntPtr ext); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_extern_as_table(IntPtr ext); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_extern_as_memory(IntPtr ext); // Extern type imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern wasm_externkind_t wasm_externtype_kind(IntPtr externType); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_externtype_as_functype_const(IntPtr externType); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_externtype_as_globaltype_const(IntPtr externType); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_externtype_as_tabletype_const(IntPtr externType); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_externtype_as_memorytype_const(IntPtr externType); // Function imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern FunctionHandle wasm_func_new(StoreHandle store, FuncTypeHandle type, WasmFuncCallback callback); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_func_delete(IntPtr function); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static unsafe extern IntPtr wasm_func_call(IntPtr function, wasm_val_t* args, wasm_val_t* results); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_func_as_extern(FunctionHandle function); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_global_as_extern(GlobalHandle global); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_memory_as_extern(MemoryHandle memory); // Function type imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern unsafe wasm_valtype_vec_t* wasm_functype_params(IntPtr funcType); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern unsafe wasm_valtype_vec_t* wasm_functype_results(IntPtr funcType); // Instance imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern unsafe InstanceHandle wasm_instance_new(StoreHandle store, ModuleHandle module, IntPtr[] imports, out IntPtr trap); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_instance_delete(IntPtr ext); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_instance_exports(InstanceHandle instance, out wasm_extern_vec_t exports); // Function type imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern FuncTypeHandle wasm_functype_new(ref wasm_valtype_vec_t parameters, ref wasm_valtype_vec_t results); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_functype_delete(IntPtr functype); // Global type imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern GlobalTypeHandle wasm_globaltype_new(IntPtr valueType, wasm_mutability_t mutability); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_globaltype_delete(IntPtr globalType); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_globaltype_content(IntPtr globalType); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern wasm_mutability_t wasm_globaltype_mutability(IntPtr globalType); // Memory type imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern unsafe MemoryTypeHandle wasm_memorytype_new(wasm_limits_t* limits); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_memorytype_delete(IntPtr memoryType); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern unsafe wasm_limits_t* wasm_memorytype_limits(MemoryTypeHandle memoryType); // Trap imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_trap_new(StoreHandle store, ref wasm_byte_vec_t message); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_trap_delete(IntPtr trap); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_trap_message(IntPtr trap, out wasm_byte_vec_t message); // Table type imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_tabletype_element(IntPtr tableType); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static unsafe extern wasm_limits_t* wasm_tabletype_limits(IntPtr tableType); // Memory type imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static unsafe extern wasm_limits_t* wasm_memorytype_limits(IntPtr memoryType); // Global imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static unsafe extern GlobalHandle wasm_global_new(StoreHandle handle, GlobalTypeHandle globalType, wasm_val_t* initialValue); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_global_delete(IntPtr global); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_global_type(IntPtr global); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static unsafe extern void wasm_global_get(IntPtr global, wasm_val_t* value); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static unsafe extern void wasm_global_set(IntPtr global, wasm_val_t* value); // Memory imports - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern MemoryHandle wasm_memory_new(StoreHandle handle, MemoryTypeHandle memoryType); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern void wasm_memory_delete(IntPtr memory); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern IntPtr wasm_memory_type(MemoryHandle memory); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static unsafe extern byte* wasm_memory_data(IntPtr memory); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern UIntPtr wasm_memory_data_size(IntPtr memory); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern uint wasm_memory_size(MemoryHandle memory); - [DllImport("wasmtime")] + [DllImport(LibraryName)] public static extern bool wasm_memory_grow(MemoryHandle memory, uint delta); + + // WASI config + + [DllImport(LibraryName)] + public static extern WasiConfigHandle wasi_config_new(); + + [DllImport(LibraryName)] + public static extern void wasi_config_delete(IntPtr config); + + [DllImport(LibraryName)] + public unsafe static extern void wasi_config_set_argv(WasiConfigHandle config, int argc, byte*[] argv); + + [DllImport(LibraryName)] + public static extern void wasi_config_inherit_argv(WasiConfigHandle config); + + [DllImport(LibraryName)] + public static extern unsafe void wasi_config_set_env( + WasiConfigHandle config, + int envc, + byte*[] names, + byte*[] values + ); + + [DllImport(LibraryName)] + public static extern void wasi_config_inherit_env(WasiConfigHandle config); + + [DllImport(LibraryName)] + public static extern bool wasi_config_set_stdin( + WasiConfigHandle config, + [MarshalAs(UnmanagedType.LPUTF8Str)] string path + ); + + [DllImport(LibraryName)] + public static extern void wasi_config_inherit_stdin(WasiConfigHandle config); + + [DllImport(LibraryName)] + public static extern bool wasi_config_set_stdout( + WasiConfigHandle config, + [MarshalAs(UnmanagedType.LPUTF8Str)] string path + ); + + [DllImport(LibraryName)] + public static extern void wasi_config_inherit_stdout(WasiConfigHandle config); + + [DllImport(LibraryName)] + public static extern bool wasi_config_set_stderr( + WasiConfigHandle config, + [MarshalAs(UnmanagedType.LPUTF8Str)] string path + ); + + [DllImport(LibraryName)] + public static extern void wasi_config_inherit_stderr(WasiConfigHandle config); + + [DllImport(LibraryName)] + public static extern bool wasi_config_preopen_dir( + WasiConfigHandle config, + [MarshalAs(UnmanagedType.LPUTF8Str)] string path, + [MarshalAs(UnmanagedType.LPUTF8Str)] string guestPath + ); + + // WASI instance + [DllImport(LibraryName)] + public static extern WasiInstanceHandle wasi_instance_new( + StoreHandle store, + WasiConfigHandle config, + out IntPtr trap + ); + + [DllImport(LibraryName)] + public static extern void wasi_instance_delete(IntPtr instance); + + [DllImport(LibraryName)] + public static extern IntPtr wasi_instance_bind_import(WasiInstanceHandle instance, IntPtr importType); } } diff --git a/crates/misc/dotnet/src/Module.cs b/crates/misc/dotnet/src/Module.cs index c1f0dda6c3..203e5f7a6a 100644 --- a/crates/misc/dotnet/src/Module.cs +++ b/crates/misc/dotnet/src/Module.cs @@ -49,20 +49,33 @@ namespace Wasmtime /// /// The host to use for the WebAssembly module's instance. /// Returns a new . - public Instance Instantiate(IHost host) + public Instance Instantiate(IHost host = null) { - if (host is null) - { - throw new ArgumentNullException(nameof(host)); - } + return Instantiate(null, host); + } - if (host.Instance != null) + /// + /// Instantiates a WebAssembly module for the given host. + /// + /// The WASI instance to use for WASI imports. + /// The host to use for the WebAssembly module's instance. + /// Returns a new . + public Instance Instantiate(Wasi wasi, IHost host = null) + { + if (!(host?.Instance is null)) { throw new InvalidOperationException("The host has already been associated with an instantiated module."); } - host.Instance = new Instance(this, host); - return host.Instance; + var instance = new Instance(this, wasi, host); + + if (!(host is null)) + { + host.Instance = instance; + return instance; + } + + return instance; } /// @@ -94,6 +107,11 @@ namespace Wasmtime Handle.Dispose(); Handle.SetHandleAsInvalid(); } + if (!(Imports is null)) + { + Imports.Dispose(); + Imports = null; + } } internal Interop.ModuleHandle Handle { get; private set; } diff --git a/crates/misc/dotnet/src/TrapException.cs b/crates/misc/dotnet/src/TrapException.cs index 2ed7df8e76..7446ab6a3d 100644 --- a/crates/misc/dotnet/src/TrapException.cs +++ b/crates/misc/dotnet/src/TrapException.cs @@ -29,9 +29,11 @@ namespace Wasmtime Interop.wasm_trap_message(trap, out var bytes); var byteSpan = new ReadOnlySpan(bytes.data, checked((int)bytes.size)); - int indexOfNull = byteSpan.IndexOf((byte)0); + int indexOfNull = byteSpan.LastIndexOf((byte)0); if (indexOfNull != -1) + { byteSpan = byteSpan.Slice(0, indexOfNull); + } var message = Encoding.UTF8.GetString(byteSpan); Interop.wasm_byte_vec_delete(ref bytes); diff --git a/crates/misc/dotnet/src/Wasi.cs b/crates/misc/dotnet/src/Wasi.cs new file mode 100644 index 0000000000..e16feaf58b --- /dev/null +++ b/crates/misc/dotnet/src/Wasi.cs @@ -0,0 +1,44 @@ +using System; +using Wasmtime.Bindings; +using Wasmtime.Imports; + +namespace Wasmtime +{ + public class Wasi + { + /// + /// Creates a default instance. + /// + public Wasi(Store store) : + this( + (store ?? throw new ArgumentNullException(nameof(store))).Handle, + Interop.wasi_config_new() + ) + { + } + + internal Wasi(Interop.StoreHandle store, Interop.WasiConfigHandle config) + { + IntPtr trap; + Handle = Interop.wasi_instance_new(store, config, out trap); + config.SetHandleAsInvalid(); + + if (trap != IntPtr.Zero) + { + throw TrapException.FromOwnedTrap(trap); + } + } + + internal WasiBinding Bind(Import import) + { + var export = Interop.wasi_instance_bind_import(Handle, import.Handle); + if (export == IntPtr.Zero) + { + return null; + } + return new WasiBinding(export); + } + + internal Interop.WasiInstanceHandle Handle { get; private set; } + } +} diff --git a/crates/misc/dotnet/src/WasiBuilder.cs b/crates/misc/dotnet/src/WasiBuilder.cs new file mode 100644 index 0000000000..1ee7c27f3a --- /dev/null +++ b/crates/misc/dotnet/src/WasiBuilder.cs @@ -0,0 +1,409 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Wasmtime +{ + /// + /// Represents a build of WASI instances. + /// + public class WasiBuilder + { + /// + /// Constructs a new . + /// + public WasiBuilder() + { + } + + /// + /// Adds a command line argument to the builder. + /// + /// The command line argument to add. + /// Returns the current builder. + public WasiBuilder WithArg(string arg) + { + if (arg is null) + { + throw new ArgumentNullException(nameof(arg)); + } + + if (_inheritArgs) + { + _args.Clear(); + _inheritArgs = false; + } + + _args.Add(arg); + return this; + } + + /// + /// Adds multiple command line arguments to the builder. + /// + /// The command line arguments to add. + /// Returns the current builder. + public WasiBuilder WithArgs(IEnumerable args) + { + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + if (_inheritArgs) + { + _args.Clear(); + _inheritArgs = false; + } + + foreach (var arg in args) + { + _args.Add(arg); + } + return this; + } + + /// + /// Adds multiple command line arguments to the builder. + /// + /// The command line arguments to add. + /// Returns the current builder. + public WasiBuilder WithArgs(params string[] args) + { + return WithArgs((IEnumerable)args); + } + + /// + /// Sets the builder to inherit command line arguments. + /// + /// Any explicitly specified command line arguments will be removed. + /// Returns the current builder. + public WasiBuilder WithInheritedArgs() + { + _inheritArgs = true; + _args.Clear(); + _args.AddRange(Environment.GetCommandLineArgs()); + return this; + } + + /// + /// Adds an environment variable to the builder. + /// + /// The name of the environment variable. + /// The value of the environment variable. + /// Returns the current builder. + public WasiBuilder WithEnvironmentVariable(string name, string value) + { + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("Environment variable name cannot be empty.", nameof(name)); + } + + _inheritEnv = false; + _vars.Add((name, value)); + return this; + } + + /// + /// Adds multiple environment variables to the builder. + /// + /// The name-value tuples of the environment variables to add. + /// Returns the current builder. + public WasiBuilder WithEnvironmentVariables(IEnumerable<(string,string)> vars) + { + if (vars is null) + { + throw new ArgumentNullException(nameof(vars)); + } + + _inheritEnv = false; + + foreach (var v in vars) + { + _vars.Add(v); + } + + return this; + } + + /// + /// Sets the builder to inherit environment variables. + /// + /// Any explicitly specified environment variables will be removed. + /// Returns the current builder. + public WasiBuilder WithInheritedEnvironment() + { + _inheritEnv = true; + _vars.Clear(); + return this; + } + + /// + /// Sets the builder to use the given file path as stdin. + /// + /// The file to use as stdin. + /// Returns the current builder. + public WasiBuilder WithStandardInput(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException("The path cannot be null or empty.", nameof(path)); + } + + _inheritStandardInput = false; + _standardInputPath = path; + return this; + } + + /// + /// Sets the builder to inherit stdin. + /// + /// Any explicitly specified stdin file will be removed. + /// Returns the current builder. + public WasiBuilder WithInheritedStandardInput() + { + _inheritStandardInput = true; + _standardInputPath = null; + return this; + } + + /// + /// Sets the builder to use the given file path as stdout. + /// + /// The file to use as stdout. + /// Returns the current builder. + public WasiBuilder WithStandardOutput(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException("The path cannot be null or empty.", nameof(path)); + } + + _inheritStandardOutput = false; + _standardOutputPath = path; + return this; + } + + /// + /// Sets the builder to inherit stdout. + /// + /// Any explicitly specified stdout file will be removed. + /// Returns the current builder. + public WasiBuilder WithInheritedStandardOutput() + { + _inheritStandardOutput = true; + _standardOutputPath = null; + return this; + } + + /// + /// Sets the builder to use the given file path as stderr. + /// + /// The file to use as stderr. + /// Returns the current builder. + public WasiBuilder WithStandardError(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException("The path cannot be null or empty.", nameof(path)); + } + + _inheritStandardError = false; + _standardErrorPath = path; + return this; + } + + /// + /// Sets the builder to inherit stderr. + /// + /// Any explicitly specified stderr file will be removed. + /// Returns the current builder. + public WasiBuilder WithInheritedStandardError() + { + _inheritStandardError = true; + _standardErrorPath = null; + return this; + } + + /// + /// Adds a preopen directory to the builder. + /// + /// The path to the directory to add. + /// The path the guest will use to open the directory. + /// Returns the current builder. + public WasiBuilder WithPreopenedDirectory(string path, string guestPath) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException("The path cannot be null or empty.", nameof(path)); + } + if (string.IsNullOrEmpty(guestPath)) + { + throw new ArgumentException("The guest path cannot be null or empty.", nameof(guestPath)); + } + + _preopenDirs.Add((path, guestPath)); + return this; + } + + /// + /// Builds the instance. + /// + /// The to use. + /// Returns the new instance. + public Wasi Build(Store store) + { + var config = Interop.wasi_config_new(); + + SetConfigArgs(config); + SetEnvironmentVariables(config); + SetStandardIn(config); + SetStandardOut(config); + SetStandardError(config); + SetPreopenDirectories(config); + + return new Wasi(store.Handle, config); + } + + private unsafe void SetConfigArgs(Interop.WasiConfigHandle config) + { + // Don't call wasi_config_inherit_argv as the command line to the .NET program may not be + // the same as the process' command line (e.g. `dotnet foo.dll foo bar baz` => "foo.dll foo bar baz"). + if (_args.Count == 0) + { + return; + } + + var (args, handles) = Interop.ToUTF8PtrArray(_args); + + try + { + Interop.wasi_config_set_argv(config, _args.Count, args); + } + finally + { + foreach (var handle in handles) + { + handle.Free(); + } + } + } + + private unsafe void SetEnvironmentVariables(Interop.WasiConfigHandle config) + { + if (_inheritEnv) + { + Interop.wasi_config_inherit_env(config); + return; + } + + if (_vars.Count == 0) + { + return; + } + + var (names, nameHandles) = Interop.ToUTF8PtrArray(_vars.Select(var => var.Name).ToArray()); + var (values, valueHandles) = Interop.ToUTF8PtrArray(_vars.Select(var => var.Value).ToArray()); + + try + { + Interop.wasi_config_set_env(config, _vars.Count, names, values); + } + finally + { + foreach (var handle in nameHandles) + { + handle.Free(); + } + + foreach (var handle in valueHandles) + { + handle.Free(); + } + } + } + + private void SetStandardIn(Interop.WasiConfigHandle config) + { + if (_inheritStandardInput) + { + Interop.wasi_config_inherit_stdin(config); + return; + } + + if (!string.IsNullOrEmpty(_standardInputPath)) + { + if (!Interop.wasi_config_set_stdin(config, _standardInputPath)) + { + throw new InvalidOperationException($"Failed to set stdin to file '{_standardInputPath}'."); + } + } + } + + private void SetStandardOut(Interop.WasiConfigHandle config) + { + if (_inheritStandardOutput) + { + Interop.wasi_config_inherit_stdout(config); + return; + } + + if (!string.IsNullOrEmpty(_standardOutputPath)) + { + if (!Interop.wasi_config_set_stdout(config, _standardOutputPath)) + { + throw new InvalidOperationException($"Failed to set stdout to file '{_standardOutputPath}'."); + } + } + } + + private void SetStandardError(Interop.WasiConfigHandle config) + { + if (_inheritStandardError) + { + Interop.wasi_config_inherit_stderr(config); + return; + } + + if (!string.IsNullOrEmpty(_standardErrorPath)) + { + if (!Interop.wasi_config_set_stderr(config, _standardErrorPath)) + { + throw new InvalidOperationException($"Failed to set stderr to file '{_standardErrorPath}'."); + } + } + } + + private void SetPreopenDirectories(Interop.WasiConfigHandle config) + { + foreach (var dir in _preopenDirs) + { + if (!Interop.wasi_config_preopen_dir(config, dir.Path, dir.GuestPath)) + { + throw new InvalidOperationException($"Failed to preopen directory '{dir.Path}'."); + } + } + } + + private readonly List _args = new List(); + private readonly List<(string Name, string Value)> _vars = new List<(string, string)>(); + private string _standardInputPath; + private string _standardOutputPath; + private string _standardErrorPath; + private readonly List<(string Path, string GuestPath)> _preopenDirs = new List<(string, string)>(); + private bool _inheritArgs = false; + private bool _inheritEnv = false; + private bool _inheritStandardInput = false; + private bool _inheritStandardOutput = false; + private bool _inheritStandardError = false; + } +} diff --git a/crates/misc/dotnet/tests/Fixtures/ModuleFixture.cs b/crates/misc/dotnet/tests/Fixtures/ModuleFixture.cs index 91bd52bbad..8c140700c1 100644 --- a/crates/misc/dotnet/tests/Fixtures/ModuleFixture.cs +++ b/crates/misc/dotnet/tests/Fixtures/ModuleFixture.cs @@ -15,19 +15,19 @@ namespace Wasmtime.Tests public void Dispose() { - if (Module != null) + if (!(Module is null)) { Module.Dispose(); Module = null; } - if (Store != null) + if (!(Store is null)) { Store.Dispose(); Store = null; } - if (Engine != null) + if (!(Engine is null)) { Engine.Dispose(); Engine = null; diff --git a/crates/misc/dotnet/tests/FunctionThunkingTests.cs b/crates/misc/dotnet/tests/FunctionThunkingTests.cs index dc7af0bd3b..f05afbc1df 100644 --- a/crates/misc/dotnet/tests/FunctionThunkingTests.cs +++ b/crates/misc/dotnet/tests/FunctionThunkingTests.cs @@ -12,7 +12,7 @@ namespace Wasmtime.Tests public class FunctionThunkingTests : IClassFixture { - const string THROW_MESSAGE = "Test error messages for wasmtime dotnet bidnings unit tests."; + const string THROW_MESSAGE = "Test error message for wasmtime dotnet unit tests."; class MyHost : IHost { @@ -53,7 +53,7 @@ namespace Wasmtime.Tests } [Fact] - public void ItPropegatesExceptionsToCallersViaTraps() + public void ItPropagatesExceptionsToCallersViaTraps() { var host = new MyHost(); using (var instance = Fixture.Module.Instantiate(host)) diff --git a/crates/wasi-common/src/ctx.rs b/crates/wasi-common/src/ctx.rs index e2299d212e..4c2cd65e6b 100644 --- a/crates/wasi-common/src/ctx.rs +++ b/crates/wasi-common/src/ctx.rs @@ -113,6 +113,27 @@ impl WasiCtxBuilder { self } + /// Inherit stdin from the host process. + pub fn inherit_stdin(mut self) -> Self { + self.fds + .insert(0, PendingFdEntry::Thunk(FdEntry::duplicate_stdin)); + self + } + + /// Inherit stdout from the host process. + pub fn inherit_stdout(mut self) -> Self { + self.fds + .insert(1, PendingFdEntry::Thunk(FdEntry::duplicate_stdout)); + self + } + + /// Inherit stdout from the host process. + pub fn inherit_stderr(mut self) -> Self { + self.fds + .insert(2, PendingFdEntry::Thunk(FdEntry::duplicate_stderr)); + self + } + /// Inherit the stdin, stdout, and stderr streams from the host process. pub fn inherit_stdio(mut self) -> Self { self.fds