diff --git a/examples/linking.rb b/examples/linking.rb index 08eb4c69..071b7586 100644 --- a/examples/linking.rb +++ b/examples/linking.rb @@ -2,9 +2,10 @@ engine = Wasmtime::Engine.new -# Create a linker to link modules together. We want to use WASI with -# the linker, so we pass in `wasi: true`. -linker = Wasmtime::Linker.new(engine, wasi: true) +# Create a linker to link modules together. +linker = Wasmtime::Linker.new(engine) +# We want to use WASI with # the linker, so we call add_to_linker_sync. +Wasmtime::WASI::P1.add_to_linker_sync(linker) mod1 = Wasmtime::Module.from_file(engine, "examples/linking1.wat") mod2 = Wasmtime::Module.from_file(engine, "examples/linking2.wat") @@ -13,7 +14,7 @@ .inherit_stdin .inherit_stdout -store = Wasmtime::Store.new(engine, wasi_config: wasi_config) +store = Wasmtime::Store.new(engine, wasi_p1_config: wasi_config) # Instantiate `mod2` which only uses WASI, then register # that instance with the linker so `mod1` can use it. diff --git a/examples/wasi-p2.rb b/examples/wasi-p2.rb new file mode 100644 index 00000000..5a0f5382 --- /dev/null +++ b/examples/wasi-p2.rb @@ -0,0 +1,17 @@ +require "wasmtime" + +engine = Wasmtime::Engine.new +component = Wasmtime::Component::Component.from_file(engine, "spec/fixtures/wasi-debug-p2.wasm") + +linker = Wasmtime::Component::Linker.new(engine) +Wasmtime::WASI::P2.add_to_linker_sync(linker) + +wasi_config = Wasmtime::WasiConfig.new + .set_stdin_string("hi!") + .inherit_stdout + .inherit_stderr + .set_argv(ARGV) + .set_env(ENV) +store = Wasmtime::Store.new(engine, wasi_config: wasi_config) + +Wasmtime::Component::WasiCommand.new(store, component, linker).call_run(store) diff --git a/examples/wasi.rb b/examples/wasi.rb index 96d46cf9..c96dc244 100644 --- a/examples/wasi.rb +++ b/examples/wasi.rb @@ -3,7 +3,8 @@ engine = Wasmtime::Engine.new mod = Wasmtime::Module.from_file(engine, "spec/fixtures/wasi-debug.wasm") -linker = Wasmtime::Linker.new(engine, wasi: true) +linker = Wasmtime::Linker.new(engine) +Wasmtime::WASI::P1.add_to_linker_sync(linker) wasi_config = Wasmtime::WasiConfig.new .set_stdin_string("hi!") @@ -11,7 +12,7 @@ .inherit_stderr .set_argv(ARGV) .set_env(ENV) -store = Wasmtime::Store.new(engine, wasi_config: wasi_config) +store = Wasmtime::Store.new(engine, wasi_p1_config: wasi_config) instance = linker.instantiate(store, mod) instance.invoke("_start") diff --git a/ext/src/ruby_api/component.rs b/ext/src/ruby_api/component.rs index 05475b23..d5ec4e2a 100644 --- a/ext/src/ruby_api/component.rs +++ b/ext/src/ruby_api/component.rs @@ -2,6 +2,7 @@ mod convert; mod func; mod instance; mod linker; +mod wasi_command; use super::root; use magnus::{ @@ -13,6 +14,8 @@ use wasmtime::component::Component as ComponentImpl; pub use func::Func; pub use instance::Instance; +pub use linker::Linker; +pub use wasi_command::WasiCommand; pub fn component_namespace(ruby: &Ruby) -> RModule { static COMPONENT_NAMESPACE: Lazy = @@ -162,6 +165,7 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> { instance::init(ruby, &namespace)?; func::init(ruby, &namespace)?; convert::init(ruby)?; + wasi_command::init(ruby, &namespace)?; Ok(()) } diff --git a/ext/src/ruby_api/component/linker.rs b/ext/src/ruby_api/component/linker.rs index cb12f1c2..e5e48f4a 100644 --- a/ext/src/ruby_api/component/linker.rs +++ b/ext/src/ruby_api/component/linker.rs @@ -2,11 +2,15 @@ use super::{Component, Instance}; use crate::{ err, ruby_api::{ + errors, store::{StoreContextValue, StoreData}, Engine, Module, Store, }, }; -use std::{borrow::BorrowMut, cell::RefCell}; +use std::{ + borrow::BorrowMut, + cell::{RefCell, RefMut}, +}; use crate::error; use magnus::{ @@ -14,6 +18,10 @@ use magnus::{ DataTypeFunctions, Error, Module as _, Object, RModule, Ruby, TryConvert, TypedData, Value, }; use wasmtime::component::{Linker as LinkerImpl, LinkerInstance as LinkerInstanceImpl}; +use wasmtime_wasi::{ + p2::{IoView, WasiCtx, WasiView}, + ResourceTable, +}; /// @yard /// @rename Wasmtime::Component::Linker @@ -23,6 +31,7 @@ use wasmtime::component::{Linker as LinkerImpl, LinkerInstance as LinkerInstance pub struct Linker { inner: RefCell>, refs: RefCell>, + has_wasi: RefCell, } unsafe impl Send for Linker {} @@ -38,13 +47,23 @@ impl Linker { /// @param engine [Engine] /// @return [Linker] pub fn new(engine: &Engine) -> Result { - let linker = LinkerImpl::new(engine.get()); + let linker: LinkerImpl = LinkerImpl::new(engine.get()); + Ok(Linker { inner: RefCell::new(linker), refs: RefCell::new(Vec::new()), + has_wasi: RefCell::new(false), }) } + pub(crate) fn inner_mut(&self) -> RefMut<'_, LinkerImpl> { + self.inner.borrow_mut() + } + + pub(crate) fn has_wasi(&self) -> bool { + *self.has_wasi.borrow() + } + /// @yard /// @def root /// Define items in the root of this {Linker}. @@ -105,6 +124,10 @@ impl Linker { store: Obj, component: &Component, ) -> Result { + if *rb_self.has_wasi.borrow() && !store.context().data().has_wasi_ctx() { + return err!("{}", errors::missing_wasi_ctx_error("linker.instantiate")); + } + let inner = rb_self.inner.borrow(); inner .instantiate(store.context_mut(), component.get()) @@ -119,6 +142,12 @@ impl Linker { }) .map_err(|e| error!("{}", e)) } + + pub(crate) fn add_wasi_p2(&self) -> Result<(), Error> { + *self.has_wasi.borrow_mut() = true; + let mut inner = self.inner.borrow_mut(); + wasmtime_wasi::p2::add_to_linker_sync(&mut inner).map_err(|e| error!("{e}")) + } } /// @yard diff --git a/ext/src/ruby_api/component/wasi_command.rs b/ext/src/ruby_api/component/wasi_command.rs new file mode 100644 index 00000000..6b0ae80a --- /dev/null +++ b/ext/src/ruby_api/component/wasi_command.rs @@ -0,0 +1,58 @@ +use magnus::{ + class, function, method, module::Module, typed_data::Obj, DataTypeFunctions, Error, Object, + RModule, Ruby, +}; +use wasmtime_wasi::p2::bindings::sync::Command; + +use crate::{ + err, error, + ruby_api::{ + component::{linker::Linker, Component}, + errors, + }, + Store, +}; + +#[magnus::wrap(class = "Wasmtime::Component::WasiCommand", size, free_immediately)] +pub struct WasiCommand { + command: Command, +} + +impl WasiCommand { + /// @yard + /// @def new(store, component, linker) + /// @param store [Store] + /// @param component [Component] + /// @param linker [Linker] + /// @return [WasiCommand] + pub fn new(store: &Store, component: &Component, linker: &Linker) -> Result { + if linker.has_wasi() && !store.context().data().has_wasi_ctx() { + return err!("{}", errors::missing_wasi_ctx_error("WasiCommand.new")); + } + let command = + Command::instantiate(store.context_mut(), component.get(), &linker.inner_mut()) + .map_err(|e| error!("{e}"))?; + Ok(Self { command }) + } + + /// @yard + /// @def call_run(store) + /// @param store [Store] + /// @return [nil] + pub fn call_run(_ruby: &Ruby, rb_self: Obj, store: &Store) -> Result<(), Error> { + rb_self + .command + .wasi_cli_run() + .call_run(store.context_mut()) + .map_err(|err| error!("{err}"))? + .map_err(|_| error!("Error running `run`")) + } +} + +pub fn init(_ruby: &Ruby, namespace: &RModule) -> Result<(), Error> { + let linker = namespace.define_class("WasiCommand", class::object())?; + linker.define_singleton_method("new", function!(WasiCommand::new, 3))?; + linker.define_method("call_run", method!(WasiCommand::call_run, 1))?; + + Ok(()) +} diff --git a/ext/src/ruby_api/errors.rs b/ext/src/ruby_api/errors.rs index 4109ac3f..5a8f6718 100644 --- a/ext/src/ruby_api/errors.rs +++ b/ext/src/ruby_api/errors.rs @@ -83,6 +83,29 @@ impl ExceptionMessage for magnus::Error { } } +pub(crate) fn missing_wasi_ctx_error(callee: &str) -> String { + missing_wasi_error(callee, "WASI", "P2", "wasi_config") +} + +pub(crate) fn missing_wasi_p1_ctx_error() -> String { + missing_wasi_error("linker.instantiate", "WASI p1", "P1", "wasi_p1_config") +} + +fn missing_wasi_error( + callee: &str, + wasi_text: &str, + add_wasi_call: &str, + option_name: &str, +) -> String { + format!( + "Store is missing {wasi_text} configuration.\n\n\ + When using `WASI::{add_wasi_call}::add_to_linker_sync(linker)`, the Store given to\n\ + `{callee}` must have a {wasi_text} configuration.\n\ + To fix this, provide the `{option_name}` when creating the Store:\n\ + Wasmtime::Store.new(engine, {option_name}: WasiConfig.new)" + ) +} + mod bundled { include!(concat!(env!("OUT_DIR"), "/bundled/error.rs")); } diff --git a/ext/src/ruby_api/linker.rs b/ext/src/ruby_api/linker.rs index 6e1c3190..9748059f 100644 --- a/ext/src/ruby_api/linker.rs +++ b/ext/src/ruby_api/linker.rs @@ -9,7 +9,7 @@ use super::{ root, store::{Store, StoreContextValue, StoreData}, }; -use crate::{define_rb_intern, err, error}; +use crate::{err, error, ruby_api::errors}; use magnus::{ block::Proc, class, function, gc::Marker, method, prelude::*, scan_args, scan_args::scan_args, typed_data::Obj, DataTypeFunctions, Error, Object, RArray, RHash, RString, Ruby, TypedData, @@ -18,10 +18,6 @@ use magnus::{ use std::cell::RefCell; use wasmtime::Linker as LinkerImpl; -define_rb_intern!( - WASI=> "wasi", -); - /// @yard /// @see https://docs.rs/wasmtime/latest/wasmtime/struct.Linker.html Wasmtime's Rust doc #[derive(TypedData)] @@ -29,7 +25,7 @@ define_rb_intern!( pub struct Linker { inner: RefCell>, refs: RefCell>, - has_wasi: bool, + has_wasi: RefCell, } unsafe impl Send for Linker {} @@ -42,25 +38,15 @@ impl DataTypeFunctions for Linker { impl Linker { /// @yard - /// @def new(engine, wasi: false) + /// @def new(engine) /// @param engine [Engine] - /// @param wasi [Boolean] Whether WASI should be defined in this Linker. Defaults to false. /// @return [Linker] - pub fn new(args: &[Value]) -> Result { - let args = scan_args::scan_args::<(&Engine,), (), (), (), _, ()>(args)?; - let kw = scan_args::get_kwargs::<_, (), (Option,), ()>(args.keywords, &[], &[*WASI])?; - let (engine,) = args.required; - let wasi = kw.optional.0.unwrap_or(false); - - let mut inner: LinkerImpl = LinkerImpl::new(engine.get()); - if wasi { - wasmtime_wasi::preview1::add_to_linker_sync(&mut inner, |s| s.wasi_ctx_mut()) - .map_err(|e| error!("{}", e))? - } + pub fn new(engine: &Engine) -> Result { + let inner: LinkerImpl = LinkerImpl::new(engine.get()); Ok(Self { inner: RefCell::new(inner), refs: Default::default(), - has_wasi: wasi, + has_wasi: RefCell::new(false), }) } @@ -280,14 +266,8 @@ impl Linker { /// @param mod [Module] /// @return [Instance] pub fn instantiate(&self, store: Obj, module: &Module) -> Result { - if self.has_wasi && !store.context().data().has_wasi_ctx() { - return err!( - "Store is missing WASI configuration.\n\n\ - When using `wasi: true`, the Store given to\n\ - `Linker#instantiate` must have a WASI configuration.\n\ - To fix this, provide the `wasi_config` when creating the Store:\n\ - Wasmtime::Store.new(engine, wasi_config: WasiConfig.new)" - ); + if *self.has_wasi.borrow() && !store.context().data().has_wasi_p1_ctx() { + return err!("{}", errors::missing_wasi_p1_ctx_error()); } self.inner @@ -322,11 +302,18 @@ impl Linker { let mut inner = self.inner.borrow_mut(); deterministic_wasi_ctx::replace_scheduling_functions(&mut inner).map_err(|e| error!("{e}")) } + + pub(crate) fn add_wasi_p1(&self) -> Result<(), Error> { + *self.has_wasi.borrow_mut() = true; + let mut inner = self.inner.borrow_mut(); + wasmtime_wasi::preview1::add_to_linker_sync(&mut inner, |s| s.wasi_p1_ctx_mut()) + .map_err(|e| error!("{e}")) + } } pub fn init() -> Result<(), Error> { let class = root().define_class("Linker", class::object())?; - class.define_singleton_method("new", function!(Linker::new, -1))?; + class.define_singleton_method("new", function!(Linker::new, 1))?; class.define_method("allow_shadowing=", method!(Linker::set_allow_shadowing, 1))?; class.define_method( "allow_unknown_exports=", diff --git a/ext/src/ruby_api/mod.rs b/ext/src/ruby_api/mod.rs index 8419c612..766551d9 100644 --- a/ext/src/ruby_api/mod.rs +++ b/ext/src/ruby_api/mod.rs @@ -27,6 +27,7 @@ mod pooling_allocation_config; mod store; mod table; mod trap; +mod wasi; mod wasi_config; pub use caller::Caller; @@ -83,6 +84,7 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> { memory::init(ruby)?; linker::init()?; externals::init()?; + wasi::init()?; wasi_config::init()?; table::init()?; global::init()?; diff --git a/ext/src/ruby_api/store.rs b/ext/src/ruby_api/store.rs index 1ad12c95..6888ea3d 100644 --- a/ext/src/ruby_api/store.rs +++ b/ext/src/ruby_api/store.rs @@ -19,20 +19,24 @@ use wasmtime::{ AsContext, AsContextMut, ResourceLimiter, Store as StoreImpl, StoreContext, StoreContextMut, StoreLimits, StoreLimitsBuilder, }; +use wasmtime_wasi::p2::{IoView, WasiCtx, WasiView}; use wasmtime_wasi::preview1::WasiP1Ctx; -use wasmtime_wasi::I32Exit; +use wasmtime_wasi::{I32Exit, ResourceTable}; define_rb_intern!( WASI_CONFIG => "wasi_config", + WASI_P1_CONFIG => "wasi_p1_config", LIMITS => "limits", ); pub struct StoreData { user_data: Value, - wasi: Option, + wasi_p1: Option, + wasi: Option, refs: Vec, last_error: Option, store_limits: TrackingResourceLimiter, + resource_table: ResourceTable, } impl StoreData { @@ -40,12 +44,18 @@ impl StoreData { self.user_data } + pub fn has_wasi_p1_ctx(&self) -> bool { + self.wasi_p1.is_some() + } + pub fn has_wasi_ctx(&self) -> bool { self.wasi.is_some() } - pub fn wasi_ctx_mut(&mut self) -> &mut WasiP1Ctx { - self.wasi.as_mut().expect("Store must have a WASI context") + pub fn wasi_p1_ctx_mut(&mut self) -> &mut WasiP1Ctx { + self.wasi_p1 + .as_mut() + .expect("Store must have a WASI context") } pub fn retain(&mut self, value: Value) { @@ -108,13 +118,15 @@ unsafe impl Send for StoreData {} impl Store { /// @yard /// - /// @def new(engine, data = nil, wasi_config: nil, limits: nil) + /// @def new(engine, data = nil, wasi_config: nil, wasi_p1_config: nil, limits: nil) /// @param engine [Wasmtime::Engine] /// The engine for this store. /// @param data [Object] /// The data attached to the store. Can be retrieved through {Wasmtime::Store#data} and {Wasmtime::Caller#data}. /// @param wasi_config [Wasmtime::WasiConfig] /// The WASI config to use in this store. + /// @param wasi_p1_config [Wasmtime::WasiConfig] + /// The WASI config to use in this store for WASI preview 1. /// @param limits [Hash] /// See the {https://docs.rs/wasmtime/latest/wasmtime/struct.StoreLimitsBuilder.html +StoreLimitsBuilder+'s Rust doc} /// for detailed description of the different options and the defaults. @@ -138,19 +150,31 @@ impl Store { pub fn new(args: &[Value]) -> Result { let ruby = Ruby::get().unwrap(); let args = scan_args::scan_args::<(&Engine,), (Option,), (), (), _, ()>(args)?; - let kw = scan_args::get_kwargs::<_, (), (Option<&WasiConfig>, Option), ()>( + let kw = scan_args::get_kwargs::< + _, + (), + (Option<&WasiConfig>, Option<&WasiConfig>, Option), + (), + >( args.keywords, &[], - &[*WASI_CONFIG, *LIMITS], + &[*WASI_CONFIG, *WASI_P1_CONFIG, *LIMITS], )?; let (engine,) = args.required; let (user_data,) = args.optional; let user_data = user_data.unwrap_or_else(|| ().into_value()); let wasi_config = kw.optional.0; - let wasi = wasi_config.map(|config| config.build(&ruby)).transpose()?; + let wasi_p1_config = kw.optional.1; + + let wasi = wasi_config + .map(|wasi_config| wasi_config.build(&ruby)) + .transpose()?; + let wasi_p1 = wasi_p1_config + .map(|wasi_config| wasi_config.build_p1(&ruby)) + .transpose()?; - let limiter = match kw.optional.1 { + let limiter = match kw.optional.2 { None => StoreLimitsBuilder::new(), Some(limits) => hash_to_store_limits_builder(limits)?, } @@ -160,10 +184,12 @@ impl Store { let eng = engine.get(); let store_data = StoreData { user_data, + wasi_p1, wasi, refs: Default::default(), last_error: Default::default(), store_limits: limiter, + resource_table: Default::default(), }; let store = Self { inner: UnsafeCell::new(StoreImpl::new(eng, store_data)), @@ -485,3 +511,17 @@ impl ResourceLimiter for TrackingResourceLimiter { self.inner.memories() } } + +impl WasiView for StoreData { + fn ctx(&mut self) -> &mut WasiCtx { + self.wasi + .as_mut() + .expect("Should have WASI context defined if using WASI p2") + } +} + +impl IoView for StoreData { + fn table(&mut self) -> &mut ResourceTable { + &mut self.resource_table + } +} diff --git a/ext/src/ruby_api/wasi.rs b/ext/src/ruby_api/wasi.rs new file mode 100644 index 00000000..e0a4b58d --- /dev/null +++ b/ext/src/ruby_api/wasi.rs @@ -0,0 +1,34 @@ +use crate::{ruby_api::component, Linker}; + +use super::root; +use magnus::{class, function, typed_data::Obj, Error, Module, Object, RModule, Ruby}; + +#[magnus::wrap(class = "Wasmtime::WASI::P1", free_immediately)] +struct P1; + +impl P1 { + pub fn add_to_linker_sync(linker: Obj) -> Result<(), Error> { + linker.add_wasi_p1() + } +} + +#[magnus::wrap(class = "Wasmtime::WASI::P2", free_immediately)] +struct P2; + +impl P2 { + pub fn add_to_linker_sync(linker: Obj) -> Result<(), Error> { + linker.add_wasi_p2() + } +} + +pub fn init() -> Result<(), Error> { + let namespace = root().define_module("WASI")?; + + let p1_class = namespace.define_class("P1", class::object())?; + p1_class.define_singleton_method("add_to_linker_sync", function!(P1::add_to_linker_sync, 1))?; + + let p2_class = namespace.define_class("P2", class::object())?; + p2_class.define_singleton_method("add_to_linker_sync", function!(P2::add_to_linker_sync, 1))?; + + Ok(()) +} diff --git a/ext/src/ruby_api/wasi_config.rs b/ext/src/ruby_api/wasi_config.rs index 04d24fae..7b028dd0 100644 --- a/ext/src/ruby_api/wasi_config.rs +++ b/ext/src/ruby_api/wasi_config.rs @@ -9,7 +9,7 @@ use std::cell::RefCell; use std::fs; use std::{fs::File, path::PathBuf}; use wasmtime_wasi::p2::pipe::MemoryInputPipe; -use wasmtime_wasi::p2::{OutputFile, WasiCtxBuilder}; +use wasmtime_wasi::p2::{OutputFile, WasiCtx, WasiCtxBuilder}; use wasmtime_wasi::preview1::WasiP1Ctx; enum ReadStream { @@ -233,7 +233,19 @@ impl WasiConfig { rb_self } - pub fn build(&self, ruby: &Ruby) -> Result { + pub fn build_p1(&self, ruby: &Ruby) -> Result { + let mut builder = self.build_impl(ruby)?; + let ctx = builder.build_p1(); + Ok(ctx) + } + + pub fn build(&self, ruby: &Ruby) -> Result { + let mut builder = self.build_impl(ruby)?; + let ctx = builder.build(); + Ok(ctx) + } + + fn build_impl(&self, ruby: &Ruby) -> Result { let mut builder = WasiCtxBuilder::new(); let inner = self.inner.borrow(); @@ -305,8 +317,7 @@ impl WasiConfig { deterministic_wasi_ctx::add_determinism_to_wasi_ctx_builder(&mut builder); } - let ctx = builder.build_p1(); - Ok(ctx) + Ok(builder) } } diff --git a/spec/fixtures/wasi-debug-p2.wasm b/spec/fixtures/wasi-debug-p2.wasm new file mode 100644 index 00000000..4204ac59 Binary files /dev/null and b/spec/fixtures/wasi-debug-p2.wasm differ diff --git a/spec/fixtures/wasi-debug/README.md b/spec/fixtures/wasi-debug/README.md index 7eeeeec4..81222afb 100644 --- a/spec/fixtures/wasi-debug/README.md +++ b/spec/fixtures/wasi-debug/README.md @@ -7,5 +7,8 @@ cargo build --release && \ wasm-opt -O \ --enable-bulk-memory \ target/wasm32-wasip1/release/wasi-debug.wasm \ - -o ../wasi-debug.wasm + -o ../wasi-debug.wasm && \ +cargo build --target=wasm32-wasip2 --release && \ + cp target/wasm32-wasip2/release/wasi-debug.wasm \ + ../wasi-debug-p2.wasm ``` diff --git a/spec/fixtures/wasi-deterministic-p2.wasm b/spec/fixtures/wasi-deterministic-p2.wasm new file mode 100644 index 00000000..b476a29a Binary files /dev/null and b/spec/fixtures/wasi-deterministic-p2.wasm differ diff --git a/spec/fixtures/wasi-deterministic/README.md b/spec/fixtures/wasi-deterministic/README.md index 882559e3..62e139b1 100644 --- a/spec/fixtures/wasi-deterministic/README.md +++ b/spec/fixtures/wasi-deterministic/README.md @@ -7,6 +7,9 @@ cargo build --release && \ wasm-opt -O \ --enable-bulk-memory \ target/wasm32-wasip1/release/wasi-deterministic.wasm \ - -o ../wasi-deterministic.wasm + -o ../wasi-deterministic.wasm && \ +cargo build --target=wasm32-wasip2 --release && \ + cp target/wasm32-wasip2/release/wasi-deterministic.wasm \ + ../wasi-deterministic-p2.wasm ``` diff --git a/spec/unit/error_spec.rb b/spec/unit/error_spec.rb index e376a0b2..2ae337b4 100644 --- a/spec/unit/error_spec.rb +++ b/spec/unit/error_spec.rb @@ -78,8 +78,9 @@ module Wasmtime end it "raises WasiExit on WASI's proc_exit" do - linker = Linker.new(engine, wasi: true) - store = Store.new(engine, wasi_config: WasiConfig.new) + linker = Linker.new(engine) + WASI::P1.add_to_linker_sync(linker) + store = Store.new(engine, wasi_p1_config: WasiConfig.new) instance = linker.instantiate(store, wasi_module_exiting) expect { instance.invoke("_start") }.to raise_error(WasiExit) do |wasi_exit| diff --git a/spec/unit/wasi_spec.rb b/spec/unit/wasi_spec.rb index fd061f7e..2d1a9b3f 100644 --- a/spec/unit/wasi_spec.rb +++ b/spec/unit/wasi_spec.rb @@ -12,11 +12,15 @@ module Wasmtime # Compile module only once for speed @compiled_wasi_module = @engine.precompile_module(IO.binread("spec/fixtures/wasi-debug.wasm")) @compiled_wasi_deterministic_module = @engine.precompile_module(IO.binread("spec/fixtures/wasi-deterministic.wasm")) + + @compiled_wasi_component = @engine.precompile_component(IO.binread("spec/fixtures/wasi-debug-p2.wasm")) + @compiled_wasi_deterministic_component = @engine.precompile_component(IO.binread("spec/fixtures/wasi-deterministic-p2.wasm")) end describe "Linker.new" do it "accepts a wasi kwarg to define WASI imports" do - linker = Linker.new(@engine, wasi: true) + linker = Linker.new(@engine) + WASI::P1.add_to_linker_sync(linker) item = linker.get(Store.new(@engine), "wasi_snapshot_preview1", "environ_get") expect(item).not_to be nil end @@ -24,20 +28,46 @@ module Wasmtime describe "Linker#instantiate" do it "prevents panic when Store doesn't have a Wasi config" do - linker = Linker.new(@engine, wasi: true) + linker = Linker.new(@engine) + WASI::P1.add_to_linker_sync(linker) expect { linker.instantiate(Store.new(@engine), wasi_module).invoke("_start") } + .to raise_error(Wasmtime::Error, /Store is missing WASI p1 configuration/) + end + + it "returns an instance that can run when store is properly configured" do + linker = Linker.new(@engine) + WASI::P1.add_to_linker_sync(linker) + store = Store.new(@engine, wasi_p1_config: WasiConfig.new.set_stdin_string("some str")) + linker.instantiate(store, wasi_module).invoke("_start") + end + end + + describe "Component::Linker::instantiate" do + it "prevents panic when Store doesn't have a WASI config" do + linker = Component::Linker.new(@engine) + WASI::P2.add_to_linker_sync(linker) + expect { linker.instantiate(Store.new(@engine), wasi_component) } + .to raise_error(Wasmtime::Error, /Store is missing WASI configuration/) + end + end + + describe "Component::WasiCommand#new" do + it "prevents panic when store doesn't have a WASI config" do + linker = Component::Linker.new(@engine) + WASI::P2.add_to_linker_sync(linker) + expect { Component::WasiCommand.new(Store.new(@engine), wasi_component, linker) } .to raise_error(Wasmtime::Error, /Store is missing WASI configuration/) end it "returns an instance that can run when store is properly configured" do - linker = Linker.new(@engine, wasi: true) + linker = Component::Linker.new(@engine) + WASI::P2.add_to_linker_sync(linker) store = Store.new(@engine, wasi_config: WasiConfig.new.set_stdin_string("some str")) - linker.instantiate(store, wasi_module).invoke("_start") + Component::WasiCommand.new(store, wasi_component, linker).call_run(store) end end - # Uses the program from spec/wasi-debug to test the WASI integration - describe WasiConfig do + shared_examples WasiConfig do it "writes std streams to files" do File.write(tempfile_path("stdin"), "stdin content") wasi_config = WasiConfig.new @@ -45,7 +75,7 @@ module Wasmtime .set_stdout_file(tempfile_path("stdout")) .set_stderr_file(tempfile_path("stderr")) - run_wasi_module(wasi_config) + run.call(wasi_config) stdout = JSON.parse(File.read(tempfile_path("stdout"))) stderr = JSON.parse(File.read(tempfile_path("stderr"))) @@ -64,7 +94,7 @@ module Wasmtime .set_stdout_buffer(stdout_str, 40000) .set_stderr_buffer(stderr_str, 40000) - run_wasi_module(wasi_config) + run.call(wasi_config) parsed_stdout = JSON.parse(stdout_str) parsed_stderr = JSON.parse(stderr_str) @@ -82,7 +112,7 @@ module Wasmtime .set_stdout_buffer(stdout_str, 5) .set_stderr_buffer(stderr_str, 10) - run_wasi_module(wasi_config) + run.call(wasi_config) expect(stdout_str).to eq("{\"nam") expect(stderr_str).to eq("{\"name\":\"s") @@ -99,7 +129,7 @@ module Wasmtime .set_stderr_buffer(stderr_str, 40000) stdout_str.freeze - expect { run_wasi_module(wasi_config) }.to raise_error do |error| + expect { run.call(wasi_config) }.to raise_error do |error| expect(error).to be_a(Wasmtime::Error) expect(error.message).to match(/error while executing at wasm backtrace:/) end @@ -119,7 +149,7 @@ module Wasmtime .set_stdout_buffer(stdout_str, 40000) stderr_str.freeze - expect { run_wasi_module(wasi_config) }.to raise_error do |error| + expect { run.call(wasi_config) }.to raise_error do |error| expect(error).to be_a(Wasmtime::Error) expect(error.message).to match(/error while executing at wasm backtrace:/) end @@ -129,27 +159,27 @@ module Wasmtime end it "reads stdin from string" do - env = wasi_module_env { |config| config.set_stdin_string("¡UTF-8 from Ruby!") } + env = wasi_env.call { |config| config.set_stdin_string("¡UTF-8 from Ruby!") } expect(env.fetch("stdin")).to eq("¡UTF-8 from Ruby!") end it "uses specified args" do - env = wasi_module_env { |config| config.set_argv(["foo", "bar"]) } + env = wasi_env.call { |config| config.set_argv(["foo", "bar"]) } expect(env.fetch("args")).to eq(["foo", "bar"]) end it "uses ARGV" do - env = wasi_module_env { |config| config.set_argv(ARGV) } + env = wasi_env.call { |config| config.set_argv(ARGV) } expect(env.fetch("args")).to eq(ARGV) end it "uses specified env" do - env = wasi_module_env { |config| config.set_env("ENV_VAR" => "VAL") } + env = wasi_env.call { |config| config.set_env("ENV_VAR" => "VAL") } expect(env.fetch("env").to_h).to eq("ENV_VAR" => "VAL") end it "uses ENV" do - env = wasi_module_env { |config| config.set_env(ENV) } + env = wasi_env.call { |config| config.set_env(ENV) } expect(env.fetch("env").to_h).to eq(ENV.to_h) end @@ -157,18 +187,12 @@ module Wasmtime before do 2.times do |t| t += 1 - wasi_config = Wasmtime::WasiConfig - .new + wasi_config = WasiConfig.new .add_determinism .set_stdout_file(tempfile_path("stdout-deterministic-#{t}")) .set_stderr_file(tempfile_path("stderr-deterministic-#{t}")) - deterministic_module = Module.deserialize(@engine, @compiled_wasi_deterministic_module) - - linker = Linker.new(@engine, wasi: true) - linker.use_deterministic_scheduling_functions - store = Store.new(@engine, wasi_config: wasi_config) - linker.instantiate(store, deterministic_module).invoke("_start") + run_deterministic.call(wasi_config) end end @@ -211,16 +235,43 @@ module Wasmtime end end + describe "WasiConfig preview 1" do + it_behaves_like WasiConfig do + let(:run) { method(:run_wasi_module) } + let(:wasi_env) { method(:wasi_module_env) } + let(:run_deterministic) { method(:run_wasi_module_deterministic) } + end + end + + describe "WasiConfig preview 2" do + it_behaves_like WasiConfig do + let(:run) { method(:run_wasi_component) } + let(:wasi_env) { method(:wasi_component_env) } + let(:run_deterministic) { method(:run_wasi_component_deterministic) } + end + end + def wasi_module Module.deserialize(@engine, @compiled_wasi_module) end def run_wasi_module(wasi_config) - linker = Linker.new(@engine, wasi: true) - store = Store.new(@engine, wasi_config: wasi_config) + linker = Linker.new(@engine) + WASI::P1.add_to_linker_sync(linker) + store = Store.new(@engine, wasi_p1_config: wasi_config) linker.instantiate(store, wasi_module).invoke("_start") end + def run_wasi_module_deterministic(wasi_config) + linker = Linker.new(@engine) + WASI::P1.add_to_linker_sync(linker) + linker.use_deterministic_scheduling_functions + store = Store.new(@engine, wasi_p1_config: wasi_config) + linker + .instantiate(store, Module.deserialize(@engine, @compiled_wasi_deterministic_module)) + .invoke("_start") + end + def wasi_module_env stdout_file = tempfile_path("stdout") @@ -233,6 +284,40 @@ def wasi_module_env JSON.parse(File.read(stdout_file)).fetch("wasi") end + def wasi_component + Component::Component.deserialize(@engine, @compiled_wasi_component) + end + + def run_wasi_component(wasi_config) + linker = Component::Linker.new(@engine) + WASI::P2.add_to_linker_sync(linker) + store = Store.new(@engine, wasi_config: wasi_config) + Component::WasiCommand.new(store, wasi_component, linker).call_run(store) + end + + def wasi_component_env + stdout_file = tempfile_path("stdout") + + wasi_config = WasiConfig.new + yield(wasi_config) + wasi_config.set_stdout_file(stdout_file) + + run_wasi_component(wasi_config) + + JSON.parse(File.read(stdout_file)).fetch("wasi") + end + + def run_wasi_component_deterministic(wasi_config) + linker = Component::Linker.new(@engine) + WASI::P2.add_to_linker_sync(linker) + store = Store.new(@engine, wasi_config: wasi_config) + Component::WasiCommand.new( + store, + Component::Component.deserialize(@engine, @compiled_wasi_deterministic_component), + linker + ).call_run(store) + end + def tempfile_path(name) File.join(tmpdir, name) end