From 708702a6d4c11a1337ce8f28a0c90e6868f9b26d Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sun, 21 Dec 2025 19:20:51 -0500 Subject: [PATCH 01/70] feat: update client --- crates/test-plugin/src/main.rs | 67 +-- crates/wasmi-plugin-hdk/benches/benchmark.rs | 16 +- crates/wasmi-plugin-hdk/src/host_handler.rs | 2 +- crates/wasmi-plugin-hdk/src/plugin.rs | 17 +- crates/wasmi-plugin-hdk/src/server.rs | 3 +- crates/wasmi-plugin-hdk/tests/integration.rs | 59 ++- crates/wasmi-plugin-pdk/src/api.rs | 2 +- crates/wasmi-plugin-pdk/src/lib.rs | 1 + crates/wasmi-plugin-pdk/src/router.rs | 89 ++++ crates/wasmi-plugin-pdk/src/rpc_message.rs | 22 + crates/wasmi-plugin-pdk/src/server.rs | 182 +++----- crates/wasmi-plugin-pdk/src/transport.rs | 426 +++++++++---------- 12 files changed, 504 insertions(+), 382 deletions(-) create mode 100644 crates/wasmi-plugin-pdk/src/router.rs diff --git a/crates/test-plugin/src/main.rs b/crates/test-plugin/src/main.rs index 6cda05d..0c9de1c 100644 --- a/crates/test-plugin/src/main.rs +++ b/crates/test-plugin/src/main.rs @@ -1,25 +1,21 @@ use rand::Rng; use serde_json::{self, Value}; -use std::{io::stderr, sync::Arc}; +use std::io::stderr; use tracing::{error, info, level_filters::LevelFilter}; use tracing_subscriber::fmt; -use wasmi_plugin_pdk::{ - rpc_message::RpcError, - server::PluginServer, - transport::{JsonRpcTransport, Transport}, -}; +use wasmi_plugin_pdk::{rpc_message::RpcError, server::PluginServer, transport::Transport}; -async fn ping(_: Arc, _: ()) -> Result { +async fn ping(_: Transport, _: ()) -> Result { Ok(Value::String("pong".to_string())) } -async fn get_random_number(_: Arc, _: ()) -> Result { +async fn get_random_number(_: Transport, _: ()) -> Result { let mut rng = rand::rng(); let random_number: u64 = rng.random(); Ok(Value::Number(random_number.into())) } -async fn get_time(_: Arc, _: ()) -> Result { +async fn get_time(_: Transport, _: ()) -> Result { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map_err(|e| { @@ -29,32 +25,50 @@ async fn get_time(_: Arc, _: ()) -> Result { Ok(Value::Number(now.as_secs().into())) } -async fn sleep(_: Arc, duration_ms: u64) -> Result<(), RpcError> { +async fn sleep(_: Transport, duration_ms: u64) -> Result<(), RpcError> { info!("Sleeping for {} milliseconds", duration_ms); - tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await; + std::thread::sleep(std::time::Duration::from_millis(duration_ms)); info!("Woke up after sleeping for {} milliseconds", duration_ms); Ok(()) } -async fn many_echo(transport: Arc, limit: u64) -> Result<(), RpcError> { - for i in 0..limit { - let resp = transport.call("echo", Value::Number(i.into())).await?; +async fn tokio_sleep(_: Transport, duration_ms: u64) -> Result<(), RpcError> { + info!("Sleeping (async) for {} milliseconds", duration_ms); + tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await; + info!( + "Woke up after sleeping (async) for {} milliseconds", + duration_ms + ); + Ok(()) +} - if resp.id != i { - error!("Incorrect response id: expected {}, got {}", i, resp.id); - return Err(RpcError::InternalError); - } +async fn call(transport: Transport, _: ()) -> Result { + let resp = transport.call("ping", Value::Null)?; + Ok(resp.result) +} - if resp.result != Value::Number(i.into()) { - error!("Incorrect response result: {:?}", resp.result); - return Err(RpcError::InternalError); - } +async fn call_many(transport: Transport, limit: u64) -> Result<(), RpcError> { + let calls = (0..limit).map(|_| ("ping", Value::Null)); + let resp = transport.call_many(calls)?; + + if resp.len() as u64 != limit { + error!( + "Incorrect number of responses: expected {}, got {}", + limit, + resp.len() + ); + return Err(RpcError::InternalError); } Ok(()) } -async fn prime_sieve(_transport: Arc, limit: u64) -> Result { +async fn async_call(transport: Transport, _: ()) -> Result { + let resp = transport.async_call("ping", Value::Null).await?; + Ok(resp.result) +} + +async fn prime_sieve(_transport: Transport, limit: u64) -> Result { let limit = limit as usize; let primes = sieve_of_eratosthenes(limit); info!("Generated {} primes up to {}", primes.len(), limit); @@ -94,12 +108,15 @@ fn main() { .init(); info!("Starting plugin..."); - PluginServer::new_with_transport() + PluginServer::new() .with_method("ping", ping) .with_method("get_random_number", get_random_number) .with_method("get_time", get_time) .with_method("sleep", sleep) - .with_method("many_echo", many_echo) + .with_method("tokio_sleep", tokio_sleep) + .with_method("call", call) + .with_method("call_many", call_many) + .with_method("async_call", async_call) .with_method("prime_sieve", prime_sieve) .run(); } diff --git a/crates/wasmi-plugin-hdk/benches/benchmark.rs b/crates/wasmi-plugin-hdk/benches/benchmark.rs index af02a12..9c98472 100644 --- a/crates/wasmi-plugin-hdk/benches/benchmark.rs +++ b/crates/wasmi-plugin-hdk/benches/benchmark.rs @@ -6,7 +6,6 @@ use wasmi_plugin_hdk::{ plugin::{Plugin, PluginId}, server::HostServer, }; -use wasmi_plugin_pdk::transport::Transport; const PLUGIN_WASM: &[u8] = include_bytes!("../../../target/wasm32-wasip1/release/test-plugin.wasm"); @@ -15,7 +14,12 @@ fn load_plugin_wasm() -> Vec { } fn get_host_server() -> HostServer<(Option, ())> { - HostServer::default().with_method("echo", |_, params: Value| async move { Ok(params) }) + HostServer::default() + .with_method( + "ping", + |_, _params: ()| async move { Ok("pong".to_string()) }, + ) + .with_method("echo", |_, params: Value| async move { Ok(params) }) } /// Benchmark a single ping request to the wasm module. @@ -84,18 +88,18 @@ pub fn bench_prime_sieve_large(c: &mut Criterion) { } /// Benchmark sending many echo requests to the host, and receiving responses. -pub fn bench_echo_many(c: &mut Criterion) { +pub fn bench_call_many(c: &mut Criterion) { let rt = Builder::new_current_thread().enable_all().build().unwrap(); let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); - c.bench_function("many_echo", |b| { + c.bench_function("call_many", |b| { b.iter(|| { let fut = async { plugin - .call("many_echo", Value::Number(200.into())) + .call("call_many", Value::Number(200.into())) .await .unwrap(); }; @@ -110,6 +114,6 @@ criterion_group!( bench_ping, bench_prime_sieve_small, bench_prime_sieve_large, - bench_echo_many + bench_call_many ); criterion_main!(benches); diff --git a/crates/wasmi-plugin-hdk/src/host_handler.rs b/crates/wasmi-plugin-hdk/src/host_handler.rs index ccbb180..de6160b 100644 --- a/crates/wasmi-plugin-hdk/src/host_handler.rs +++ b/crates/wasmi-plugin-hdk/src/host_handler.rs @@ -1,5 +1,5 @@ use serde_json::Value; -use wasmi_plugin_pdk::{rpc_message::RpcError, server::BoxFuture}; +use wasmi_plugin_pdk::{router::BoxFuture, rpc_message::RpcError}; use crate::plugin::PluginId; diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index 9f7e0f6..d9500d1 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -1,6 +1,5 @@ use std::{fmt::Display, io::BufReader, sync::Arc}; -use async_trait::async_trait; use futures::{AsyncBufReadExt, FutureExt}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -10,9 +9,9 @@ use uuid::Uuid; use wasmi::{Engine, Module}; use wasmi_plugin_pdk::{ api::RequestHandler, + router::BoxFuture, rpc_message::{RpcError, RpcResponse}, - server::BoxFuture, - transport::{JsonRpcTransport, Transport}, + transport::Transport, }; use crate::{ @@ -152,10 +151,8 @@ impl From for RpcError { } } -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl Transport for Plugin { - async fn call(&self, method: &str, params: Value) -> Result { +impl Plugin { + pub async fn call(&self, method: &str, params: Value) -> Result { let (stdin_writer, stdout_reader, stderr_reader, is_running, instance_task) = spawn_plugin(&self.engine, &self.module, self.max_fuel)?; @@ -176,8 +173,10 @@ impl Transport for Plugin { }; let buf_reader = BufReader::new(stdout_reader); - let transport = JsonRpcTransport::with_handler(buf_reader, stdin_writer, handler); - let rpc_task = transport.call(method, params).fuse(); + let transport = Transport::new(buf_reader, stdin_writer); + let rpc_task = transport + .async_call_with_handler(method, params, &handler) + .fuse(); let instance_task = instance_task.fuse(); futures::pin_mut!(rpc_task, instance_task, stderr_task); diff --git a/crates/wasmi-plugin-hdk/src/server.rs b/crates/wasmi-plugin-hdk/src/server.rs index b2d4491..c3db0dd 100644 --- a/crates/wasmi-plugin-hdk/src/server.rs +++ b/crates/wasmi-plugin-hdk/src/server.rs @@ -1,8 +1,9 @@ +use futures::future::BoxFuture; use serde::{Serialize, de::DeserializeOwned}; use serde_json::Value; use wasmi_plugin_pdk::{ + router::{MaybeSend, Router}, rpc_message::RpcError, - server::{BoxFuture, MaybeSend, Router}, }; use crate::{host_handler::HostHandler, plugin::PluginId}; diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index d1118f5..4592e7c 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -2,7 +2,6 @@ use serde_json::Value; use std::sync::Arc; use tracing::info; use wasmi_plugin_hdk::{plugin::Plugin, server::HostServer}; -use wasmi_plugin_pdk::transport::Transport; use web_time::{Instant, SystemTime}; #[cfg(target_family = "wasm")] @@ -103,17 +102,69 @@ async fn test_sleep() { #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_many_echo() { - info!("Starting many echo test..."); +async fn test_tokio_sleep() { + info!("Starting tokio sleep test..."); let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + + let sleep_duration = 1500; // milliseconds + let start = Instant::now(); plugin - .call("many_echo", Value::Number(200.into())) + .call("tokio_sleep", Value::Number(sleep_duration.into())) .await .unwrap(); + let elapsed = start.elapsed().as_millis(); + + assert!( + elapsed >= sleep_duration as u128, + "Sleep duration too short: {} ms", + elapsed + ); +} + +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_call() { + info!("Starting call test..."); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + + let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + let resp = plugin.call("call", Value::Null).await.unwrap(); + + assert_eq!(resp.result.as_str().unwrap(), "pong"); +} + +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_call_many() { + info!("Starting call_many test..."); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + + let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + plugin + .call("call_many", Value::Number(200.into())) + .await + .unwrap(); +} + +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_async_call() { + info!("Starting async_call test..."); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + + let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + let resp = plugin.call("async_call", Value::Null).await.unwrap(); + assert_eq!(resp.result.as_str().unwrap(), "pong"); } #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] diff --git a/crates/wasmi-plugin-pdk/src/api.rs b/crates/wasmi-plugin-pdk/src/api.rs index e81c8b8..ebc5144 100644 --- a/crates/wasmi-plugin-pdk/src/api.rs +++ b/crates/wasmi-plugin-pdk/src/api.rs @@ -1,6 +1,6 @@ use serde_json::Value; -use crate::{rpc_message::RpcError, server::BoxFuture}; +use crate::{router::BoxFuture, rpc_message::RpcError}; pub trait ApiError: From + Send + Sync {} impl ApiError for T where T: From + Send + Sync {} diff --git a/crates/wasmi-plugin-pdk/src/lib.rs b/crates/wasmi-plugin-pdk/src/lib.rs index 39ec3c4..d9b98cc 100644 --- a/crates/wasmi-plugin-pdk/src/lib.rs +++ b/crates/wasmi-plugin-pdk/src/lib.rs @@ -1,4 +1,5 @@ pub mod api; +pub mod router; pub mod rpc_message; pub mod server; pub mod transport; diff --git a/crates/wasmi-plugin-pdk/src/router.rs b/crates/wasmi-plugin-pdk/src/router.rs new file mode 100644 index 0000000..6a66048 --- /dev/null +++ b/crates/wasmi-plugin-pdk/src/router.rs @@ -0,0 +1,89 @@ +use std::collections::HashMap; + +use serde::{Serialize, de::DeserializeOwned}; +use serde_json::Value; +use tracing::warn; + +use crate::rpc_message::RpcError; + +#[cfg(target_arch = "wasm32")] +pub type BoxFuture<'a, T> = futures::future::LocalBoxFuture<'a, T>; + +#[cfg(not(target_arch = "wasm32"))] +pub type BoxFuture<'a, T> = futures::future::BoxFuture<'a, T>; + +#[cfg(target_arch = "wasm32")] +pub trait MaybeSend {} +#[cfg(target_arch = "wasm32")] +impl MaybeSend for T {} + +#[cfg(not(target_arch = "wasm32"))] +pub trait MaybeSend: Send {} +#[cfg(not(target_arch = "wasm32"))] +impl MaybeSend for T {} + +type HandlerFn = dyn Send + Sync + Fn(S, Value) -> BoxFuture<'static, Result>; + +pub struct Router { + handlers: HashMap>>, +} + +impl Default for Router { + fn default() -> Self { + Router { + handlers: HashMap::new(), + } + } +} + +impl Router { + pub fn new() -> Router { + Self::default() + } + + /// Register a new RPC method with the router. The method is identified by the + /// given name, and the handler function should accept the shared state and + /// deserialized params. + /// + /// Handlers should implement: `async fn handler(state: S, params: P) -> Result` + pub fn with_method(mut self, name: &str, func: F) -> Self + where + P: DeserializeOwned + 'static, + R: Serialize + 'static, + F: Fn(S, P) -> Fut + Send + Sync + 'static, + Fut: Future> + MaybeSend + 'static, + { + // Handler function that parses json params, calls the provided func, + // and serializes the result back to json. + let f = Box::new( + move |state: S, params: Value| -> BoxFuture<'static, Result> { + let parsed = serde_json::from_value(params); + let Ok(p) = parsed else { + return Box::pin(async move { Err(RpcError::InvalidParams) }); + }; + + let fut = func(state, p); + Box::pin(async move { + let result = fut.await?; + Ok(serde_json::to_value(result).unwrap()) + }) + }, + ); + + self.handlers.insert(name.to_string(), f); + self + } + + pub async fn handle_with_state( + &self, + state: S, + method: &str, + params: Value, + ) -> Result { + let Some(handler) = self.handlers.get(method) else { + warn!("Method not found: {}", method); + return Err(RpcError::MethodNotFound); + }; + handler(state, params).await + } +} diff --git a/crates/wasmi-plugin-pdk/src/rpc_message.rs b/crates/wasmi-plugin-pdk/src/rpc_message.rs index 30859d6..0b625b5 100644 --- a/crates/wasmi-plugin-pdk/src/rpc_message.rs +++ b/crates/wasmi-plugin-pdk/src/rpc_message.rs @@ -48,6 +48,24 @@ pub enum RpcError { Custom(String), } +impl RpcMessage { + pub fn response(id: u64, result: Value) -> Self { + RpcMessage::RpcResponse(RpcResponse { + jsonrpc: "2.0".to_string(), + id, + result, + }) + } + + pub fn error_response(id: u64, error: RpcError) -> Self { + RpcMessage::RpcErrorResponse(RpcErrorResponse { + jsonrpc: "2.0".to_string(), + id, + error, + }) + } +} + impl RpcError { pub fn code(&self) -> i64 { match self { @@ -63,6 +81,10 @@ impl RpcError { pub fn message(&self) -> String { self.to_string() } + + pub fn custom(msg: impl Into) -> Self { + RpcError::Custom(msg.into()) + } } pub fn to_rpc_err(e: E) -> RpcError { diff --git a/crates/wasmi-plugin-pdk/src/server.rs b/crates/wasmi-plugin-pdk/src/server.rs index 09a8574..ba4df42 100644 --- a/crates/wasmi-plugin-pdk/src/server.rs +++ b/crates/wasmi-plugin-pdk/src/server.rs @@ -1,66 +1,47 @@ -use std::{collections::HashMap, sync::Arc}; +use std::io::Write; use serde::{Serialize, de::DeserializeOwned}; -use serde_json::Value; +use thiserror::Error; use tokio::runtime::Builder; -use tracing::warn; -use crate::{api::RequestHandler, rpc_message::RpcError, transport::JsonRpcTransport}; - -#[cfg(target_arch = "wasm32")] -pub type BoxFuture<'a, T> = futures::future::LocalBoxFuture<'a, T>; - -#[cfg(not(target_arch = "wasm32"))] -pub type BoxFuture<'a, T> = futures::future::BoxFuture<'a, T>; - -#[cfg(target_arch = "wasm32")] -pub trait MaybeSend {} -#[cfg(target_arch = "wasm32")] -impl MaybeSend for T {} - -#[cfg(not(target_arch = "wasm32"))] -pub trait MaybeSend: Send {} -#[cfg(not(target_arch = "wasm32"))] -impl MaybeSend for T {} - -type HandlerFn = dyn Send + Sync + Fn(S, Value) -> BoxFuture<'static, Result>; +use crate::{ + router::{MaybeSend, Router}, + rpc_message::{RpcError, RpcMessage}, + transport::Transport, +}; /// Server is a RPC server that can handle requests by dispatching them to registered /// handler functions. It stores a shared state `S` that is passed into each handler. pub struct PluginServer { - transport: Arc, - router: Router>, + router: Router, } -pub struct Router { - handlers: HashMap>>, -} +#[derive(Debug, Error)] +enum PluginServerError { + #[error("Missing Request")] + MissingRequest, -impl PluginServer { - pub fn new(transport: Arc) -> Self { - Self { - transport, - router: Router::new(), - } - } + #[error("Invalid Message: {0:?}")] + InvalidMessage(RpcMessage), - pub fn new_with_transport() -> Self { - let reader = std::io::BufReader::new(std::io::stdin()); - let writer = std::io::stdout(); - let transport = JsonRpcTransport::new(reader, writer); - let transport = Arc::new(transport); + #[error("serde_json error: {0}")] + Serde(#[from] serde_json::Error), - Self { - transport, - router: Router::new(), - } + #[error("IO error: {0}")] + Io(#[from] std::io::Error), +} + +impl PluginServer { + pub fn new() -> Self { + let router = Router::new(); + Self { router } } pub fn with_method(mut self, name: &str, func: F) -> Self where P: DeserializeOwned + 'static, R: Serialize + 'static, - F: Fn(Arc, P) -> Fut + Send + Sync + 'static, + F: Fn(Transport, P) -> Fut + Send + Sync + 'static, Fut: Future> + MaybeSend + 'static, { self.router = self.router.with_method(name, func); @@ -68,84 +49,57 @@ impl PluginServer { } pub fn run(self) { - let transport = self.transport.clone(); + match self.try_run() { + Ok(()) => {} + Err(e) => { + eprintln!("Plugin server error: {}", e); + std::process::exit(1); + } + } + // let transport = self.transport.clone(); - let rt = Builder::new_current_thread().enable_time().build().unwrap(); - let local = tokio::task::LocalSet::new(); + // let rt = Builder::new_current_thread().enable_time().build().unwrap(); + // let local = tokio::task::LocalSet::new(); - rt.block_on(local.run_until(async move { - let _ = transport.process_next_line(Some(&self)).await; - })); + // rt.block_on(local.run_until(async move { + // let _ = transport.process_next_line(Some(&self)).await; + // })); } -} -impl RequestHandler for PluginServer { - fn handle<'a>( - &'a self, - method: &'a str, - params: Value, - ) -> BoxFuture<'a, Result> { - let state = self.transport.clone(); - Box::pin(async move { self.router.handle_with_state(state, method, params).await }) - } -} + fn try_run(&self) -> Result<(), PluginServerError> { + //? Read request + let mut line = String::new(); + std::io::stdin() + .read_line(&mut line) + .map_err(|_| PluginServerError::MissingRequest)?; -impl Default for Router { - fn default() -> Self { - Router { - handlers: HashMap::new(), - } - } -} + let message: RpcMessage = serde_json::from_str(&line)?; + let RpcMessage::RpcRequest(request) = message else { + return Err(PluginServerError::InvalidMessage(message)); + }; -impl Router { - pub fn new() -> Router { - Self::default() - } + //? Dispatch handler + let rt = Builder::new_current_thread().enable_time().build()?; + let local = tokio::task::LocalSet::new(); - /// Register a new RPC method with the router. The method is identified by the - /// given name, and the handler function should accept the shared state and - /// deserialized params. - /// - /// Handlers should implement: `async fn handler(state: S, params: P) -> Result` - pub fn with_method(mut self, name: &str, func: F) -> Self - where - P: DeserializeOwned + 'static, - R: Serialize + 'static, - F: Fn(S, P) -> Fut + Send + Sync + 'static, - Fut: Future> + MaybeSend + 'static, - { - // Handler function that parses json params, calls the provided func, - // and serializes the result back to json. - let f = Box::new( - move |state: S, params: Value| -> BoxFuture<'static, Result> { - let parsed = serde_json::from_value(params); - let Ok(p) = parsed else { - return Box::pin(async move { Err(RpcError::InvalidParams) }); - }; - - let fut = func(state, p); - Box::pin(async move { - let result = fut.await?; - Ok(serde_json::to_value(result).unwrap()) - }) - }, - ); - - self.handlers.insert(name.to_string(), f); - self - } + let resp = rt.block_on(local.run_until(async move { + let transport = Transport::new(std::io::stdin(), std::io::stdout()); + self.router + .handle_with_state(transport, &request.method, request.params) + .await + })); - pub async fn handle_with_state( - &self, - state: S, - method: &str, - params: Value, - ) -> Result { - let Some(handler) = self.handlers.get(method) else { - warn!("Method not found: {}", method); - return Err(RpcError::MethodNotFound); + //? Send response + let resp = match resp { + Ok(result) => RpcMessage::response(request.id, result), + Err(error) => RpcMessage::error_response(request.id, error), }; - handler(state, params).await + + let serialized = serde_json::to_string(&resp)?; + let msg = format!("{}\n", serialized); + std::io::stdout().write_all(msg.as_bytes())?; + std::io::stdout().flush()?; + + Ok(()) } } diff --git a/crates/wasmi-plugin-pdk/src/transport.rs b/crates/wasmi-plugin-pdk/src/transport.rs index fde6dce..54d580b 100644 --- a/crates/wasmi-plugin-pdk/src/transport.rs +++ b/crates/wasmi-plugin-pdk/src/transport.rs @@ -1,282 +1,266 @@ use std::{ - collections::HashMap, - io::{BufRead, Write}, - sync::atomic::AtomicU64, + collections::{HashMap, HashSet}, + io::{BufRead, BufReader, Read, Write}, + sync::{ + Arc, Mutex, + atomic::{AtomicU64, Ordering}, + }, }; -use async_trait::async_trait; -use futures::{ - channel::oneshot::{self, Sender}, - lock::Mutex, -}; use serde_json::Value; -use tracing::{trace, warn}; +use tracing::info; use wasmi_plugin_rt::yield_now; use crate::{ - api::{ApiError, RequestHandler}, - rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcRequest, RpcResponse}, + api::RequestHandler, + rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcRequest, RpcResponse, to_rpc_err}, }; -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait Transport { - async fn call(&self, method: &str, params: Value) -> Result; -} - /// Single-threaded, concurrent-safe json-rpc transport layer -pub struct JsonRpcTransport { - id: AtomicU64, - pending: Mutex>>, - reader: Mutex>, - writer: Mutex>, - handler: Option>>, -} +#[derive(Clone)] +pub struct Transport { + id: Arc, + reader: Arc>>, + writer: Arc>>, -const JSON_RPC_VERSION: &str = "2.0"; + pending: Arc>>>, +} -impl JsonRpcTransport { +impl Transport { pub fn new( - reader: impl BufRead + Send + Sync + 'static, + reader: impl Read + Send + Sync + 'static, writer: impl Write + Send + Sync + 'static, ) -> Self { Self { - id: AtomicU64::new(0), - pending: Mutex::new(HashMap::new()), - reader: Mutex::new(Box::new(reader)), - writer: Mutex::new(Box::new(writer)), - handler: None, + id: Arc::new(AtomicU64::new(0)), + pending: Arc::new(Mutex::new(HashMap::new())), + reader: Arc::new(Mutex::new(Box::new(BufReader::new(reader)))), + writer: Arc::new(Mutex::new(Box::new(writer))), } } +} - pub fn with_handler( - reader: impl BufRead + Send + Sync + 'static, - writer: impl Write + Send + Sync + 'static, - handler: impl RequestHandler + 'static, - ) -> Self { - Self { - id: AtomicU64::new(0), - pending: Mutex::new(HashMap::new()), - reader: Mutex::new(Box::new(reader)), - writer: Mutex::new(Box::new(writer)), - handler: Some(Box::new(handler)), - } +impl Transport { + /// Send a single call and wait for the response. + pub fn call(&self, method: &str, params: Value) -> Result { + let responses = self.call_many(std::iter::once((method, params)))?; + let resp = responses + .into_iter() + .next() + .ok_or(RpcError::custom("Response does not exist"))?; + + Ok(resp) } -} -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl Transport for JsonRpcTransport { - /// Sends a json-rpc request and waits for the response. `Call` does not - /// pump the reader, so you must call `process_next_line` in another task - /// to read responses / handle incoming requests. - async fn call(&self, method: &str, params: Value) -> Result { - let (tx, mut rx) = oneshot::channel(); - let id = self.id.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - self.pending.lock().await.insert(id, tx); - self.write_request(id, method, params).await?; - - //? Loop handles all incoming messages until we get a response for our call - loop { - match rx.try_recv() { - Ok(Some(msg)) => { - return Ok(msg); - } - Ok(None) => {} - Err(_) => { - warn!("Caller dropped"); - return Err(RpcError::InternalError); + /// Send multiple calls and wait for all responses. This allows async-like + /// behavior because the host will process requests concurrently. + /// + /// The order of responses matches the order of requests. If any call fails, + /// the entire batch will fail. + pub fn call_many(&self, calls: I) -> Result, RpcError> + where + I: IntoIterator, + S: Into, + { + let ids: Result, RpcError> = calls + .into_iter() + .map(|(method, params)| { + let id = self.next_id(); + self.write(id, &method.into(), params)?; + Ok(id) + }) + .collect(); + let ids = ids?; + + let mut results = Vec::with_capacity(ids.len()); + let mut remaining: HashSet = ids.iter().copied().collect(); + + while !remaining.is_empty() { + let message = self.read()?; + self.insert_response(message); + + //? Retrieve any fulfilled requests + let mut fulfilled = Vec::new(); + for &id in &remaining { + if let Some(result) = self.pending.lock().unwrap().remove(&id) { + results.push(result.map_err(|e| to_rpc_err(e.error))?); + fulfilled.push(id); } } - self.process_next_line(self.handler.as_deref()).await?; - } - } -} - -impl JsonRpcTransport { - /// Processes a single request from the reader. Can be used to manually - /// pump the reader with a custom handler. - pub async fn process_next_line( - &self, - handler: Option<&dyn RequestHandler>, - ) -> Result<(), RpcError> { - let line: String = self.next_line().await?; - let message = match serde_json::from_str::(line.trim()) { - Ok(msg) => msg, - Err(_) => { - warn!("Failed to parse line: {}", line.trim()); - return Err(RpcError::ParseError); + for id in fulfilled { + remaining.remove(&id); } - }; + } - trace!("JsonRpcTransport::process_next_line() - {:?}", message); + results.sort_by_key(|r| r.id); - self.process_message(message, handler).await + Ok(results) } - async fn write_request(&self, id: u64, method: &str, params: Value) -> Result<(), RpcError> { - let msg = RpcMessage::RpcRequest(RpcRequest { - jsonrpc: JSON_RPC_VERSION.to_string(), + fn write(&self, id: u64, method: &str, params: Value) -> Result<(), RpcError> { + let request = RpcMessage::RpcRequest(crate::rpc_message::RpcRequest { + jsonrpc: "2.0".into(), id, - method: method.to_string(), + method: method.into(), params, }); - self.write_message(&msg).await?; - Ok(()) - } - async fn write_message(&self, msg: &RpcMessage) -> Result<(), RpcError> { - let serialized = serde_json::to_string(msg).map_err(|_| { - warn!("Failed to serialize message: {:?}", msg); - RpcError::InternalError - })?; - let msg = format!("{}\n", serialized); - - trace!("JsonRpcTransport::write_message() - {}", msg.trim()); - let mut writer = self.writer.lock().await; - writer.write_all(msg.as_bytes()).map_err(|_| { - warn!("Failed to write message: {}", msg.trim()); - RpcError::InternalError - })?; - writer.flush().map_err(|_| { - warn!("Failed to flush message: {}", msg.trim()); - RpcError::InternalError - })?; - Ok(()) + self.write_message(&request) } - async fn process_message( - &self, - message: RpcMessage, - handler: Option<&dyn RequestHandler>, - ) -> Result<(), RpcError> { - match message.clone() { - RpcMessage::RpcResponse(resp) => { - if let Some(tx) = self.pending.lock().await.remove(&resp.id) { - let _ = tx.send(resp); - } - } - RpcMessage::RpcErrorResponse(err) => { - warn!("Received error response: {:?}", err.error.message()); - return Err(err.error); - } - RpcMessage::RpcRequest(RpcRequest { - jsonrpc: _, - id, - method, - params, - }) => { - let handler = handler.ok_or(RpcError::Custom("Handler not found".into()))?; - match handler.handle(&method, params).await { - Ok(result) => { - let response = RpcMessage::RpcResponse(RpcResponse { - jsonrpc: JSON_RPC_VERSION.to_string(), - id, - result, - }); - self.write_message(&response).await?; - } - Err(error) => { - let response = RpcMessage::RpcErrorResponse(RpcErrorResponse { - jsonrpc: JSON_RPC_VERSION.to_string(), - id, - error, - }); - self.write_message(&response).await?; - } - } - } + fn write_message(&self, message: &RpcMessage) -> Result<(), RpcError> { + let message_str = serde_json::to_string(message).map_err(to_rpc_err)?; + let msg = format!("{}\n", message_str); + { + let mut writer = self.writer.lock().unwrap(); + writer.write_all(msg.as_bytes()).map_err(to_rpc_err)?; + writer.flush().map_err(to_rpc_err)?; } Ok(()) } - async fn next_line(&self) -> Result { + /// Blocking read of a single message + fn read(&self) -> Result { let mut line = String::new(); - // Loop until we get a line, awaiting if we would block - //? Uses a loop here instead of an async reader since need this to - //? also work in the plugin wasi environment. There it'll receive the - //? `WouldBlock` error from the host's non-blocking pipe, but since - //? it's just getting that through stdio it can't be async. And using - //? async stdio won't work well because wasi. + //? Loop until we successfully read a line or hit EOF, continuing on WouldBlock + //? errors. loop { - match self.reader.lock().await.read_line(&mut line) { - Ok(0) => { - warn!("EOF reached while reading line"); - return Err(RpcError::InternalError); + let res = self.reader.lock().unwrap().read_line(&mut line); + match res { + Ok(0) => return Err(RpcError::custom("EOF reached while reading from transport")), + Ok(_) => break, + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + continue; + } + Err(e) => { + return Err(RpcError::custom(&format!( + "Failed to read from transport: {}", + e + ))); } - Ok(_) => return Ok(line), - Err(e) => match e.kind() { - std::io::ErrorKind::WouldBlock => { - yield_now().await; - continue; - } - _ => { - warn!("Failed to read line: {}", e); - return Err(RpcError::InternalError); - } - }, } } - } -} -#[cfg(test)] -mod test { - use super::*; - use futures::executor::block_on; - use std::{ - io::{BufReader, Cursor}, - sync::Arc, - }; - - struct MockWriter { - buffer: Arc>>, + Ok(serde_json::from_str(&line).map_err(to_rpc_err)?) } - impl MockWriter { - fn new(buffer: Arc>>) -> Self { - Self { buffer } + /// Inserts a response message into the pending map. If the message is a request, + /// it is ignored. + fn insert_response(&self, message: RpcMessage) { + match message { + RpcMessage::RpcResponse(resp) => { + self.pending.lock().unwrap().insert(resp.id, Ok(resp)); + } + RpcMessage::RpcErrorResponse(err_resp) => { + self.pending + .lock() + .unwrap() + .insert(err_resp.id, Err(err_resp)); + } + RpcMessage::RpcRequest(_) => { + info!("Ignoring request message"); + } } } - impl Write for MockWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - block_on(async { - self.buffer.lock().await.extend_from_slice(buf); - }); - Ok(buf.len()) - } + fn next_id(&self) -> u64 { + self.id.fetch_add(1, Ordering::Relaxed) + } +} - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } +impl Transport { + /// Asynchronously send a single call and wait for the response. This allows + /// other async tasks to run while waiting for the response. + pub async fn async_call(&self, method: &str, params: Value) -> Result { + self.async_call_impl(method, params, None).await } - #[tokio::test] - async fn test_response_received_and_pending_removed() { - let response = RpcResponse { - jsonrpc: "2.0".into(), - id: 42, - result: serde_json::json!({"success": true}), - }; - let response_json = serde_json::to_string(&response).unwrap(); + /// Asynchronously send a single call and wait for the response, while also handling + /// incoming requests using the provided handler. + /// + /// Used primarily by hosts to handle requests from guests while waiting for responses. + /// + /// ```text + /// host -> guest: balance() + /// host <- guest: balanceOf(1) + /// host ->> guest: 42 + /// host <- guest: balanceOf(2) + /// host ->> guest: 100 + /// host <<- guest: (42, 100) + /// ``` + pub async fn async_call_with_handler( + &self, + method: &str, + params: Value, + handler: &dyn RequestHandler, + ) -> Result { + self.async_call_impl(method, params, Some(handler)).await + } - let reader = BufReader::new(Cursor::new(format!("{}\n", response_json))); - let output_buffer = Arc::new(Mutex::new(Vec::new())); - let writer = MockWriter::new(output_buffer); - let transport = JsonRpcTransport::new(reader, writer); + async fn async_call_impl( + &self, + method: &str, + params: Value, + handler: Option<&dyn RequestHandler>, + ) -> Result { + let id = self.next_id(); + self.write(id, method, params)?; + + loop { + let message = self.async_read().await?; + match message { + RpcMessage::RpcRequest(RpcRequest { + id, method, params, .. + }) => { + let Some(handler) = handler else { + info!("Ignoring request message as no handler is provided"); + continue; + }; + + match handler.handle(&method, params).await { + Ok(result) => { + self.write_message(&RpcMessage::response(id, result))?; + } + Err(error) => { + self.write_message(&RpcMessage::error_response(id, error))?; + } + } + } + _ => self.insert_response(message), + } + + //? If our request has been fulfilled, return the result + if let Some(result) = self.pending.lock().unwrap().remove(&id) { + return result.map_err(|e| to_rpc_err(e.error)); + } + } + } - // Inject a fake pending request with the ID and a rx we control - let (tx, mut rx) = oneshot::channel(); - transport.pending.lock().await.insert(42, tx); + /// Async read of a single message, yielding when the reader would block + async fn async_read(&self) -> Result { + let mut line = String::new(); - // Process the next line containing our expected response - transport.process_next_line(None).await.unwrap(); + loop { + let res = self.reader.lock().unwrap().read_line(&mut line); + match res { + Ok(0) => return Err(RpcError::custom("EOF reached while reading from transport")), + Ok(_) => break, + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + yield_now().await; + continue; + } + Err(e) => { + return Err(RpcError::custom(&format!( + "Failed to read from transport: {}", + e + ))); + } + } + } - assert!(!transport.pending.lock().await.contains_key(&42)); - assert_eq!(rx.try_recv().unwrap().unwrap(), response); + Ok(serde_json::from_str(&line).map_err(to_rpc_err)?) } } From c64a3aea54f16353c5f55d55d8fd094b10a7e96a Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Mon, 22 Dec 2025 14:02:52 -0500 Subject: [PATCH 02/70] feat: working native wasmer --- .github/workflows/ci.yml | 2 + Cargo.lock | 4573 +++++++++++++++-- Cargo.toml | 13 +- crates/test-plugin/Cargo.toml | 3 + crates/wasmi-plugin-hdk/Cargo.toml | 18 +- crates/wasmi-plugin-hdk/benches/benchmark.rs | 26 +- crates/wasmi-plugin-hdk/src/bridge/mod.rs | 46 + .../src/bridge/native_bridge.rs | 106 + .../src/bridge/worker_bridge.rs | 115 + crates/wasmi-plugin-hdk/src/compile.rs | 43 +- crates/wasmi-plugin-hdk/src/lib.rs | 3 +- crates/wasmi-plugin-hdk/src/plugin.rs | 124 +- crates/wasmi-plugin-hdk/src/server.rs | 3 +- crates/wasmi-plugin-hdk/src/wasi/linker.rs | 679 +++ crates/wasmi-plugin-hdk/src/wasi/mod.rs | 4 +- crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs | 404 +- crates/wasmi-plugin-hdk/tests/integration.rs | 1 + shell.nix | 1 + 18 files changed, 5341 insertions(+), 823 deletions(-) create mode 100644 crates/wasmi-plugin-hdk/src/bridge/mod.rs create mode 100644 crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs create mode 100644 crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs create mode 100644 crates/wasmi-plugin-hdk/src/wasi/linker.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8d3473..157645b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,7 @@ jobs: with: file: target/wasm32-wasip1/release/*.wasm optimize_all: true + options: -O3 - run: cargo build - run: cargo test @@ -53,6 +54,7 @@ jobs: with: file: target/wasm32-wasip1/release/*.wasm optimize_all: true + options: -O3 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/Cargo.lock b/Cargo.lock index f4f73c2..42710a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ - "gimli", + "gimli 0.32.3", ] [[package]] @@ -17,6 +17,18 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -35,6 +47,15 @@ dependencies = [ "cc", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -47,6 +68,36 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +[[package]] +name = "any_ascii" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-trait" version = "0.1.89" @@ -55,9 +106,15 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -74,23 +131,176 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object", + "object 0.37.3", "rustc-demangle", "windows-link", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.111", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bus" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7118d0221d84fada881b657c2ddb7cd55108db79c8764c9ee212c0c259b783" +dependencies = [ + "crossbeam-channel", + "num_cpus", + "parking_lot_core", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive 0.6.12", + "ptr_meta 0.1.4", + "simdutf8", +] + +[[package]] +name = "bytecheck" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" +dependencies = [ + "bytecheck_derive 0.8.2", + "ptr_meta 0.3.1", + "rancor", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytecheck_derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +dependencies = [ + "serde", +] + +[[package]] +name = "bytesize" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" +dependencies = [ + "serde", +] + [[package]] name = "cast" version = "0.3.0" @@ -107,12 +317,40 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -140,6 +378,17 @@ dependencies = [ "half", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.53" @@ -166,829 +415,3147 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] -name = "criterion" -version = "0.8.0" +name = "cmake" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0dfe5e9e71bdcf4e4954f7d14da74d1cdb92a3a07686452d1509652684b1aab" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" dependencies = [ - "alloca", - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "itertools", - "num-traits", - "oorandom", - "page_size", - "plotters", - "rayon", - "regex", - "serde", - "serde_json", - "tinytemplate", - "walkdir", + "cc", ] [[package]] -name = "criterion-plot" -version = "0.8.0" +name = "console" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de36c2bee19fba779808f92bf5d9b0fa5a40095c277aba10c458a12b35d21d6" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" dependencies = [ - "cast", - "itertools", + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", ] [[package]] -name = "crossbeam-deque" -version = "0.8.6" +name = "constant_time_eq" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] -name = "crossbeam-epoch" -version = "0.9.18" +name = "convert_case" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ - "crossbeam-utils", + "unicode-segmentation", ] [[package]] -name = "crossbeam-utils" -version = "0.8.21" +name = "cooked-waker" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "147be55d677052dabc6b22252d5dd0fd4c29c8c27aa4f2fbef0f94aa003b406f" [[package]] -name = "crunchy" -version = "0.2.4" +name = "core-foundation" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] -name = "dhat" -version = "0.3.3" +name = "core-foundation-sys" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827" -dependencies = [ - "backtrace", - "lazy_static", - "mintex", - "parking_lot", - "rustc-hash", - "serde", - "serde_json", - "thousands", -] +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "either" -version = "1.15.0" +name = "corosensei" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "2b2b4c7e3e97730e6b0b8c5ff5ca82c663d1a645e4f630f4fa4c24e80626787e" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "scopeguard", + "windows-sys 0.59.0", +] [[package]] -name = "equivalent" -version = "1.0.2" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] [[package]] -name = "find-msvc-tools" -version = "0.1.5" +name = "cranelift-bforest" +version = "0.110.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "305d51c180ebdc46ef61bc60c54ae6512db3bc9a05842a1f1e762e45977019ab" +dependencies = [ + "cranelift-entity", +] [[package]] -name = "foldhash" -version = "0.1.5" +name = "cranelift-bitset" +version = "0.110.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "690d8ae6c73748e5ce3d8fe59034dceadb8823e6c8994ba324141c5eae909b0e" [[package]] -name = "futures" -version = "0.3.31" +name = "cranelift-codegen" +version = "0.110.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "bd7ca95e831c18d1356da783765c344207cbdffea91e13e47fa9327dbb2e0719" dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "bumpalo", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli 0.28.1", + "hashbrown 0.14.5", + "log", + "regalloc2", + "rustc-hash 1.1.0", + "smallvec", + "target-lexicon", ] [[package]] -name = "futures-channel" -version = "0.3.31" +name = "cranelift-codegen-meta" +version = "0.110.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "f0a2d2ab65e6cbf91f81781d8da65ec2005510f18300eff21a99526ed6785863" dependencies = [ - "futures-core", - "futures-sink", + "cranelift-codegen-shared", ] [[package]] -name = "futures-core" -version = "0.3.31" +name = "cranelift-codegen-shared" +version = "0.110.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "efcff860573cf3db9ae98fbd949240d78b319df686cc306872e7fab60e9c84d7" [[package]] -name = "futures-executor" -version = "0.3.31" +name = "cranelift-control" +version = "0.110.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "69d70e5b75c2d5541ef80a99966ccd97aaa54d2a6af19ea31759a28538e1685a" dependencies = [ - "futures-core", - "futures-task", - "futures-util", + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.110.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a48cb0a194c9ba82fec35a1e492055388d89b2e3c03dee9dcf2488892be8004d" +dependencies = [ + "cranelift-bitset", +] + +[[package]] +name = "cranelift-frontend" +version = "0.110.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8327afc6c1c05f4be62fefce5b439fa83521c65363a322e86ea32c85e7ceaf64" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.110.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56b08621c00321efcfa3eee6a3179adc009e21ea8d24ca7adc3c326184bc3f48" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0dfe5e9e71bdcf4e4954f7d14da74d1cdb92a3a07686452d1509652684b1aab" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools 0.13.0", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de36c2bee19fba779808f92bf5d9b0fa5a40095c277aba10c458a12b35d21d6" +dependencies = [ + "cast", + "itertools 0.13.0", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling 0.14.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "derive_more" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.111", + "unicode-xid", +] + +[[package]] +name = "dhat" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827" +dependencies = [ + "backtrace", + "lazy_static", + "mintex", + "parking_lot", + "rustc-hash 1.1.0", + "serde", + "serde_json", + "thousands", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "enum-iterator" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "enumset" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", ] [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.1", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "insta" +version = "1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b76866be74d68b1595eb8060cb9191dca9c021db2316558e52ddc5d55d41b66c" +dependencies = [ + "console", + "once_cell", + "regex", + "serde", + "similar", + "tempfile", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iprange" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37209be0ad225457e63814401415e748e2453a5297f9b637338f5fb8afa4ec00" +dependencies = [ + "ipnet", +] + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lexical-sort" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c09e4591611e231daf4d4c685a66cb0410cc1e502027a20ae55f2bb9e997207a" +dependencies = [ + "any_ascii", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall 0.6.0", +] + +[[package]] +name = "libunwind" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6639b70a7ce854b79c70d7e83f16b5dc0137cc914f3d7d03803b513ecc67ac" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linked_hash_set" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" +dependencies = [ + "twox-hash 2.1.2", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "macho-unwind-info" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4bdc8b0ce69932332cf76d24af69c3a155242af95c226b2ab6c2e371ed1149" +dependencies = [ + "thiserror 2.0.17", + "zerocopy", + "zerocopy-derive", +] + +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memmap2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mintex" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "more-asserts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7843ec2de400bcbc6a6328c958dc38e5359da6e93e72e37bc5246bf1ae776389" + +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "5.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "crc32fast", + "flate2", + "hashbrown 0.14.5", + "indexmap", + "memchr", + "ruzstd", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "path-clean" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.111", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive 0.1.4", +] + +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive 0.3.1", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "pulldown-cmark" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" +dependencies = [ + "bitflags 1.3.2", + "memchr", + "unicase", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2 0.6.1", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.1", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rancor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +dependencies = [ + "ptr_meta 0.3.1", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] [[package]] -name = "futures-macro" -version = "0.3.31" +name = "rand" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "proc-macro2", - "quote", - "syn", + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] -name = "futures-sink" -version = "0.3.31" +name = "rand_chacha" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] [[package]] -name = "futures-task" -version = "0.3.31" +name = "rand_chacha" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] [[package]] -name = "futures-util" -version = "0.3.31" +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_syscall" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "regalloc2" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad156d539c879b7a24a363a2016d77961786e71f48f2e2fc8302a92abd2429a6" +dependencies = [ + "hashbrown 0.13.2", + "log", + "rustc-hash 1.1.0", + "slice-group-by", + "smallvec", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "region" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" +dependencies = [ + "bitflags 1.3.2", + "libc", + "mach2", + "windows-sys 0.52.0", +] + +[[package]] +name = "rend" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" dependencies = [ + "bytecheck 0.8.2", +] + +[[package]] +name = "replace_with" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" + +[[package]] +name = "reqwest" +version = "0.12.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" +dependencies = [ + "base64", + "bytes", "futures-channel", "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", "pin-project-lite", - "pin-utils", - "slab", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a640b26f007713818e9a9b65d34da1cf58538207b052916a83d80e43f3ffa4" +dependencies = [ + "bytecheck 0.8.2", + "bytes", + "hashbrown 0.15.5", + "indexmap", + "munge", + "ptr_meta 0.3.1", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd83f5f173ff41e00337d97f6572e416d022ef8a19f371817259ae960324c482" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty_pool" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed36cdb20de66d89a17ea04b8883fc7a386f2cf877aaedca5005583ce4876ff" +dependencies = [ + "crossbeam-channel", + "futures", + "futures-channel", + "futures-executor", + "num_cpus", +] + +[[package]] +name = "ruzstd" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c4eb8a81997cf040a091d1f7e1938aeab6749d3a0dfa73af43cdc32393483d" +dependencies = [ + "byteorder", + "derive_more 0.99.20", + "twox-hash 1.6.3", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "saffron" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03fb9a628596fc7590eb7edbf7b0613287be78df107f5f97b118aad59fb2eea9" +dependencies = [ + "chrono", + "nom 5.1.3", ] [[package]] -name = "getrandom" -version = "0.2.16" +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", + "winapi-util", ] [[package]] -name = "getrandom" -version = "0.3.4" +name = "schannel" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", + "windows-sys 0.61.2", ] [[package]] -name = "gimli" -version = "0.32.3" +name = "schemars" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap", + "schemars_derive", + "serde", + "serde_json", + "url", +] [[package]] -name = "gloo-timers" -version = "0.3.0" +name = "schemars_derive" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.111", ] [[package]] -name = "half" -version = "2.7.1" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", + "bitflags 2.10.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", ] [[package]] -name = "hashbrown" -version = "0.15.5" +name = "security-framework-sys" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ - "foldhash", + "core-foundation-sys", + "libc", ] [[package]] -name = "hashbrown" -version = "0.16.1" +name = "self_cell" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" [[package]] -name = "indexmap" -version = "2.12.1" +name = "semver" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ - "equivalent", - "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] -name = "itertools" -version = "0.13.0" +name = "serde" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ - "either", + "serde_core", + "serde_derive", ] [[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "js-sys" -version = "0.3.83" +name = "serde-wasm-bindgen" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" dependencies = [ - "once_cell", + "js-sys", + "serde", "wasm-bindgen", ] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] [[package]] -name = "leb128fmt" -version = "0.1.0" +name = "serde_derive" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] [[package]] -name = "libc" -version = "0.2.178" +name = "serde_derive_internals" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] [[package]] -name = "libm" -version = "0.2.15" +name = "serde_json" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] [[package]] -name = "lock_api" -version = "0.4.14" +name = "serde_spanned" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ - "scopeguard", + "serde", ] [[package]] -name = "log" -version = "0.4.29" +name = "serde_urlencoded" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] [[package]] -name = "matchers" -version = "0.2.0" +name = "serde_yaml" +version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "regex-automata", + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", ] [[package]] -name = "memchr" -version = "2.7.6" +name = "sha2" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] [[package]] -name = "minicov" -version = "0.3.8" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ - "cc", - "walkdir", + "lazy_static", ] [[package]] -name = "miniz_oxide" -version = "0.8.9" +name = "shared-buffer" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "f6c99835bad52957e7aa241d3975ed17c1e5f8c92026377d117a606f36b84b16" dependencies = [ - "adler2", + "bytes", + "memmap2", ] [[package]] -name = "mintex" -version = "0.1.4" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "nu-ansi-term" -version = "0.50.3" +name = "signal-hook-registry" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ - "windows-sys", + "libc", ] [[package]] -name = "num-traits" -version = "0.2.19" +name = "simd-adler32" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] -name = "object" -version = "0.37.3" +name = "simdutf8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] -name = "once_cell" -version = "1.21.3" +name = "similar" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] -name = "oorandom" -version = "11.1.5" +name = "slab" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] -name = "page_size" -version = "0.6.0" +name = "slice-group-by" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smoltcp" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee34c1e1bfc7e9206cc0fb8030a90129b4e319ab53856249bb27642cab914fb3" dependencies = [ - "libc", - "winapi", + "bitflags 1.3.2", + "byteorder", + "managed", ] [[package]] -name = "parking_lot" -version = "0.12.5" +name = "socket2" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ - "lock_api", - "parking_lot_core", + "libc", + "windows-sys 0.52.0", ] [[package]] -name = "parking_lot_core" -version = "0.9.12" +name = "socket2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ - "cfg-if", "libc", - "redox_syscall", - "smallvec", - "windows-link", + "windows-sys 0.60.2", ] [[package]] -name = "pin-project-lite" -version = "0.2.16" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "pin-utils" -version = "0.1.0" +name = "static_assertions" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "plotters" -version = "0.3.7" +name = "strsim" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] -name = "plotters-backend" -version = "0.3.7" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] [[package]] -name = "plotters-svg" -version = "0.3.7" +name = "syn" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ - "plotters-backend", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "sync_wrapper" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ - "zerocopy", + "futures-core", ] [[package]] -name = "proc-macro2" -version = "1.0.103" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "unicode-ident", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] -name = "quote" -version = "1.0.42" +name = "tar" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" dependencies = [ - "proc-macro2", + "filetime", + "libc", + "xattr", ] [[package]] -name = "r-efi" -version = "5.3.0" +name = "target-lexicon" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] -name = "rand" -version = "0.9.2" +name = "tempfile" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ - "rand_chacha", - "rand_core", + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix 1.1.2", + "windows-sys 0.61.2", ] [[package]] -name = "rand_chacha" -version = "0.9.0" +name = "terminal_size" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "ppv-lite86", - "rand_core", + "rustix 0.38.44", + "windows-sys 0.48.0", ] [[package]] -name = "rand_core" -version = "0.9.3" +name = "termios" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" dependencies = [ - "getrandom 0.3.4", + "libc", ] [[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +name = "test-plugin" +version = "0.1.0" dependencies = [ - "either", - "rayon-core", + "rand 0.9.2", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", + "wasmi-plugin-pdk", ] [[package]] -name = "rayon-core" -version = "1.13.0" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "crossbeam-deque", - "crossbeam-utils", + "thiserror-impl 1.0.69", ] [[package]] -name = "redox_syscall" -version = "0.5.18" +name = "thiserror" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "bitflags", + "thiserror-impl 2.0.17", ] [[package]] -name = "regex" -version = "1.12.2" +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] -name = "regex-automata" -version = "0.4.13" +name = "thiserror-impl" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] -name = "regex-syntax" -version = "0.8.8" +name = "thousands" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" [[package]] -name = "rustc-demangle" -version = "0.1.26" +name = "thread_local" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] [[package]] -name = "rustc-hash" -version = "1.1.0" +name = "time" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] [[package]] -name = "rustversion" -version = "1.0.22" +name = "time-core" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] -name = "ryu" -version = "1.0.20" +name = "time-macros" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] [[package]] -name = "same-file" -version = "1.0.6" +name = "tinystr" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ - "winapi-util", + "displaydoc", + "zerovec", ] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "tinytemplate" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] [[package]] -name = "serde" -version = "1.0.228" +name = "tinyvec" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ - "serde_core", - "serde_derive", + "tinyvec_macros", ] [[package]] -name = "serde_core" -version = "1.0.228" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "serde_derive", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.1", + "tokio-macros", + "windows-sys 0.61.2", ] [[package]] -name = "serde_derive" -version = "1.0.228" +name = "tokio-macros" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] -name = "serde_json" -version = "1.0.145" +name = "tokio-native-tls" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", + "native-tls", + "tokio", ] [[package]] -name = "sharded-slab" -version = "0.1.7" +name = "tokio-rustls" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "lazy_static", + "rustls", + "tokio", ] [[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "slab" -version = "0.4.11" +name = "tokio-stream" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] [[package]] -name = "smallvec" -version = "1.15.1" +name = "tokio-util" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] [[package]] -name = "spin" -version = "0.9.8" +name = "toml" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] [[package]] -name = "string-interner" -version = "0.19.0" +name = "toml_datetime" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23de088478b31c349c9ba67816fa55d9355232d63c3afea8bf513e31f0f1d2c0" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ - "hashbrown 0.15.5", "serde", ] [[package]] -name = "syn" -version = "2.0.111" +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "serde_core", ] [[package]] -name = "test-plugin" -version = "0.1.0" +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "rand", - "serde_json", - "tokio", - "tracing", - "tracing-subscriber", - "wasmi-plugin-pdk", + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow", ] [[package]] -name = "thiserror" -version = "2.0.17" +name = "toml_edit" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ - "thiserror-impl", + "indexmap", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow", ] [[package]] -name = "thiserror-impl" -version = "2.0.17" +name = "toml_parser" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ - "proc-macro2", - "quote", - "syn", + "winnow", ] [[package]] -name = "thousands" -version = "0.2.0" +name = "toml_write" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] -name = "thread_local" -version = "1.1.9" +name = "tower" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ - "cfg-if", + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", ] [[package]] -name = "tinytemplate" -version = "1.2.1" +name = "tower-http" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "serde", - "serde_json", + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", ] [[package]] -name = "tokio" -version = "1.48.0" +name = "tower-layer" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" -dependencies = [ - "pin-project-lite", - "tokio-macros", -] +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] -name = "tokio-macros" -version = "2.6.0" +name = "tower-service" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -996,6 +3563,7 @@ version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1009,7 +3577,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1069,21 +3637,112 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn", + "syn 2.0.111", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", ] +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "uuid" version = "1.19.0" @@ -1102,6 +3761,166 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtual-fs" +version = "0.601.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8defc513ce70576bdbb4305ee67c0cba647af18c0995de8f1fa1271b724c9859" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "dashmap", + "derive_more 2.1.0", + "dunce", + "filetime", + "fs_extra", + "futures", + "getrandom 0.2.16", + "indexmap", + "libc", + "pin-project-lite", + "replace_with", + "shared-buffer", + "slab", + "thiserror 1.0.69", + "tokio", + "tracing", + "virtual-mio", + "wasmer-package", + "web-time", + "webc", +] + +[[package]] +name = "virtual-mio" +version = "0.601.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a14806a3f25b70315764760c376d9f68937f5467013891bc7497a33d24105097" +dependencies = [ + "async-trait", + "bytes", + "futures", + "mio", + "serde", + "socket2 0.5.10", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "virtual-net" +version = "0.601.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7dccbe5d8d3a49e2b43a2c5a4287139e33ba04c03279bfc8a0aef82def2043" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "bincode", + "bytecheck 0.6.12", + "bytes", + "derive_more 2.1.0", + "futures-util", + "ipnet", + "iprange", + "libc", + "mio", + "pin-project-lite", + "rkyv", + "serde", + "smoltcp", + "socket2 0.5.10", + "thiserror 1.0.69", + "tokio", + "tracing", + "virtual-mio", +] + +[[package]] +name = "wai-bindgen-gen-core" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa3dc41b510811122b3088197234c27e08fcad63ef936306dd8e11e2803876c" +dependencies = [ + "anyhow", + "wai-parser", +] + +[[package]] +name = "wai-bindgen-gen-rust" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc05e8380515c4337c40ef03b2ff233e391315b178a320de8640703d522efe" +dependencies = [ + "heck", + "wai-bindgen-gen-core", +] + +[[package]] +name = "wai-bindgen-gen-rust-wasm" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f35ce5e74086fac87f3a7bd50f643f00fe3559adb75c88521ecaa01c8a6199" +dependencies = [ + "heck", + "wai-bindgen-gen-core", + "wai-bindgen-gen-rust", +] + +[[package]] +name = "wai-bindgen-rust" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e5601c6f448c063e83a5e931b8fefcdf7e01ada424ad42372c948d2e3d67741" +dependencies = [ + "bitflags 1.3.2", + "wai-bindgen-rust-impl", +] + +[[package]] +name = "wai-bindgen-rust-impl" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeb5c1170246de8425a3e123e7ef260dc05ba2b522a1d369fe2315376efea4" +dependencies = [ + "proc-macro2", + "syn 1.0.109", + "wai-bindgen-gen-core", + "wai-bindgen-gen-rust-wasm", +] + +[[package]] +name = "wai-parser" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd0acb6d70885ea0c343749019ba74f015f64a9d30542e66db69b49b7e28186" +dependencies = [ + "anyhow", + "id-arena", + "pulldown-cmark", + "unicode-normalization", + "unicode-xid", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + [[package]] name = "walkdir" version = "2.5.0" @@ -1112,6 +3931,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1172,7 +4000,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -1214,7 +4042,17 @@ checksum = "7150335716dce6028bead2b848e72f47b45e7b9422f64cccdc23bedca89affc1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", +] + +[[package]] +name = "wasm-encoder" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bc393c395cb621367ff02d854179882b9a351b4e0c93d1397e6090b53a5c2a" +dependencies = [ + "leb128fmt", + "wasmparser 0.235.0", ] [[package]] @@ -1228,17 +4066,343 @@ dependencies = [ ] [[package]] -name = "wasmi" -version = "1.0.3" +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmer" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d85671948f8886a1cc946141c0b688a5617603c103699a5fceeebeb4e75b0b6" +dependencies = [ + "bindgen", + "bytes", + "cfg-if", + "cmake", + "derive_more 2.1.0", + "indexmap", + "js-sys", + "more-asserts", + "paste", + "rustc-demangle", + "serde", + "serde-wasm-bindgen", + "shared-buffer", + "tar", + "target-lexicon", + "thiserror 1.0.69", + "tracing", + "wasm-bindgen", + "wasmer-compiler", + "wasmer-compiler-cranelift", + "wasmer-derive", + "wasmer-types", + "wasmer-vm", + "wasmparser 0.224.1", + "wat", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmer-compiler" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4946475adc0af265af8f10aadf4d4a3c64845bcd3801c655bdd81ce5e3ee869b" +dependencies = [ + "backtrace", + "bytes", + "cfg-if", + "enum-iterator", + "enumset", + "leb128", + "libc", + "macho-unwind-info", + "memmap2", + "more-asserts", + "object 0.32.2", + "region", + "rkyv", + "self_cell", + "shared-buffer", + "smallvec", + "target-lexicon", + "thiserror 1.0.69", + "wasmer-types", + "wasmer-vm", + "wasmparser 0.224.1", + "windows-sys 0.59.0", + "xxhash-rust", +] + +[[package]] +name = "wasmer-compiler-cranelift" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780f9c2050941b4e3f6bb82d32bd796a6c1750d2f97cbd892f07b384bb6af6f8" +dependencies = [ + "cranelift-codegen", + "cranelift-entity", + "cranelift-frontend", + "gimli 0.28.1", + "itertools 0.12.1", + "more-asserts", + "rayon", + "smallvec", + "target-lexicon", + "tracing", + "wasmer-compiler", + "wasmer-types", +] + +[[package]] +name = "wasmer-config" +version = "0.601.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c7a72bfcba1000b7609e034e0d470575fae8215b597c2c6554661729d64c22" +dependencies = [ + "anyhow", + "bytesize", + "ciborium", + "derive_builder", + "hex", + "indexmap", + "saffron", + "schemars", + "semver", + "serde", + "serde_json", + "serde_yaml", + "thiserror 1.0.69", + "toml", + "url", +] + +[[package]] +name = "wasmer-derive" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546f3380840cd63fdcc390f04cd19002f2dfa19b4691b77ecbd27642bd93452" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wasmer-journal" +version = "0.601.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6459cd00a0000e38dd4386bec1d99687813677bb01bfe69120f0f3eaedae0a06" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "bincode", + "bytecheck 0.6.12", + "bytes", + "derive_more 2.1.0", + "lz4_flex", + "num_enum", + "rkyv", + "serde", + "serde_json", + "shared-buffer", + "thiserror 1.0.69", + "tracing", + "virtual-fs", + "virtual-net", + "wasmer", + "wasmer-config", + "wasmer-wasix-types", +] + +[[package]] +name = "wasmer-package" +version = "0.601.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedc3a5cd721e8b12778d1858b9583aca8b91927a73f5fdf0c876cd5bbb6ac56" +dependencies = [ + "anyhow", + "bytes", + "cfg-if", + "ciborium", + "flate2", + "ignore", + "insta", + "libc", + "semver", + "serde", + "serde_json", + "sha2", + "shared-buffer", + "tar", + "tempfile", + "thiserror 1.0.69", + "toml", + "url", + "wasmer-config", + "wasmer-types", + "webc", +] + +[[package]] +name = "wasmer-types" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a4027ce165e8dc776dc5e2a3231a96983e6dc7330efd97b793cfc4e973ad0c" +dependencies = [ + "bytecheck 0.6.12", + "enum-iterator", + "enumset", + "getrandom 0.2.16", + "hex", + "indexmap", + "more-asserts", + "rkyv", + "serde", + "sha2", + "target-lexicon", + "thiserror 1.0.69", + "wasmparser 0.224.1", + "xxhash-rust", +] + +[[package]] +name = "wasmer-vm" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c37d5be291eea00a00d077ce3a427bb3074709ee386ec358f18f0b7da33be01" +dependencies = [ + "backtrace", + "cc", + "cfg-if", + "corosensei", + "crossbeam-queue", + "dashmap", + "enum-iterator", + "fnv", + "indexmap", + "libc", + "libunwind", + "mach2", + "memoffset", + "more-asserts", + "region", + "rustversion", + "scopeguard", + "thiserror 1.0.69", + "wasmer-types", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmer-wasix" +version = "0.601.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a804fcfbdbbfe5b2d6fb30e741e22a21e88e964dedfea9e609f13a52823bfe" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "bincode", + "blake3", + "bus", + "bytecheck 0.6.12", + "bytes", + "cfg-if", + "chrono", + "cooked-waker", + "crossbeam-channel", + "dashmap", + "derive_more 2.1.0", + "futures", + "getrandom 0.2.16", + "heapless", + "hex", + "http", + "js-sys", + "libc", + "linked_hash_set", + "lz4_flex", + "num_enum", + "once_cell", + "petgraph", + "pin-project", + "pin-utils", + "rand 0.8.5", + "reqwest", + "rkyv", + "rusty_pool", + "semver", + "serde", + "serde_derive", + "serde_json", + "serde_yaml", + "sha2", + "shared-buffer", + "tempfile", + "terminal_size", + "termios", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "toml", + "tracing", + "url", + "urlencoding", + "virtual-fs", + "virtual-mio", + "virtual-net", + "waker-fn", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-encoder 0.235.0", + "wasmer", + "wasmer-config", + "wasmer-journal", + "wasmer-package", + "wasmer-types", + "wasmer-wasix-types", + "wasmparser 0.224.1", + "web-sys", + "webc", + "weezl", + "windows-sys 0.59.0", + "xxhash-rust", +] + +[[package]] +name = "wasmer-wasix-types" +version = "0.601.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172f6e29cb11ad08894abcf257cd899a7e559734514ad3b08c16fc3dc1596439" +checksum = "7d2b7dad6c826f48a972709e5950a80157cb3b6bb12c6f51778282e85dc17096" dependencies = [ - "spin", - "wasmi_collections", - "wasmi_core", - "wasmi_ir", - "wasmparser 0.228.0", - "wat", + "anyhow", + "bitflags 1.3.2", + "byteorder", + "cfg-if", + "num_enum", + "serde", + "time", + "tracing", + "wai-bindgen-gen-core", + "wai-bindgen-gen-rust", + "wai-bindgen-gen-rust-wasm", + "wai-bindgen-rust", + "wai-parser", + "wasmer", + "wasmer-derive", + "wasmer-types", ] [[package]] @@ -1249,18 +4413,22 @@ dependencies = [ "criterion", "dhat", "futures", - "getrandom 0.2.16", + "getrandom 0.3.4", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", "tokio", "tracing", "tracing-test", "uuid", + "wasm-bindgen-futures", "wasm-bindgen-test", - "wasmi", + "wasmer", + "wasmer-types", + "wasmer-wasix", "wasmi-plugin-pdk", "wasmi-plugin-rt", + "web-sys", "web-time", ] @@ -1272,7 +4440,7 @@ dependencies = [ "futures", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", "tokio", "tracing", "tracing-subscriber", @@ -1289,39 +4457,21 @@ dependencies = [ ] [[package]] -name = "wasmi_collections" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697e11ec2200705314d6a1437cf6bc04d5362b1b30f15c43b31eb7549a3b568e" -dependencies = [ - "string-interner", -] - -[[package]] -name = "wasmi_core" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bb8d179d27c757e1475bf805d02cfc88c998fbc0265155fbf426c0e85dd7fb9" -dependencies = [ - "libm", -] - -[[package]] -name = "wasmi_ir" -version = "1.0.3" +name = "wasmparser" +version = "0.224.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8551984e96c9c53c6a602f2babead4dea1122e8bbdaed456efb0881bf94b82c5" +checksum = "04f17a5917c2ddd3819e84c661fae0d6ba29d7b9c1f0e96c708c65a9c4188e11" dependencies = [ - "wasmi_core", + "bitflags 2.10.0", ] [[package]] name = "wasmparser" -version = "0.228.0" +version = "0.235.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3" +checksum = "161296c618fa2d63f6ed5fffd1112937e803cb9ec71b32b01a76321555660917" dependencies = [ - "bitflags", + "bitflags 2.10.0", "indexmap", ] @@ -1331,8 +4481,9 @@ version = "0.243.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d" dependencies = [ - "bitflags", + "bitflags 2.10.0", "indexmap", + "semver", ] [[package]] @@ -1345,7 +4496,7 @@ dependencies = [ "leb128fmt", "memchr", "unicode-width", - "wasm-encoder", + "wasm-encoder 0.243.0", ] [[package]] @@ -1377,6 +4528,49 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webc" +version = "10.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637506ca3c48eae3301e97f0167aa1b46cdfdd479d008b868a19715182e48ae3" +dependencies = [ + "anyhow", + "base64", + "bytes", + "cfg-if", + "ciborium", + "document-features", + "ignore", + "indexmap", + "leb128", + "lexical-sort", + "libc", + "once_cell", + "path-clean", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "shared-buffer", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi" version = "0.3.9" @@ -1399,7 +4593,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1408,12 +4602,101 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1423,12 +4706,252 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.2", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.31" @@ -1446,5 +4969,65 @@ checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", ] diff --git a/Cargo.toml b/Cargo.toml index 826cf22..5636472 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ exclude = ["tests", "benches"] [workspace.dependencies] async-trait = "0.1" futures = "0.3" -getrandom = { version = "0.2", features = ["js"] } +getrandom = { version = "0.3.4", features = ["wasm_js"] } gloo-timers = { version = "0.3", features = ["futures"] } rand = "0.9" serde = { version = "1", features = ["derive"] } @@ -32,9 +32,14 @@ thiserror = "2" tokio = { version = "1", features = ["rt", "time", "sync", "macros"] } tracing = "0.1" tracing-subscriber = "0.3" -uuid = { version = "1", features = [ "v4", "serde", "js" ] } +uuid = { version = "1", features = ["v4", "serde", "js"] } wasm-bindgen-futures = "0.4" -wasmi = "1.0" +# wasmi = { version = "1.0", default-features=false, features = ["std", "hash-collections", "prefer-btree-collections"] } +# wasmer-middlewares = "6" +wasmer = { version = "6", default-features = false } +wasmer-wasix = { version = "0.601", default-features = false } +wasmer-types = "6" +web-sys = "0.3" wasmi-plugin-pdk = { version = "0.1.0", path = "./crates/wasmi-plugin-pdk" } wasmi-plugin-rt = { version = "0.1.0", path = "./crates/wasmi-plugin-rt" } -web-time = "1" +web-time = "1" \ No newline at end of file diff --git a/crates/test-plugin/Cargo.toml b/crates/test-plugin/Cargo.toml index b3784b2..ca93293 100644 --- a/crates/test-plugin/Cargo.toml +++ b/crates/test-plugin/Cargo.toml @@ -22,5 +22,8 @@ tracing-subscriber = { workspace = true } wasmi-plugin-pdk = { workspace = true } [profile.release] +opt-level = 3 lto = "fat" codegen-units = 1 +strip = true +panic = "abort" \ No newline at end of file diff --git a/crates/wasmi-plugin-hdk/Cargo.toml b/crates/wasmi-plugin-hdk/Cargo.toml index ac9d754..2e6b63b 100644 --- a/crates/wasmi-plugin-hdk/Cargo.toml +++ b/crates/wasmi-plugin-hdk/Cargo.toml @@ -21,15 +21,27 @@ serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } -wasmi = { workspace = true } +# wasmi = { workspace = true } wasmi-plugin-pdk = { workspace = true } wasmi-plugin-rt = { workspace = true } web-time = { workspace = true } +tokio = { workspace = true } +wasmer-types = { workspace = true } +# wasmer-middlewares = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +wasmer = { workspace = true, default-features = true } +wasmer-wasix = { workspace = true, default-features = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasmer = { workspace = true, features = ["js-default"] } +wasmer-wasix = { workspace = true, features = ["js-default"] } +web-sys = { workspace = true, features=["Worker", "MessageEvent"] } +wasm-bindgen-futures = { workspace = true } [dev-dependencies] -dhat = "0.3" -tokio = { workspace = true } tracing-test = { version = "0.2.5", features = ["no-env-filter"] } +dhat = "0.3" wasm-bindgen-test = "0.3" # Disables criterion for wasm targets since it's not compatible diff --git a/crates/wasmi-plugin-hdk/benches/benchmark.rs b/crates/wasmi-plugin-hdk/benches/benchmark.rs index 9c98472..72baf26 100644 --- a/crates/wasmi-plugin-hdk/benches/benchmark.rs +++ b/crates/wasmi-plugin-hdk/benches/benchmark.rs @@ -77,7 +77,7 @@ pub fn bench_prime_sieve_large(c: &mut Criterion) { b.iter(|| { let fut = async { plugin - .call("prime_sieve", Value::Number(100_000.into())) + .call("prime_sieve", Value::Number(100_0000.into())) .await .unwrap(); }; @@ -109,10 +109,30 @@ pub fn bench_call_many(c: &mut Criterion) { }); } +fn sieve_of_eratosthenes(limit: usize) -> Vec { + if limit < 2 { + return vec![]; + } + + let mut is_prime = vec![true; limit + 1]; + is_prime[0] = false; + is_prime[1] = false; + + for i in 2..=((limit as f64).sqrt() as usize) { + if is_prime[i] { + for j in ((i * i)..=limit).step_by(i) { + is_prime[j] = false; + } + } + } + + (2..=limit).filter(|&i| is_prime[i]).collect() +} + criterion_group!( benches, - bench_ping, - bench_prime_sieve_small, + // bench_ping, + // bench_prime_sieve_small, bench_prime_sieve_large, bench_call_many ); diff --git a/crates/wasmi-plugin-hdk/src/bridge/mod.rs b/crates/wasmi-plugin-hdk/src/bridge/mod.rs new file mode 100644 index 0000000..7f783d4 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/bridge/mod.rs @@ -0,0 +1,46 @@ +#[cfg(not(target_arch = "wasm32"))] +mod native_bridge; + +#[cfg(target_arch = "wasm32")] +mod worker_bridge; + +#[cfg(not(target_arch = "wasm32"))] +pub use native_bridge::NativeBridge; + +#[cfg(target_arch = "wasm32")] +pub use worker_bridge::WorkerBridge; + +pub trait Bridge { + fn terminate(self: Box); +} + +// use std::io::{Read, Write}; + +// // Define a common error type for the bridge setup +// pub type BridgeError = Box; + +// pub fn spawn_bridge( +// name: &str, +// wasm_bytes: &[u8], +// ) -> Result< +// ( +// Box, +// impl Read + Send + Sync + 'static, +// impl Write + Send + Sync + 'static, +// ), +// BridgeError, +// > { +// #[cfg(not(target_arch = "wasm32"))] +// { +// let (bridge, reader, writer) = native_bridge::NativeBridge::new(name, wasm_bytes) +// .map_err(|e| Box::new(e) as BridgeError)?; +// Ok((Box::new(bridge), reader, writer)) +// } + +// #[cfg(target_arch = "wasm32")] +// { +// let (bridge, reader, writer) = worker_bridge::WorkerBridge::new(name, wasm_bytes) +// .map_err(|js_val| format!("{:?}", js_val))?; +// Ok((Box::new(bridge), reader, writer)) +// } +// } diff --git a/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs b/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs new file mode 100644 index 0000000..be97e80 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs @@ -0,0 +1,106 @@ +use futures::AsyncBufReadExt; +use std::io::{Read, Write}; +use thiserror::Error; +use tokio::spawn; +use tokio::task::{JoinHandle, spawn_blocking}; +use tracing::info; +use wasmer::{FunctionEnv, Instance, Module, Store}; + +use crate::bridge::Bridge; +use crate::compile::Compiled; +use crate::wasi::non_blocking_pipe::non_blocking_pipe; +use crate::wasi::wasi_ctx::WasiCtx; + +pub struct NativeBridge { + wasm_handle: JoinHandle<()>, + stderr_handle: JoinHandle<()>, +} + +#[derive(Debug, Error)] +pub enum NativeBridgeError { + #[error("Wasmer runtime error: {0}")] + WasmerError(#[from] wasmer::RuntimeError), + #[error("IO error: {0}")] + IOError(#[from] std::io::Error), +} + +impl NativeBridge { + pub fn new( + name: &str, + compiled: Compiled, + ) -> Result< + ( + Self, + impl Read + Send + Sync + 'static, + impl Write + Send + Sync + 'static, + ), + NativeBridgeError, + > { + let (stdout_reader, stdout_writer) = non_blocking_pipe(); + let (stdin_reader, stdin_writer) = non_blocking_pipe(); + let (stderr_reader, stderr_writer) = non_blocking_pipe(); + + let wasm_handle = spawn_blocking(move || { + let result = Self::run_instance(compiled, stdin_reader, stdout_writer, stderr_writer); + if let Err(e) = result { + eprintln!("Plugin execution error: {:?}", e); + } + }); + + let name = name.to_string(); + let stderr_handle = spawn(async move { + let mut buf_reader = futures::io::BufReader::new(stderr_reader); + let mut line = String::new(); + while buf_reader.read_line(&mut line).await.is_ok_and(|n| n > 0) { + info!("[plugin] [{}], {}", name, line.trim_end()); + line.clear(); + } + }); + + let bridge = NativeBridge { + wasm_handle, + stderr_handle, + }; + + Ok((bridge, stdout_reader, stdin_writer)) + } + + fn run_instance( + compiled: Compiled, + stdin: impl Read + Send + Sync + 'static, + stdout: impl Write + Send + Sync + 'static, + stderr: impl Write + Send + Sync + 'static, + ) -> Result<(), Box> { + let engine = compiled.engine; + let module = compiled.module; + + let mut store = Store::new(engine); + + // TODO: Switch to using wasmer-wasix + let wasi_ctx = WasiCtx::new() + .set_stdin(Box::new(stdin)) + .set_stdout(Box::new(stdout)) + .set_stderr(Box::new(stderr)); + let function_env = FunctionEnv::new(&mut store, wasi_ctx); + let import_object = WasiCtx::import_object(&mut store, &function_env); + let instance = Instance::new(&mut store, &module, &import_object)?; + function_env.as_mut(&mut store).memory = + Some(instance.exports.get_memory("memory")?.clone()); + + // Start the plugin + let start = instance.exports.get_function("_start")?; + start.call(&mut store, &[])?; + + Ok(()) + } +} + +impl Bridge for NativeBridge { + fn terminate(self: Box) { + self.stderr_handle.abort(); + self.wasm_handle.abort(); + + // Currently just drops the handle to stop the thread + // TODO: Trigger out of gas termination + } +} diff --git a/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs b/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs new file mode 100644 index 0000000..27ae326 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs @@ -0,0 +1,115 @@ +use std::io::{self, Read, Write}; + +use futures::StreamExt; +use futures::channel::mpsc; +use thiserror::Error; +use wasm_bindgen_futures::spawn_local; +use web_sys::Worker; +use web_sys::{MessageEvent, js_sys, wasm_bindgen::prelude::Closure}; + +use web_sys::wasm_bindgen::{JsCast, JsError, JsValue}; + +use crate::bridge::Bridge; +use crate::wasi::non_blocking_pipe::non_blocking_pipe; + +// TODO: Add stdin_writer and stdout_reader methods +pub struct WorkerBridge { + worker: SendWorker, + // We keep this to ensure the JS callback isn't garbage collected + _onmessage_closure: Closure, +} + +#[derive(Clone)] +struct SendWorker(Worker); + +struct WorkerWriter { + tx: mpsc::UnboundedSender>, +} + +#[derive(Debug, Error)] +pub enum WorkerBridgeError { + #[error("JavaScript error: {0:?}")] + JsValue(JsValue), +} + +impl WorkerBridge { + pub fn new( + name: &str, + wasm_bytes: &[u8], + ) -> Result< + ( + Self, + impl Read + Send + Sync + 'static, + impl Write + Send + Sync + 'static, + ), + WorkerBridgeError, + > { + let worker_url = ""; + let worker = + SendWorker(Worker::new(worker_url).map_err(|e| WorkerBridgeError::JsValue(e))?); + + let (stdout_reader, mut stdout_writer) = non_blocking_pipe(); + + let onmessage_callback = Closure::wrap(Box::new(move |e: MessageEvent| { + if let Ok(array) = e.data().dyn_into::() { + let _ = stdout_writer.write(&array.to_vec()); + } + }) as Box); + + worker + .0 + .set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref())); + + let (stdin_tx, mut stdin_rx) = mpsc::unbounded::>(); + let worker_inner = worker.clone(); + + spawn_local(async move { + while let Some(data) = stdin_rx.next().await { + let array = js_sys::Uint8Array::from(&data[..]); + if let Err(_) = worker_inner.post_message(&array) { + break; + } + } + }); + + let bridge = WorkerBridge { + worker, + _onmessage_closure: onmessage_callback, + }; + + let writer = WorkerWriter { tx: stdin_tx }; + + Ok((bridge, stdout_reader, writer)) + } +} + +unsafe impl Send for WorkerBridge {} +unsafe impl Sync for WorkerBridge {} + +impl Bridge for WorkerBridge { + fn terminate(self: Box) { + self.worker.terminate(); + } +} + +impl SendWorker { + fn post_message(&self, msg: &JsValue) -> Result<(), JsValue> { + self.0.post_message(msg) + } + + fn terminate(&self) { + self.0.terminate(); + } +} + +impl Write for WorkerWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.tx + .unbounded_send(buf.to_vec()) + .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "Worker task dropped"))?; + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} diff --git a/crates/wasmi-plugin-hdk/src/compile.rs b/crates/wasmi-plugin-hdk/src/compile.rs index e326da2..831a0e0 100644 --- a/crates/wasmi-plugin-hdk/src/compile.rs +++ b/crates/wasmi-plugin-hdk/src/compile.rs @@ -1,14 +1,31 @@ -use wasmi::{Config, Engine, Module}; - -pub fn compile_plugin(wasm_bytes: Vec) -> Result<(Engine, Module), wasmi::Error> { - let mut config = Config::default(); - config.consume_fuel(true); - // https://github.com/wasmi-labs/wasmi/issues/1647 - // TODO: Switch to lazy execution, seems faster. - config.compilation_mode(wasmi::CompilationMode::Eager); - config.set_max_cached_stacks(100); - let engine = Engine::new(&config); - let module = Module::new(&engine, wasm_bytes)?; - - Ok((engine, module)) +// use wasmi::{Config, Engine, Module}; + +// pub fn compile_plugin(wasm_bytes: Vec) -> Result<(Engine, Module), wasmi::Error> { +// let mut config = Config::default(); +// config.consume_fuel(true); +// // https://github.com/wasmi-labs/wasmi/issues/1647 +// // TODO: Switch to lazy execution, seems faster. +// config.compilation_mode(wasmi::CompilationMode::Eager); +// config.set_max_cached_stacks(100); +// let engine = Engine::new(&config); +// let module = Module::new(&engine, wasm_bytes)?; + +// Ok((engine, module)) +// } + +use wasmer::{Engine, Module}; + +#[derive(Clone)] +pub struct Compiled { + pub engine: Engine, + pub module: Module, +} + +impl Compiled { + pub fn new(wasm_bytes: &[u8]) -> Result { + let engine = Engine::default(); + let module = Module::new(&engine, wasm_bytes)?; + + Ok(Compiled { engine, module }) + } } diff --git a/crates/wasmi-plugin-hdk/src/lib.rs b/crates/wasmi-plugin-hdk/src/lib.rs index a0ba714..475ad0d 100644 --- a/crates/wasmi-plugin-hdk/src/lib.rs +++ b/crates/wasmi-plugin-hdk/src/lib.rs @@ -1,6 +1,7 @@ mod compile; pub mod host_handler; pub mod plugin; -mod plugin_instance; +// mod plugin_instance; +mod bridge; pub mod server; mod wasi; diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index d9500d1..6822390 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -1,24 +1,24 @@ -use std::{fmt::Display, io::BufReader, sync::Arc}; +use std::io::{Read, Write}; +use std::{fmt::Display, sync::Arc, time::Duration}; -use futures::{AsyncBufReadExt, FutureExt}; +use futures::FutureExt; use serde::{Deserialize, Serialize}; use serde_json::Value; use thiserror::Error; use tracing::info; use uuid::Uuid; -use wasmi::{Engine, Module}; use wasmi_plugin_pdk::{ api::RequestHandler, router::BoxFuture, rpc_message::{RpcError, RpcResponse}, transport::Transport, }; +use wasmi_plugin_rt::sleep; -use crate::{ - compile::compile_plugin, - host_handler::HostHandler, - plugin_instance::{SpawnError, spawn_plugin}, -}; +use crate::bridge::{self, Bridge}; +#[cfg(not(target_arch = "wasm32"))] +use crate::compile::Compiled; +use crate::host_handler::HostHandler; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct PluginId(Uuid); @@ -59,16 +59,17 @@ pub struct Plugin { name: String, id: PluginId, handler: Arc, - engine: Engine, - module: Module, logger: Logger, max_fuel: Option, + #[cfg(not(target_arch = "wasm32"))] + compiled: Compiled, + wasm_bytes: Vec, } #[derive(Debug, Error)] pub enum PluginError { - #[error("spawn error")] - SpawnError(#[from] SpawnError), + // #[error("spawn error")] + // SpawnError(#[from] SpawnError), #[error("transport error")] RpcError(#[from] RpcError), #[error("plugin died")] @@ -80,17 +81,16 @@ impl Plugin { name: &str, wasm_bytes: Vec, handler: Arc, - ) -> Result { - let (engine, module) = compile_plugin(wasm_bytes.clone())?; - + ) -> Result { Ok(Plugin { name: name.to_string(), id: PluginId::new(), handler, - engine, - module, - max_fuel: None, logger: Box::new(default_plugin_logger), + max_fuel: None, + #[cfg(not(target_arch = "wasm32"))] + compiled: Compiled::new(&wasm_bytes)?, + wasm_bytes, }) } @@ -109,21 +109,6 @@ impl Plugin { self } - /// Sets the maximum fuel for the plugin instance. - /// - /// The fuel limit controls how frequently the plugin is interrupted to - /// check for cancellation and yield to other tasks. Lower fuel limits - /// result in more frequent interruptions, which can improve responsiveness - /// for long-running compute-intensive tasks, but will also incur more overhead. - /// - /// Generally a fuel between 10_000 and 1_000_000 is a good starting point. - /// - /// Leave as None to use the default fuel limit, a sensible default of 100_000. - pub fn with_max_fuel(mut self, max_fuel: u64) -> Self { - self.max_fuel = Some(max_fuel); - self - } - pub fn name(&self) -> &str { &self.name } @@ -153,41 +138,64 @@ impl From for RpcError { impl Plugin { pub async fn call(&self, method: &str, params: Value) -> Result { - let (stdin_writer, stdout_reader, stderr_reader, is_running, instance_task) = - spawn_plugin(&self.engine, &self.module, self.max_fuel)?; - - let name = self.name.clone(); - let stderr_task = async move { - let mut buf_reader = futures::io::BufReader::new(stderr_reader); - let mut line = String::new(); - while buf_reader.read_line(&mut line).await.is_ok_and(|n| n > 0) { - (self.logger)(&name, line.trim_end()); - line.clear(); - } - } - .fuse(); + //? Construct URL from worker bytes + let (bridge, stdout_reader, stdin_writer) = self.create_bridge().unwrap(); + let transport = Transport::new(stdout_reader, stdin_writer); let handler = PluginCallback { handler: self.handler.clone(), uuid: self.id, }; - let buf_reader = BufReader::new(stdout_reader); - let transport = Transport::new(buf_reader, stdin_writer); - let rpc_task = transport + let transport_task = transport .async_call_with_handler(method, params, &handler) .fuse(); + let timeout = sleep(Duration::from_secs(60)).fuse(); + futures::pin_mut!(transport_task, timeout); + + futures::select! { + res = transport_task => { + bridge.terminate(); + return Ok(res?); + }, + _ = timeout => { + bridge.terminate(); + return Err(PluginError::PluginDied); + } + }; + } - let instance_task = instance_task.fuse(); - futures::pin_mut!(rpc_task, instance_task, stderr_task); - - //? Run the transport, plugin, and stderr logger until one of them completes - let (res, _, _) = futures::join!(rpc_task, instance_task, stderr_task); - let res = res?; - - is_running.store(false, std::sync::atomic::Ordering::SeqCst); + #[cfg(not(target_arch = "wasm32"))] + fn create_bridge( + &self, + ) -> Result< + ( + Box, + Box, + Box, + ), + PluginError, + > { + let (bridge, stdout, stdin) = bridge::NativeBridge::new(&self.name, self.compiled.clone()) + .map_err(|_| PluginError::PluginDied)?; + Ok((Box::new(bridge), Box::new(stdout), Box::new(stdin))) + } - Ok(res) + #[cfg(target_arch = "wasm32")] + fn create_bridge( + &self, + ) -> Result< + ( + Box, + Box, + Box, + ), + PluginError, + > { + // Web uses the bytes to send to the Worker + let (bridge, stdout, stdin) = bridge::WorkerBridge::new(&self.name, &self.wasm_bytes) + .map_err(|_| PluginError::PluginDied)?; + Ok((Box::new(bridge), Box::new(stdout), Box::new(stdin))) } } diff --git a/crates/wasmi-plugin-hdk/src/server.rs b/crates/wasmi-plugin-hdk/src/server.rs index c3db0dd..d4a85fb 100644 --- a/crates/wasmi-plugin-hdk/src/server.rs +++ b/crates/wasmi-plugin-hdk/src/server.rs @@ -1,8 +1,7 @@ -use futures::future::BoxFuture; use serde::{Serialize, de::DeserializeOwned}; use serde_json::Value; use wasmi_plugin_pdk::{ - router::{MaybeSend, Router}, + router::{BoxFuture, MaybeSend, Router}, rpc_message::RpcError, }; diff --git a/crates/wasmi-plugin-hdk/src/wasi/linker.rs b/crates/wasmi-plugin-hdk/src/wasi/linker.rs new file mode 100644 index 0000000..92c29c5 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/wasi/linker.rs @@ -0,0 +1,679 @@ +use std::{ + io::{Read, Write}, + time::Duration, +}; +use tracing::{error, info, trace, warn}; +use wasmi_plugin_rt::web_time::{Instant, SystemTime}; + +use crate::wasi::wasi_ctx::WasiCtx; + +#[repr(i32)] +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +enum Errno { + Success = 0, + Access = 2, + Again = 6, + Badf = 8, + Fault = 21, + Inval = 28, + Io = 29, +} + +/// Adds the WASI context to the given wasmi linker. +pub fn add_to_linker(linker: &mut wasmi::Linker) -> Result<(), wasmi::Error> { + linker.func_wrap("wasi_snapshot_preview1", "args_get", args_get)?; + linker.func_wrap("wasi_snapshot_preview1", "args_sizes_get", args_sizes_get)?; + linker.func_wrap("wasi_snapshot_preview1", "environ_get", env_get)?; + linker.func_wrap("wasi_snapshot_preview1", "environ_sizes_get", env_sizes_get)?; + linker.func_wrap("wasi_snapshot_preview1", "fd_read", fd_read)?; + linker.func_wrap("wasi_snapshot_preview1", "fd_fdstat_get", fd_fdstat_get)?; + linker.func_wrap("wasi_snapshot_preview1", "fd_write", fd_write)?; + linker.func_wrap("wasi_snapshot_preview1", "clock_time_get", clock_time_get)?; + linker.func_wrap("wasi_snapshot_preview1", "fd_close", fd_close)?; + linker.func_wrap("wasi_snapshot_preview1", "random_get", random_get)?; + linker.func_wrap("wasi_snapshot_preview1", "sched_yield", sched_yield)?; + linker.func_wrap("wasi_snapshot_preview1", "poll_oneoff", poll_oneoff)?; + linker.func_wrap("wasi_snapshot_preview1", "proc_raise", proc_raise)?; + linker.func_wrap("wasi_snapshot_preview1", "proc_exit", proc_exit)?; + + Ok(()) +} + +/// Read command-line argument data. The size of the array should match that returned by args_sizes_get. Each argument is expected to be \0 terminated. +fn args_get(mut caller: wasmi::Caller<'_, WasiCtx>, argv: u32, argv_buf: u32) -> i32 { + trace!("wasi args_get({}, {})", argv, argv_buf); + + let ctx = caller.data_mut(); + let args = &ctx.args.clone(); + let memory = caller + .get_export("memory") + .and_then(|e| e.into_memory()) + .expect("guest must have memory"); + + // Pointer in guest memory to an array of pointers that will be filled with the start of each argument string. + let mut argv_ptr = argv as usize; + // Pointer in guest memory to a buffer that will be filled with the `\0`-terminated argument strings. + let mut buf_ptr = argv_buf as usize; + + for arg in args { + if write_pointer_and_string(&mut caller, &memory, &mut argv_ptr, &mut buf_ptr, arg).is_err() + { + return Errno::Fault as i32; + } + } + + Errno::Success as i32 +} + +/// Returns the number of arguments and the size of the argument string data, or an error. +fn args_sizes_get(mut caller: wasmi::Caller<'_, WasiCtx>, offset0: u32, offset1: u32) -> i32 { + trace!("wasi args_sizes_get({}, {})", offset0, offset1); + + let ctx = caller.data_mut(); + let argc = ctx.args.len() as u32; + let argv_buf_len: u32 = ctx.args.iter().map(|s| s.len() as u32 + 1).sum(); // +1 for null terminator + + let memory = caller + .get_export("memory") + .and_then(|e| e.into_memory()) + .expect("guest must have memory"); + + if memory + .write(&mut caller, offset0 as usize, &argc.to_le_bytes()) + .is_err() + { + return Errno::Fault as i32; + } + if memory + .write(&mut caller, offset1 as usize, &argv_buf_len.to_le_bytes()) + .is_err() + { + return Errno::Fault as i32; + } + + Errno::Success as i32 +} + +/// Read environment variable data. The sizes of the buffers should match that +/// returned by environ_sizes_get. Key/value pairs are expected to be joined +/// with =s, and terminated with \0s. +fn env_get(mut caller: wasmi::Caller<'_, WasiCtx>, environ: u32, environ_buf: u32) -> i32 { + trace!("wasi environ_get({}, {})", environ, environ_buf); + + let ctx = caller.data_mut(); + let env = &ctx.env.clone(); + let memory = caller + .get_export("memory") + .and_then(|e| e.into_memory()) + .expect("guest must have memory"); + + // Pointer in guest memory to an array of pointers that will be filled with the start of each environment string. + let mut environ_ptr = environ as usize; + // Pointer in guest memory to a buffer that will be filled with the `\0`-terminated environment strings. + let mut buf_ptr = environ_buf as usize; + + for var in env { + if write_pointer_and_string(&mut caller, &memory, &mut environ_ptr, &mut buf_ptr, var) + .is_err() + { + return Errno::Fault as i32; + } + } + + Errno::Success as i32 +} + +/// Returns the number of environment variable arguments and the size of the environment variable data. +fn env_sizes_get(mut caller: wasmi::Caller<'_, WasiCtx>, offset0: u32, offset1: u32) -> i32 { + trace!("wasi environ_sizes_get({}, {})", offset0, offset1); + + let ctx = caller.data_mut(); + let envc = ctx.env.len() as u32; + let env_buf_len: u32 = ctx.env.iter().map(|s| s.len() as u32 + 1).sum(); // +1 for null terminator + + let memory = caller + .get_export("memory") + .and_then(|e| e.into_memory()) + .expect("guest must have memory"); + + if memory + .write(&mut caller, offset0 as usize, &envc.to_le_bytes()) + .is_err() + { + return Errno::Fault as i32; + } + if memory + .write(&mut caller, offset1 as usize, &env_buf_len.to_le_bytes()) + .is_err() + { + return Errno::Fault as i32; + } + + Errno::Success as i32 +} + +/// Read from a file descriptor. Note: This is similar to readv in POSIX. +/// Basically that means that instead of reading into a single buffer, we read +/// into multiple buffers described by an array of iovec structures. So we +/// need to read `iov_len` elements from the `iov_ptr` array, each of which +/// describes a buffer we need to fill with data read from this file descriptor. +/// +/// - `fd`: The file descriptor. +/// - `iov_ptr`: Pointer to an array of iovec structures. +/// - `iov_len`: Number of iovec structures in the array +/// - `nread_ptr`: Number of bytes read. +/// +/// For now, we only bother implementing reading from fd 0 (stdin). +fn fd_read( + mut caller: wasmi::Caller<'_, WasiCtx>, + fd: u32, + iov_ptr: i32, + iov_len: i32, + nread_ptr: i32, +) -> i32 { + trace!( + "wasi fd_read({}, {}, {}, {})", + fd, iov_ptr, iov_len, nread_ptr + ); + + if fd != 0 { + return Errno::Badf as i32; + } + + let memory = caller + .get_export("memory") + .and_then(|e| e.into_memory()) + .expect("guest must have memory"); + + let mut total_read = 0usize; + + for i in 0..iov_len { + let base = (iov_ptr as usize) + (i as usize * 8); + let mut buf_bytes = [0u8; 4]; + + memory.read(&caller, base, &mut buf_bytes).unwrap(); + let buf_addr = u32::from_le_bytes(buf_bytes) as usize; + + memory.read(&caller, base + 4, &mut buf_bytes).unwrap(); + let buf_len = u32::from_le_bytes(buf_bytes) as usize; + + // Allocate host buffer + let mut host_buf = vec![0u8; buf_len]; + + // Narrow scope: borrow reader only while calling `read` + let n = { + let ctx = caller.data_mut(); + match ctx.stdin_reader.as_mut() { + Some(r) => match r.read(&mut host_buf) { + Ok(n) => n, + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + trace!("fd_read: WouldBlock, yielding to host"); + ctx.awaiting_stdin = true; + let _ = sched_yield(caller); + return Errno::Again as i32; + } + Err(_) => return Errno::Fault as i32, + }, + None => return Errno::Badf as i32, + } + }; // <- borrow of ctx ends here + + if n == 0 { + trace!("fd_read: EOF"); + break; + } + + total_read += n; + memory.write(&mut caller, buf_addr, &host_buf[..n]).unwrap(); + if n < buf_len { + break; // short read + } + } + + memory + .write( + &mut caller, + nread_ptr as usize, + &(total_read as u32).to_le_bytes(), + ) + .unwrap(); + + trace!("fd_read: total_read={}", total_read); + Errno::Success as i32 +} + +/// Write to a file descriptor. Note: This is similar to writev in POSIX. +/// +/// # Parameters +/// +/// - `fd`: The file descriptor. +/// - `ciov_ptr`: Pointer to an array of iovec structures. +/// - `ciov_len`: Number of iovec structures in the array +/// - `nwrite_ptr`: Number of bytes written. +/// +/// For now we only bother implementing writing to fd 1 (stdout) and fd 2 (stderr). +fn fd_write( + mut caller: wasmi::Caller<'_, WasiCtx>, + fd: i32, + ciov_ptr: i32, + ciov_len: i32, + nwrite_ptr: i32, +) -> i32 { + trace!( + "wasi fd_write({}, {}, {}, {})", + fd, ciov_ptr, ciov_len, nwrite_ptr + ); + + let memory = caller + .get_export("memory") + .and_then(|e| e.into_memory()) + .expect("guest must have memory"); + + let mut total_written = 0usize; + + for i in 0..ciov_len { + // Each ciovec is { buf: u32, len: u32 } + let base = (ciov_ptr as usize) + (i as usize * 8); + + let mut buf_bytes = [0u8; 4]; + + // Read buf pointer + memory.read(&caller, base, &mut buf_bytes).unwrap(); + let buf_addr = u32::from_le_bytes(buf_bytes) as usize; + + // Read buf length + memory.read(&caller, base + 4, &mut buf_bytes).unwrap(); + let buf_len = u32::from_le_bytes(buf_bytes) as usize; + + // Copy from guest memory + let mut host_buf = vec![0u8; buf_len]; + if memory.read(&caller, buf_addr, &mut host_buf).is_err() { + return Errno::Fault as i32; + } + + // Borrow writer and write message + //? Narrow scope since we're also mutably borrowing caller elsewhere for memory access + let ctx = caller.data_mut(); + let writer = match fd { + 1 => ctx.stdout_writer.as_mut(), + 2 => ctx.stderr_writer.as_mut(), + _ => return Errno::Badf as i32, + }; + let writer = match writer { + Some(w) => w, + None => return Errno::Badf as i32, + }; + + match writer.write(&host_buf) { + Ok(n) => { + total_written += n; + if n < buf_len { + break; // partial write, stop + } + } + Err(_) => return Errno::Io as i32, + } + } + + // Write total_written into nwrite_ptr + if memory + .write( + &mut caller, + nwrite_ptr as usize, + &(total_written as u32).to_le_bytes(), + ) + .is_err() + { + return Errno::Fault as i32; + } + + Errno::Success as i32 +} + +fn fd_fdstat_get(mut caller: wasmi::Caller<'_, WasiCtx>, fd: i32, buf_ptr: i32) -> i32 { + trace!("wasi fd_fdstat_get({}, {})", fd, buf_ptr); + + // Constants from wasi_snapshot_preview1 + const FILETYPE_CHARACTER_DEVICE: u8 = 2; + + // fdflags (none set) + let fs_flags: u16 = 0; + + // rights (for simplicity, we just allow read or write) + const RIGHTS_FD_READ: u64 = 1 << 1; + const RIGHTS_FD_WRITE: u64 = 1 << 6; + + let (filetype, rights_base) = match fd { + 0 => (FILETYPE_CHARACTER_DEVICE, RIGHTS_FD_READ), + 1 | 2 => (FILETYPE_CHARACTER_DEVICE, RIGHTS_FD_WRITE), + _ => return Errno::Badf as i32, + }; + + let rights_inheriting: u64 = 0; + + let mut buf = [0u8; 24]; + buf[0] = filetype; + buf[1..3].copy_from_slice(&fs_flags.to_le_bytes()); + buf[8..16].copy_from_slice(&rights_base.to_le_bytes()); + buf[16..24].copy_from_slice(&rights_inheriting.to_le_bytes()); + + let memory = caller + .get_export("memory") + .and_then(|e| e.into_memory()) + .expect("guest must have memory"); + + if memory.write(&mut caller, buf_ptr as usize, &buf).is_err() { + return Errno::Fault as i32; + } + + Errno::Success as i32 +} + +/// Close a file descriptor. For now we only support closing stdin, stdout, stderr. +fn fd_close(mut caller: wasmi::Caller<'_, WasiCtx>, fd: i32) -> i32 { + trace!("wasi fd_close({})", fd); + + let ctx = caller.data_mut(); + + match fd { + 0 => ctx.stdin_reader = None, + 1 => ctx.stdout_writer = None, + 2 => ctx.stderr_writer = None, + _ => return Errno::Badf as i32, + } + + Errno::Success as i32 +} + +fn clock_time_get( + mut caller: wasmi::Caller<'_, WasiCtx>, + clock_id: i32, + _precision: i64, + result_ptr: i32, +) -> i32 { + trace!("wasi clock_time_get({}, _, {})", clock_id, result_ptr); + + let memory = caller + .get_export("memory") + .and_then(|e| e.into_memory()) + .expect("guest must have memory"); + + let now = match clock_id { + // Realtime: nanoseconds since UNIX epoch + 0 | 1 => { + match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { + Ok(dur) => dur.as_nanos() as u64, + Err(_) => return Errno::Inval as i32, // time before epoch shouldn't happen + } + } + _ => { + warn!("unsupported clock_id {}", clock_id); + return Errno::Inval as i32; // unsupported clock + } + }; + + if memory + .write(&mut caller, result_ptr as usize, &now.to_le_bytes()) + .is_err() + { + return Errno::Fault as i32; + } + Errno::Success as i32 +} + +fn random_get(mut caller: wasmi::Caller<'_, WasiCtx>, buf_ptr: i32, buf_len: i32) -> i32 { + trace!("wasi random_get({}, {})", buf_ptr, buf_len); + + let memory = caller + .get_export("memory") + .and_then(|e| e.into_memory()) + .expect("guest must have memory"); + + let mut buf = vec![0u8; buf_len as usize]; + if let Err(e) = getrandom::getrandom(&mut buf) { + eprintln!("random_get failed: {:?}", e); + return Errno::Io as i32; + } + + if memory.write(&mut caller, buf_ptr as usize, &buf).is_err() { + return Errno::Fault as i32; + } + + Errno::Success as i32 +} + +fn sched_yield(mut caller: wasmi::Caller<'_, WasiCtx>) -> i32 { + trace!("wasi sched_yield()"); + + // TODO: Would probably better to yield here instead of setting fuel to 0. + // I can't test this, but I expect interrupting & restarting execution isn't very + // effective, expecially for repeated calls like from `fd_read` loops. + caller.set_fuel(0).unwrap(); + + Errno::Success as i32 +} + +const CLOCK_EVENT_TYPE: u8 = 0; + +/// Concurrently poll for the occurrence of a set of events. +/// +/// Essentially allows the guest to wait for multiple events (timers, fd write/read +/// readiness - subscription_u). +/// +/// Based on my understanding & reading other implementations, the function is +/// stateless. So each time it's called it waits until one of the subscriptions +/// is ready, writes the corresponding events into `events_ptr` and returns the +/// number of events written. +/// +/// It is not persistent across calls - so if a guest wants to wait for multiple +/// events, it needs to call this function with the same subscriptions again. +/// +/// For this impl we only care about timers, so we'll ignore the fd subscriptions +/// and return immediately with no event. +fn poll_oneoff( + mut caller: wasmi::Caller<'_, WasiCtx>, + subs_ptr: i32, + events_ptr: i32, + nsubscriptions: i32, + result_ptr: i32, +) -> i32 { + info!( + "wasi poll_oneoff({}, {}, {}, {})", + subs_ptr, events_ptr, nsubscriptions, result_ptr + ); + + if nsubscriptions == 0 { + error!("poll_oneoff called with zero subscriptions"); + return Errno::Inval as i32; + } + + let memory = caller + .get_export("memory") + .and_then(|e| e.into_memory()) + .expect("guest must have memory"); + + let mut clock_subscriptions = Vec::new(); + for i in 0..nsubscriptions { + // Each subscription is 48 bytes + let sub_offset = (subs_ptr as usize) + (i as usize * 48); + + // Read the subscription struct + // subscription layout (48 bytes total): + // - userdata: u64 at offset 0 + // - type: u8 at offset 8 (eventtype: 0=clock, 1=fd_read, 2=fd_write) + // - padding: 7 bytes + // - union data: 32 bytes at offset 16 + // + // For clock subscription (subscription_clock at offset 16): + // - id: u32 (clockid) at offset 16 + // - padding: 4 bytes + // - timeout: u64 (timestamp) at offset 24 + // - precision: u64 (timestamp) at offset 32 + // - flags: u16 (subclockflags) at offset 40 + + let mut sub_bytes = [0u8; 48]; + memory.read(&caller, sub_offset, &mut sub_bytes).unwrap(); + + let userdata = u64::from_le_bytes(sub_bytes[0..8].try_into().unwrap()); + let sub_type = sub_bytes[8]; + + if sub_type != CLOCK_EVENT_TYPE { + warn!( + "poll_oneoff: only clock subscriptions supported, got type {}", + sub_type + ); + continue; + } + + let clock_id = u32::from_le_bytes(sub_bytes[16..20].try_into().unwrap()); + let timeout_ns = u64::from_le_bytes(sub_bytes[24..32].try_into().unwrap()); + let precision_ns = u64::from_le_bytes(sub_bytes[32..40].try_into().unwrap()); + let flags = u16::from_le_bytes(sub_bytes[40..42].try_into().unwrap()); + + if clock_id > 1 { + // Only realtime (0) and monotonic (1) + warn!("poll_oneoff: unsupported clock_id {}", clock_id); + continue; + } + + let is_absolute = (flags & 1) != 0; + trace!( + "poll_oneoff: clock_id={}, timeout={}, precision={}, flags={:#x}, is_absolute={}", + clock_id, timeout_ns, precision_ns, flags, is_absolute + ); + + clock_subscriptions.push((userdata, clock_id, timeout_ns, is_absolute)); + } + + info!( + "poll_oneoff: {} clock subscriptions", + clock_subscriptions.len() + ); + + if clock_subscriptions.is_empty() { + warn!("poll_oneoff: no valid clock subscriptions found"); + return Errno::Inval as i32; + } + + // Convert all relative timeouts to absolute deadlines + let now_ns = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { + Ok(dur) => dur.as_nanos() as u64, + Err(_) => return Errno::Inval as i32, // time before epoch shouldn't happen + }; + + clock_subscriptions.iter_mut().for_each(|sub| { + if !sub.3 { + sub.2 = now_ns.saturating_add(sub.2); + } + }); + + // Find the earliest deadline + let (earliest_userdata, _earliest_clock_id, earliest_deadline_ns, _) = + clock_subscriptions.iter().min_by_key(|sub| sub.2).unwrap(); + + // poll_oneoff MUST block from the guest's perspective until 1+ + // events are ready. If we try to return immediately with no events (either) + // via an Errno::Again or Errno::Success with 0 events, the guest will panic. + // + // We can't block here since that this isn't an async block (due to wasmi's API) + // and we don't want to block the entire executor thread since we're in a single- + // threaded-context. SO instead we instantly return a successful result for the + // earliest timer, and if the deadline hasn't been reached yet we set the fuel + // to zero to yield execution back to the host. The host then waits until the + // deadline is reached before resuming execution. + + //? Future deadline trap + if earliest_deadline_ns > &now_ns { + let wait_duration = Duration::from_nanos(earliest_deadline_ns - now_ns); + caller.data_mut().sleep_until = Some(Instant::now() + wait_duration); + info!( + "poll_oneoff: waiting for {:?} before resuming guest", + wait_duration + ); + + //? Need to yield, so set fuel to zero + caller.set_fuel(0).unwrap(); + } + + // Write the event for the earliest deadline + // size: 32 + // - userdata: at offset 0 + // - error: at offset 8 + // - type: at offset 10 + // - fd_readwrite: at offset 16 (empty for clock events) + let mut event_buf = [0u8; 32]; + event_buf[0..8].copy_from_slice(&earliest_userdata.to_le_bytes()); + event_buf[8..10].copy_from_slice(&(Errno::Success as u16).to_le_bytes()); + event_buf[10] = CLOCK_EVENT_TYPE; + // fd_readwrite is empty for clock events + + info!( + "Writing event: userdata={}, error={}, type={}", + earliest_userdata, + Errno::Success as u16, + CLOCK_EVENT_TYPE + ); + info!("Event buffer: {:?}", &event_buf[..16]); + + let event_offset = events_ptr as usize; + memory.write(&mut caller, event_offset, &event_buf).unwrap(); + + // Write number of events (1) into result_ptr + memory + .write(&mut caller, result_ptr as usize, &1u32.to_le_bytes()) + .unwrap(); + + Errno::Success as i32 +} + +fn proc_raise(_caller: wasmi::Caller<'_, WasiCtx>, sig: i32) -> Result<(), wasmi::Error> { + info!("wasi proc_raise({})", sig); + + // https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#signal + match sig { + 0 | 13 | 16 | 17 | 22 | 27 => { + // Ignored signals + Ok(()) + } + _ => { + // Termination signals + Err(wasmi::Error::i32_exit(128 + sig)) + } + } +} + +fn proc_exit(_caller: wasmi::Caller<'_, WasiCtx>, status: i32) -> Result<(), wasmi::Error> { + info!("wasi proc_exit({})", status); + Err(wasmi::Error::i32_exit(status)) +} + +/// Write a pointer to a string into a table, and the string itself into a buffer, +/// then advance the table and buffer pointers. +/// +/// Utility for writing into guest memory. +fn write_pointer_and_string( + caller: &mut wasmi::Caller<'_, WasiCtx>, + memory: &wasmi::Memory, + table_ptr: &mut usize, + buf_ptr: &mut usize, + s: &str, +) -> Result<(), Errno> { + // 1. Write pointer into table + let ptr_le = (*buf_ptr as u32).to_le_bytes(); + memory + .write(&mut *caller, *table_ptr, &ptr_le) + .map_err(|_| Errno::Fault)?; + *table_ptr += 4; + + // 2. Write string+null into buffer + let mut tmp = Vec::with_capacity(s.len() + 1); + tmp.extend_from_slice(s.as_bytes()); + tmp.push(0); // null terminator + memory + .write(&mut *caller, *buf_ptr, &tmp) + .map_err(|_| Errno::Fault)?; + *buf_ptr += tmp.len(); + + Ok(()) +} diff --git a/crates/wasmi-plugin-hdk/src/wasi/mod.rs b/crates/wasmi-plugin-hdk/src/wasi/mod.rs index b981651..87db2d0 100644 --- a/crates/wasmi-plugin-hdk/src/wasi/mod.rs +++ b/crates/wasmi-plugin-hdk/src/wasi/mod.rs @@ -1,3 +1,5 @@ pub mod non_blocking_pipe; pub mod wasi_ctx; -pub mod wasmi; +// pub mod wasmi; +// pub mod linker; +// pub mod wasmer_linker; diff --git a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs index 6358d2a..2b83d37 100644 --- a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs +++ b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs @@ -1,9 +1,10 @@ use std::{ io::{BufRead, Read, Write}, - time::Duration, + time::{Duration, SystemTime}, }; use tracing::{error, info, trace, warn}; -use wasmi_plugin_rt::web_time::{Instant, SystemTime}; +use wasmer::{FunctionEnv, FunctionEnvMut, RuntimeError, imports}; +use wasmi_plugin_rt::web_time::Instant; /// A WASI context that can be attached to a wasmi instance. Attaches /// a subset of WASI syscalls to the instance, allowing it to @@ -29,19 +30,8 @@ pub struct WasiCtx { /// Whether the Wasi instance is current paused waiting for stdin. If true, /// the host may choose to wait for stdin to be ready before resuming execution. pub awaiting_stdin: bool, -} -#[repr(i32)] -#[derive(Debug, Clone, Copy)] -#[allow(dead_code)] -enum Errno { - Success = 0, - Access = 2, - Again = 6, - Badf = 8, - Fault = 21, - Inval = 28, - Io = 29, + pub memory: Option, } impl Default for WasiCtx { @@ -60,6 +50,31 @@ impl WasiCtx { stderr_writer: None, sleep_until: None, awaiting_stdin: false, + memory: None, + } + } + + pub fn import_object( + mut store: &mut wasmer::Store, + ctx: &FunctionEnv, + ) -> wasmer::Imports { + imports! { + "wasi_snapshot_preview1" => { + "args_get" => wasmer::Function::new_typed_with_env(&mut store, ctx, args_get), + "args_sizes_get" => wasmer::Function::new_typed_with_env(&mut store, ctx, args_sizes_get), + "environ_get" => wasmer::Function::new_typed_with_env(&mut store, ctx, env_get), + "environ_sizes_get" => wasmer::Function::new_typed_with_env(&mut store, ctx, env_sizes_get), + "fd_read" => wasmer::Function::new_typed_with_env(&mut store, ctx, fd_read), + "fd_write" => wasmer::Function::new_typed_with_env(&mut store, ctx, fd_write), + "fd_fdstat_get" => wasmer::Function::new_typed_with_env(&mut store, ctx, fd_fdstat_get), + "fd_close" => wasmer::Function::new_typed_with_env(&mut store, ctx, fd_close), + "clock_time_get" => wasmer::Function::new_typed_with_env(&mut store, ctx, clock_time_get), + "random_get" => wasmer::Function::new_typed_with_env(&mut store, ctx, random_get), + "poll_oneoff" => wasmer::Function::new_typed_with_env(&mut store, ctx, poll_oneoff), + "sched_yield" => wasmer::Function::new_typed_with_env(&mut store, ctx, sched_yield), + "proc_raise" => wasmer::Function::new_typed_with_env(&mut store, ctx, proc_raise), + "proc_exit" => wasmer::Function::new_typed_with_env(&mut store, ctx, proc_exit), + } } } @@ -92,6 +107,7 @@ impl WasiCtx { } /// Returns true if there is data available to read from stdin. + #[allow(dead_code)] pub fn stdin_available(&mut self) -> bool { let Some(reader) = &mut self.stdin_reader else { return false; @@ -104,45 +120,33 @@ impl WasiCtx { } } -/// Adds the WASI context to the given wasmi linker. -pub fn add_to_linker(linker: &mut wasmi::Linker) -> Result<(), wasmi::Error> { - linker.func_wrap("wasi_snapshot_preview1", "args_get", args_get)?; - linker.func_wrap("wasi_snapshot_preview1", "args_sizes_get", args_sizes_get)?; - linker.func_wrap("wasi_snapshot_preview1", "environ_get", env_get)?; - linker.func_wrap("wasi_snapshot_preview1", "environ_sizes_get", env_sizes_get)?; - linker.func_wrap("wasi_snapshot_preview1", "fd_read", fd_read)?; - linker.func_wrap("wasi_snapshot_preview1", "fd_fdstat_get", fd_fdstat_get)?; - linker.func_wrap("wasi_snapshot_preview1", "fd_write", fd_write)?; - linker.func_wrap("wasi_snapshot_preview1", "clock_time_get", clock_time_get)?; - linker.func_wrap("wasi_snapshot_preview1", "fd_close", fd_close)?; - linker.func_wrap("wasi_snapshot_preview1", "random_get", random_get)?; - linker.func_wrap("wasi_snapshot_preview1", "sched_yield", sched_yield)?; - linker.func_wrap("wasi_snapshot_preview1", "poll_oneoff", poll_oneoff)?; - linker.func_wrap("wasi_snapshot_preview1", "proc_raise", proc_raise)?; - linker.func_wrap("wasi_snapshot_preview1", "proc_exit", proc_exit)?; - - Ok(()) +#[repr(i32)] +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +enum Errno { + Success = 0, + Access = 2, + Again = 6, + Badf = 8, + Fault = 21, + Inval = 28, + Io = 29, } -/// Read command-line argument data. The size of the array should match that returned by args_sizes_get. Each argument is expected to be \0 terminated. -fn args_get(mut caller: wasmi::Caller<'_, WasiCtx>, argv: u32, argv_buf: u32) -> i32 { +pub fn args_get(mut env: FunctionEnvMut, argv: i32, argv_buf: i32) -> i32 { trace!("wasi args_get({}, {})", argv, argv_buf); - let ctx = caller.data_mut(); - let args = &ctx.args.clone(); - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); // Pointer in guest memory to an array of pointers that will be filled with the start of each argument string. - let mut argv_ptr = argv as usize; + let mut argv_ptr = argv as u64; // Pointer in guest memory to a buffer that will be filled with the `\0`-terminated argument strings. - let mut buf_ptr = argv_buf as usize; + let mut buf_ptr = argv_buf as u64; - for arg in args { - if write_pointer_and_string(&mut caller, &memory, &mut argv_ptr, &mut buf_ptr, arg).is_err() - { + for arg in &ctx.args { + if write_pointer_and_string(&view, &mut argv_ptr, &mut buf_ptr, arg).is_err() { return Errno::Fault as i32; } } @@ -150,27 +154,21 @@ fn args_get(mut caller: wasmi::Caller<'_, WasiCtx>, argv: u32, argv_buf: u32) -> Errno::Success as i32 } -/// Returns the number of arguments and the size of the argument string data, or an error. -fn args_sizes_get(mut caller: wasmi::Caller<'_, WasiCtx>, offset0: u32, offset1: u32) -> i32 { +pub fn args_sizes_get(mut env: FunctionEnvMut, offset0: i32, offset1: i32) -> i32 { trace!("wasi args_sizes_get({}, {})", offset0, offset1); - let ctx = caller.data_mut(); - let argc = ctx.args.len() as u32; - let argv_buf_len: u32 = ctx.args.iter().map(|s| s.len() as u32 + 1).sum(); // +1 for null terminator + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); + let argc = ctx.args.len() as u64; + let argv_buf_len: u64 = ctx.args.iter().map(|s| s.len() as u64 + 1).sum(); // +1 for null terminator - if memory - .write(&mut caller, offset0 as usize, &argc.to_le_bytes()) - .is_err() - { + if view.write(offset0 as u64, &argc.to_le_bytes()).is_err() { return Errno::Fault as i32; } - if memory - .write(&mut caller, offset1 as usize, &argv_buf_len.to_le_bytes()) + if view + .write(offset1 as u64, &argv_buf_len.to_le_bytes()) .is_err() { return Errno::Fault as i32; @@ -182,25 +180,20 @@ fn args_sizes_get(mut caller: wasmi::Caller<'_, WasiCtx>, offset0: u32, offset1: /// Read environment variable data. The sizes of the buffers should match that /// returned by environ_sizes_get. Key/value pairs are expected to be joined /// with =s, and terminated with \0s. -fn env_get(mut caller: wasmi::Caller<'_, WasiCtx>, environ: u32, environ_buf: u32) -> i32 { +pub fn env_get(mut env: FunctionEnvMut, environ: i32, environ_buf: i32) -> i32 { trace!("wasi environ_get({}, {})", environ, environ_buf); - let ctx = caller.data_mut(); - let env = &ctx.env.clone(); - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); // Pointer in guest memory to an array of pointers that will be filled with the start of each environment string. - let mut environ_ptr = environ as usize; + let mut environ_ptr = environ as u64; // Pointer in guest memory to a buffer that will be filled with the `\0`-terminated environment strings. - let mut buf_ptr = environ_buf as usize; + let mut buf_ptr = environ_buf as u64; - for var in env { - if write_pointer_and_string(&mut caller, &memory, &mut environ_ptr, &mut buf_ptr, var) - .is_err() - { + for var in &ctx.env { + if write_pointer_and_string(&view, &mut environ_ptr, &mut buf_ptr, var).is_err() { return Errno::Fault as i32; } } @@ -209,26 +202,21 @@ fn env_get(mut caller: wasmi::Caller<'_, WasiCtx>, environ: u32, environ_buf: u3 } /// Returns the number of environment variable arguments and the size of the environment variable data. -fn env_sizes_get(mut caller: wasmi::Caller<'_, WasiCtx>, offset0: u32, offset1: u32) -> i32 { +pub fn env_sizes_get(mut env: FunctionEnvMut, offset0: i32, offset1: i32) -> i32 { trace!("wasi environ_sizes_get({}, {})", offset0, offset1); - let ctx = caller.data_mut(); - let envc = ctx.env.len() as u32; - let env_buf_len: u32 = ctx.env.iter().map(|s| s.len() as u32 + 1).sum(); // +1 for null terminator + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); + let envc = ctx.env.len() as u64; + let env_buf_len: u64 = ctx.env.iter().map(|s| s.len() as u64 + 1).sum(); // +1 for null terminator - if memory - .write(&mut caller, offset0 as usize, &envc.to_le_bytes()) - .is_err() - { + if view.write(offset0 as u64, &envc.to_le_bytes()).is_err() { return Errno::Fault as i32; } - if memory - .write(&mut caller, offset1 as usize, &env_buf_len.to_le_bytes()) + if view + .write(offset1 as u64, &env_buf_len.to_le_bytes()) .is_err() { return Errno::Fault as i32; @@ -237,20 +225,8 @@ fn env_sizes_get(mut caller: wasmi::Caller<'_, WasiCtx>, offset0: u32, offset1: Errno::Success as i32 } -/// Read from a file descriptor. Note: This is similar to readv in POSIX. -/// Basically that means that instead of reading into a single buffer, we read -/// into multiple buffers described by an array of iovec structures. So we -/// need to read `iov_len` elements from the `iov_ptr` array, each of which -/// describes a buffer we need to fill with data read from this file descriptor. -/// -/// - `fd`: The file descriptor. -/// - `iov_ptr`: Pointer to an array of iovec structures. -/// - `iov_len`: Number of iovec structures in the array -/// - `nread_ptr`: Number of bytes read. -/// -/// For now, we only bother implementing reading from fd 0 (stdin). -fn fd_read( - mut caller: wasmi::Caller<'_, WasiCtx>, +pub fn fd_read( + mut env: FunctionEnvMut, fd: u32, iov_ptr: i32, iov_len: i32, @@ -265,21 +241,20 @@ fn fd_read( return Errno::Badf as i32; } - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); - let mut total_read = 0usize; + let mut total_read = 0u64; for i in 0..iov_len { - let base = (iov_ptr as usize) + (i as usize * 8); + let base = (iov_ptr as u64) + (i as u64 * 8); let mut buf_bytes = [0u8; 4]; - memory.read(&caller, base, &mut buf_bytes).unwrap(); - let buf_addr = u32::from_le_bytes(buf_bytes) as usize; + view.read(base, &mut buf_bytes).unwrap(); + let buf_addr = u32::from_le_bytes(buf_bytes) as u64; - memory.read(&caller, base + 4, &mut buf_bytes).unwrap(); + view.read(base + 4, &mut buf_bytes).unwrap(); let buf_len = u32::from_le_bytes(buf_bytes) as usize; // Allocate host buffer @@ -287,14 +262,13 @@ fn fd_read( // Narrow scope: borrow reader only while calling `read` let n = { - let ctx = caller.data_mut(); match ctx.stdin_reader.as_mut() { Some(r) => match r.read(&mut host_buf) { Ok(n) => n, Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - trace!("fd_read: WouldBlock, yielding to host"); - ctx.awaiting_stdin = true; - let _ = sched_yield(caller); + // trace!("fd_read: WouldBlock, yielding to host"); + // ctx.awaiting_stdin = true; + // let _ = sched_yield(caller); return Errno::Again as i32; } Err(_) => return Errno::Fault as i32, @@ -308,37 +282,22 @@ fn fd_read( break; } - total_read += n; - memory.write(&mut caller, buf_addr, &host_buf[..n]).unwrap(); + total_read += n as u64; + view.write(buf_addr, &host_buf[..n]).unwrap(); if n < buf_len { break; // short read } } - memory - .write( - &mut caller, - nread_ptr as usize, - &(total_read as u32).to_le_bytes(), - ) + view.write(nread_ptr as u64, &(total_read).to_le_bytes()) .unwrap(); trace!("fd_read: total_read={}", total_read); Errno::Success as i32 } -/// Write to a file descriptor. Note: This is similar to writev in POSIX. -/// -/// # Parameters -/// -/// - `fd`: The file descriptor. -/// - `ciov_ptr`: Pointer to an array of iovec structures. -/// - `ciov_len`: Number of iovec structures in the array -/// - `nwrite_ptr`: Number of bytes written. -/// -/// For now we only bother implementing writing to fd 1 (stdout) and fd 2 (stderr). -fn fd_write( - mut caller: wasmi::Caller<'_, WasiCtx>, +pub fn fd_write( + mut env: FunctionEnvMut, fd: i32, ciov_ptr: i32, ciov_len: i32, @@ -349,12 +308,11 @@ fn fd_write( fd, ciov_ptr, ciov_len, nwrite_ptr ); - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); - let mut total_written = 0usize; + let mut total_written = 0u64; for i in 0..ciov_len { // Each ciovec is { buf: u32, len: u32 } @@ -363,22 +321,20 @@ fn fd_write( let mut buf_bytes = [0u8; 4]; // Read buf pointer - memory.read(&caller, base, &mut buf_bytes).unwrap(); - let buf_addr = u32::from_le_bytes(buf_bytes) as usize; + view.read(base as u64, &mut buf_bytes).unwrap(); + let buf_addr = u32::from_le_bytes(buf_bytes) as u64; // Read buf length - memory.read(&caller, base + 4, &mut buf_bytes).unwrap(); + view.read((base + 4) as u64, &mut buf_bytes).unwrap(); let buf_len = u32::from_le_bytes(buf_bytes) as usize; // Copy from guest memory let mut host_buf = vec![0u8; buf_len]; - if memory.read(&caller, buf_addr, &mut host_buf).is_err() { + if view.read(buf_addr, &mut host_buf).is_err() { return Errno::Fault as i32; } // Borrow writer and write message - //? Narrow scope since we're also mutably borrowing caller elsewhere for memory access - let ctx = caller.data_mut(); let writer = match fd { 1 => ctx.stdout_writer.as_mut(), 2 => ctx.stderr_writer.as_mut(), @@ -391,7 +347,7 @@ fn fd_write( match writer.write(&host_buf) { Ok(n) => { - total_written += n; + total_written += n as u64; if n < buf_len { break; // partial write, stop } @@ -401,12 +357,8 @@ fn fd_write( } // Write total_written into nwrite_ptr - if memory - .write( - &mut caller, - nwrite_ptr as usize, - &(total_written as u32).to_le_bytes(), - ) + if view + .write(nwrite_ptr as u64, &(total_written as u32).to_le_bytes()) .is_err() { return Errno::Fault as i32; @@ -415,9 +367,13 @@ fn fd_write( Errno::Success as i32 } -fn fd_fdstat_get(mut caller: wasmi::Caller<'_, WasiCtx>, fd: i32, buf_ptr: i32) -> i32 { +pub fn fd_fdstat_get(mut env: FunctionEnvMut, fd: i32, buf_ptr: i32) -> i32 { trace!("wasi fd_fdstat_get({}, {})", fd, buf_ptr); + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); + // Constants from wasi_snapshot_preview1 const FILETYPE_CHARACTER_DEVICE: u8 = 2; @@ -442,12 +398,7 @@ fn fd_fdstat_get(mut caller: wasmi::Caller<'_, WasiCtx>, fd: i32, buf_ptr: i32) buf[8..16].copy_from_slice(&rights_base.to_le_bytes()); buf[16..24].copy_from_slice(&rights_inheriting.to_le_bytes()); - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - if memory.write(&mut caller, buf_ptr as usize, &buf).is_err() { + if view.write(buf_ptr as u64, &buf).is_err() { return Errno::Fault as i32; } @@ -455,10 +406,10 @@ fn fd_fdstat_get(mut caller: wasmi::Caller<'_, WasiCtx>, fd: i32, buf_ptr: i32) } /// Close a file descriptor. For now we only support closing stdin, stdout, stderr. -fn fd_close(mut caller: wasmi::Caller<'_, WasiCtx>, fd: i32) -> i32 { +pub fn fd_close(mut env: FunctionEnvMut, fd: i32) -> i32 { trace!("wasi fd_close({})", fd); - let ctx = caller.data_mut(); + let ctx = env.data_mut(); match fd { 0 => ctx.stdin_reader = None, @@ -470,18 +421,17 @@ fn fd_close(mut caller: wasmi::Caller<'_, WasiCtx>, fd: i32) -> i32 { Errno::Success as i32 } -fn clock_time_get( - mut caller: wasmi::Caller<'_, WasiCtx>, +pub fn clock_time_get( + mut env: FunctionEnvMut, clock_id: i32, _precision: i64, result_ptr: i32, ) -> i32 { trace!("wasi clock_time_get({}, _, {})", clock_id, result_ptr); - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); let now = match clock_id { // Realtime: nanoseconds since UNIX epoch @@ -497,72 +447,42 @@ fn clock_time_get( } }; - if memory - .write(&mut caller, result_ptr as usize, &now.to_le_bytes()) - .is_err() - { + if view.write(result_ptr as u64, &now.to_le_bytes()).is_err() { return Errno::Fault as i32; } Errno::Success as i32 } -fn random_get(mut caller: wasmi::Caller<'_, WasiCtx>, buf_ptr: i32, buf_len: i32) -> i32 { +pub fn random_get(mut env: FunctionEnvMut, buf_ptr: i32, buf_len: i32) -> i32 { trace!("wasi random_get({}, {})", buf_ptr, buf_len); - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); let mut buf = vec![0u8; buf_len as usize]; - if let Err(e) = getrandom::getrandom(&mut buf) { + if let Err(e) = getrandom::fill(&mut buf) { eprintln!("random_get failed: {:?}", e); return Errno::Io as i32; } - if memory.write(&mut caller, buf_ptr as usize, &buf).is_err() { + if view.write(buf_ptr as u64, &buf).is_err() { return Errno::Fault as i32; } Errno::Success as i32 } -fn sched_yield(mut caller: wasmi::Caller<'_, WasiCtx>) -> i32 { - trace!("wasi sched_yield()"); - - // TODO: Would probably better to yield here instead of setting fuel to 0. - // I can't test this, but I expect interrupting & restarting execution isn't very - // effective, expecially for repeated calls like from `fd_read` loops. - caller.set_fuel(0).unwrap(); - - Errno::Success as i32 -} - const CLOCK_EVENT_TYPE: u8 = 0; -/// Concurrently poll for the occurrence of a set of events. -/// -/// Essentially allows the guest to wait for multiple events (timers, fd write/read -/// readiness - subscription_u). -/// -/// Based on my understanding & reading other implementations, the function is -/// stateless. So each time it's called it waits until one of the subscriptions -/// is ready, writes the corresponding events into `events_ptr` and returns the -/// number of events written. -/// -/// It is not persistent across calls - so if a guest wants to wait for multiple -/// events, it needs to call this function with the same subscriptions again. -/// -/// For this impl we only care about timers, so we'll ignore the fd subscriptions -/// and return immediately with no event. -fn poll_oneoff( - mut caller: wasmi::Caller<'_, WasiCtx>, +pub fn poll_oneoff( + mut env: FunctionEnvMut, subs_ptr: i32, events_ptr: i32, nsubscriptions: i32, result_ptr: i32, ) -> i32 { - info!( + trace!( "wasi poll_oneoff({}, {}, {}, {})", subs_ptr, events_ptr, nsubscriptions, result_ptr ); @@ -572,10 +492,9 @@ fn poll_oneoff( return Errno::Inval as i32; } - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); + let (ctx, store) = env.data_and_store_mut(); + let memory = ctx.memory.as_ref().expect("Memory not initialized"); + let view = memory.view(&store); let mut clock_subscriptions = Vec::new(); for i in 0..nsubscriptions { @@ -597,7 +516,7 @@ fn poll_oneoff( // - flags: u16 (subclockflags) at offset 40 let mut sub_bytes = [0u8; 48]; - memory.read(&caller, sub_offset, &mut sub_bytes).unwrap(); + view.read(sub_offset as u64, &mut sub_bytes).unwrap(); let userdata = u64::from_le_bytes(sub_bytes[0..8].try_into().unwrap()); let sub_type = sub_bytes[8]; @@ -667,17 +586,21 @@ fn poll_oneoff( // to zero to yield execution back to the host. The host then waits until the // deadline is reached before resuming execution. + // TODO: While testing wasmer we will just block until the deadline is reached. + //? Future deadline trap if earliest_deadline_ns > &now_ns { let wait_duration = Duration::from_nanos(earliest_deadline_ns - now_ns); - caller.data_mut().sleep_until = Some(Instant::now() + wait_duration); - info!( - "poll_oneoff: waiting for {:?} before resuming guest", - wait_duration - ); - - //? Need to yield, so set fuel to zero - caller.set_fuel(0).unwrap(); + std::thread::sleep(wait_duration); + // let wait_duration = Duration::from_nanos(earliest_deadline_ns - now_ns); + // caller.data_mut().sleep_until = Some(Instant::now() + wait_duration); + // info!( + // "poll_oneoff: waiting for {:?} before resuming guest", + // wait_duration + // ); + + // //? Need to yield, so set fuel to zero + // caller.set_fuel(0).unwrap(); } // Write the event for the earliest deadline @@ -701,17 +624,21 @@ fn poll_oneoff( info!("Event buffer: {:?}", &event_buf[..16]); let event_offset = events_ptr as usize; - memory.write(&mut caller, event_offset, &event_buf).unwrap(); + view.write(event_offset as u64, &event_buf).unwrap(); // Write number of events (1) into result_ptr - memory - .write(&mut caller, result_ptr as usize, &1u32.to_le_bytes()) - .unwrap(); + view.write(result_ptr as u64, &1u32.to_le_bytes()).unwrap(); + + Errno::Success as i32 +} + +pub fn sched_yield(mut _env: FunctionEnvMut) -> i32 { + trace!("wasi sched_yield()"); Errno::Success as i32 } -fn proc_raise(_caller: wasmi::Caller<'_, WasiCtx>, sig: i32) -> Result<(), wasmi::Error> { +pub fn proc_raise(_: FunctionEnvMut, sig: i32) -> Result<(), RuntimeError> { info!("wasi proc_raise({})", sig); // https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#signal @@ -722,42 +649,33 @@ fn proc_raise(_caller: wasmi::Caller<'_, WasiCtx>, sig: i32) -> Result<(), wasmi } _ => { // Termination signals - Err(wasmi::Error::i32_exit(128 + sig)) + Err(RuntimeError::new(format!("exit: {}", sig + 128))) } } } -fn proc_exit(_caller: wasmi::Caller<'_, WasiCtx>, status: i32) -> Result<(), wasmi::Error> { +pub fn proc_exit(_: FunctionEnvMut, status: i32) -> Result<(), RuntimeError> { info!("wasi proc_exit({})", status); - Err(wasmi::Error::i32_exit(status)) + Err(RuntimeError::new(format!("exit: {}", status))) } -/// Write a pointer to a string into a table, and the string itself into a buffer, -/// then advance the table and buffer pointers. -/// -/// Utility for writing into guest memory. fn write_pointer_and_string( - caller: &mut wasmi::Caller<'_, WasiCtx>, - memory: &wasmi::Memory, - table_ptr: &mut usize, - buf_ptr: &mut usize, + view: &wasmer::MemoryView, + table_ptr: &mut u64, + buf_ptr: &mut u64, s: &str, ) -> Result<(), Errno> { // 1. Write pointer into table let ptr_le = (*buf_ptr as u32).to_le_bytes(); - memory - .write(&mut *caller, *table_ptr, &ptr_le) - .map_err(|_| Errno::Fault)?; + view.write(*table_ptr, &ptr_le).map_err(|_| Errno::Fault)?; *table_ptr += 4; // 2. Write string+null into buffer let mut tmp = Vec::with_capacity(s.len() + 1); tmp.extend_from_slice(s.as_bytes()); tmp.push(0); // null terminator - memory - .write(&mut *caller, *buf_ptr, &tmp) - .map_err(|_| Errno::Fault)?; - *buf_ptr += tmp.len(); + view.write(*buf_ptr, &tmp).map_err(|_| Errno::Fault)?; + *buf_ptr += tmp.len() as u64; Ok(()) } diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index 4592e7c..8b3d115 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -27,6 +27,7 @@ fn get_host_server() -> HostServer<()> { #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] +#[tracing_test::traced_test] async fn test_plugin() { info!("Starting test_plugin..."); diff --git a/shell.nix b/shell.nix index 74d32b0..add42aa 100644 --- a/shell.nix +++ b/shell.nix @@ -23,6 +23,7 @@ pkgs.mkShell { cargo-machete samply rustfmt + binaryen rust-bin.stable.latest.rust-analyzer ]; } From 2530462449a2a223d2475da9e4f9b981bc6578ba Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Mon, 22 Dec 2025 15:26:19 -0500 Subject: [PATCH 03/70] feat: switch to wasmer-wasix --- crates/wasmi-plugin-hdk/benches/benchmark.rs | 24 +-- crates/wasmi-plugin-hdk/src/bridge/mod.rs | 8 +- .../src/bridge/native_bridge.rs | 51 ++--- .../src/bridge/virtual_file.rs | 193 ++++++++++++++++++ .../src/bridge/worker_bridge.rs | 2 + crates/wasmi-plugin-hdk/src/compile.rs | 27 +-- crates/wasmi-plugin-hdk/src/plugin.rs | 4 +- crates/wasmi-plugin-hdk/src/wasi/mod.rs | 2 +- 8 files changed, 241 insertions(+), 70 deletions(-) create mode 100644 crates/wasmi-plugin-hdk/src/bridge/virtual_file.rs diff --git a/crates/wasmi-plugin-hdk/benches/benchmark.rs b/crates/wasmi-plugin-hdk/benches/benchmark.rs index 72baf26..24fa195 100644 --- a/crates/wasmi-plugin-hdk/benches/benchmark.rs +++ b/crates/wasmi-plugin-hdk/benches/benchmark.rs @@ -109,30 +109,10 @@ pub fn bench_call_many(c: &mut Criterion) { }); } -fn sieve_of_eratosthenes(limit: usize) -> Vec { - if limit < 2 { - return vec![]; - } - - let mut is_prime = vec![true; limit + 1]; - is_prime[0] = false; - is_prime[1] = false; - - for i in 2..=((limit as f64).sqrt() as usize) { - if is_prime[i] { - for j in ((i * i)..=limit).step_by(i) { - is_prime[j] = false; - } - } - } - - (2..=limit).filter(|&i| is_prime[i]).collect() -} - criterion_group!( benches, - // bench_ping, - // bench_prime_sieve_small, + bench_ping, + bench_prime_sieve_small, bench_prime_sieve_large, bench_call_many ); diff --git a/crates/wasmi-plugin-hdk/src/bridge/mod.rs b/crates/wasmi-plugin-hdk/src/bridge/mod.rs index 7f783d4..fb87083 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/mod.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/mod.rs @@ -1,14 +1,14 @@ #[cfg(not(target_arch = "wasm32"))] mod native_bridge; - -#[cfg(target_arch = "wasm32")] -mod worker_bridge; - #[cfg(not(target_arch = "wasm32"))] pub use native_bridge::NativeBridge; +#[cfg(not(target_arch = "wasm32"))] +mod virtual_file; #[cfg(target_arch = "wasm32")] pub use worker_bridge::WorkerBridge; +#[cfg(target_arch = "wasm32")] +mod worker_bridge; pub trait Bridge { fn terminate(self: Box); diff --git a/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs b/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs index be97e80..fce7b78 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs @@ -1,15 +1,19 @@ +//! Native wasmer bridge, running the Wasm module in a separate thread. + use futures::AsyncBufReadExt; use std::io::{Read, Write}; use thiserror::Error; use tokio::spawn; use tokio::task::{JoinHandle, spawn_blocking}; use tracing::info; -use wasmer::{FunctionEnv, Instance, Module, Store}; +use wasmer_wasix::runners::wasi::{RuntimeOrEngine, WasiRunner}; use crate::bridge::Bridge; +use crate::bridge::virtual_file::{WasiInputFile, WasiOutputFile}; use crate::compile::Compiled; -use crate::wasi::non_blocking_pipe::non_blocking_pipe; -use crate::wasi::wasi_ctx::WasiCtx; +use crate::wasi::non_blocking_pipe::{ + NonBlockingPipeReader, NonBlockingPipeWriter, non_blocking_pipe, +}; pub struct NativeBridge { wasm_handle: JoinHandle<()>, @@ -26,7 +30,6 @@ pub enum NativeBridgeError { impl NativeBridge { pub fn new( - name: &str, compiled: Compiled, ) -> Result< ( @@ -36,6 +39,7 @@ impl NativeBridge { ), NativeBridgeError, > { + let name = compiled.name.clone(); let (stdout_reader, stdout_writer) = non_blocking_pipe(); let (stdin_reader, stdin_writer) = non_blocking_pipe(); let (stderr_reader, stderr_writer) = non_blocking_pipe(); @@ -47,7 +51,6 @@ impl NativeBridge { } }); - let name = name.to_string(); let stderr_handle = spawn(async move { let mut buf_reader = futures::io::BufReader::new(stderr_reader); let mut line = String::new(); @@ -67,29 +70,29 @@ impl NativeBridge { fn run_instance( compiled: Compiled, - stdin: impl Read + Send + Sync + 'static, - stdout: impl Write + Send + Sync + 'static, - stderr: impl Write + Send + Sync + 'static, + stdin: NonBlockingPipeReader, + stdout: NonBlockingPipeWriter, + stderr: NonBlockingPipeWriter, ) -> Result<(), Box> { let engine = compiled.engine; let module = compiled.module; - let mut store = Store::new(engine); - - // TODO: Switch to using wasmer-wasix - let wasi_ctx = WasiCtx::new() - .set_stdin(Box::new(stdin)) - .set_stdout(Box::new(stdout)) - .set_stderr(Box::new(stderr)); - let function_env = FunctionEnv::new(&mut store, wasi_ctx); - let import_object = WasiCtx::import_object(&mut store, &function_env); - let instance = Instance::new(&mut store, &module, &import_object)?; - function_env.as_mut(&mut store).memory = - Some(instance.exports.get_memory("memory")?.clone()); - - // Start the plugin - let start = instance.exports.get_function("_start")?; - start.call(&mut store, &[])?; + let stdin = WasiInputFile { reader: stdin }; + let stdout = WasiOutputFile { writer: stdout }; + let stderr = WasiOutputFile { writer: stderr }; + + let mut runner = WasiRunner::new(); + runner + .with_stdin(Box::new(stdin)) + .with_stdout(Box::new(stdout)) + .with_stderr(Box::new(stderr)); + + let _ = runner.run_wasm( + RuntimeOrEngine::Engine(engine), + &compiled.name, + module, + compiled.module_hash, + ); Ok(()) } diff --git a/crates/wasmi-plugin-hdk/src/bridge/virtual_file.rs b/crates/wasmi-plugin-hdk/src/bridge/virtual_file.rs new file mode 100644 index 0000000..c41efba --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/bridge/virtual_file.rs @@ -0,0 +1,193 @@ +use std::fmt::Debug; +use std::io::SeekFrom; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf}; +use wasmer_wasix::virtual_fs::{Result as FsResult, VirtualFile}; + +pub struct WasiInputFile { + pub reader: R, +} + +pub struct WasiOutputFile { + pub writer: W, +} + +impl Debug for WasiInputFile { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WasiInputFile").finish() + } +} + +impl Debug for WasiOutputFile { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WasiOutputFile").finish() + } +} + +impl VirtualFile for WasiInputFile +where + R: futures::AsyncRead + Send + Sync + Unpin + 'static, +{ + fn last_accessed(&self) -> u64 { + 0 + } + fn last_modified(&self) -> u64 { + 0 + } + fn created_time(&self) -> u64 { + 0 + } + fn size(&self) -> u64 { + 0 + } + fn set_len(&mut self, _: u64) -> FsResult<()> { + Ok(()) + } + fn unlink(&mut self) -> FsResult<()> { + Ok(()) + } + + fn poll_read_ready( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(0)) + } + + fn poll_write_ready( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Err(std::io::ErrorKind::Unsupported.into())) + } +} + +impl AsyncRead for WasiInputFile { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let inner_buf = buf.initialize_unfilled(); + match Pin::new(&mut self.reader).poll_read(cx, inner_buf) { + Poll::Ready(Ok(n)) => { + buf.advance(n); + Poll::Ready(Ok(())) + } + Poll::Ready(Err(e)) => Poll::Ready(Err(e)), + Poll::Pending => Poll::Pending, + } + } +} + +impl AsyncSeek for WasiInputFile { + fn start_seek(self: Pin<&mut Self>, _position: SeekFrom) -> std::io::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Pipes are not seekable", + )) + } + + fn poll_complete(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Pipes are not seekable", + ))) + } +} + +impl AsyncWrite for WasiInputFile { + fn poll_write( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + _buf: &[u8], + ) -> Poll> { + Poll::Ready(Err(std::io::ErrorKind::Unsupported.into())) + } + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +impl VirtualFile for WasiOutputFile +where + W: std::io::Write + Send + Sync + Unpin + 'static, +{ + fn last_accessed(&self) -> u64 { + 0 + } + fn last_modified(&self) -> u64 { + 0 + } + fn created_time(&self) -> u64 { + 0 + } + fn size(&self) -> u64 { + 0 + } + fn set_len(&mut self, _: u64) -> FsResult<()> { + Ok(()) + } + fn unlink(&mut self) -> FsResult<()> { + Ok(()) + } + + fn poll_read_ready( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Err(std::io::ErrorKind::Unsupported.into())) + } + + fn poll_write_ready( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(0)) + } +} + +impl AsyncWrite for WasiOutputFile { + fn poll_write( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Poll::Ready(self.writer.write(buf)) + } + fn poll_flush(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(self.writer.flush()) + } + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +impl AsyncSeek for WasiOutputFile { + fn start_seek(self: Pin<&mut Self>, _position: SeekFrom) -> std::io::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Pipes are not seekable", + )) + } + fn poll_complete(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Pipes are not seekable", + ))) + } +} + +impl AsyncRead for WasiOutputFile { + fn poll_read( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + _buf: &mut ReadBuf<'_>, + ) -> Poll> { + Poll::Ready(Err(std::io::ErrorKind::Unsupported.into())) + } +} diff --git a/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs b/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs index 27ae326..1040841 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs @@ -1,3 +1,5 @@ +//! Web Worker wasmer bridge, running the Wasm module in a Web Worker. + use std::io::{self, Read, Write}; use futures::StreamExt; diff --git a/crates/wasmi-plugin-hdk/src/compile.rs b/crates/wasmi-plugin-hdk/src/compile.rs index 831a0e0..ad4ebf9 100644 --- a/crates/wasmi-plugin-hdk/src/compile.rs +++ b/crates/wasmi-plugin-hdk/src/compile.rs @@ -1,31 +1,24 @@ -// use wasmi::{Config, Engine, Module}; - -// pub fn compile_plugin(wasm_bytes: Vec) -> Result<(Engine, Module), wasmi::Error> { -// let mut config = Config::default(); -// config.consume_fuel(true); -// // https://github.com/wasmi-labs/wasmi/issues/1647 -// // TODO: Switch to lazy execution, seems faster. -// config.compilation_mode(wasmi::CompilationMode::Eager); -// config.set_max_cached_stacks(100); -// let engine = Engine::new(&config); -// let module = Module::new(&engine, wasm_bytes)?; - -// Ok((engine, module)) -// } - use wasmer::{Engine, Module}; #[derive(Clone)] pub struct Compiled { + pub name: String, pub engine: Engine, pub module: Module, + pub module_hash: wasmer_types::ModuleHash, } impl Compiled { - pub fn new(wasm_bytes: &[u8]) -> Result { + pub fn new(name: &str, wasm_bytes: &[u8]) -> Result { let engine = Engine::default(); let module = Module::new(&engine, wasm_bytes)?; + let module_hash = wasmer_types::ModuleHash::xxhash(wasm_bytes); - Ok(Compiled { engine, module }) + Ok(Compiled { + name: name.to_string(), + engine, + module, + module_hash, + }) } } diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index 6822390..59b3e9e 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -89,7 +89,7 @@ impl Plugin { logger: Box::new(default_plugin_logger), max_fuel: None, #[cfg(not(target_arch = "wasm32"))] - compiled: Compiled::new(&wasm_bytes)?, + compiled: Compiled::new(name, &wasm_bytes)?, wasm_bytes, }) } @@ -176,7 +176,7 @@ impl Plugin { ), PluginError, > { - let (bridge, stdout, stdin) = bridge::NativeBridge::new(&self.name, self.compiled.clone()) + let (bridge, stdout, stdin) = bridge::NativeBridge::new(self.compiled.clone()) .map_err(|_| PluginError::PluginDied)?; Ok((Box::new(bridge), Box::new(stdout), Box::new(stdin))) } diff --git a/crates/wasmi-plugin-hdk/src/wasi/mod.rs b/crates/wasmi-plugin-hdk/src/wasi/mod.rs index 87db2d0..27840d7 100644 --- a/crates/wasmi-plugin-hdk/src/wasi/mod.rs +++ b/crates/wasmi-plugin-hdk/src/wasi/mod.rs @@ -1,5 +1,5 @@ pub mod non_blocking_pipe; -pub mod wasi_ctx; +// pub mod wasi_ctx; // pub mod wasmi; // pub mod linker; // pub mod wasmer_linker; From e67b04b1d9bcdab55e745550415fe42da37b3c95 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Tue, 23 Dec 2025 22:49:05 -0500 Subject: [PATCH 04/70] feat: mostly-working web worker wasm --- .vscode/settings.json | 6 + Cargo.lock | 690 ++---------------- Cargo.toml | 6 + crates/test-plugin/Cargo.toml | 1 - crates/test-plugin/src/main.rs | 11 - crates/wasmi-plugin-hdk/Cargo.toml | 15 +- crates/wasmi-plugin-hdk/benches/benchmark.rs | 241 +++--- crates/wasmi-plugin-hdk/src/bridge/mod.rs | 41 +- .../src/bridge/native_bridge.rs | 54 +- .../src/{wasi => bridge}/non_blocking_pipe.rs | 0 .../src/bridge/shared_pipe.rs | 222 ++++++ .../src/bridge/virtual_file.rs | 193 ----- .../src/bridge/worker_bridge.rs | 237 ++++-- .../src/bridge/worker_protocol.rs | 19 + crates/wasmi-plugin-hdk/src/compile.rs | 17 +- crates/wasmi-plugin-hdk/src/lib.rs | 7 +- crates/wasmi-plugin-hdk/src/plugin.rs | 23 +- crates/wasmi-plugin-hdk/src/time.rs | 27 + crates/wasmi-plugin-hdk/src/wasi/linker.rs | 679 ----------------- crates/wasmi-plugin-hdk/src/wasi/mod.rs | 6 +- crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs | 196 ++--- crates/wasmi-plugin-hdk/src/wasi/wasmi.rs | 125 ---- crates/wasmi-plugin-hdk/tests/integration.rs | 210 +++--- crates/wasmi-plugin-pdk/Cargo.toml | 1 - crates/wasmi-plugin-pdk/src/server.rs | 21 +- crates/wasmi-plugin-pdk/src/transport.rs | 6 + crates/wasmi-plugin-rt/Cargo.toml | 1 - crates/wasmi-plugin-rt/src/lib.rs | 22 +- crates/worker/Cargo.toml | 33 + crates/worker/src/lib.rs | 114 +++ shell.nix | 2 + 31 files changed, 1076 insertions(+), 2150 deletions(-) create mode 100644 .vscode/settings.json rename crates/wasmi-plugin-hdk/src/{wasi => bridge}/non_blocking_pipe.rs (100%) create mode 100644 crates/wasmi-plugin-hdk/src/bridge/shared_pipe.rs delete mode 100644 crates/wasmi-plugin-hdk/src/bridge/virtual_file.rs create mode 100644 crates/wasmi-plugin-hdk/src/bridge/worker_protocol.rs create mode 100644 crates/wasmi-plugin-hdk/src/time.rs delete mode 100644 crates/wasmi-plugin-hdk/src/wasi/linker.rs delete mode 100644 crates/wasmi-plugin-hdk/src/wasi/wasmi.rs create mode 100644 crates/worker/Cargo.toml create mode 100644 crates/worker/src/lib.rs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..52e9704 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "rust-analyzer.cargo.target": "wasm32-unknown-unknown", + "rust-analyzer.check.targets": "wasm32-unknown-unknown", + "rust-analyzer.cargo.features": "all", + "rust-analyzer.procMacro.enable": true +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 42710a8..990e216 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,12 +109,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.5.0" @@ -166,7 +160,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 1.1.0", + "rustc-hash", "shlex", "syn 2.0.111", ] @@ -332,12 +326,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chrono" version = "0.4.42" @@ -456,16 +444,6 @@ version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147be55d677052dabc6b22252d5dd0fd4c29c8c27aa4f2fbef0f94aa003b406f" -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -527,7 +505,7 @@ dependencies = [ "hashbrown 0.14.5", "log", "regalloc2", - "rustc-hash 1.1.0", + "rustc-hash", "smallvec", "target-lexicon", ] @@ -853,7 +831,7 @@ dependencies = [ "lazy_static", "mintex", "parking_lot", - "rustc-hash 1.1.0", + "rustc-hash", "serde", "serde_json", "thousands", @@ -1022,21 +1000,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1046,12 +1009,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futures" version = "0.3.31" @@ -1317,113 +1274,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.1", - "tokio", - "tower-service", - "tracing", -] - [[package]] name = "iana-time-zone" version = "0.1.64" @@ -1619,16 +1469,6 @@ dependencies = [ "ipnet", ] -[[package]] -name = "iri-string" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "itertools" version = "0.12.1" @@ -1783,12 +1623,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "lz4_flex" version = "0.11.5" @@ -1889,18 +1723,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.61.2", -] - [[package]] name = "more-asserts" version = "0.2.2" @@ -1927,23 +1749,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nom" version = "5.1.3" @@ -2056,50 +1861,6 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "page_size" version = "0.6.0" @@ -2193,12 +1954,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - [[package]] name = "plotters" version = "0.3.7" @@ -2352,61 +2107,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.1", - "rustls", - "socket2 0.6.1", - "thiserror 2.0.17", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash 2.1.1", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.17", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.1", - "tracing", - "windows-sys 0.60.2", -] - [[package]] name = "quote" version = "1.0.42" @@ -2536,7 +2236,7 @@ checksum = "ad156d539c879b7a24a363a2016d77961786e71f48f2e2fc8302a92abd2429a6" dependencies = [ "hashbrown 0.13.2", "log", - "rustc-hash 1.1.0", + "rustc-hash", "slice-group-by", "smallvec", ] @@ -2597,65 +2297,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" -[[package]] -name = "reqwest" -version = "0.12.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-tls", - "hyper-util", - "js-sys", - "log", - "native-tls", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-native-tls", - "tokio-rustls", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - [[package]] name = "rkyv" version = "0.8.12" @@ -2698,12 +2339,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - [[package]] name = "rustc_version" version = "0.4.1" @@ -2739,60 +2374,12 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustls" -version = "0.23.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "rusty_pool" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed36cdb20de66d89a17ea04b8883fc7a386f2cf877aaedca5005583ce4876ff" -dependencies = [ - "crossbeam-channel", - "futures", - "futures-channel", - "futures-executor", - "num_cpus", -] - [[package]] name = "ruzstd" version = "0.5.0" @@ -2829,15 +2416,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "schemars" version = "0.8.22" @@ -2870,29 +2448,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.10.0", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "self_cell" version = "1.2.1" @@ -2930,6 +2485,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -2983,18 +2548,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -3044,15 +2597,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" -dependencies = [ - "libc", -] - [[package]] name = "simd-adler32" version = "0.3.8" @@ -3100,26 +2644,6 @@ dependencies = [ "managed", ] -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -3138,12 +2662,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" version = "1.0.109" @@ -3166,15 +2684,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -3241,7 +2750,6 @@ version = "0.1.0" dependencies = [ "rand 0.9.2", "serde_json", - "tokio", "tracing", "tracing-subscriber", "wasmi-plugin-pdk", @@ -3375,13 +2883,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ "bytes", - "libc", - "mio", "pin-project-lite", - "signal-hook-registry", - "socket2 0.6.1", "tokio-macros", - "windows-sys 0.61.2", ] [[package]] @@ -3395,26 +2898,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.17" @@ -3512,58 +2995,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags 2.10.0", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - [[package]] name = "tracing" version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3641,10 +3078,15 @@ dependencies = [ ] [[package]] -name = "try-lock" -version = "0.2.5" +name = "tracing-wasm" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] [[package]] name = "twox-hash" @@ -3713,12 +3155,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - [[package]] name = "url" version = "2.5.7" @@ -3761,12 +3197,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.5" @@ -3785,12 +3215,9 @@ dependencies = [ "dashmap", "derive_more 2.1.0", "dunce", - "filetime", - "fs_extra", "futures", "getrandom 0.2.16", "indexmap", - "libc", "pin-project-lite", "replace_with", "shared-buffer", @@ -3813,9 +3240,7 @@ dependencies = [ "async-trait", "bytes", "futures", - "mio", "serde", - "socket2 0.5.10", "thiserror 1.0.69", "tracing", ] @@ -3836,13 +3261,10 @@ dependencies = [ "futures-util", "ipnet", "iprange", - "libc", - "mio", "pin-project-lite", "rkyv", "serde", "smoltcp", - "socket2 0.5.10", "thiserror 1.0.69", "tokio", "tracing", @@ -3931,15 +3353,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -4065,19 +3478,6 @@ dependencies = [ "wasmparser 0.243.0", ] -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "wasmer" version = "6.1.0" @@ -4216,7 +3616,6 @@ dependencies = [ "rkyv", "serde", "serde_json", - "shared-buffer", "thiserror 1.0.69", "tracing", "virtual-fs", @@ -4270,6 +3669,7 @@ dependencies = [ "more-asserts", "rkyv", "serde", + "serde_bytes", "sha2", "target-lexicon", "thiserror 1.0.69", @@ -4340,9 +3740,7 @@ dependencies = [ "pin-project", "pin-utils", "rand 0.8.5", - "reqwest", "rkyv", - "rusty_pool", "semver", "serde", "serde_derive", @@ -4414,12 +3812,16 @@ dependencies = [ "dhat", "futures", "getrandom 0.3.4", + "gloo-timers", "serde", + "serde-wasm-bindgen", + "serde_bytes", "serde_json", "thiserror 2.0.17", "tokio", "tracing", "tracing-test", + "tracing-wasm", "uuid", "wasm-bindgen-futures", "wasm-bindgen-test", @@ -4441,7 +3843,6 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.17", - "tokio", "tracing", "tracing-subscriber", "wasmi-plugin-rt", @@ -4453,7 +3854,6 @@ version = "0.1.0" dependencies = [ "gloo-timers", "tokio", - "web-time", ] [[package]] @@ -4556,15 +3956,6 @@ dependencies = [ "url", ] -[[package]] -name = "webpki-roots" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "weezl" version = "0.1.12" @@ -4907,6 +4298,27 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "worker" +version = "0.1.0" +dependencies = [ + "futures", + "serde", + "serde-wasm-bindgen", + "serde_json", + "thiserror 2.0.17", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasmer", + "wasmer-types", + "wasmer-wasix", + "wasmi-plugin-hdk", + "wasmi-plugin-pdk", + "wasmi-plugin-rt", + "web-sys", + "web-time", +] + [[package]] name = "writeable" version = "0.6.2" @@ -4993,12 +4405,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - [[package]] name = "zerotrie" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 5636472..a84c9a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/wasmi-plugin-hdk", "crates/wasmi-plugin-pdk", "crates/wasmi-plugin-rt", + "crates/worker", ] resolver = "2" @@ -27,12 +28,16 @@ getrandom = { version = "0.3.4", features = ["wasm_js"] } gloo-timers = { version = "0.3", features = ["futures"] } rand = "0.9" serde = { version = "1", features = ["derive"] } +serde_bytes = "0.11" serde_json = "1" +serde-wasm-bindgen = "0.6" thiserror = "2" tokio = { version = "1", features = ["rt", "time", "sync", "macros"] } tracing = "0.1" tracing-subscriber = "0.3" +tracing-wasm = "0.2" uuid = { version = "1", features = ["v4", "serde", "js"] } +wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" # wasmi = { version = "1.0", default-features=false, features = ["std", "hash-collections", "prefer-btree-collections"] } # wasmer-middlewares = "6" @@ -40,6 +45,7 @@ wasmer = { version = "6", default-features = false } wasmer-wasix = { version = "0.601", default-features = false } wasmer-types = "6" web-sys = "0.3" +wasmi-plugin-hdk = { version = "0.1.0", path = "./crates/wasmi-plugin-hdk" } wasmi-plugin-pdk = { version = "0.1.0", path = "./crates/wasmi-plugin-pdk" } wasmi-plugin-rt = { version = "0.1.0", path = "./crates/wasmi-plugin-rt" } web-time = "1" \ No newline at end of file diff --git a/crates/test-plugin/Cargo.toml b/crates/test-plugin/Cargo.toml index ca93293..8b19bfa 100644 --- a/crates/test-plugin/Cargo.toml +++ b/crates/test-plugin/Cargo.toml @@ -16,7 +16,6 @@ publish = false [dependencies] rand = { workspace = true } serde_json = { workspace = true } -tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } wasmi-plugin-pdk = { workspace = true } diff --git a/crates/test-plugin/src/main.rs b/crates/test-plugin/src/main.rs index 0c9de1c..9f116f5 100644 --- a/crates/test-plugin/src/main.rs +++ b/crates/test-plugin/src/main.rs @@ -32,16 +32,6 @@ async fn sleep(_: Transport, duration_ms: u64) -> Result<(), RpcError> { Ok(()) } -async fn tokio_sleep(_: Transport, duration_ms: u64) -> Result<(), RpcError> { - info!("Sleeping (async) for {} milliseconds", duration_ms); - tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await; - info!( - "Woke up after sleeping (async) for {} milliseconds", - duration_ms - ); - Ok(()) -} - async fn call(transport: Transport, _: ()) -> Result { let resp = transport.call("ping", Value::Null)?; Ok(resp.result) @@ -113,7 +103,6 @@ fn main() { .with_method("get_random_number", get_random_number) .with_method("get_time", get_time) .with_method("sleep", sleep) - .with_method("tokio_sleep", tokio_sleep) .with_method("call", call) .with_method("call_many", call_many) .with_method("async_call", async_call) diff --git a/crates/wasmi-plugin-hdk/Cargo.toml b/crates/wasmi-plugin-hdk/Cargo.toml index 2e6b63b..f3a4f8d 100644 --- a/crates/wasmi-plugin-hdk/Cargo.toml +++ b/crates/wasmi-plugin-hdk/Cargo.toml @@ -17,27 +17,30 @@ async-trait = { workspace = true } futures = { workspace = true } getrandom = { workspace = true } serde = { workspace = true } +serde_bytes = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } -# wasmi = { workspace = true } wasmi-plugin-pdk = { workspace = true } wasmi-plugin-rt = { workspace = true } web-time = { workspace = true } -tokio = { workspace = true } wasmer-types = { workspace = true } -# wasmer-middlewares = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -wasmer = { workspace = true, default-features = true } -wasmer-wasix = { workspace = true, default-features = true } +tokio = { workspace = true } +wasmer = { workspace = true, features = ["sys", "cranelift"] } +wasmer-wasix = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] +gloo-timers = { workspace = true } +tracing-wasm = { workspace = true } +serde-wasm-bindgen = { workspace = true } wasmer = { workspace = true, features = ["js-default"] } wasmer-wasix = { workspace = true, features = ["js-default"] } -web-sys = { workspace = true, features=["Worker", "MessageEvent"] } +web-sys = { workspace = true, features=["Worker", "MessageEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } wasm-bindgen-futures = { workspace = true } +wasmer-types = { workspace = true, features=["enable-serde"] } [dev-dependencies] tracing-test = { version = "0.2.5", features = ["no-env-filter"] } diff --git a/crates/wasmi-plugin-hdk/benches/benchmark.rs b/crates/wasmi-plugin-hdk/benches/benchmark.rs index 24fa195..f638608 100644 --- a/crates/wasmi-plugin-hdk/benches/benchmark.rs +++ b/crates/wasmi-plugin-hdk/benches/benchmark.rs @@ -1,119 +1,126 @@ -use criterion::{Criterion, criterion_group, criterion_main}; -use serde_json::Value; -use std::sync::Arc; -use tokio::runtime::Builder; -use wasmi_plugin_hdk::{ - plugin::{Plugin, PluginId}, - server::HostServer, -}; - -const PLUGIN_WASM: &[u8] = include_bytes!("../../../target/wasm32-wasip1/release/test-plugin.wasm"); - -fn load_plugin_wasm() -> Vec { - PLUGIN_WASM.to_vec() +#[cfg(not(target_arch = "wasm32"))] +mod bench { + use criterion::{Criterion, criterion_group, criterion_main}; + use serde_json::Value; + use std::sync::Arc; + use tokio::runtime::Builder; + use wasmi_plugin_hdk::{ + plugin::{Plugin, PluginId}, + server::HostServer, + }; + + const PLUGIN_WASM: &[u8] = + include_bytes!("../../../target/wasm32-wasip1/release/test-plugin.wasm"); + + fn load_plugin_wasm() -> Vec { + PLUGIN_WASM.to_vec() + } + + fn get_host_server() -> HostServer<(Option, ())> { + HostServer::default() + .with_method( + "ping", + |_, _params: ()| async move { Ok("pong".to_string()) }, + ) + .with_method("echo", |_, params: Value| async move { Ok(params) }) + } + + /// Benchmark a single ping request to the wasm module. + pub fn bench_ping(c: &mut Criterion) { + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler.clone()).unwrap(); + + c.bench_function("ping", |b| { + b.iter(|| { + let fut = async { + plugin.call("ping", Value::Null).await.unwrap(); + }; + + rt.block_on(fut); + }) + }); + } + + /// Benchmark the prime sieve function with a small input. Primarily tests the overhead + /// of calling into the wasm module. + pub fn bench_prime_sieve_small(c: &mut Criterion) { + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); + + c.bench_function("prime_sieve_small", |b| { + b.iter(|| { + let fut = async { + plugin + .call("prime_sieve", Value::Number(1.into())) + .await + .unwrap(); + }; + + rt.block_on(fut); + }) + }); + } + + /// Benchmark the prime sieve function with a large input. Tests both the overhead + /// of calling into the wasm module and performance within the wasm module. + pub fn bench_prime_sieve_large(c: &mut Criterion) { + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); + + c.bench_function("prime_sieve_large", |b| { + b.iter(|| { + let fut = async { + plugin + .call("prime_sieve", Value::Number(100_0000.into())) + .await + .unwrap(); + }; + + rt.block_on(fut); + }) + }); + } + + /// Benchmark sending many echo requests to the host, and receiving responses. + pub fn bench_call_many(c: &mut Criterion) { + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); + + c.bench_function("call_many", |b| { + b.iter(|| { + let fut = async { + plugin + .call("call_many", Value::Number(200.into())) + .await + .unwrap(); + }; + + rt.block_on(fut); + }) + }); + } + + criterion_group!( + benches, + bench_ping, + bench_prime_sieve_small, + bench_prime_sieve_large, + bench_call_many + ); + criterion_main!(benches); } -fn get_host_server() -> HostServer<(Option, ())> { - HostServer::default() - .with_method( - "ping", - |_, _params: ()| async move { Ok("pong".to_string()) }, - ) - .with_method("echo", |_, params: Value| async move { Ok(params) }) -} - -/// Benchmark a single ping request to the wasm module. -pub fn bench_ping(c: &mut Criterion) { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler.clone()).unwrap(); - - c.bench_function("ping", |b| { - b.iter(|| { - let fut = async { - plugin.call("ping", Value::Null).await.unwrap(); - }; - - rt.block_on(fut); - }) - }); -} - -/// Benchmark the prime sieve function with a small input. Primarily tests the overhead -/// of calling into the wasm module. -pub fn bench_prime_sieve_small(c: &mut Criterion) { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); - - c.bench_function("prime_sieve_small", |b| { - b.iter(|| { - let fut = async { - plugin - .call("prime_sieve", Value::Number(1.into())) - .await - .unwrap(); - }; - - rt.block_on(fut); - }) - }); -} - -/// Benchmark the prime sieve function with a large input. Tests both the overhead -/// of calling into the wasm module and performance within the wasm module. -pub fn bench_prime_sieve_large(c: &mut Criterion) { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); - - c.bench_function("prime_sieve_large", |b| { - b.iter(|| { - let fut = async { - plugin - .call("prime_sieve", Value::Number(100_0000.into())) - .await - .unwrap(); - }; - - rt.block_on(fut); - }) - }); -} - -/// Benchmark sending many echo requests to the host, and receiving responses. -pub fn bench_call_many(c: &mut Criterion) { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); - - c.bench_function("call_many", |b| { - b.iter(|| { - let fut = async { - plugin - .call("call_many", Value::Number(200.into())) - .await - .unwrap(); - }; - - rt.block_on(fut); - }) - }); -} - -criterion_group!( - benches, - bench_ping, - bench_prime_sieve_small, - bench_prime_sieve_large, - bench_call_many -); -criterion_main!(benches); +#[cfg(target_arch = "wasm32")] +fn main() {} diff --git a/crates/wasmi-plugin-hdk/src/bridge/mod.rs b/crates/wasmi-plugin-hdk/src/bridge/mod.rs index fb87083..8891b88 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/mod.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/mod.rs @@ -2,45 +2,18 @@ mod native_bridge; #[cfg(not(target_arch = "wasm32"))] pub use native_bridge::NativeBridge; -#[cfg(not(target_arch = "wasm32"))] -mod virtual_file; +#[cfg(target_arch = "wasm32")] +mod worker_bridge; #[cfg(target_arch = "wasm32")] pub use worker_bridge::WorkerBridge; #[cfg(target_arch = "wasm32")] -mod worker_bridge; +pub mod shared_pipe; +#[cfg(target_arch = "wasm32")] +pub mod worker_protocol; + +mod non_blocking_pipe; pub trait Bridge { fn terminate(self: Box); } - -// use std::io::{Read, Write}; - -// // Define a common error type for the bridge setup -// pub type BridgeError = Box; - -// pub fn spawn_bridge( -// name: &str, -// wasm_bytes: &[u8], -// ) -> Result< -// ( -// Box, -// impl Read + Send + Sync + 'static, -// impl Write + Send + Sync + 'static, -// ), -// BridgeError, -// > { -// #[cfg(not(target_arch = "wasm32"))] -// { -// let (bridge, reader, writer) = native_bridge::NativeBridge::new(name, wasm_bytes) -// .map_err(|e| Box::new(e) as BridgeError)?; -// Ok((Box::new(bridge), reader, writer)) -// } - -// #[cfg(target_arch = "wasm32")] -// { -// let (bridge, reader, writer) = worker_bridge::WorkerBridge::new(name, wasm_bytes) -// .map_err(|js_val| format!("{:?}", js_val))?; -// Ok((Box::new(bridge), reader, writer)) -// } -// } diff --git a/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs b/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs index fce7b78..adf0ead 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs @@ -6,14 +6,15 @@ use thiserror::Error; use tokio::spawn; use tokio::task::{JoinHandle, spawn_blocking}; use tracing::info; -use wasmer_wasix::runners::wasi::{RuntimeOrEngine, WasiRunner}; +use wasmer::{Instance, Store}; use crate::bridge::Bridge; -use crate::bridge::virtual_file::{WasiInputFile, WasiOutputFile}; -use crate::compile::Compiled; -use crate::wasi::non_blocking_pipe::{ +use crate::bridge::non_blocking_pipe::{ NonBlockingPipeReader, NonBlockingPipeWriter, non_blocking_pipe, }; +use crate::bridge::virtual_file::{WasiInputFile, WasiOutputFile}; +use crate::compile::Compiled; +use crate::wasi::wasi_ctx::WasiCtx; pub struct NativeBridge { wasm_handle: JoinHandle<()>, @@ -34,14 +35,14 @@ impl NativeBridge { ) -> Result< ( Self, - impl Read + Send + Sync + 'static, impl Write + Send + Sync + 'static, + impl Read + Send + Sync + 'static, ), NativeBridgeError, > { let name = compiled.name.clone(); - let (stdout_reader, stdout_writer) = non_blocking_pipe(); let (stdin_reader, stdin_writer) = non_blocking_pipe(); + let (stdout_reader, stdout_writer) = non_blocking_pipe(); let (stderr_reader, stderr_writer) = non_blocking_pipe(); let wasm_handle = spawn_blocking(move || { @@ -51,21 +52,14 @@ impl NativeBridge { } }); - let stderr_handle = spawn(async move { - let mut buf_reader = futures::io::BufReader::new(stderr_reader); - let mut line = String::new(); - while buf_reader.read_line(&mut line).await.is_ok_and(|n| n > 0) { - info!("[plugin] [{}], {}", name, line.trim_end()); - line.clear(); - } - }); + // TODO: stderr task to log to console let bridge = NativeBridge { wasm_handle, stderr_handle, }; - Ok((bridge, stdout_reader, stdin_writer)) + Ok((bridge, stdin_writer, stdout_reader)) } fn run_instance( @@ -77,22 +71,20 @@ impl NativeBridge { let engine = compiled.engine; let module = compiled.module; - let stdin = WasiInputFile { reader: stdin }; - let stdout = WasiOutputFile { writer: stdout }; - let stderr = WasiOutputFile { writer: stderr }; - - let mut runner = WasiRunner::new(); - runner - .with_stdin(Box::new(stdin)) - .with_stdout(Box::new(stdout)) - .with_stderr(Box::new(stderr)); - - let _ = runner.run_wasm( - RuntimeOrEngine::Engine(engine), - &compiled.name, - module, - compiled.module_hash, - ); + let store = Store::new(&engine); + + let mut wasi_ctx = WasiCtx::new() + .set_stdin(stdin) + .set_stdout(stdout) + .set_stderr(stderr); + let imports = wasi_ctx.import_object(&store, &module); + let instance = Instance::new(&mut store, module, imports)?; + + let memory = instance.exports.get_memory("memory")?; + wasi_ctx.set_memory(memory); + + let start = instance.exports.get_function("_start")?; + start.call(&mut store, &[])?; Ok(()) } diff --git a/crates/wasmi-plugin-hdk/src/wasi/non_blocking_pipe.rs b/crates/wasmi-plugin-hdk/src/bridge/non_blocking_pipe.rs similarity index 100% rename from crates/wasmi-plugin-hdk/src/wasi/non_blocking_pipe.rs rename to crates/wasmi-plugin-hdk/src/bridge/non_blocking_pipe.rs diff --git a/crates/wasmi-plugin-hdk/src/bridge/shared_pipe.rs b/crates/wasmi-plugin-hdk/src/bridge/shared_pipe.rs new file mode 100644 index 0000000..ed58e11 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/bridge/shared_pipe.rs @@ -0,0 +1,222 @@ +//! Shared pipe implementation for worker bridge. + +use std::io::{Read, Write}; + +use web_sys::{ + js_sys::{self, Int32Array, SharedArrayBuffer, Uint8Array}, + wasm_bindgen::JsCast, +}; + +// 4xi32 header: (16 bytes) +const CLOSED_IDX: u32 = 0; // 0 = open, 1 = closed +const WRITE_IDX: u32 = 1; +const READ_IDX: u32 = 2; +const DATA_OFFSET: u32 = 16; + +// TODO: Impl AsyncRead so we don't need to busy-wait with WouldBlock +pub struct SharedPipe { + header: Int32Array, + data: Uint8Array, + capacity: u32, +} + +unsafe impl Send for SharedPipe {} +unsafe impl Sync for SharedPipe {} + +impl SharedPipe { + pub fn new(sab: &SharedArrayBuffer) -> Self { + let status = js_sys::Int32Array::new_with_byte_offset_and_length(&sab, 0, 4); + let data_len = sab.byte_length() - DATA_OFFSET; + let data = js_sys::Uint8Array::new_with_byte_offset_and_length(&sab, DATA_OFFSET, data_len); + let capacity = data_len; + + Self { + header: status, + data, + capacity, + } + } + + pub fn new_with_capacity(capacity: u32) -> Self { + let sab = SharedArrayBuffer::new((DATA_OFFSET + capacity) as u32); + Self::new(&sab) + } + + fn is_closed(&self) -> bool { + js_sys::Atomics::load(&self.header, CLOSED_IDX).unwrap() == 1 + } + + pub fn close(&self) { + js_sys::Atomics::store(&self.header, CLOSED_IDX, 1).unwrap(); + // Notify both pointers to wake up anyone waiting on either end + js_sys::Atomics::notify(&self.header, READ_IDX).unwrap(); + js_sys::Atomics::notify(&self.header, WRITE_IDX).unwrap(); + } + + fn read_idx(&self) -> u32 { + js_sys::Atomics::load(&self.header, READ_IDX).unwrap() as u32 + } + + fn set_read(&self, value: u32) { + js_sys::Atomics::store(&self.header, READ_IDX, value as i32).unwrap(); + js_sys::Atomics::notify(&self.header, READ_IDX).unwrap(); + } + + fn write_idx(&self) -> u32 { + js_sys::Atomics::load(&self.header, WRITE_IDX).unwrap() as u32 + } + + fn set_write(&self, value: u32) { + js_sys::Atomics::store(&self.header, WRITE_IDX, value as i32).unwrap(); + js_sys::Atomics::notify(&self.header, WRITE_IDX).unwrap(); + } + + pub fn buffer(&self) -> SharedArrayBuffer { + self.header.buffer().unchecked_into::() + } +} + +impl Read for SharedPipe { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let r = self.read_idx(); + let w = self.write_idx(); + + if r == w { + if self.is_closed() { + return Ok(0); + } + return Err(std::io::ErrorKind::WouldBlock.into()); + } + + let buf_len = buf.len() as u32; + let occupied = if w >= r { w - r } else { self.capacity - r + w }; + let amount = occupied.min(buf_len); + + // Chunk 1: From R to (End of Buffer or W) + let first_chunk_len = if w >= r { + amount + } else { + (self.capacity - r).min(amount) + }; + self.data + .subarray(r, r + first_chunk_len) + .copy_to(&mut buf[..first_chunk_len as usize]); + + // Chunk 2: From 0 to remaining (only if we wrapped) + if amount > first_chunk_len { + let second_chunk_len = amount - first_chunk_len; + self.data + .subarray(0, second_chunk_len) + .copy_to(&mut buf[first_chunk_len as usize..amount as usize]); + } + + self.set_read((r + amount) % self.capacity); + Ok(amount as usize) + } +} + +impl Write for SharedPipe { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + if self.is_closed() { + return Err(std::io::ErrorKind::BrokenPipe.into()); + } + + let r = self.read_idx(); + let w = self.write_idx(); + + let occupied = if w >= r { w - r } else { self.capacity - r + w }; + let available = self.capacity - occupied - 1; // N-1 rule + + if available == 0 { + return Err(std::io::ErrorKind::WouldBlock.into()); + } + + let n = (buf.len() as u32).min(available); + + // Chunk 1: From W to (End of Buffer or R-1) + let first_chunk_len = if r > w { n } else { (self.capacity - w).min(n) }; + self.data.set( + &js_sys::Uint8Array::from(&buf[..first_chunk_len as usize]), + w, + ); + + // Chunk 2: From 0 to remaining (only if we wrap) + if n > first_chunk_len { + let second_chunk_len = n - first_chunk_len; + let src = &buf[first_chunk_len as usize..(first_chunk_len + second_chunk_len) as usize]; + self.data.set(&js_sys::Uint8Array::from(src), 0); + } + + self.set_write((w + n) % self.capacity); + Ok(n as usize) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + #[cfg(target_family = "wasm")] + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + + use super::*; + use wasm_bindgen_test::wasm_bindgen_test; + + #[wasm_bindgen_test] + fn test_basic_read_write() { + let mut pipe = SharedPipe::new_with_capacity(10); + + let input = b"hello"; + let written = pipe.write(input).expect("Write failed"); + assert_eq!(written, 5); + + let mut output = [0u8; 5]; + let read = pipe.read(&mut output).expect("Read failed"); + assert_eq!(read, 5); + assert_eq!(&output, b"hello"); + + // Should now be WouldBlock + let res = pipe.read(&mut output); + assert!(res.is_err()); + + pipe.close(); + let res = pipe.write(b"more"); + assert!(res.is_err()); + + let res = pipe.read(&mut output); + assert_eq!(res.ok(), Some(0)); // EOF + } + + #[wasm_bindgen_test] + fn test_wrap_around_saturation() { + let mut pipe = SharedPipe::new_with_capacity(10); + + // 1. Fill 7 bytes + pipe.write(b"hello_w").unwrap(); + + // 2. Read 4 bytes (read_idx moves to 4, write_idx is at 7) + let mut tmp = [0u8; 4]; + pipe.read(&mut tmp).unwrap(); + assert_eq!(&tmp, b"hell"); + + // 3. Write 5 more bytes. + // Current state: r=4, w=7. Space available: 10 - (7-4) - 1 = 6. + // This write will wrap! (7, 8, 9, then 0, 1) + let written = pipe.write(b"orld_").unwrap(); + assert_eq!(written, 5); + + // 4. Read all available data (should be "efg" + "hijkl" = 8 bytes) + // This read MUST saturate the output buffer across the wrap point. + let mut output = [0u8; 8]; + let read = pipe.read(&mut output).expect("Wrap read failed"); + + assert_eq!(read, 8); + assert_eq!(&output, b"o_world_"); + + // Verify pointers are back in sync + assert_eq!(pipe.read_idx(), pipe.write_idx()); + } +} diff --git a/crates/wasmi-plugin-hdk/src/bridge/virtual_file.rs b/crates/wasmi-plugin-hdk/src/bridge/virtual_file.rs deleted file mode 100644 index c41efba..0000000 --- a/crates/wasmi-plugin-hdk/src/bridge/virtual_file.rs +++ /dev/null @@ -1,193 +0,0 @@ -use std::fmt::Debug; -use std::io::SeekFrom; -use std::pin::Pin; -use std::task::{Context, Poll}; -use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf}; -use wasmer_wasix::virtual_fs::{Result as FsResult, VirtualFile}; - -pub struct WasiInputFile { - pub reader: R, -} - -pub struct WasiOutputFile { - pub writer: W, -} - -impl Debug for WasiInputFile { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WasiInputFile").finish() - } -} - -impl Debug for WasiOutputFile { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WasiOutputFile").finish() - } -} - -impl VirtualFile for WasiInputFile -where - R: futures::AsyncRead + Send + Sync + Unpin + 'static, -{ - fn last_accessed(&self) -> u64 { - 0 - } - fn last_modified(&self) -> u64 { - 0 - } - fn created_time(&self) -> u64 { - 0 - } - fn size(&self) -> u64 { - 0 - } - fn set_len(&mut self, _: u64) -> FsResult<()> { - Ok(()) - } - fn unlink(&mut self) -> FsResult<()> { - Ok(()) - } - - fn poll_read_ready( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll> { - Poll::Ready(Ok(0)) - } - - fn poll_write_ready( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll> { - Poll::Ready(Err(std::io::ErrorKind::Unsupported.into())) - } -} - -impl AsyncRead for WasiInputFile { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - let inner_buf = buf.initialize_unfilled(); - match Pin::new(&mut self.reader).poll_read(cx, inner_buf) { - Poll::Ready(Ok(n)) => { - buf.advance(n); - Poll::Ready(Ok(())) - } - Poll::Ready(Err(e)) => Poll::Ready(Err(e)), - Poll::Pending => Poll::Pending, - } - } -} - -impl AsyncSeek for WasiInputFile { - fn start_seek(self: Pin<&mut Self>, _position: SeekFrom) -> std::io::Result<()> { - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "Pipes are not seekable", - )) - } - - fn poll_complete(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "Pipes are not seekable", - ))) - } -} - -impl AsyncWrite for WasiInputFile { - fn poll_write( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - _buf: &[u8], - ) -> Poll> { - Poll::Ready(Err(std::io::ErrorKind::Unsupported.into())) - } - fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } -} - -impl VirtualFile for WasiOutputFile -where - W: std::io::Write + Send + Sync + Unpin + 'static, -{ - fn last_accessed(&self) -> u64 { - 0 - } - fn last_modified(&self) -> u64 { - 0 - } - fn created_time(&self) -> u64 { - 0 - } - fn size(&self) -> u64 { - 0 - } - fn set_len(&mut self, _: u64) -> FsResult<()> { - Ok(()) - } - fn unlink(&mut self) -> FsResult<()> { - Ok(()) - } - - fn poll_read_ready( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll> { - Poll::Ready(Err(std::io::ErrorKind::Unsupported.into())) - } - - fn poll_write_ready( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll> { - Poll::Ready(Ok(0)) - } -} - -impl AsyncWrite for WasiOutputFile { - fn poll_write( - mut self: Pin<&mut Self>, - _cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - Poll::Ready(self.writer.write(buf)) - } - fn poll_flush(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(self.writer.flush()) - } - fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } -} - -impl AsyncSeek for WasiOutputFile { - fn start_seek(self: Pin<&mut Self>, _position: SeekFrom) -> std::io::Result<()> { - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "Pipes are not seekable", - )) - } - fn poll_complete(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "Pipes are not seekable", - ))) - } -} - -impl AsyncRead for WasiOutputFile { - fn poll_read( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - _buf: &mut ReadBuf<'_>, - ) -> Poll> { - Poll::Ready(Err(std::io::ErrorKind::Unsupported.into())) - } -} diff --git a/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs b/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs index 1040841..baf2a21 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs @@ -1,39 +1,46 @@ //! Web Worker wasmer bridge, running the Wasm module in a Web Worker. -use std::io::{self, Read, Write}; +use std::io::{BufRead, BufReader, Read, Write}; -use futures::StreamExt; -use futures::channel::mpsc; +use futures::channel::oneshot::{Receiver, Sender}; use thiserror::Error; +use tracing::{error, info}; use wasm_bindgen_futures::spawn_local; use web_sys::Worker; +use web_sys::js_sys::{Reflect, SharedArrayBuffer}; use web_sys::{MessageEvent, js_sys, wasm_bindgen::prelude::Closure}; -use web_sys::wasm_bindgen::{JsCast, JsError, JsValue}; +use web_sys::wasm_bindgen::{JsCast, JsValue}; use crate::bridge::Bridge; -use crate::wasi::non_blocking_pipe::non_blocking_pipe; +use crate::bridge::shared_pipe::SharedPipe; +use crate::bridge::worker_protocol::WorkerMessage; -// TODO: Add stdin_writer and stdout_reader methods pub struct WorkerBridge { worker: SendWorker, - // We keep this to ensure the JS callback isn't garbage collected + // Keep this to ensure the JS callback isn't garbage collected _onmessage_closure: Closure, } #[derive(Clone)] struct SendWorker(Worker); -struct WorkerWriter { - tx: mpsc::UnboundedSender>, -} - #[derive(Debug, Error)] pub enum WorkerBridgeError { #[error("JavaScript error: {0:?}")] JsValue(JsValue), + #[error("Serde serialization error: {0}")] + Serde(#[from] serde_wasm_bindgen::Error), + #[error("Receiver error: {0}")] + Receiver(#[from] futures::channel::oneshot::Canceled), } +const WORKER_JS_GLUE: &str = include_str!("../../../worker/pkg/worker.js"); +const WORKER_WASM_BYTES: &[u8] = include_bytes!("../../../worker/pkg/worker_bg.wasm"); + +unsafe impl Send for WorkerBridge {} +unsafe impl Sync for WorkerBridge {} + impl WorkerBridge { pub fn new( name: &str, @@ -41,35 +48,75 @@ impl WorkerBridge { ) -> Result< ( Self, - impl Read + Send + Sync + 'static, impl Write + Send + Sync + 'static, + impl Read + Send + Sync + 'static, ), WorkerBridgeError, > { - let worker_url = ""; - let worker = - SendWorker(Worker::new(worker_url).map_err(|e| WorkerBridgeError::JsValue(e))?); + info!("Creating WorkerBridge for plugin: {}", &name); - let (stdout_reader, mut stdout_writer) = non_blocking_pipe(); + let (ready_tx, ready_rx) = futures::channel::oneshot::channel::<()>(); + let stdin = SharedPipe::new_with_capacity(16384); + let stdout = SharedPipe::new_with_capacity(16384); + let stderr = SharedPipe::new_with_capacity(16384); - let onmessage_callback = Closure::wrap(Box::new(move |e: MessageEvent| { - if let Ok(array) = e.data().dyn_into::() { - let _ = stdout_writer.write(&array.to_vec()); - } - }) as Box); + //? Setup and send worker + let worker_url = worker_script()?; + let worker = + SendWorker(Worker::new(&worker_url).map_err(|e| WorkerBridgeError::JsValue(e))?); + //? Worker onmessage callback + let onmessage_callback = Closure::wrap(on_message(ready_tx)); worker .0 .set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref())); - let (stdin_tx, mut stdin_rx) = mpsc::unbounded::>(); + //? Send all stdin data to worker let worker_inner = worker.clone(); + let wasm = wasm_bytes.to_vec(); + let stdin_buf = stdin.buffer(); + let stdout_buf = stdout.buffer(); + let stderr_buf = stderr.buffer(); + spawn_local(async move { + match load( + ready_rx, + wasm, + stdin_buf, + stdout_buf, + stderr_buf, + &worker_inner, + ) + .await + { + Ok(_) => {} + Err(e) => { + error!("[WorkerBridge] Failed to load Wasm: {}", e); + } + } + }); + //? Log messages from stderr as plugin logs + let name = name.to_string(); spawn_local(async move { - while let Some(data) = stdin_rx.next().await { - let array = js_sys::Uint8Array::from(&data[..]); - if let Err(_) = worker_inner.post_message(&array) { - break; + let mut stderr = BufReader::new(stderr); + let mut buffer = String::new(); + loop { + buffer.clear(); + match stderr.read_line(&mut buffer) { + Ok(0) => { + break; + } + Ok(_) => { + info!("[plugin] [{}] {}", name, buffer.trim_end()); + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + gloo_timers::future::TimeoutFuture::new(0).await; + continue; + } + Err(e) => { + error!("[WorkerBridge] Error reading from stderr: {}", e); + break; + } } } }); @@ -78,15 +125,123 @@ impl WorkerBridge { worker, _onmessage_closure: onmessage_callback, }; + Ok((bridge, stdin, stdout)) + } +} - let writer = WorkerWriter { tx: stdin_tx }; +/// Creates a blob URL containing the worker script that lodas and executes the +/// wasm module. +fn worker_script() -> Result { + let options = web_sys::BlobPropertyBag::new(); + options.set_type("application/wasm"); + let wasm_blob = web_sys::Blob::new_with_u8_array_sequence_and_options( + &js_sys::Array::of1(&js_sys::Uint8Array::from(WORKER_WASM_BYTES)), + &options, + ) + .map_err(WorkerBridgeError::JsValue)?; + let wasm_url = web_sys::Url::create_object_url_with_blob(&wasm_blob) + .map_err(WorkerBridgeError::JsValue)?; - Ok((bridge, stdout_reader, writer)) - } + // 2. Create a script that initializes the glue + let loader_script = format!( + r#" + console.log = function(...args) {{ + self.postMessage({{ type: 'Log', message: args.join(' ') }}); + }}; + console.error = function(...args) {{ + self.postMessage({{ type: 'Error', message: args.join(' ') }}); + }}; + + console.log("Worker Shim: Script starting..."); + + {0} // WORKER_JS_GLUE + + wasm_bindgen('{1}').then(() => {{ + console.log("Worker Shim: Rust Wasm Initialized"); + wasm_bindgen.start_worker(); + }}).catch(err => console.error("Worker Init Failed:", err)); + "#, + WORKER_JS_GLUE, wasm_url + ); + + let options = web_sys::BlobPropertyBag::new(); + options.set_type("text/javascript"); + let blob = web_sys::Blob::new_with_u8_array_sequence_and_options( + &js_sys::Array::of1(&js_sys::JsString::from(loader_script)), + &options, + ) + .map_err(WorkerBridgeError::JsValue)?; + + web_sys::Url::create_object_url_with_blob(&blob).map_err(WorkerBridgeError::JsValue) } -unsafe impl Send for WorkerBridge {} -unsafe impl Sync for WorkerBridge {} +// Callback for worker onmessage events, when the host receives a message from the worker. +fn on_message(ready_tx: Sender<()>) -> Box { + let mut ready_tx = Some(ready_tx); + Box::new(move |e: MessageEvent| { + let Ok(msg) = serde_wasm_bindgen::from_value::(e.data()) else { + error!( + "WorkerBridge: Received unknown message from worker: {:?}", + e.data() + ); + return; + }; + + match msg { + WorkerMessage::Log { message } => { + info!("[WORKER] {}", message); + } + WorkerMessage::Error { message } => { + error!("[WORKER ERROR] {}", message); + } + WorkerMessage::Ready => { + info!("[WorkerBridge] Worker is ready"); + + if let Some(tx) = ready_tx.take() { + let _ = tx.send(()); + } + } + _ => { + error!("[WorkerBridge] Received unexpected message type from worker"); + } + } + }) +} + +/// Sends the Load message to the worker, transferring the Wasm bytes and pipes +async fn load( + ready_rx: Receiver<()>, + wasm: Vec, + stdin: SharedArrayBuffer, + stdout: SharedArrayBuffer, + stderr: SharedArrayBuffer, + worker_inner: &SendWorker, +) -> Result<(), WorkerBridgeError> { + ready_rx.await?; + + // Create Load message + let msg = serde_wasm_bindgen::to_value(&WorkerMessage::Load { wasm })?; + Reflect::set(&msg, &"stdin".into(), &stdin).map_err(WorkerBridgeError::JsValue)?; + Reflect::set(&msg, &"stdout".into(), &stdout).map_err(WorkerBridgeError::JsValue)?; + Reflect::set(&msg, &"stderr".into(), &stderr).map_err(WorkerBridgeError::JsValue)?; + + // Extract the wasm as Uint8Array for transfer + let payload = js_sys::Reflect::get(&msg, &"wasm".into()).map_err(WorkerBridgeError::JsValue)?; + let uint8_array = payload + .dyn_into::() + .map_err(WorkerBridgeError::JsValue)?; + let buffer = uint8_array.buffer(); + + // Post the message with transfer + //? Transfer will more efficiently move the Wasm bytes to the worker by + //? moving the underlying ArrayBuffer instead of copying it. This will + //? invalidate the original buffer in the main thread (zero it). + let transfer_list = js_sys::Array::of1(&buffer); + worker_inner + .post_message_with_transfer(&msg, &transfer_list) + .map_err(WorkerBridgeError::JsValue)?; + Ok(()) +} impl Bridge for WorkerBridge { fn terminate(self: Box) { @@ -95,23 +250,15 @@ impl Bridge for WorkerBridge { } impl SendWorker { - fn post_message(&self, msg: &JsValue) -> Result<(), JsValue> { - self.0.post_message(msg) + pub fn post_message_with_transfer( + &self, + msg: &JsValue, + transfer: &js_sys::Array, + ) -> Result<(), JsValue> { + self.0.post_message_with_transfer(msg, transfer) } fn terminate(&self) { self.0.terminate(); } } - -impl Write for WorkerWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.tx - .unbounded_send(buf.to_vec()) - .map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "Worker task dropped"))?; - Ok(buf.len()) - } - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} diff --git a/crates/wasmi-plugin-hdk/src/bridge/worker_protocol.rs b/crates/wasmi-plugin-hdk/src/bridge/worker_protocol.rs new file mode 100644 index 0000000..413fd86 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/bridge/worker_protocol.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum WorkerMessage { + Ready, + /// Load wasm bytes into the worker. Also has attached stdin/stdout/stderr + /// pipes, which are SharedArrayBuffers but can't be serialized. + Load { + #[serde(with = "serde_bytes")] + wasm: Vec, + }, + Log { + message: String, + }, + Error { + message: String, + }, +} diff --git a/crates/wasmi-plugin-hdk/src/compile.rs b/crates/wasmi-plugin-hdk/src/compile.rs index ad4ebf9..de7096f 100644 --- a/crates/wasmi-plugin-hdk/src/compile.rs +++ b/crates/wasmi-plugin-hdk/src/compile.rs @@ -1,14 +1,15 @@ -use wasmer::{Engine, Module}; - #[derive(Clone)] pub struct Compiled { pub name: String, + pub module_hash: wasmer_types::ModuleHash, + #[cfg(not(target_arch = "wasm32"))] pub engine: Engine, + #[cfg(not(target_arch = "wasm32"))] pub module: Module, - pub module_hash: wasmer_types::ModuleHash, } impl Compiled { + #[cfg(not(target_arch = "wasm32"))] pub fn new(name: &str, wasm_bytes: &[u8]) -> Result { let engine = Engine::default(); let module = Module::new(&engine, wasm_bytes)?; @@ -16,8 +17,18 @@ impl Compiled { Ok(Compiled { name: name.to_string(), + module_hash, engine, module, + }) + } + + #[cfg(target_arch = "wasm32")] + pub fn new(name: &str, wasm_bytes: &[u8]) -> Result { + let module_hash = wasmer_types::ModuleHash::xxhash(wasm_bytes); + + Ok(Compiled { + name: name.to_string(), module_hash, }) } diff --git a/crates/wasmi-plugin-hdk/src/lib.rs b/crates/wasmi-plugin-hdk/src/lib.rs index 475ad0d..1374d2c 100644 --- a/crates/wasmi-plugin-hdk/src/lib.rs +++ b/crates/wasmi-plugin-hdk/src/lib.rs @@ -2,6 +2,9 @@ mod compile; pub mod host_handler; pub mod plugin; // mod plugin_instance; -mod bridge; +pub mod bridge; pub mod server; -mod wasi; +mod time; +pub mod wasi; + +pub use bridge::worker_protocol::WorkerMessage; diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index 59b3e9e..baae89d 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -13,12 +13,13 @@ use wasmi_plugin_pdk::{ rpc_message::{RpcError, RpcResponse}, transport::Transport, }; -use wasmi_plugin_rt::sleep; use crate::bridge::{self, Bridge}; +use crate::compile::Compiled; #[cfg(not(target_arch = "wasm32"))] use crate::compile::Compiled; use crate::host_handler::HostHandler; +use crate::time::sleep; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct PluginId(Uuid); @@ -61,7 +62,6 @@ pub struct Plugin { handler: Arc, logger: Logger, max_fuel: Option, - #[cfg(not(target_arch = "wasm32"))] compiled: Compiled, wasm_bytes: Vec, } @@ -88,7 +88,6 @@ impl Plugin { handler, logger: Box::new(default_plugin_logger), max_fuel: None, - #[cfg(not(target_arch = "wasm32"))] compiled: Compiled::new(name, &wasm_bytes)?, wasm_bytes, }) @@ -139,7 +138,7 @@ impl From for RpcError { impl Plugin { pub async fn call(&self, method: &str, params: Value) -> Result { //? Construct URL from worker bytes - let (bridge, stdout_reader, stdin_writer) = self.create_bridge().unwrap(); + let (bridge, stdin_writer, stdout_reader) = self.create_bridge().unwrap(); let transport = Transport::new(stdout_reader, stdin_writer); let handler = PluginCallback { @@ -153,6 +152,7 @@ impl Plugin { let timeout = sleep(Duration::from_secs(60)).fuse(); futures::pin_mut!(transport_task, timeout); + info!("Plugin: Waiting for call to complete or timeout..."); futures::select! { res = transport_task => { bridge.terminate(); @@ -171,14 +171,14 @@ impl Plugin { ) -> Result< ( Box, - Box, Box, + Box, ), PluginError, > { - let (bridge, stdout, stdin) = bridge::NativeBridge::new(self.compiled.clone()) + let (bridge, stdin, stdout) = bridge::NativeBridge::new(self.compiled.clone()) .map_err(|_| PluginError::PluginDied)?; - Ok((Box::new(bridge), Box::new(stdout), Box::new(stdin))) + Ok((Box::new(bridge), Box::new(stdin), Box::new(stdout))) } #[cfg(target_arch = "wasm32")] @@ -187,15 +187,16 @@ impl Plugin { ) -> Result< ( Box, - Box, Box, + Box, ), PluginError, > { // Web uses the bytes to send to the Worker - let (bridge, stdout, stdin) = bridge::WorkerBridge::new(&self.name, &self.wasm_bytes) - .map_err(|_| PluginError::PluginDied)?; - Ok((Box::new(bridge), Box::new(stdout), Box::new(stdin))) + let (bridge, stdin, stdout) = + bridge::WorkerBridge::new(&self.compiled.name, &self.wasm_bytes) + .map_err(|_| PluginError::PluginDied)?; + Ok((Box::new(bridge), Box::new(stdin), Box::new(stdout))) } } diff --git a/crates/wasmi-plugin-hdk/src/time.rs b/crates/wasmi-plugin-hdk/src/time.rs new file mode 100644 index 0000000..4c62ac4 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/time.rs @@ -0,0 +1,27 @@ +pub async fn sleep(dur: web_time::Duration) { + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + { + gloo_timers::future::TimeoutFuture::new(dur.as_millis() as u32).await; + } + + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + { + tokio::time::sleep(dur).await; + } +} + +pub fn blocking_sleep(dur: web_time::Duration) { + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + { + let stn = web_sys::js_sys::SharedArrayBuffer::new(4); + let typed_array = web_sys::js_sys::Int32Array::new(&stn); + + let millis = dur.as_millis() as f64; + let _ = web_sys::js_sys::Atomics::wait_with_timeout(&typed_array, 0, 0, millis); + } + + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + { + std::thread::sleep(dur); + } +} diff --git a/crates/wasmi-plugin-hdk/src/wasi/linker.rs b/crates/wasmi-plugin-hdk/src/wasi/linker.rs deleted file mode 100644 index 92c29c5..0000000 --- a/crates/wasmi-plugin-hdk/src/wasi/linker.rs +++ /dev/null @@ -1,679 +0,0 @@ -use std::{ - io::{Read, Write}, - time::Duration, -}; -use tracing::{error, info, trace, warn}; -use wasmi_plugin_rt::web_time::{Instant, SystemTime}; - -use crate::wasi::wasi_ctx::WasiCtx; - -#[repr(i32)] -#[derive(Debug, Clone, Copy)] -#[allow(dead_code)] -enum Errno { - Success = 0, - Access = 2, - Again = 6, - Badf = 8, - Fault = 21, - Inval = 28, - Io = 29, -} - -/// Adds the WASI context to the given wasmi linker. -pub fn add_to_linker(linker: &mut wasmi::Linker) -> Result<(), wasmi::Error> { - linker.func_wrap("wasi_snapshot_preview1", "args_get", args_get)?; - linker.func_wrap("wasi_snapshot_preview1", "args_sizes_get", args_sizes_get)?; - linker.func_wrap("wasi_snapshot_preview1", "environ_get", env_get)?; - linker.func_wrap("wasi_snapshot_preview1", "environ_sizes_get", env_sizes_get)?; - linker.func_wrap("wasi_snapshot_preview1", "fd_read", fd_read)?; - linker.func_wrap("wasi_snapshot_preview1", "fd_fdstat_get", fd_fdstat_get)?; - linker.func_wrap("wasi_snapshot_preview1", "fd_write", fd_write)?; - linker.func_wrap("wasi_snapshot_preview1", "clock_time_get", clock_time_get)?; - linker.func_wrap("wasi_snapshot_preview1", "fd_close", fd_close)?; - linker.func_wrap("wasi_snapshot_preview1", "random_get", random_get)?; - linker.func_wrap("wasi_snapshot_preview1", "sched_yield", sched_yield)?; - linker.func_wrap("wasi_snapshot_preview1", "poll_oneoff", poll_oneoff)?; - linker.func_wrap("wasi_snapshot_preview1", "proc_raise", proc_raise)?; - linker.func_wrap("wasi_snapshot_preview1", "proc_exit", proc_exit)?; - - Ok(()) -} - -/// Read command-line argument data. The size of the array should match that returned by args_sizes_get. Each argument is expected to be \0 terminated. -fn args_get(mut caller: wasmi::Caller<'_, WasiCtx>, argv: u32, argv_buf: u32) -> i32 { - trace!("wasi args_get({}, {})", argv, argv_buf); - - let ctx = caller.data_mut(); - let args = &ctx.args.clone(); - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - // Pointer in guest memory to an array of pointers that will be filled with the start of each argument string. - let mut argv_ptr = argv as usize; - // Pointer in guest memory to a buffer that will be filled with the `\0`-terminated argument strings. - let mut buf_ptr = argv_buf as usize; - - for arg in args { - if write_pointer_and_string(&mut caller, &memory, &mut argv_ptr, &mut buf_ptr, arg).is_err() - { - return Errno::Fault as i32; - } - } - - Errno::Success as i32 -} - -/// Returns the number of arguments and the size of the argument string data, or an error. -fn args_sizes_get(mut caller: wasmi::Caller<'_, WasiCtx>, offset0: u32, offset1: u32) -> i32 { - trace!("wasi args_sizes_get({}, {})", offset0, offset1); - - let ctx = caller.data_mut(); - let argc = ctx.args.len() as u32; - let argv_buf_len: u32 = ctx.args.iter().map(|s| s.len() as u32 + 1).sum(); // +1 for null terminator - - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - if memory - .write(&mut caller, offset0 as usize, &argc.to_le_bytes()) - .is_err() - { - return Errno::Fault as i32; - } - if memory - .write(&mut caller, offset1 as usize, &argv_buf_len.to_le_bytes()) - .is_err() - { - return Errno::Fault as i32; - } - - Errno::Success as i32 -} - -/// Read environment variable data. The sizes of the buffers should match that -/// returned by environ_sizes_get. Key/value pairs are expected to be joined -/// with =s, and terminated with \0s. -fn env_get(mut caller: wasmi::Caller<'_, WasiCtx>, environ: u32, environ_buf: u32) -> i32 { - trace!("wasi environ_get({}, {})", environ, environ_buf); - - let ctx = caller.data_mut(); - let env = &ctx.env.clone(); - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - // Pointer in guest memory to an array of pointers that will be filled with the start of each environment string. - let mut environ_ptr = environ as usize; - // Pointer in guest memory to a buffer that will be filled with the `\0`-terminated environment strings. - let mut buf_ptr = environ_buf as usize; - - for var in env { - if write_pointer_and_string(&mut caller, &memory, &mut environ_ptr, &mut buf_ptr, var) - .is_err() - { - return Errno::Fault as i32; - } - } - - Errno::Success as i32 -} - -/// Returns the number of environment variable arguments and the size of the environment variable data. -fn env_sizes_get(mut caller: wasmi::Caller<'_, WasiCtx>, offset0: u32, offset1: u32) -> i32 { - trace!("wasi environ_sizes_get({}, {})", offset0, offset1); - - let ctx = caller.data_mut(); - let envc = ctx.env.len() as u32; - let env_buf_len: u32 = ctx.env.iter().map(|s| s.len() as u32 + 1).sum(); // +1 for null terminator - - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - if memory - .write(&mut caller, offset0 as usize, &envc.to_le_bytes()) - .is_err() - { - return Errno::Fault as i32; - } - if memory - .write(&mut caller, offset1 as usize, &env_buf_len.to_le_bytes()) - .is_err() - { - return Errno::Fault as i32; - } - - Errno::Success as i32 -} - -/// Read from a file descriptor. Note: This is similar to readv in POSIX. -/// Basically that means that instead of reading into a single buffer, we read -/// into multiple buffers described by an array of iovec structures. So we -/// need to read `iov_len` elements from the `iov_ptr` array, each of which -/// describes a buffer we need to fill with data read from this file descriptor. -/// -/// - `fd`: The file descriptor. -/// - `iov_ptr`: Pointer to an array of iovec structures. -/// - `iov_len`: Number of iovec structures in the array -/// - `nread_ptr`: Number of bytes read. -/// -/// For now, we only bother implementing reading from fd 0 (stdin). -fn fd_read( - mut caller: wasmi::Caller<'_, WasiCtx>, - fd: u32, - iov_ptr: i32, - iov_len: i32, - nread_ptr: i32, -) -> i32 { - trace!( - "wasi fd_read({}, {}, {}, {})", - fd, iov_ptr, iov_len, nread_ptr - ); - - if fd != 0 { - return Errno::Badf as i32; - } - - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - let mut total_read = 0usize; - - for i in 0..iov_len { - let base = (iov_ptr as usize) + (i as usize * 8); - let mut buf_bytes = [0u8; 4]; - - memory.read(&caller, base, &mut buf_bytes).unwrap(); - let buf_addr = u32::from_le_bytes(buf_bytes) as usize; - - memory.read(&caller, base + 4, &mut buf_bytes).unwrap(); - let buf_len = u32::from_le_bytes(buf_bytes) as usize; - - // Allocate host buffer - let mut host_buf = vec![0u8; buf_len]; - - // Narrow scope: borrow reader only while calling `read` - let n = { - let ctx = caller.data_mut(); - match ctx.stdin_reader.as_mut() { - Some(r) => match r.read(&mut host_buf) { - Ok(n) => n, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - trace!("fd_read: WouldBlock, yielding to host"); - ctx.awaiting_stdin = true; - let _ = sched_yield(caller); - return Errno::Again as i32; - } - Err(_) => return Errno::Fault as i32, - }, - None => return Errno::Badf as i32, - } - }; // <- borrow of ctx ends here - - if n == 0 { - trace!("fd_read: EOF"); - break; - } - - total_read += n; - memory.write(&mut caller, buf_addr, &host_buf[..n]).unwrap(); - if n < buf_len { - break; // short read - } - } - - memory - .write( - &mut caller, - nread_ptr as usize, - &(total_read as u32).to_le_bytes(), - ) - .unwrap(); - - trace!("fd_read: total_read={}", total_read); - Errno::Success as i32 -} - -/// Write to a file descriptor. Note: This is similar to writev in POSIX. -/// -/// # Parameters -/// -/// - `fd`: The file descriptor. -/// - `ciov_ptr`: Pointer to an array of iovec structures. -/// - `ciov_len`: Number of iovec structures in the array -/// - `nwrite_ptr`: Number of bytes written. -/// -/// For now we only bother implementing writing to fd 1 (stdout) and fd 2 (stderr). -fn fd_write( - mut caller: wasmi::Caller<'_, WasiCtx>, - fd: i32, - ciov_ptr: i32, - ciov_len: i32, - nwrite_ptr: i32, -) -> i32 { - trace!( - "wasi fd_write({}, {}, {}, {})", - fd, ciov_ptr, ciov_len, nwrite_ptr - ); - - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - let mut total_written = 0usize; - - for i in 0..ciov_len { - // Each ciovec is { buf: u32, len: u32 } - let base = (ciov_ptr as usize) + (i as usize * 8); - - let mut buf_bytes = [0u8; 4]; - - // Read buf pointer - memory.read(&caller, base, &mut buf_bytes).unwrap(); - let buf_addr = u32::from_le_bytes(buf_bytes) as usize; - - // Read buf length - memory.read(&caller, base + 4, &mut buf_bytes).unwrap(); - let buf_len = u32::from_le_bytes(buf_bytes) as usize; - - // Copy from guest memory - let mut host_buf = vec![0u8; buf_len]; - if memory.read(&caller, buf_addr, &mut host_buf).is_err() { - return Errno::Fault as i32; - } - - // Borrow writer and write message - //? Narrow scope since we're also mutably borrowing caller elsewhere for memory access - let ctx = caller.data_mut(); - let writer = match fd { - 1 => ctx.stdout_writer.as_mut(), - 2 => ctx.stderr_writer.as_mut(), - _ => return Errno::Badf as i32, - }; - let writer = match writer { - Some(w) => w, - None => return Errno::Badf as i32, - }; - - match writer.write(&host_buf) { - Ok(n) => { - total_written += n; - if n < buf_len { - break; // partial write, stop - } - } - Err(_) => return Errno::Io as i32, - } - } - - // Write total_written into nwrite_ptr - if memory - .write( - &mut caller, - nwrite_ptr as usize, - &(total_written as u32).to_le_bytes(), - ) - .is_err() - { - return Errno::Fault as i32; - } - - Errno::Success as i32 -} - -fn fd_fdstat_get(mut caller: wasmi::Caller<'_, WasiCtx>, fd: i32, buf_ptr: i32) -> i32 { - trace!("wasi fd_fdstat_get({}, {})", fd, buf_ptr); - - // Constants from wasi_snapshot_preview1 - const FILETYPE_CHARACTER_DEVICE: u8 = 2; - - // fdflags (none set) - let fs_flags: u16 = 0; - - // rights (for simplicity, we just allow read or write) - const RIGHTS_FD_READ: u64 = 1 << 1; - const RIGHTS_FD_WRITE: u64 = 1 << 6; - - let (filetype, rights_base) = match fd { - 0 => (FILETYPE_CHARACTER_DEVICE, RIGHTS_FD_READ), - 1 | 2 => (FILETYPE_CHARACTER_DEVICE, RIGHTS_FD_WRITE), - _ => return Errno::Badf as i32, - }; - - let rights_inheriting: u64 = 0; - - let mut buf = [0u8; 24]; - buf[0] = filetype; - buf[1..3].copy_from_slice(&fs_flags.to_le_bytes()); - buf[8..16].copy_from_slice(&rights_base.to_le_bytes()); - buf[16..24].copy_from_slice(&rights_inheriting.to_le_bytes()); - - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - if memory.write(&mut caller, buf_ptr as usize, &buf).is_err() { - return Errno::Fault as i32; - } - - Errno::Success as i32 -} - -/// Close a file descriptor. For now we only support closing stdin, stdout, stderr. -fn fd_close(mut caller: wasmi::Caller<'_, WasiCtx>, fd: i32) -> i32 { - trace!("wasi fd_close({})", fd); - - let ctx = caller.data_mut(); - - match fd { - 0 => ctx.stdin_reader = None, - 1 => ctx.stdout_writer = None, - 2 => ctx.stderr_writer = None, - _ => return Errno::Badf as i32, - } - - Errno::Success as i32 -} - -fn clock_time_get( - mut caller: wasmi::Caller<'_, WasiCtx>, - clock_id: i32, - _precision: i64, - result_ptr: i32, -) -> i32 { - trace!("wasi clock_time_get({}, _, {})", clock_id, result_ptr); - - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - let now = match clock_id { - // Realtime: nanoseconds since UNIX epoch - 0 | 1 => { - match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { - Ok(dur) => dur.as_nanos() as u64, - Err(_) => return Errno::Inval as i32, // time before epoch shouldn't happen - } - } - _ => { - warn!("unsupported clock_id {}", clock_id); - return Errno::Inval as i32; // unsupported clock - } - }; - - if memory - .write(&mut caller, result_ptr as usize, &now.to_le_bytes()) - .is_err() - { - return Errno::Fault as i32; - } - Errno::Success as i32 -} - -fn random_get(mut caller: wasmi::Caller<'_, WasiCtx>, buf_ptr: i32, buf_len: i32) -> i32 { - trace!("wasi random_get({}, {})", buf_ptr, buf_len); - - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - let mut buf = vec![0u8; buf_len as usize]; - if let Err(e) = getrandom::getrandom(&mut buf) { - eprintln!("random_get failed: {:?}", e); - return Errno::Io as i32; - } - - if memory.write(&mut caller, buf_ptr as usize, &buf).is_err() { - return Errno::Fault as i32; - } - - Errno::Success as i32 -} - -fn sched_yield(mut caller: wasmi::Caller<'_, WasiCtx>) -> i32 { - trace!("wasi sched_yield()"); - - // TODO: Would probably better to yield here instead of setting fuel to 0. - // I can't test this, but I expect interrupting & restarting execution isn't very - // effective, expecially for repeated calls like from `fd_read` loops. - caller.set_fuel(0).unwrap(); - - Errno::Success as i32 -} - -const CLOCK_EVENT_TYPE: u8 = 0; - -/// Concurrently poll for the occurrence of a set of events. -/// -/// Essentially allows the guest to wait for multiple events (timers, fd write/read -/// readiness - subscription_u). -/// -/// Based on my understanding & reading other implementations, the function is -/// stateless. So each time it's called it waits until one of the subscriptions -/// is ready, writes the corresponding events into `events_ptr` and returns the -/// number of events written. -/// -/// It is not persistent across calls - so if a guest wants to wait for multiple -/// events, it needs to call this function with the same subscriptions again. -/// -/// For this impl we only care about timers, so we'll ignore the fd subscriptions -/// and return immediately with no event. -fn poll_oneoff( - mut caller: wasmi::Caller<'_, WasiCtx>, - subs_ptr: i32, - events_ptr: i32, - nsubscriptions: i32, - result_ptr: i32, -) -> i32 { - info!( - "wasi poll_oneoff({}, {}, {}, {})", - subs_ptr, events_ptr, nsubscriptions, result_ptr - ); - - if nsubscriptions == 0 { - error!("poll_oneoff called with zero subscriptions"); - return Errno::Inval as i32; - } - - let memory = caller - .get_export("memory") - .and_then(|e| e.into_memory()) - .expect("guest must have memory"); - - let mut clock_subscriptions = Vec::new(); - for i in 0..nsubscriptions { - // Each subscription is 48 bytes - let sub_offset = (subs_ptr as usize) + (i as usize * 48); - - // Read the subscription struct - // subscription layout (48 bytes total): - // - userdata: u64 at offset 0 - // - type: u8 at offset 8 (eventtype: 0=clock, 1=fd_read, 2=fd_write) - // - padding: 7 bytes - // - union data: 32 bytes at offset 16 - // - // For clock subscription (subscription_clock at offset 16): - // - id: u32 (clockid) at offset 16 - // - padding: 4 bytes - // - timeout: u64 (timestamp) at offset 24 - // - precision: u64 (timestamp) at offset 32 - // - flags: u16 (subclockflags) at offset 40 - - let mut sub_bytes = [0u8; 48]; - memory.read(&caller, sub_offset, &mut sub_bytes).unwrap(); - - let userdata = u64::from_le_bytes(sub_bytes[0..8].try_into().unwrap()); - let sub_type = sub_bytes[8]; - - if sub_type != CLOCK_EVENT_TYPE { - warn!( - "poll_oneoff: only clock subscriptions supported, got type {}", - sub_type - ); - continue; - } - - let clock_id = u32::from_le_bytes(sub_bytes[16..20].try_into().unwrap()); - let timeout_ns = u64::from_le_bytes(sub_bytes[24..32].try_into().unwrap()); - let precision_ns = u64::from_le_bytes(sub_bytes[32..40].try_into().unwrap()); - let flags = u16::from_le_bytes(sub_bytes[40..42].try_into().unwrap()); - - if clock_id > 1 { - // Only realtime (0) and monotonic (1) - warn!("poll_oneoff: unsupported clock_id {}", clock_id); - continue; - } - - let is_absolute = (flags & 1) != 0; - trace!( - "poll_oneoff: clock_id={}, timeout={}, precision={}, flags={:#x}, is_absolute={}", - clock_id, timeout_ns, precision_ns, flags, is_absolute - ); - - clock_subscriptions.push((userdata, clock_id, timeout_ns, is_absolute)); - } - - info!( - "poll_oneoff: {} clock subscriptions", - clock_subscriptions.len() - ); - - if clock_subscriptions.is_empty() { - warn!("poll_oneoff: no valid clock subscriptions found"); - return Errno::Inval as i32; - } - - // Convert all relative timeouts to absolute deadlines - let now_ns = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { - Ok(dur) => dur.as_nanos() as u64, - Err(_) => return Errno::Inval as i32, // time before epoch shouldn't happen - }; - - clock_subscriptions.iter_mut().for_each(|sub| { - if !sub.3 { - sub.2 = now_ns.saturating_add(sub.2); - } - }); - - // Find the earliest deadline - let (earliest_userdata, _earliest_clock_id, earliest_deadline_ns, _) = - clock_subscriptions.iter().min_by_key(|sub| sub.2).unwrap(); - - // poll_oneoff MUST block from the guest's perspective until 1+ - // events are ready. If we try to return immediately with no events (either) - // via an Errno::Again or Errno::Success with 0 events, the guest will panic. - // - // We can't block here since that this isn't an async block (due to wasmi's API) - // and we don't want to block the entire executor thread since we're in a single- - // threaded-context. SO instead we instantly return a successful result for the - // earliest timer, and if the deadline hasn't been reached yet we set the fuel - // to zero to yield execution back to the host. The host then waits until the - // deadline is reached before resuming execution. - - //? Future deadline trap - if earliest_deadline_ns > &now_ns { - let wait_duration = Duration::from_nanos(earliest_deadline_ns - now_ns); - caller.data_mut().sleep_until = Some(Instant::now() + wait_duration); - info!( - "poll_oneoff: waiting for {:?} before resuming guest", - wait_duration - ); - - //? Need to yield, so set fuel to zero - caller.set_fuel(0).unwrap(); - } - - // Write the event for the earliest deadline - // size: 32 - // - userdata: at offset 0 - // - error: at offset 8 - // - type: at offset 10 - // - fd_readwrite: at offset 16 (empty for clock events) - let mut event_buf = [0u8; 32]; - event_buf[0..8].copy_from_slice(&earliest_userdata.to_le_bytes()); - event_buf[8..10].copy_from_slice(&(Errno::Success as u16).to_le_bytes()); - event_buf[10] = CLOCK_EVENT_TYPE; - // fd_readwrite is empty for clock events - - info!( - "Writing event: userdata={}, error={}, type={}", - earliest_userdata, - Errno::Success as u16, - CLOCK_EVENT_TYPE - ); - info!("Event buffer: {:?}", &event_buf[..16]); - - let event_offset = events_ptr as usize; - memory.write(&mut caller, event_offset, &event_buf).unwrap(); - - // Write number of events (1) into result_ptr - memory - .write(&mut caller, result_ptr as usize, &1u32.to_le_bytes()) - .unwrap(); - - Errno::Success as i32 -} - -fn proc_raise(_caller: wasmi::Caller<'_, WasiCtx>, sig: i32) -> Result<(), wasmi::Error> { - info!("wasi proc_raise({})", sig); - - // https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#signal - match sig { - 0 | 13 | 16 | 17 | 22 | 27 => { - // Ignored signals - Ok(()) - } - _ => { - // Termination signals - Err(wasmi::Error::i32_exit(128 + sig)) - } - } -} - -fn proc_exit(_caller: wasmi::Caller<'_, WasiCtx>, status: i32) -> Result<(), wasmi::Error> { - info!("wasi proc_exit({})", status); - Err(wasmi::Error::i32_exit(status)) -} - -/// Write a pointer to a string into a table, and the string itself into a buffer, -/// then advance the table and buffer pointers. -/// -/// Utility for writing into guest memory. -fn write_pointer_and_string( - caller: &mut wasmi::Caller<'_, WasiCtx>, - memory: &wasmi::Memory, - table_ptr: &mut usize, - buf_ptr: &mut usize, - s: &str, -) -> Result<(), Errno> { - // 1. Write pointer into table - let ptr_le = (*buf_ptr as u32).to_le_bytes(); - memory - .write(&mut *caller, *table_ptr, &ptr_le) - .map_err(|_| Errno::Fault)?; - *table_ptr += 4; - - // 2. Write string+null into buffer - let mut tmp = Vec::with_capacity(s.len() + 1); - tmp.extend_from_slice(s.as_bytes()); - tmp.push(0); // null terminator - memory - .write(&mut *caller, *buf_ptr, &tmp) - .map_err(|_| Errno::Fault)?; - *buf_ptr += tmp.len(); - - Ok(()) -} diff --git a/crates/wasmi-plugin-hdk/src/wasi/mod.rs b/crates/wasmi-plugin-hdk/src/wasi/mod.rs index 27840d7..d2ad2d6 100644 --- a/crates/wasmi-plugin-hdk/src/wasi/mod.rs +++ b/crates/wasmi-plugin-hdk/src/wasi/mod.rs @@ -1,5 +1 @@ -pub mod non_blocking_pipe; -// pub mod wasi_ctx; -// pub mod wasmi; -// pub mod linker; -// pub mod wasmer_linker; +pub mod wasi_ctx; diff --git a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs index 2b83d37..e40cb7e 100644 --- a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs +++ b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs @@ -1,10 +1,10 @@ -use std::{ - io::{BufRead, Read, Write}, - time::{Duration, SystemTime}, -}; +use std::io::{BufRead, Read, Write}; +use thiserror::Error; use tracing::{error, info, trace, warn}; -use wasmer::{FunctionEnv, FunctionEnvMut, RuntimeError, imports}; -use wasmi_plugin_rt::web_time::Instant; +use wasmer::{FunctionEnvMut, RuntimeError}; +use web_time::{Duration, Instant, SystemTime}; + +use crate::time::blocking_sleep; /// A WASI context that can be attached to a wasmi instance. Attaches /// a subset of WASI syscalls to the instance, allowing it to @@ -31,7 +31,15 @@ pub struct WasiCtx { /// the host may choose to wait for stdin to be ready before resuming execution. pub awaiting_stdin: bool, - pub memory: Option, + memory: Option, +} + +#[derive(Debug, Error)] +pub enum WasiError { + #[error("Instantiation Error: {0}")] + InstantiationError(#[from] wasmer::InstantiationError), + #[error("Export Error: {0}")] + ExportError(#[from] wasmer::ExportError), } impl Default for WasiCtx { @@ -54,30 +62,6 @@ impl WasiCtx { } } - pub fn import_object( - mut store: &mut wasmer::Store, - ctx: &FunctionEnv, - ) -> wasmer::Imports { - imports! { - "wasi_snapshot_preview1" => { - "args_get" => wasmer::Function::new_typed_with_env(&mut store, ctx, args_get), - "args_sizes_get" => wasmer::Function::new_typed_with_env(&mut store, ctx, args_sizes_get), - "environ_get" => wasmer::Function::new_typed_with_env(&mut store, ctx, env_get), - "environ_sizes_get" => wasmer::Function::new_typed_with_env(&mut store, ctx, env_sizes_get), - "fd_read" => wasmer::Function::new_typed_with_env(&mut store, ctx, fd_read), - "fd_write" => wasmer::Function::new_typed_with_env(&mut store, ctx, fd_write), - "fd_fdstat_get" => wasmer::Function::new_typed_with_env(&mut store, ctx, fd_fdstat_get), - "fd_close" => wasmer::Function::new_typed_with_env(&mut store, ctx, fd_close), - "clock_time_get" => wasmer::Function::new_typed_with_env(&mut store, ctx, clock_time_get), - "random_get" => wasmer::Function::new_typed_with_env(&mut store, ctx, random_get), - "poll_oneoff" => wasmer::Function::new_typed_with_env(&mut store, ctx, poll_oneoff), - "sched_yield" => wasmer::Function::new_typed_with_env(&mut store, ctx, sched_yield), - "proc_raise" => wasmer::Function::new_typed_with_env(&mut store, ctx, proc_raise), - "proc_exit" => wasmer::Function::new_typed_with_env(&mut store, ctx, proc_exit), - } - } - } - #[allow(dead_code)] pub fn add_arg(mut self, arg: &str) -> Self { self.args.push(arg.to_string()); @@ -106,17 +90,42 @@ impl WasiCtx { self } - /// Returns true if there is data available to read from stdin. - #[allow(dead_code)] - pub fn stdin_available(&mut self) -> bool { - let Some(reader) = &mut self.stdin_reader else { - return false; + pub fn into_fn( + self, + mut store: &mut wasmer::Store, + module: &wasmer::Module, + ) -> Result { + let ctx = wasmer::FunctionEnv::new(&mut store, self); + let imports = wasmer::imports! { + "wasi_snapshot_preview1" => { + "args_get" => wasmer::Function::new_typed_with_env(&mut store, &ctx, args_get), + "args_sizes_get" => wasmer::Function::new_typed_with_env(&mut store, &ctx, args_sizes_get), + "environ_get" => wasmer::Function::new_typed_with_env(&mut store, &ctx, env_get), + "environ_sizes_get" => wasmer::Function::new_typed_with_env(&mut store, &ctx, env_sizes_get), + "fd_read" => wasmer::Function::new_typed_with_env(&mut store, &ctx, fd_read), + "fd_write" => wasmer::Function::new_typed_with_env(&mut store, &ctx, fd_write), + "fd_fdstat_get" => wasmer::Function::new_typed_with_env(&mut store, &ctx, fd_fdstat_get), + "fd_close" => wasmer::Function::new_typed_with_env(&mut store, &ctx, fd_close), + "clock_time_get" => wasmer::Function::new_typed_with_env(&mut store, &ctx, clock_time_get), + "random_get" => wasmer::Function::new_typed_with_env(&mut store, &ctx, random_get), + "poll_oneoff" => wasmer::Function::new_typed_with_env(&mut store, &ctx, poll_oneoff), + "sched_yield" => wasmer::Function::new_typed_with_env(&mut store, &ctx, sched_yield), + "proc_raise" => wasmer::Function::new_typed_with_env(&mut store, &ctx, proc_raise), + "proc_exit" => wasmer::Function::new_typed_with_env(&mut store, &ctx, proc_exit), + } }; - match reader.fill_buf() { - Ok(buf) => !buf.is_empty(), - Err(_) => false, - } + let instance = wasmer::Instance::new(&mut store, &module, &imports)?; + let memory = instance.exports.get_memory("memory")?; + ctx.as_mut(&mut store).set_memory(memory.clone()); + + let start_fn = instance.exports.get_function("_start")?; + + Ok(start_fn.clone()) + } + + fn set_memory(&mut self, memory: wasmer::Memory) { + self.memory = Some(memory); } } @@ -312,53 +321,68 @@ pub fn fd_write( let memory = ctx.memory.as_ref().expect("Memory not initialized"); let view = memory.view(&store); - let mut total_written = 0u64; - - for i in 0..ciov_len { - // Each ciovec is { buf: u32, len: u32 } - let base = (ciov_ptr as usize) + (i as usize * 8); + let mut total_written = 0u32; - let mut buf_bytes = [0u8; 4]; + let writer = match fd { + 1 => ctx.stdout_writer.as_mut(), + 2 => ctx.stderr_writer.as_mut(), + _ => return Errno::Badf as i32, + }; - // Read buf pointer - view.read(base as u64, &mut buf_bytes).unwrap(); - let buf_addr = u32::from_le_bytes(buf_bytes) as u64; + let Some(writer) = writer else { + return Errno::Badf as i32; + }; - // Read buf length - view.read((base + 4) as u64, &mut buf_bytes).unwrap(); - let buf_len = u32::from_le_bytes(buf_bytes) as usize; + let mut scratch = [0u8; 8192]; + for i in 0..ciov_len { + let base = (ciov_ptr as u64) + (i as u64 * 8); - // Copy from guest memory - let mut host_buf = vec![0u8; buf_len]; - if view.read(buf_addr, &mut host_buf).is_err() { + // Read the ciovec (ptr: u32, len: u32) + let mut cio_buf = [0u8; 8]; + if view.read(base, &mut cio_buf).is_err() { return Errno::Fault as i32; } - // Borrow writer and write message - let writer = match fd { - 1 => ctx.stdout_writer.as_mut(), - 2 => ctx.stderr_writer.as_mut(), - _ => return Errno::Badf as i32, - }; - let writer = match writer { - Some(w) => w, - None => return Errno::Badf as i32, - }; + let buf_addr = u32::from_le_bytes(cio_buf[0..4].try_into().unwrap()) as u64; + let buf_len = u32::from_le_bytes(cio_buf[4..8].try_into().unwrap()) as usize; + + if buf_len == 0 { + continue; + } + + // Process iovecs in chunks with the scratch buffer + let mut written = 0; + while written < buf_len { + let to_copy = (buf_len - written).min(scratch.len()); + if view + .read(buf_addr + written as u64, &mut scratch[..to_copy]) + .is_err() + { + return Errno::Fault as i32; + } - match writer.write(&host_buf) { - Ok(n) => { - total_written += n as u64; - if n < buf_len { - break; // partial write, stop + match writer.write(&scratch[..to_copy]) { + Ok(0) => break, // Writer closed + Ok(n) => { + written += n; + total_written += n as u32; + if n < to_copy { + break; // partial write, stop + } } + Err(_) => return Errno::Io as i32, } - Err(_) => return Errno::Io as i32, + } + + //? If we couldn't write the full iovec, stop processing further iovecs + if written < buf_len { + break; } } // Write total_written into nwrite_ptr if view - .write(nwrite_ptr as u64, &(total_written as u32).to_le_bytes()) + .write(nwrite_ptr as u64, &total_written.to_le_bytes()) .is_err() { return Errno::Fault as i32; @@ -575,32 +599,11 @@ pub fn poll_oneoff( let (earliest_userdata, _earliest_clock_id, earliest_deadline_ns, _) = clock_subscriptions.iter().min_by_key(|sub| sub.2).unwrap(); - // poll_oneoff MUST block from the guest's perspective until 1+ - // events are ready. If we try to return immediately with no events (either) - // via an Errno::Again or Errno::Success with 0 events, the guest will panic. - // - // We can't block here since that this isn't an async block (due to wasmi's API) - // and we don't want to block the entire executor thread since we're in a single- - // threaded-context. SO instead we instantly return a successful result for the - // earliest timer, and if the deadline hasn't been reached yet we set the fuel - // to zero to yield execution back to the host. The host then waits until the - // deadline is reached before resuming execution. - - // TODO: While testing wasmer we will just block until the deadline is reached. - - //? Future deadline trap + // Just blocking sleep until the earliest deadline is hit. if earliest_deadline_ns > &now_ns { let wait_duration = Duration::from_nanos(earliest_deadline_ns - now_ns); - std::thread::sleep(wait_duration); - // let wait_duration = Duration::from_nanos(earliest_deadline_ns - now_ns); - // caller.data_mut().sleep_until = Some(Instant::now() + wait_duration); - // info!( - // "poll_oneoff: waiting for {:?} before resuming guest", - // wait_duration - // ); - - // //? Need to yield, so set fuel to zero - // caller.set_fuel(0).unwrap(); + + blocking_sleep(wait_duration); } // Write the event for the earliest deadline @@ -634,7 +637,6 @@ pub fn poll_oneoff( pub fn sched_yield(mut _env: FunctionEnvMut) -> i32 { trace!("wasi sched_yield()"); - Errno::Success as i32 } diff --git a/crates/wasmi-plugin-hdk/src/wasi/wasmi.rs b/crates/wasmi-plugin-hdk/src/wasi/wasmi.rs deleted file mode 100644 index 3b4bd67..0000000 --- a/crates/wasmi-plugin-hdk/src/wasi/wasmi.rs +++ /dev/null @@ -1,125 +0,0 @@ -use std::sync::{Arc, atomic::AtomicBool}; - -use thiserror::Error; -use tracing::{error, trace}; -use wasmi::{Func, Store}; -use wasmi_plugin_rt::{now, sleep, yield_now}; - -use crate::wasi::wasi_ctx::WasiCtx; - -#[derive(Error, Debug)] -pub enum RunError { - #[error("wasmi error")] - WasmiError(#[from] wasmi::Error), - #[error("resume error")] - ResumeError(wasmi::Error), - #[error("host trap")] - HostTrap(wasmi::ResumableCallHostTrap), -} - -/// Notes on interuptability and performance implications (Robert's desktop - ryzen 5 3600). -/// -/// For a task of running a prime number sieve on 10_000 elements, it: -/// - takes 499 ms when MAX_FUEL is 100_000_000 (no refuels needed) -/// - takes 528 ms when MAX_FUEL is 1_000_000 (8 refuels needed, each one taking 62 ms) -/// - takes ~550 ms when MAX_FUEL is 100_000 (83 refuels needed, each one taking ~6.6 ms) -/// - takes ~575 ms when MAX_FUEL is 10_000 (836 refuels needed, each one taking ~600 us) -/// -/// Significantly lower fuel does not significantly lower performance. -/// For this reason, I figure it's fine to keep MAX_FUEL very low and to build in -/// async yielding into the `run_wasm` function so it works better in single-thread -/// environments (like within wasm when building to target the web). If this later -/// becomes a performance issue we can test it properly. -pub const MAX_FUEL: u64 = 100_000; - -/// Spawns a new async task to run the given wasm start function. `is_running` -/// is an atomic signal that can be used to forcefully exit the plugin early. -pub fn spawn_wasm( - store: Store, - start_func: Func, - is_running: Arc, - max_fuel: Option, -) -> impl Future { - trace!("Spawning plugin task"); - let is_running = is_running.clone(); - async move { - if let Err(e) = run_wasm(store, start_func, is_running.clone(), max_fuel).await { - error!("Plugin error: {:?}", e); - } - is_running.store(false, std::sync::atomic::Ordering::SeqCst); - } -} - -/// run_wasm manages the plugin's lifecycle. Because -/// wasmi doesn't support plugin intercepts or async execution, we -/// need some manual way of interrupting the plugin every so often to check if -/// it's been killed and yield to other cooperative tasks. Here I do that by setting -/// a low fuel limit, catching the out-of-fuel condition and resuming the plugin -/// when it's not been killed. -async fn run_wasm( - mut store: Store, - start_func: Func, - is_running: Arc, - max_fuel: Option, -) -> Result<(), RunError> { - trace!("Plugin task started"); - let max_fuel = max_fuel.unwrap_or(MAX_FUEL); - - //? Starts with zero fuel so we fall into the resumable loop - store.set_fuel(0).unwrap(); - let mut resumable = start_func.call_resumable(&mut store, &[], &mut [])?; - - loop { - match resumable { - wasmi::ResumableCall::Finished => return Ok(()), - wasmi::ResumableCall::HostTrap(trap) => { - return Err(RunError::HostTrap(trap)); - } - wasmi::ResumableCall::OutOfFuel(out_of_fuel) => { - if !is_running.load(std::sync::atomic::Ordering::SeqCst) { - return Ok(()); - } - - let required = out_of_fuel.required_fuel(); - let top_up = required.max(max_fuel); - store.set_fuel(top_up).unwrap(); - - // Handle any sleep requests - if let Some(resume_time) = store.data().sleep_until { - let now = now(); - if resume_time > now { - trace!( - "Plugin requested sleep until {:?}, sleeping...", - resume_time - ); - let dur = resume_time - now; - sleep(dur).await; - } - store.data_mut().sleep_until = None; - } - - // Handle any stdin wait requests, avoids busy waiting - if store.data().awaiting_stdin && !store.data_mut().stdin_available() { - trace!("Plugin is paused waiting for stdin, yielding..."); - yield_now().await; - store.data_mut().awaiting_stdin = false; - } - - // Courtesy yield to other tasks - trace!("Plugin out of fuel, yielding..."); - yield_now().await; - - match out_of_fuel.resume(&mut store, &mut []) { - Ok(next) => { - trace!("Plugin resumed after yielding"); - resumable = next; - } - Err(e) => { - error!("Error resuming plugin: {:?}", e); - return Err(RunError::ResumeError(e)); - } - } - } - } - } -} diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index 8b3d115..649184b 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -1,12 +1,17 @@ use serde_json::Value; use std::sync::Arc; use tracing::info; +#[cfg(target_family = "wasm")] +use tracing_wasm::{ConsoleConfig, WASMLayerConfigBuilder}; use wasmi_plugin_hdk::{plugin::Plugin, server::HostServer}; use web_time::{Instant, SystemTime}; #[cfg(target_family = "wasm")] use wasm_bindgen_test::*; +#[cfg(target_family = "wasm")] +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + const PLUGIN_WASM: &[u8] = include_bytes!("../../../target/wasm32-wasip1/release/test-plugin.wasm"); fn load_plugin_wasm() -> Vec { @@ -25,57 +30,67 @@ fn get_host_server() -> HostServer<()> { }) } -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -#[tracing_test::traced_test] -async fn test_plugin() { - info!("Starting test_plugin..."); +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// #[cfg_attr(not(target_family = "wasm"), tokio::test)] +// #[cfg_attr(not(target_family = "wasm"), tracing_test::traced_test)] +// async fn test_plugin() { +// #[cfg(target_family = "wasm")] +// { +// let config = WASMLayerConfigBuilder::new() +// .set_console_config(ConsoleConfig::ReportWithoutConsoleColor) +// .build(); +// tracing_wasm::set_as_global_default_with_config(config); +// } - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); +// info!("Starting test_plugin..."); - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - plugin.call("ping", Value::Null).await.unwrap(); -} +// let wasm_bytes = load_plugin_wasm(); +// let handler = Arc::new(get_host_server()); -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_get_random_number() { - info!("Starting get_random_number test..."); +// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); +// let resp = plugin.call("ping", Value::Null).await.unwrap(); +// assert_eq!(resp.result.as_str().unwrap(), "pong"); +// } - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// #[cfg_attr(not(target_family = "wasm"), tokio::test)] +// async fn test_get_random_number() { +// info!("Starting get_random_number test..."); - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - let response = plugin.call("get_random_number", Value::Null).await.unwrap(); +// let wasm_bytes = load_plugin_wasm(); +// let handler = Arc::new(get_host_server()); - info!("Random number response: {:?}", response); - response.result.as_u64().unwrap(); -} +// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); +// let response = plugin.call("get_random_number", Value::Null).await.unwrap(); -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_get_time() { - info!("Starting get_time test..."); +// info!("Random number response: {:?}", response); +// response.result.as_u64().unwrap(); +// } - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// #[cfg_attr(not(target_family = "wasm"), tokio::test)] +// async fn test_get_time() { +// info!("Starting get_time test..."); - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - let response = plugin.call("get_time", Value::Null).await.unwrap(); +// let wasm_bytes = load_plugin_wasm(); +// let handler = Arc::new(get_host_server()); - info!("Get time response: {:?}", response); +// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); +// let response = plugin.call("get_time", Value::Null).await.unwrap(); - let timestamp = response.result.as_u64().unwrap(); - let now = SystemTime::now() - .duration_since(web_time::UNIX_EPOCH) - .unwrap() - .as_secs(); +// info!("Get time response: {:?}", response); - let diff = timestamp.abs_diff(now); - assert!(diff < 2, "Timestamp difference too large: {}", diff); -} +// let timestamp = response.result.as_u64().unwrap(); +// let now = SystemTime::now() +// .duration_since(web_time::UNIX_EPOCH) +// .unwrap() +// .as_secs(); + +// let diff = timestamp.abs_diff(now); +// assert!(diff < 2, "Timestamp difference too large: {}", diff); +// } +// TODO: Fix this for `wasm-pack test --headless --firefox ./crates/wasmi-plugin-hdk` #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] async fn test_sleep() { @@ -101,89 +116,64 @@ async fn test_sleep() { ); } -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_tokio_sleep() { - info!("Starting tokio sleep test..."); +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// #[cfg_attr(not(target_family = "wasm"), tokio::test)] +// async fn test_call() { +// info!("Starting call test..."); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); +// let wasm_bytes = load_plugin_wasm(); +// let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); +// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); +// let resp = plugin.call("call", Value::Null).await.unwrap(); - let sleep_duration = 1500; // milliseconds - let start = Instant::now(); - plugin - .call("tokio_sleep", Value::Number(sleep_duration.into())) - .await - .unwrap(); - let elapsed = start.elapsed().as_millis(); +// assert_eq!(resp.result.as_str().unwrap(), "pong"); +// } - assert!( - elapsed >= sleep_duration as u128, - "Sleep duration too short: {} ms", - elapsed - ); -} - -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_call() { - info!("Starting call test..."); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - let resp = plugin.call("call", Value::Null).await.unwrap(); - - assert_eq!(resp.result.as_str().unwrap(), "pong"); -} +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// #[cfg_attr(not(target_family = "wasm"), tokio::test)] +// async fn test_call_many() { +// info!("Starting call_many test..."); -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_call_many() { - info!("Starting call_many test..."); +// let wasm_bytes = load_plugin_wasm(); +// let handler = Arc::new(get_host_server()); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - plugin - .call("call_many", Value::Number(200.into())) - .await - .unwrap(); -} +// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); +// plugin +// .call("call_many", Value::Number(200.into())) +// .await +// .unwrap(); +// } -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_async_call() { - info!("Starting async_call test..."); +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// #[cfg_attr(not(target_family = "wasm"), tokio::test)] +// async fn test_async_call() { +// info!("Starting async_call test..."); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); +// let wasm_bytes = load_plugin_wasm(); +// let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - let resp = plugin.call("async_call", Value::Null).await.unwrap(); - assert_eq!(resp.result.as_str().unwrap(), "pong"); -} +// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); +// let resp = plugin.call("async_call", Value::Null).await.unwrap(); +// assert_eq!(resp.result.as_str().unwrap(), "pong"); +// } -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_prime_sieve() { - info!("Starting prime sieve test..."); +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// #[cfg_attr(not(target_family = "wasm"), tokio::test)] +// async fn test_prime_sieve() { +// info!("Starting prime sieve test..."); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); +// let wasm_bytes = load_plugin_wasm(); +// let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); +// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - let response = plugin - .call("prime_sieve", Value::Number(1000.into())) - .await - .unwrap(); +// let response = plugin +// .call("prime_sieve", Value::Number(1000.into())) +// .await +// .unwrap(); - info!("Prime sieve response: {:?}", response); - let count = response.result["count"].as_u64().unwrap(); - assert_eq!(count, 168); -} +// info!("Prime sieve response: {:?}", response); +// let count = response.result["count"].as_u64().unwrap(); +// assert_eq!(count, 168); +// } diff --git a/crates/wasmi-plugin-pdk/Cargo.toml b/crates/wasmi-plugin-pdk/Cargo.toml index 4472ed4..5ea9709 100644 --- a/crates/wasmi-plugin-pdk/Cargo.toml +++ b/crates/wasmi-plugin-pdk/Cargo.toml @@ -18,7 +18,6 @@ futures = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } -tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } wasmi-plugin-rt = { workspace = true } diff --git a/crates/wasmi-plugin-pdk/src/server.rs b/crates/wasmi-plugin-pdk/src/server.rs index ba4df42..d6ef861 100644 --- a/crates/wasmi-plugin-pdk/src/server.rs +++ b/crates/wasmi-plugin-pdk/src/server.rs @@ -2,7 +2,7 @@ use std::io::Write; use serde::{Serialize, de::DeserializeOwned}; use thiserror::Error; -use tokio::runtime::Builder; +use tracing::info; use crate::{ router::{MaybeSend, Router}, @@ -56,14 +56,6 @@ impl PluginServer { std::process::exit(1); } } - // let transport = self.transport.clone(); - - // let rt = Builder::new_current_thread().enable_time().build().unwrap(); - // let local = tokio::task::LocalSet::new(); - - // rt.block_on(local.run_until(async move { - // let _ = transport.process_next_line(Some(&self)).await; - // })); } fn try_run(&self) -> Result<(), PluginServerError> { @@ -78,22 +70,21 @@ impl PluginServer { return Err(PluginServerError::InvalidMessage(message)); }; - //? Dispatch handler - let rt = Builder::new_current_thread().enable_time().build()?; - let local = tokio::task::LocalSet::new(); + info!("PluginServer: Received request: {:?}", request); - let resp = rt.block_on(local.run_until(async move { - let transport = Transport::new(std::io::stdin(), std::io::stdout()); + let transport = Transport::new(std::io::stdin(), std::io::stdout()); + let resp = futures::executor::block_on(async move { self.router .handle_with_state(transport, &request.method, request.params) .await - })); + }); //? Send response let resp = match resp { Ok(result) => RpcMessage::response(request.id, result), Err(error) => RpcMessage::error_response(request.id, error), }; + info!("PluginServer: Sending response: {:?}", resp); let serialized = serde_json::to_string(&resp)?; let msg = format!("{}\n", serialized); diff --git a/crates/wasmi-plugin-pdk/src/transport.rs b/crates/wasmi-plugin-pdk/src/transport.rs index 54d580b..e2fd6d9 100644 --- a/crates/wasmi-plugin-pdk/src/transport.rs +++ b/crates/wasmi-plugin-pdk/src/transport.rs @@ -17,6 +17,12 @@ use crate::{ }; /// Single-threaded, concurrent-safe json-rpc transport layer +/// +/// TODO: Create async version that accepts async readers/writers and yields properly +/// For the client-side, considering setting up a centralized "reactor" to handle all reads. +/// Then we can have the transport send requests to the reactor and await responses via channels. +/// If the main executor notices that everything but the reactor is blocked, it can yield via +/// poll_oneoff to free up the web worker and not mindlessly spin. #[derive(Clone)] pub struct Transport { id: Arc, diff --git a/crates/wasmi-plugin-rt/Cargo.toml b/crates/wasmi-plugin-rt/Cargo.toml index 7b76dd4..da7175c 100644 --- a/crates/wasmi-plugin-rt/Cargo.toml +++ b/crates/wasmi-plugin-rt/Cargo.toml @@ -13,7 +13,6 @@ categories.workspace = true exclude.workspace = true [dependencies] -web-time = { workspace = true } [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] gloo-timers = { workspace = true } diff --git a/crates/wasmi-plugin-rt/src/lib.rs b/crates/wasmi-plugin-rt/src/lib.rs index fd15115..259ea2b 100644 --- a/crates/wasmi-plugin-rt/src/lib.rs +++ b/crates/wasmi-plugin-rt/src/lib.rs @@ -1,11 +1,7 @@ -use std::time::Duration; -pub use web_time; - pub fn yield_now() -> impl std::future::Future { #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] { - use gloo_timers::future::TimeoutFuture; - TimeoutFuture::new(0) + gloo_timers::future::TimeoutFuture::new(0) } #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] @@ -13,19 +9,3 @@ pub fn yield_now() -> impl std::future::Future { tokio::task::yield_now() } } - -pub fn now() -> web_time::Instant { - web_time::Instant::now() -} - -pub async fn sleep(dur: Duration) { - #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] - { - gloo_timers::future::TimeoutFuture::new(dur.as_millis() as u32).await; - } - - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] - { - tokio::time::sleep(dur).await; - } -} diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml new file mode 100644 index 0000000..0250a78 --- /dev/null +++ b/crates/worker/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "worker" +version.workspace = true +rust-version.workspace = true +description = "Lightweight web worker used to run WASIX" +authors.workspace = true +repository.workspace = true +edition.workspace = true +readme.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true +exclude.workspace = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +futures = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde-wasm-bindgen = { workspace = true } +thiserror = { workspace = true } +wasmi-plugin-hdk = { workspace = true } +wasmi-plugin-pdk = { workspace = true } +wasmi-plugin-rt = { workspace = true } +web-time = { workspace = true } +wasmer-types = { workspace = true } +wasmer = { workspace = true, features = ["js-default"] } +wasmer-wasix = { workspace = true, features = ["js-default"] } +web-sys = { workspace = true, features=["Worker", "DedicatedWorkerGlobalScope", "MessageEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } +wasm-bindgen = { workspace = true } +wasm-bindgen-futures = { workspace = true } diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs new file mode 100644 index 0000000..9b59311 --- /dev/null +++ b/crates/worker/src/lib.rs @@ -0,0 +1,114 @@ +use thiserror::Error; +use wasm_bindgen::{ + JsCast, JsValue, + prelude::{Closure, wasm_bindgen}, +}; +use wasmi_plugin_hdk::{ + WorkerMessage, + bridge::shared_pipe::SharedPipe, + wasi::wasi_ctx::{self, WasiCtx}, +}; +use web_sys::{ + DedicatedWorkerGlobalScope, MessageEvent, console, + js_sys::{self, SharedArrayBuffer}, +}; + +#[derive(Debug, Error)] +enum WorkerError { + #[error("Compile Error: {0}")] + CompileError(#[from] wasmer::CompileError), + #[error("WASI Error: {0}")] + WasiError(#[from] wasi_ctx::WasiError), + #[error("Runtime Error: {0}")] + RuntimeError(#[from] wasmer::RuntimeError), +} + +#[wasm_bindgen] +pub fn start_worker() { + log("Worker: Starting up..."); + let global = js_sys::global().unchecked_into::(); + + // Register onmessage with channels + let onmessage: Closure = Closure::wrap(Box::new(on_message)); + global.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + onmessage.forget(); + + // Send ready + let ready_msg = WorkerMessage::Ready; + let ready_msg = serde_wasm_bindgen::to_value(&ready_msg).unwrap(); + global.post_message(&ready_msg).unwrap(); +} + +fn on_message(e: MessageEvent) { + let Ok(msg) = serde_wasm_bindgen::from_value::(e.data()) else { + log("Worker: Failed to deserialize message from Host"); + return; + }; + + let WorkerMessage::Load { wasm } = msg else { + error("Worker: Received unexpected message type"); + return; + }; + + let stdin_buf = get_sab(&e.data(), "stdin"); + let stdout_buf = get_sab(&e.data(), "stdout"); + let stderr_buf = get_sab(&e.data(), "stderr"); + + let Some(stdin) = stdin_buf else { + error("Load message missing stdin SharedArrayBuffer"); + return; + }; + + let Some(stdout) = stdout_buf else { + error("Load message missing stdout SharedArrayBuffer"); + return; + }; + + let Some(stderr) = stderr_buf else { + error("Load message missing stderr SharedArrayBuffer"); + return; + }; + + if let Err(err) = run_instance(&wasm, stdin, stdout, stderr) { + error(&format!("Worker: Error running instance: {:?}", err)); + } else { + log("Worker: Instance finished execution"); + } +} + +fn get_sab(obj: &JsValue, key: &str) -> Option { + js_sys::Reflect::get(obj, &key.into()) + .ok() + .and_then(|v| v.dyn_into::().ok()) +} + +fn run_instance( + wasm_bytes: &[u8], + stdin: SharedArrayBuffer, + stdout: SharedArrayBuffer, + stderr: SharedArrayBuffer, +) -> Result<(), WorkerError> { + let stdin = SharedPipe::new(&stdin); + let stdout = SharedPipe::new(&stdout); + let stderr = SharedPipe::new(&stderr); + + let mut store = wasmer::Store::default(); + let module = wasmer::Module::new(&store, wasm_bytes)?; + + let wasi_ctx = WasiCtx::new() + .set_stdin(stdin) + .set_stdout(stdout) + .set_stderr(stderr); + let start = wasi_ctx.into_fn(&mut store, &module)?; + start.call(&mut store, &[])?; + + Ok(()) +} + +fn log(s: &str) { + console::log_1(&s.into()); +} + +fn error(s: &str) { + console::error_1(&s.into()); +} diff --git a/shell.nix b/shell.nix index add42aa..080288b 100644 --- a/shell.nix +++ b/shell.nix @@ -25,5 +25,7 @@ pkgs.mkShell { rustfmt binaryen rust-bin.stable.latest.rust-analyzer + wasm-pack + geckodriver ]; } From 467218832f174eecb6f5b1c0e244399414c100ab Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Tue, 23 Dec 2025 23:06:16 -0500 Subject: [PATCH 05/70] fix: Fix native & remove wasmer-wasix --- Cargo.lock | 1785 +---------------- Cargo.toml | 3 - crates/wasmi-plugin-hdk/Cargo.toml | 4 +- crates/wasmi-plugin-hdk/benches/benchmark.rs | 240 ++- crates/wasmi-plugin-hdk/src/bridge/mod.rs | 4 +- .../src/bridge/native_bridge.rs | 45 +- .../src/bridge/worker_bridge.rs | 5 +- crates/wasmi-plugin-hdk/src/compile.rs | 16 +- crates/wasmi-plugin-hdk/src/lib.rs | 5 +- crates/wasmi-plugin-hdk/src/plugin.rs | 13 +- crates/wasmi-plugin-hdk/tests/integration.rs | 212 +- crates/worker/Cargo.toml | 3 +- crates/worker/src/lib.rs | 5 +- 13 files changed, 340 insertions(+), 2000 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 990e216..bf25644 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,15 +47,6 @@ dependencies = [ "cc", ] -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anes" version = "0.1.6" @@ -68,36 +59,12 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" -[[package]] -name = "any_ascii" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e" - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - [[package]] name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - [[package]] name = "async-trait" version = "0.1.89" @@ -130,21 +97,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bindgen" version = "0.70.1" @@ -177,19 +129,6 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -[[package]] -name = "blake3" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -199,33 +138,12 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bstr" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" -[[package]] -name = "bus" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b7118d0221d84fada881b657c2ddb7cd55108db79c8764c9ee212c0c259b783" -dependencies = [ - "crossbeam-channel", - "num_cpus", - "parking_lot_core", -] - [[package]] name = "bytecheck" version = "0.6.12" @@ -282,18 +200,6 @@ name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" -dependencies = [ - "serde", -] - -[[package]] -name = "bytesize" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" -dependencies = [ - "serde", -] [[package]] name = "cast" @@ -317,7 +223,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom 7.1.3", + "nom", ] [[package]] @@ -326,19 +232,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - [[package]] name = "ciborium" version = "0.2.2" @@ -411,24 +304,6 @@ dependencies = [ "cc", ] -[[package]] -name = "console" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "windows-sys 0.59.0", -] - -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - [[package]] name = "convert_case" version = "0.10.0" @@ -438,18 +313,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "cooked-waker" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147be55d677052dabc6b22252d5dd0fd4c29c8c27aa4f2fbef0f94aa003b406f" - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "corosensei" version = "0.3.2" @@ -605,15 +468,6 @@ dependencies = [ "itertools 0.13.0", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -664,38 +518,14 @@ dependencies = [ "typenum", ] -[[package]] -name = "darling" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" -dependencies = [ - "darling_core 0.14.4", - "darling_macro 0.14.4", -] - [[package]] name = "darling" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - -[[package]] -name = "darling_core" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 1.0.109", + "darling_core", + "darling_macro", ] [[package]] @@ -711,24 +541,13 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "darling_macro" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" -dependencies = [ - "darling_core 0.14.4", - "quote", - "syn 1.0.109", -] - [[package]] name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core 0.21.3", + "darling_core", "quote", "syn 2.0.111", ] @@ -747,46 +566,6 @@ dependencies = [ "parking_lot_core", ] -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive_builder" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" -dependencies = [ - "darling 0.14.4", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive_builder_macro" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" -dependencies = [ - "derive_builder_core", - "syn 1.0.109", -] - [[package]] name = "derive_more" version = "0.99.20" @@ -847,50 +626,12 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "enum-iterator" version = "0.7.0" @@ -926,7 +667,7 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" dependencies = [ - "darling 0.21.3", + "darling", "proc-macro2", "quote", "syn 2.0.111", @@ -954,12 +695,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - [[package]] name = "filetime" version = "0.2.26" @@ -978,12 +713,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - [[package]] name = "flate2" version = "1.1.5" @@ -1000,15 +729,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - [[package]] name = "futures" version = "0.3.31" @@ -1158,19 +878,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "globset" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" -dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata", - "regex-syntax", -] - [[package]] name = "gloo-timers" version = "0.3.0" @@ -1194,15 +901,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - [[package]] name = "hashbrown" version = "0.13.2" @@ -1233,31 +931,6 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -[[package]] -name = "heapless" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" -dependencies = [ - "hash32", - "stable_deref_trait", -] - -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - [[package]] name = "hex" version = "0.4.3" @@ -1265,253 +938,66 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "http" -version = "1.4.0" +name = "ident_case" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] -name = "iana-time-zone" -version = "0.1.64" +name = "indexmap" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" +name = "itertools" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ - "cc", + "either", ] [[package]] -name = "icu_collections" -version = "2.1.1" +name = "itertools" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", + "either", ] [[package]] -name = "icu_locale_core" -version = "2.1.1" +name = "itoa" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] -name = "icu_normalizer" -version = "2.1.1" +name = "js-sys" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", + "once_cell", + "wasm-bindgen", ] [[package]] -name = "icu_normalizer_data" -version = "2.1.1" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "ignore" -version = "0.4.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" -dependencies = [ - "crossbeam-deque", - "globset", - "log", - "memchr", - "regex-automata", - "same-file", - "walkdir", - "winapi-util", -] - -[[package]] -name = "indexmap" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "insta" -version = "1.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b76866be74d68b1595eb8060cb9191dca9c021db2316558e52ddc5d55d41b66c" -dependencies = [ - "console", - "once_cell", - "regex", - "serde", - "similar", - "tempfile", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iprange" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37209be0ad225457e63814401415e748e2453a5297f9b637338f5fb8afa4ec00" -dependencies = [ - "ipnet", -] - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "js-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128" -version = "0.2.5" +name = "leb128" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" @@ -1521,15 +1007,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" -[[package]] -name = "lexical-sort" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c09e4591611e231daf4d4c685a66cb0410cc1e502027a20ae55f2bb9e997207a" -dependencies = [ - "any_ascii", -] - [[package]] name = "libc" version = "0.2.178" @@ -1569,45 +1046,12 @@ version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c6639b70a7ce854b79c70d7e83f16b5dc0137cc914f3d7d03803b513ecc67ac" -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "linked_hash_set" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" -dependencies = [ - "linked-hash-map", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - [[package]] name = "lock_api" version = "0.4.14" @@ -1623,15 +1067,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "lz4_flex" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" -dependencies = [ - "twox-hash 2.1.2", -] - [[package]] name = "mach2" version = "0.4.3" @@ -1652,12 +1087,6 @@ dependencies = [ "zerocopy-derive", ] -[[package]] -name = "managed" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" - [[package]] name = "matchers" version = "0.2.0" @@ -1749,16 +1178,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "nom" -version = "5.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" -dependencies = [ - "memchr", - "version_check", -] - [[package]] name = "nom" version = "7.1.3" @@ -1778,12 +1197,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-traits" version = "0.2.19" @@ -1794,38 +1207,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_enum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "object" version = "0.32.2" @@ -1900,48 +1281,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "path-clean" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset", - "indexmap", -] - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1982,21 +1321,6 @@ dependencies = [ "plotters-backend", ] -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2016,15 +1340,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", -] - [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -2096,17 +1411,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "pulldown-cmark" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" -dependencies = [ - "bitflags 1.3.2", - "memchr", - "unicase", -] - [[package]] name = "quote" version = "1.0.42" @@ -2131,35 +1435,14 @@ dependencies = [ "ptr_meta 0.3.1", ] -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "rand_chacha", + "rand_core", ] [[package]] @@ -2169,16 +1452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", + "rand_core", ] [[package]] @@ -2292,14 +1566,8 @@ dependencies = [ ] [[package]] -name = "replace_with" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" - -[[package]] -name = "rkyv" -version = "0.8.12" +name = "rkyv" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35a640b26f007713818e9a9b65d34da1cf58538207b052916a83d80e43f3ffa4" dependencies = [ @@ -2348,19 +1616,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - [[package]] name = "rustix" version = "1.1.2" @@ -2370,7 +1625,7 @@ dependencies = [ "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys", "windows-sys 0.61.2", ] @@ -2388,7 +1643,7 @@ checksum = "58c4eb8a81997cf040a091d1f7e1938aeab6749d3a0dfa73af43cdc32393483d" dependencies = [ "byteorder", "derive_more 0.99.20", - "twox-hash 1.6.3", + "twox-hash", ] [[package]] @@ -2397,16 +1652,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "saffron" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03fb9a628596fc7590eb7edbf7b0613287be78df107f5f97b118aad59fb2eea9" -dependencies = [ - "chrono", - "nom 5.1.3", -] - [[package]] name = "same-file" version = "1.0.6" @@ -2416,32 +1661,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "dyn-clone", - "indexmap", - "schemars_derive", - "serde", - "serde_json", - "url", -] - -[[package]] -name = "schemars_derive" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.111", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -2459,10 +1678,6 @@ name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -dependencies = [ - "serde", - "serde_core", -] [[package]] name = "serde" @@ -2515,17 +1730,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "serde_json" version = "1.0.145" @@ -2539,28 +1743,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "sha2" version = "0.10.9" @@ -2609,12 +1791,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" -[[package]] -name = "similar" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" - [[package]] name = "slab" version = "0.4.11" @@ -2633,17 +1809,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "smoltcp" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee34c1e1bfc7e9206cc0fb8030a90129b4e319ab53856249bb27642cab914fb3" -dependencies = [ - "bitflags 1.3.2", - "byteorder", - "managed", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2656,12 +1821,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "syn" version = "1.0.109" @@ -2684,17 +1843,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "tar" version = "0.4.44" @@ -2712,43 +1860,11 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" -[[package]] -name = "tempfile" -version = "3.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix 1.1.2", - "windows-sys 0.61.2", -] - -[[package]] -name = "terminal_size" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" -dependencies = [ - "rustix 0.38.44", - "windows-sys 0.48.0", -] - -[[package]] -name = "termios" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" -dependencies = [ - "libc", -] - [[package]] name = "test-plugin" version = "0.1.0" dependencies = [ - "rand 0.9.2", + "rand", "serde_json", "tracing", "tracing-subscriber", @@ -2810,47 +1926,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tinytemplate" version = "1.2.1" @@ -2882,7 +1957,6 @@ version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "bytes", "pin-project-lite", "tokio-macros", ] @@ -2898,103 +1972,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-util" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime 0.6.11", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_edit" -version = "0.23.10+spec-1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" -dependencies = [ - "indexmap", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - [[package]] name = "tracing" version = "0.1.43" @@ -3098,39 +2075,18 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "twox-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" - [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" -[[package]] -name = "unicase" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" - [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -3149,36 +2105,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "uuid" version = "1.19.0" @@ -3204,173 +2130,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "virtual-fs" -version = "0.601.0" +name = "walkdir" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8defc513ce70576bdbb4305ee67c0cba647af18c0995de8f1fa1271b724c9859" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ - "anyhow", - "async-trait", - "bytes", - "dashmap", - "derive_more 2.1.0", - "dunce", - "futures", - "getrandom 0.2.16", - "indexmap", - "pin-project-lite", - "replace_with", - "shared-buffer", - "slab", - "thiserror 1.0.69", - "tokio", - "tracing", - "virtual-mio", - "wasmer-package", - "web-time", - "webc", + "same-file", + "winapi-util", ] [[package]] -name = "virtual-mio" -version = "0.601.0" +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a14806a3f25b70315764760c376d9f68937f5467013891bc7497a33d24105097" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "async-trait", - "bytes", - "futures", - "serde", - "thiserror 1.0.69", - "tracing", + "wit-bindgen", ] [[package]] -name = "virtual-net" -version = "0.601.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7dccbe5d8d3a49e2b43a2c5a4287139e33ba04c03279bfc8a0aef82def2043" -dependencies = [ - "anyhow", - "async-trait", - "base64", - "bincode", - "bytecheck 0.6.12", - "bytes", - "derive_more 2.1.0", - "futures-util", - "ipnet", - "iprange", - "pin-project-lite", - "rkyv", - "serde", - "smoltcp", - "thiserror 1.0.69", - "tokio", - "tracing", - "virtual-mio", -] - -[[package]] -name = "wai-bindgen-gen-core" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa3dc41b510811122b3088197234c27e08fcad63ef936306dd8e11e2803876c" -dependencies = [ - "anyhow", - "wai-parser", -] - -[[package]] -name = "wai-bindgen-gen-rust" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc05e8380515c4337c40ef03b2ff233e391315b178a320de8640703d522efe" -dependencies = [ - "heck", - "wai-bindgen-gen-core", -] - -[[package]] -name = "wai-bindgen-gen-rust-wasm" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f35ce5e74086fac87f3a7bd50f643f00fe3559adb75c88521ecaa01c8a6199" -dependencies = [ - "heck", - "wai-bindgen-gen-core", - "wai-bindgen-gen-rust", -] - -[[package]] -name = "wai-bindgen-rust" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e5601c6f448c063e83a5e931b8fefcdf7e01ada424ad42372c948d2e3d67741" -dependencies = [ - "bitflags 1.3.2", - "wai-bindgen-rust-impl", -] - -[[package]] -name = "wai-bindgen-rust-impl" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdeeb5c1170246de8425a3e123e7ef260dc05ba2b522a1d369fe2315376efea4" -dependencies = [ - "proc-macro2", - "syn 1.0.109", - "wai-bindgen-gen-core", - "wai-bindgen-gen-rust-wasm", -] - -[[package]] -name = "wai-parser" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd0acb6d70885ea0c343749019ba74f015f64a9d30542e66db69b49b7e28186" -dependencies = [ - "anyhow", - "id-arena", - "pulldown-cmark", - "unicode-normalization", - "unicode-xid", -] - -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.106" +name = "wasm-bindgen" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ @@ -3458,16 +2244,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "wasm-encoder" -version = "0.235.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3bc393c395cb621367ff02d854179882b9a351b4e0c93d1397e6090b53a5c2a" -dependencies = [ - "leb128fmt", - "wasmparser 0.235.0", -] - [[package]] name = "wasm-encoder" version = "0.243.0" @@ -3563,29 +2339,6 @@ dependencies = [ "wasmer-types", ] -[[package]] -name = "wasmer-config" -version = "0.601.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6c7a72bfcba1000b7609e034e0d470575fae8215b597c2c6554661729d64c22" -dependencies = [ - "anyhow", - "bytesize", - "ciborium", - "derive_builder", - "hex", - "indexmap", - "saffron", - "schemars", - "semver", - "serde", - "serde_json", - "serde_yaml", - "thiserror 1.0.69", - "toml", - "url", -] - [[package]] name = "wasmer-derive" version = "6.1.0" @@ -3598,62 +2351,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "wasmer-journal" -version = "0.601.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6459cd00a0000e38dd4386bec1d99687813677bb01bfe69120f0f3eaedae0a06" -dependencies = [ - "anyhow", - "async-trait", - "base64", - "bincode", - "bytecheck 0.6.12", - "bytes", - "derive_more 2.1.0", - "lz4_flex", - "num_enum", - "rkyv", - "serde", - "serde_json", - "thiserror 1.0.69", - "tracing", - "virtual-fs", - "virtual-net", - "wasmer", - "wasmer-config", - "wasmer-wasix-types", -] - -[[package]] -name = "wasmer-package" -version = "0.601.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedc3a5cd721e8b12778d1858b9583aca8b91927a73f5fdf0c876cd5bbb6ac56" -dependencies = [ - "anyhow", - "bytes", - "cfg-if", - "ciborium", - "flate2", - "ignore", - "insta", - "libc", - "semver", - "serde", - "serde_json", - "sha2", - "shared-buffer", - "tar", - "tempfile", - "thiserror 1.0.69", - "toml", - "url", - "wasmer-config", - "wasmer-types", - "webc", -] - [[package]] name = "wasmer-types" version = "6.1.0" @@ -3673,7 +2370,6 @@ dependencies = [ "sha2", "target-lexicon", "thiserror 1.0.69", - "wasmparser 0.224.1", "xxhash-rust", ] @@ -3705,104 +2401,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "wasmer-wasix" -version = "0.601.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a804fcfbdbbfe5b2d6fb30e741e22a21e88e964dedfea9e609f13a52823bfe" -dependencies = [ - "anyhow", - "async-trait", - "base64", - "bincode", - "blake3", - "bus", - "bytecheck 0.6.12", - "bytes", - "cfg-if", - "chrono", - "cooked-waker", - "crossbeam-channel", - "dashmap", - "derive_more 2.1.0", - "futures", - "getrandom 0.2.16", - "heapless", - "hex", - "http", - "js-sys", - "libc", - "linked_hash_set", - "lz4_flex", - "num_enum", - "once_cell", - "petgraph", - "pin-project", - "pin-utils", - "rand 0.8.5", - "rkyv", - "semver", - "serde", - "serde_derive", - "serde_json", - "serde_yaml", - "sha2", - "shared-buffer", - "tempfile", - "terminal_size", - "termios", - "thiserror 1.0.69", - "tokio", - "tokio-stream", - "toml", - "tracing", - "url", - "urlencoding", - "virtual-fs", - "virtual-mio", - "virtual-net", - "waker-fn", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-encoder 0.235.0", - "wasmer", - "wasmer-config", - "wasmer-journal", - "wasmer-package", - "wasmer-types", - "wasmer-wasix-types", - "wasmparser 0.224.1", - "web-sys", - "webc", - "weezl", - "windows-sys 0.59.0", - "xxhash-rust", -] - -[[package]] -name = "wasmer-wasix-types" -version = "0.601.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d2b7dad6c826f48a972709e5950a80157cb3b6bb12c6f51778282e85dc17096" -dependencies = [ - "anyhow", - "bitflags 1.3.2", - "byteorder", - "cfg-if", - "num_enum", - "serde", - "time", - "tracing", - "wai-bindgen-gen-core", - "wai-bindgen-gen-rust", - "wai-bindgen-gen-rust-wasm", - "wai-bindgen-rust", - "wai-parser", - "wasmer", - "wasmer-derive", - "wasmer-types", -] - [[package]] name = "wasmi-plugin-hdk" version = "0.1.0" @@ -3827,7 +2425,6 @@ dependencies = [ "wasm-bindgen-test", "wasmer", "wasmer-types", - "wasmer-wasix", "wasmi-plugin-pdk", "wasmi-plugin-rt", "web-sys", @@ -3865,16 +2462,6 @@ dependencies = [ "bitflags 2.10.0", ] -[[package]] -name = "wasmparser" -version = "0.235.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "161296c618fa2d63f6ed5fffd1112937e803cb9ec71b32b01a76321555660917" -dependencies = [ - "bitflags 2.10.0", - "indexmap", -] - [[package]] name = "wasmparser" version = "0.243.0" @@ -3896,7 +2483,7 @@ dependencies = [ "leb128fmt", "memchr", "unicode-width", - "wasm-encoder 0.243.0", + "wasm-encoder", ] [[package]] @@ -3928,40 +2515,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webc" -version = "10.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637506ca3c48eae3301e97f0167aa1b46cdfdd479d008b868a19715182e48ae3" -dependencies = [ - "anyhow", - "base64", - "bytes", - "cfg-if", - "ciborium", - "document-features", - "ignore", - "indexmap", - "leb128", - "lexical-sort", - "libc", - "once_cell", - "path-clean", - "rand 0.8.5", - "serde", - "serde_json", - "sha2", - "shared-buffer", - "thiserror 1.0.69", - "url", -] - -[[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - [[package]] name = "winapi" version = "0.3.9" @@ -3993,74 +2546,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -4097,21 +2588,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -4145,12 +2621,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4163,12 +2633,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4181,12 +2645,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4211,12 +2669,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -4229,12 +2681,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4247,12 +2693,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -4265,12 +2705,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -4283,15 +2717,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - [[package]] name = "wit-bindgen" version = "0.46.0" @@ -4311,7 +2736,6 @@ dependencies = [ "wasm-bindgen-futures", "wasmer", "wasmer-types", - "wasmer-wasix", "wasmi-plugin-hdk", "wasmi-plugin-pdk", "wasmi-plugin-rt", @@ -4319,12 +2743,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - [[package]] name = "xattr" version = "1.6.1" @@ -4332,7 +2750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.2", + "rustix", ] [[package]] @@ -4341,29 +2759,6 @@ version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.8.31" @@ -4383,57 +2778,3 @@ dependencies = [ "quote", "syn 2.0.111", ] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] diff --git a/Cargo.toml b/Cargo.toml index a84c9a7..af05c59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,10 +39,7 @@ tracing-wasm = "0.2" uuid = { version = "1", features = ["v4", "serde", "js"] } wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" -# wasmi = { version = "1.0", default-features=false, features = ["std", "hash-collections", "prefer-btree-collections"] } -# wasmer-middlewares = "6" wasmer = { version = "6", default-features = false } -wasmer-wasix = { version = "0.601", default-features = false } wasmer-types = "6" web-sys = "0.3" wasmi-plugin-hdk = { version = "0.1.0", path = "./crates/wasmi-plugin-hdk" } diff --git a/crates/wasmi-plugin-hdk/Cargo.toml b/crates/wasmi-plugin-hdk/Cargo.toml index f3a4f8d..8a433c3 100644 --- a/crates/wasmi-plugin-hdk/Cargo.toml +++ b/crates/wasmi-plugin-hdk/Cargo.toml @@ -29,15 +29,13 @@ wasmer-types = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { workspace = true } -wasmer = { workspace = true, features = ["sys", "cranelift"] } -wasmer-wasix = { workspace = true } +wasmer = { workspace = true, default-features = true} [target.'cfg(target_arch = "wasm32")'.dependencies] gloo-timers = { workspace = true } tracing-wasm = { workspace = true } serde-wasm-bindgen = { workspace = true } wasmer = { workspace = true, features = ["js-default"] } -wasmer-wasix = { workspace = true, features = ["js-default"] } web-sys = { workspace = true, features=["Worker", "MessageEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } wasm-bindgen-futures = { workspace = true } wasmer-types = { workspace = true, features=["enable-serde"] } diff --git a/crates/wasmi-plugin-hdk/benches/benchmark.rs b/crates/wasmi-plugin-hdk/benches/benchmark.rs index f638608..1b0e481 100644 --- a/crates/wasmi-plugin-hdk/benches/benchmark.rs +++ b/crates/wasmi-plugin-hdk/benches/benchmark.rs @@ -1,126 +1,120 @@ #[cfg(not(target_arch = "wasm32"))] -mod bench { - use criterion::{Criterion, criterion_group, criterion_main}; - use serde_json::Value; - use std::sync::Arc; - use tokio::runtime::Builder; - use wasmi_plugin_hdk::{ - plugin::{Plugin, PluginId}, - server::HostServer, - }; - - const PLUGIN_WASM: &[u8] = - include_bytes!("../../../target/wasm32-wasip1/release/test-plugin.wasm"); - - fn load_plugin_wasm() -> Vec { - PLUGIN_WASM.to_vec() - } - - fn get_host_server() -> HostServer<(Option, ())> { - HostServer::default() - .with_method( - "ping", - |_, _params: ()| async move { Ok("pong".to_string()) }, - ) - .with_method("echo", |_, params: Value| async move { Ok(params) }) - } - - /// Benchmark a single ping request to the wasm module. - pub fn bench_ping(c: &mut Criterion) { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler.clone()).unwrap(); - - c.bench_function("ping", |b| { - b.iter(|| { - let fut = async { - plugin.call("ping", Value::Null).await.unwrap(); - }; - - rt.block_on(fut); - }) - }); - } - - /// Benchmark the prime sieve function with a small input. Primarily tests the overhead - /// of calling into the wasm module. - pub fn bench_prime_sieve_small(c: &mut Criterion) { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); - - c.bench_function("prime_sieve_small", |b| { - b.iter(|| { - let fut = async { - plugin - .call("prime_sieve", Value::Number(1.into())) - .await - .unwrap(); - }; - - rt.block_on(fut); - }) - }); - } - - /// Benchmark the prime sieve function with a large input. Tests both the overhead - /// of calling into the wasm module and performance within the wasm module. - pub fn bench_prime_sieve_large(c: &mut Criterion) { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); - - c.bench_function("prime_sieve_large", |b| { - b.iter(|| { - let fut = async { - plugin - .call("prime_sieve", Value::Number(100_0000.into())) - .await - .unwrap(); - }; - - rt.block_on(fut); - }) - }); - } - - /// Benchmark sending many echo requests to the host, and receiving responses. - pub fn bench_call_many(c: &mut Criterion) { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); - - c.bench_function("call_many", |b| { - b.iter(|| { - let fut = async { - plugin - .call("call_many", Value::Number(200.into())) - .await - .unwrap(); - }; - - rt.block_on(fut); - }) - }); - } - - criterion_group!( - benches, - bench_ping, - bench_prime_sieve_small, - bench_prime_sieve_large, - bench_call_many - ); - criterion_main!(benches); +use criterion::{Criterion, criterion_group, criterion_main}; +use serde_json::Value; +use std::sync::Arc; +use tokio::runtime::Builder; +use wasmi_plugin_hdk::{ + plugin::{Plugin, PluginId}, + server::HostServer, +}; + +const PLUGIN_WASM: &[u8] = include_bytes!("../../../target/wasm32-wasip1/release/test-plugin.wasm"); + +fn load_plugin_wasm() -> Vec { + PLUGIN_WASM.to_vec() } -#[cfg(target_arch = "wasm32")] -fn main() {} +fn get_host_server() -> HostServer<(Option, ())> { + HostServer::default() + .with_method( + "ping", + |_, _params: ()| async move { Ok("pong".to_string()) }, + ) + .with_method("echo", |_, params: Value| async move { Ok(params) }) +} + +/// Benchmark a single ping request to the wasm module. +pub fn bench_ping(c: &mut Criterion) { + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler.clone()).unwrap(); + + c.bench_function("ping", |b| { + b.iter(|| { + let fut = async { + plugin.call("ping", Value::Null).await.unwrap(); + }; + + rt.block_on(fut); + }) + }); +} + +/// Benchmark the prime sieve function with a small input. Primarily tests the overhead +/// of calling into the wasm module. +pub fn bench_prime_sieve_small(c: &mut Criterion) { + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); + + c.bench_function("prime_sieve_small", |b| { + b.iter(|| { + let fut = async { + plugin + .call("prime_sieve", Value::Number(1.into())) + .await + .unwrap(); + }; + + rt.block_on(fut); + }) + }); +} + +/// Benchmark the prime sieve function with a large input. Tests both the overhead +/// of calling into the wasm module and performance within the wasm module. +pub fn bench_prime_sieve_large(c: &mut Criterion) { + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); + + c.bench_function("prime_sieve_large", |b| { + b.iter(|| { + let fut = async { + plugin + .call("prime_sieve", Value::Number(100_0000.into())) + .await + .unwrap(); + }; + + rt.block_on(fut); + }) + }); +} + +/// Benchmark sending many echo requests to the host, and receiving responses. +pub fn bench_call_many(c: &mut Criterion) { + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); + + c.bench_function("call_many", |b| { + b.iter(|| { + let fut = async { + plugin + .call("call_many", Value::Number(200.into())) + .await + .unwrap(); + }; + + rt.block_on(fut); + }) + }); +} + +criterion_group!( + benches, + bench_ping, + bench_prime_sieve_small, + bench_prime_sieve_large, + bench_call_many +); +criterion_main!(benches); diff --git a/crates/wasmi-plugin-hdk/src/bridge/mod.rs b/crates/wasmi-plugin-hdk/src/bridge/mod.rs index 8891b88..6a1e4a4 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/mod.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/mod.rs @@ -2,6 +2,8 @@ mod native_bridge; #[cfg(not(target_arch = "wasm32"))] pub use native_bridge::NativeBridge; +#[cfg(not(target_arch = "wasm32"))] +mod non_blocking_pipe; #[cfg(target_arch = "wasm32")] mod worker_bridge; @@ -12,8 +14,6 @@ pub mod shared_pipe; #[cfg(target_arch = "wasm32")] pub mod worker_protocol; -mod non_blocking_pipe; - pub trait Bridge { fn terminate(self: Box); } diff --git a/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs b/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs index adf0ead..4b31261 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs @@ -1,18 +1,18 @@ //! Native wasmer bridge, running the Wasm module in a separate thread. use futures::AsyncBufReadExt; +use futures::io::BufReader; use std::io::{Read, Write}; use thiserror::Error; use tokio::spawn; use tokio::task::{JoinHandle, spawn_blocking}; -use tracing::info; -use wasmer::{Instance, Store}; +use tracing::{error, info}; +use wasmer::Store; use crate::bridge::Bridge; use crate::bridge::non_blocking_pipe::{ NonBlockingPipeReader, NonBlockingPipeWriter, non_blocking_pipe, }; -use crate::bridge::virtual_file::{WasiInputFile, WasiOutputFile}; use crate::compile::Compiled; use crate::wasi::wasi_ctx::WasiCtx; @@ -31,7 +31,8 @@ pub enum NativeBridgeError { impl NativeBridge { pub fn new( - compiled: Compiled, + compiled: &Compiled, + _wasm_bytes: &[u8], ) -> Result< ( Self, @@ -45,6 +46,7 @@ impl NativeBridge { let (stdout_reader, stdout_writer) = non_blocking_pipe(); let (stderr_reader, stderr_writer) = non_blocking_pipe(); + let compiled = compiled.clone(); let wasm_handle = spawn_blocking(move || { let result = Self::run_instance(compiled, stdin_reader, stdout_writer, stderr_writer); if let Err(e) = result { @@ -52,7 +54,29 @@ impl NativeBridge { } }); - // TODO: stderr task to log to console + let stderr_handle = spawn(async move { + let mut stderr = BufReader::new(stderr_reader); + let mut buffer = String::new(); + loop { + buffer.clear(); + match stderr.read_line(&mut buffer).await { + Ok(0) => { + break; + } + Ok(_) => { + info!("[plugin] [{}] {}", name, buffer.trim_end()); + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + tokio::task::yield_now().await; + continue; + } + Err(e) => { + error!("[WorkerBridge] Error reading from stderr: {}", e); + break; + } + } + } + }); let bridge = NativeBridge { wasm_handle, @@ -71,19 +95,14 @@ impl NativeBridge { let engine = compiled.engine; let module = compiled.module; - let store = Store::new(&engine); + let mut store = Store::new(engine); - let mut wasi_ctx = WasiCtx::new() + let wasi_ctx = WasiCtx::new() .set_stdin(stdin) .set_stdout(stdout) .set_stderr(stderr); - let imports = wasi_ctx.import_object(&store, &module); - let instance = Instance::new(&mut store, module, imports)?; - - let memory = instance.exports.get_memory("memory")?; - wasi_ctx.set_memory(memory); - let start = instance.exports.get_function("_start")?; + let start = wasi_ctx.into_fn(&mut store, &module)?; start.call(&mut store, &[])?; Ok(()) diff --git a/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs b/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs index baf2a21..95a8c84 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs @@ -15,6 +15,7 @@ use web_sys::wasm_bindgen::{JsCast, JsValue}; use crate::bridge::Bridge; use crate::bridge::shared_pipe::SharedPipe; use crate::bridge::worker_protocol::WorkerMessage; +use crate::compile::Compiled; pub struct WorkerBridge { worker: SendWorker, @@ -43,7 +44,7 @@ unsafe impl Sync for WorkerBridge {} impl WorkerBridge { pub fn new( - name: &str, + compiled: &Compiled, wasm_bytes: &[u8], ) -> Result< ( @@ -53,6 +54,7 @@ impl WorkerBridge { ), WorkerBridgeError, > { + let name = compiled.name.clone(); info!("Creating WorkerBridge for plugin: {}", &name); let (ready_tx, ready_rx) = futures::channel::oneshot::channel::<()>(); @@ -96,7 +98,6 @@ impl WorkerBridge { }); //? Log messages from stderr as plugin logs - let name = name.to_string(); spawn_local(async move { let mut stderr = BufReader::new(stderr); let mut buffer = String::new(); diff --git a/crates/wasmi-plugin-hdk/src/compile.rs b/crates/wasmi-plugin-hdk/src/compile.rs index de7096f..c17f57d 100644 --- a/crates/wasmi-plugin-hdk/src/compile.rs +++ b/crates/wasmi-plugin-hdk/src/compile.rs @@ -1,35 +1,29 @@ #[derive(Clone)] pub struct Compiled { pub name: String, - pub module_hash: wasmer_types::ModuleHash, #[cfg(not(target_arch = "wasm32"))] - pub engine: Engine, + pub engine: wasmer::Engine, #[cfg(not(target_arch = "wasm32"))] - pub module: Module, + pub module: wasmer::Module, } impl Compiled { #[cfg(not(target_arch = "wasm32"))] pub fn new(name: &str, wasm_bytes: &[u8]) -> Result { - let engine = Engine::default(); - let module = Module::new(&engine, wasm_bytes)?; - let module_hash = wasmer_types::ModuleHash::xxhash(wasm_bytes); + let engine = wasmer::Engine::default(); + let module = wasmer::Module::new(&engine, wasm_bytes)?; Ok(Compiled { name: name.to_string(), - module_hash, engine, module, }) } #[cfg(target_arch = "wasm32")] - pub fn new(name: &str, wasm_bytes: &[u8]) -> Result { - let module_hash = wasmer_types::ModuleHash::xxhash(wasm_bytes); - + pub fn new(name: &str, _wasm_bytes: &[u8]) -> Result { Ok(Compiled { name: name.to_string(), - module_hash, }) } } diff --git a/crates/wasmi-plugin-hdk/src/lib.rs b/crates/wasmi-plugin-hdk/src/lib.rs index 1374d2c..181a967 100644 --- a/crates/wasmi-plugin-hdk/src/lib.rs +++ b/crates/wasmi-plugin-hdk/src/lib.rs @@ -1,10 +1,7 @@ +pub mod bridge; mod compile; pub mod host_handler; pub mod plugin; -// mod plugin_instance; -pub mod bridge; pub mod server; mod time; pub mod wasi; - -pub use bridge::worker_protocol::WorkerMessage; diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index baae89d..aebbcd5 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -16,8 +16,6 @@ use wasmi_plugin_pdk::{ use crate::bridge::{self, Bridge}; use crate::compile::Compiled; -#[cfg(not(target_arch = "wasm32"))] -use crate::compile::Compiled; use crate::host_handler::HostHandler; use crate::time::sleep; @@ -61,9 +59,11 @@ pub struct Plugin { id: PluginId, handler: Arc, logger: Logger, - max_fuel: Option, compiled: Compiled, wasm_bytes: Vec, + #[allow(dead_code)] + // TODO: Use fuel to terminate native plugins + max_fuel: Option, } #[derive(Debug, Error)] @@ -176,7 +176,7 @@ impl Plugin { ), PluginError, > { - let (bridge, stdin, stdout) = bridge::NativeBridge::new(self.compiled.clone()) + let (bridge, stdin, stdout) = bridge::NativeBridge::new(&self.compiled, &self.wasm_bytes) .map_err(|_| PluginError::PluginDied)?; Ok((Box::new(bridge), Box::new(stdin), Box::new(stdout))) } @@ -193,9 +193,8 @@ impl Plugin { PluginError, > { // Web uses the bytes to send to the Worker - let (bridge, stdin, stdout) = - bridge::WorkerBridge::new(&self.compiled.name, &self.wasm_bytes) - .map_err(|_| PluginError::PluginDied)?; + let (bridge, stdin, stdout) = bridge::WorkerBridge::new(&self.compiled, &self.wasm_bytes) + .map_err(|_| PluginError::PluginDied)?; Ok((Box::new(bridge), Box::new(stdin), Box::new(stdout))) } } diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index 649184b..5f3f565 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -30,65 +30,65 @@ fn get_host_server() -> HostServer<()> { }) } -// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -// #[cfg_attr(not(target_family = "wasm"), tokio::test)] -// #[cfg_attr(not(target_family = "wasm"), tracing_test::traced_test)] -// async fn test_plugin() { -// #[cfg(target_family = "wasm")] -// { -// let config = WASMLayerConfigBuilder::new() -// .set_console_config(ConsoleConfig::ReportWithoutConsoleColor) -// .build(); -// tracing_wasm::set_as_global_default_with_config(config); -// } - -// info!("Starting test_plugin..."); - -// let wasm_bytes = load_plugin_wasm(); -// let handler = Arc::new(get_host_server()); - -// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); -// let resp = plugin.call("ping", Value::Null).await.unwrap(); -// assert_eq!(resp.result.as_str().unwrap(), "pong"); -// } - -// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -// #[cfg_attr(not(target_family = "wasm"), tokio::test)] -// async fn test_get_random_number() { -// info!("Starting get_random_number test..."); - -// let wasm_bytes = load_plugin_wasm(); -// let handler = Arc::new(get_host_server()); - -// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); -// let response = plugin.call("get_random_number", Value::Null).await.unwrap(); - -// info!("Random number response: {:?}", response); -// response.result.as_u64().unwrap(); -// } - -// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -// #[cfg_attr(not(target_family = "wasm"), tokio::test)] -// async fn test_get_time() { -// info!("Starting get_time test..."); - -// let wasm_bytes = load_plugin_wasm(); -// let handler = Arc::new(get_host_server()); - -// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); -// let response = plugin.call("get_time", Value::Null).await.unwrap(); - -// info!("Get time response: {:?}", response); - -// let timestamp = response.result.as_u64().unwrap(); -// let now = SystemTime::now() -// .duration_since(web_time::UNIX_EPOCH) -// .unwrap() -// .as_secs(); - -// let diff = timestamp.abs_diff(now); -// assert!(diff < 2, "Timestamp difference too large: {}", diff); -// } +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +#[cfg_attr(not(target_family = "wasm"), tracing_test::traced_test)] +async fn test_plugin() { + #[cfg(target_family = "wasm")] + { + let config = WASMLayerConfigBuilder::new() + .set_console_config(ConsoleConfig::ReportWithoutConsoleColor) + .build(); + tracing_wasm::set_as_global_default_with_config(config); + } + + info!("Starting test_plugin..."); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + + let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + let resp = plugin.call("ping", Value::Null).await.unwrap(); + assert_eq!(resp.result.as_str().unwrap(), "pong"); +} + +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_get_random_number() { + info!("Starting get_random_number test..."); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + + let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + let response = plugin.call("get_random_number", Value::Null).await.unwrap(); + + info!("Random number response: {:?}", response); + response.result.as_u64().unwrap(); +} + +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_get_time() { + info!("Starting get_time test..."); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + + let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + let response = plugin.call("get_time", Value::Null).await.unwrap(); + + info!("Get time response: {:?}", response); + + let timestamp = response.result.as_u64().unwrap(); + let now = SystemTime::now() + .duration_since(web_time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let diff = timestamp.abs_diff(now); + assert!(diff < 2, "Timestamp difference too large: {}", diff); +} // TODO: Fix this for `wasm-pack test --headless --firefox ./crates/wasmi-plugin-hdk` #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] @@ -116,64 +116,64 @@ async fn test_sleep() { ); } -// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -// #[cfg_attr(not(target_family = "wasm"), tokio::test)] -// async fn test_call() { -// info!("Starting call test..."); +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_call() { + info!("Starting call test..."); -// let wasm_bytes = load_plugin_wasm(); -// let handler = Arc::new(get_host_server()); + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); -// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); -// let resp = plugin.call("call", Value::Null).await.unwrap(); + let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + let resp = plugin.call("call", Value::Null).await.unwrap(); -// assert_eq!(resp.result.as_str().unwrap(), "pong"); -// } + assert_eq!(resp.result.as_str().unwrap(), "pong"); +} -// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -// #[cfg_attr(not(target_family = "wasm"), tokio::test)] -// async fn test_call_many() { -// info!("Starting call_many test..."); +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_call_many() { + info!("Starting call_many test..."); -// let wasm_bytes = load_plugin_wasm(); -// let handler = Arc::new(get_host_server()); + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); -// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); -// plugin -// .call("call_many", Value::Number(200.into())) -// .await -// .unwrap(); -// } + let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + plugin + .call("call_many", Value::Number(200.into())) + .await + .unwrap(); +} -// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -// #[cfg_attr(not(target_family = "wasm"), tokio::test)] -// async fn test_async_call() { -// info!("Starting async_call test..."); +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_async_call() { + info!("Starting async_call test..."); -// let wasm_bytes = load_plugin_wasm(); -// let handler = Arc::new(get_host_server()); + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); -// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); -// let resp = plugin.call("async_call", Value::Null).await.unwrap(); -// assert_eq!(resp.result.as_str().unwrap(), "pong"); -// } + let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + let resp = plugin.call("async_call", Value::Null).await.unwrap(); + assert_eq!(resp.result.as_str().unwrap(), "pong"); +} -// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -// #[cfg_attr(not(target_family = "wasm"), tokio::test)] -// async fn test_prime_sieve() { -// info!("Starting prime sieve test..."); +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_prime_sieve() { + info!("Starting prime sieve test..."); -// let wasm_bytes = load_plugin_wasm(); -// let handler = Arc::new(get_host_server()); + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); -// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); -// let response = plugin -// .call("prime_sieve", Value::Number(1000.into())) -// .await -// .unwrap(); + let response = plugin + .call("prime_sieve", Value::Number(1000.into())) + .await + .unwrap(); -// info!("Prime sieve response: {:?}", response); -// let count = response.result["count"].as_u64().unwrap(); -// assert_eq!(count, 168); -// } + info!("Prime sieve response: {:?}", response); + let count = response.result["count"].as_u64().unwrap(); + assert_eq!(count, 168); +} diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml index 0250a78..07f8577 100644 --- a/crates/worker/Cargo.toml +++ b/crates/worker/Cargo.toml @@ -15,7 +15,7 @@ exclude.workspace = true [lib] crate-type = ["cdylib"] -[dependencies] +[target.'cfg(target_arch = "wasm32")'.dependencies] futures = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -27,7 +27,6 @@ wasmi-plugin-rt = { workspace = true } web-time = { workspace = true } wasmer-types = { workspace = true } wasmer = { workspace = true, features = ["js-default"] } -wasmer-wasix = { workspace = true, features = ["js-default"] } web-sys = { workspace = true, features=["Worker", "DedicatedWorkerGlobalScope", "MessageEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index 9b59311..bb66f46 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -1,11 +1,12 @@ +#![cfg(target_arch = "wasm32")] + use thiserror::Error; use wasm_bindgen::{ JsCast, JsValue, prelude::{Closure, wasm_bindgen}, }; use wasmi_plugin_hdk::{ - WorkerMessage, - bridge::shared_pipe::SharedPipe, + bridge::{shared_pipe::SharedPipe, worker_protocol::WorkerMessage}, wasi::wasi_ctx::{self, WasiCtx}, }; use web_sys::{ From f596e7753d435b88e297b388533ee5f5b931e8c0 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Tue, 23 Dec 2025 23:43:59 -0500 Subject: [PATCH 06/70] fix: native benchmark --- Cargo.lock | 1 + crates/wasmi-plugin-hdk/Cargo.toml | 1 + crates/wasmi-plugin-hdk/benches/benchmark.rs | 6 ++++++ .../wasmi-plugin-hdk/src/bridge/native_bridge.rs | 3 +-- .../wasmi-plugin-hdk/src/bridge/worker_bridge.rs | 4 ++-- crates/wasmi-plugin-hdk/src/plugin.rs | 11 ++++++----- crates/wasmi-plugin-pdk/src/server.rs | 16 +++++++--------- crates/wasmi-plugin-pdk/src/transport.rs | 2 +- 8 files changed, 25 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf25644..e15a564 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2418,6 +2418,7 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tracing", + "tracing-subscriber", "tracing-test", "tracing-wasm", "uuid", diff --git a/crates/wasmi-plugin-hdk/Cargo.toml b/crates/wasmi-plugin-hdk/Cargo.toml index 8a433c3..fbb9e35 100644 --- a/crates/wasmi-plugin-hdk/Cargo.toml +++ b/crates/wasmi-plugin-hdk/Cargo.toml @@ -42,6 +42,7 @@ wasmer-types = { workspace = true, features=["enable-serde"] } [dev-dependencies] tracing-test = { version = "0.2.5", features = ["no-env-filter"] } +tracing-subscriber = { workspace = true } dhat = "0.3" wasm-bindgen-test = "0.3" diff --git a/crates/wasmi-plugin-hdk/benches/benchmark.rs b/crates/wasmi-plugin-hdk/benches/benchmark.rs index 1b0e481..c0b5658 100644 --- a/crates/wasmi-plugin-hdk/benches/benchmark.rs +++ b/crates/wasmi-plugin-hdk/benches/benchmark.rs @@ -3,6 +3,7 @@ use criterion::{Criterion, criterion_group, criterion_main}; use serde_json::Value; use std::sync::Arc; use tokio::runtime::Builder; +use tracing_subscriber::EnvFilter; use wasmi_plugin_hdk::{ plugin::{Plugin, PluginId}, server::HostServer, @@ -25,6 +26,11 @@ fn get_host_server() -> HostServer<(Option, ())> { /// Benchmark a single ping request to the wasm module. pub fn bench_ping(c: &mut Criterion) { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .with_test_writer() + .init(); + let rt = Builder::new_current_thread().enable_all().build().unwrap(); let wasm_bytes = load_plugin_wasm(); diff --git a/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs b/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs index 4b31261..10869fa 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs @@ -31,7 +31,7 @@ pub enum NativeBridgeError { impl NativeBridge { pub fn new( - compiled: &Compiled, + compiled: Compiled, _wasm_bytes: &[u8], ) -> Result< ( @@ -46,7 +46,6 @@ impl NativeBridge { let (stdout_reader, stdout_writer) = non_blocking_pipe(); let (stderr_reader, stderr_writer) = non_blocking_pipe(); - let compiled = compiled.clone(); let wasm_handle = spawn_blocking(move || { let result = Self::run_instance(compiled, stdin_reader, stdout_writer, stderr_writer); if let Err(e) = result { diff --git a/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs b/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs index 95a8c84..ddff9d0 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs @@ -44,7 +44,7 @@ unsafe impl Sync for WorkerBridge {} impl WorkerBridge { pub fn new( - compiled: &Compiled, + compiled: Compiled, wasm_bytes: &[u8], ) -> Result< ( @@ -54,7 +54,7 @@ impl WorkerBridge { ), WorkerBridgeError, > { - let name = compiled.name.clone(); + let name = compiled.name; info!("Creating WorkerBridge for plugin: {}", &name); let (ready_tx, ready_rx) = futures::channel::oneshot::channel::<()>(); diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index aebbcd5..318b716 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -176,8 +176,9 @@ impl Plugin { ), PluginError, > { - let (bridge, stdin, stdout) = bridge::NativeBridge::new(&self.compiled, &self.wasm_bytes) - .map_err(|_| PluginError::PluginDied)?; + let (bridge, stdin, stdout) = + bridge::NativeBridge::new(self.compiled.clone(), &self.wasm_bytes) + .map_err(|_| PluginError::PluginDied)?; Ok((Box::new(bridge), Box::new(stdin), Box::new(stdout))) } @@ -192,9 +193,9 @@ impl Plugin { ), PluginError, > { - // Web uses the bytes to send to the Worker - let (bridge, stdin, stdout) = bridge::WorkerBridge::new(&self.compiled, &self.wasm_bytes) - .map_err(|_| PluginError::PluginDied)?; + let (bridge, stdin, stdout) = + bridge::WorkerBridge::new(self.compiled.clone(), &self.wasm_bytes) + .map_err(|_| PluginError::PluginDied)?; Ok((Box::new(bridge), Box::new(stdin), Box::new(stdout))) } } diff --git a/crates/wasmi-plugin-pdk/src/server.rs b/crates/wasmi-plugin-pdk/src/server.rs index d6ef861..1e08d3d 100644 --- a/crates/wasmi-plugin-pdk/src/server.rs +++ b/crates/wasmi-plugin-pdk/src/server.rs @@ -29,6 +29,9 @@ enum PluginServerError { #[error("IO error: {0}")] Io(#[from] std::io::Error), + + #[error("RPC error: {0}")] + Rpc(#[from] RpcError), } impl PluginServer { @@ -60,19 +63,14 @@ impl PluginServer { fn try_run(&self) -> Result<(), PluginServerError> { //? Read request - let mut line = String::new(); - std::io::stdin() - .read_line(&mut line) - .map_err(|_| PluginServerError::MissingRequest)?; - - let message: RpcMessage = serde_json::from_str(&line)?; - let RpcMessage::RpcRequest(request) = message else { - return Err(PluginServerError::InvalidMessage(message)); + let transport = Transport::new(std::io::stdin(), std::io::stdout()); + let request = transport.read()?; + let RpcMessage::RpcRequest(request) = request else { + return Err(PluginServerError::InvalidMessage(request)); }; info!("PluginServer: Received request: {:?}", request); - let transport = Transport::new(std::io::stdin(), std::io::stdout()); let resp = futures::executor::block_on(async move { self.router .handle_with_state(transport, &request.method, request.params) diff --git a/crates/wasmi-plugin-pdk/src/transport.rs b/crates/wasmi-plugin-pdk/src/transport.rs index e2fd6d9..443bcd2 100644 --- a/crates/wasmi-plugin-pdk/src/transport.rs +++ b/crates/wasmi-plugin-pdk/src/transport.rs @@ -128,7 +128,7 @@ impl Transport { } /// Blocking read of a single message - fn read(&self) -> Result { + pub fn read(&self) -> Result { let mut line = String::new(); //? Loop until we successfully read a line or hit EOF, continuing on WouldBlock From e49a585c28c38f12280f04ea066d99d2652a5a89 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Wed, 24 Dec 2025 01:03:23 -0500 Subject: [PATCH 07/70] feat: wasm-pack benchmarks --- .gitignore | 2 +- Cargo.lock | 39 --- crates/wasmi-plugin-hdk/Cargo.toml | 11 +- crates/wasmi-plugin-hdk/benches/benchmark.rs | 288 ++++++++++-------- crates/wasmi-plugin-hdk/benches/wasm_bench.rs | 132 ++++++++ crates/wasmi-plugin-hdk/tests/integration.rs | 12 +- crates/wasmi-plugin-pdk/src/server.rs | 3 - flakes/wasm-bindgen-cli.nix | 19 ++ makefile | 30 ++ shell.nix | 1 + 10 files changed, 356 insertions(+), 181 deletions(-) create mode 100644 crates/wasmi-plugin-hdk/benches/wasm_bench.rs create mode 100644 flakes/wasm-bindgen-cli.nix create mode 100644 makefile diff --git a/.gitignore b/.gitignore index 4a39dac..461dba6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -/target +target/ .direnv .envrc \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e15a564..79367ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -600,22 +600,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "dhat" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827" -dependencies = [ - "backtrace", - "lazy_static", - "mintex", - "parking_lot", - "rustc-hash", - "serde", - "serde_json", - "thousands", -] - [[package]] name = "digest" version = "0.10.7" @@ -1146,12 +1130,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "mintex" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" - [[package]] name = "more-asserts" version = "0.2.2" @@ -1252,16 +1230,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - [[package]] name = "parking_lot_core" version = "0.9.12" @@ -1911,12 +1879,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "thousands" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" - [[package]] name = "thread_local" version = "1.1.9" @@ -2407,7 +2369,6 @@ version = "0.1.0" dependencies = [ "async-trait", "criterion", - "dhat", "futures", "getrandom 0.3.4", "gloo-timers", diff --git a/crates/wasmi-plugin-hdk/Cargo.toml b/crates/wasmi-plugin-hdk/Cargo.toml index fbb9e35..5d6c65c 100644 --- a/crates/wasmi-plugin-hdk/Cargo.toml +++ b/crates/wasmi-plugin-hdk/Cargo.toml @@ -41,15 +41,16 @@ wasm-bindgen-futures = { workspace = true } wasmer-types = { workspace = true, features=["enable-serde"] } [dev-dependencies] -tracing-test = { version = "0.2.5", features = ["no-env-filter"] } tracing-subscriber = { workspace = true } -dhat = "0.3" -wasm-bindgen-test = "0.3" -# Disables criterion for wasm targets since it's not compatible -[target.'cfg(not(target_family = "wasm"))'.dev-dependencies] +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +tracing-test = { version = "0.2.5", features = ["no-env-filter"] } criterion = "0.8.0" +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test = "0.3" +web-sys = { version = "0.3", features = ["Performance", "Window"] } + [[bench]] name = "benchmark" harness = false diff --git a/crates/wasmi-plugin-hdk/benches/benchmark.rs b/crates/wasmi-plugin-hdk/benches/benchmark.rs index c0b5658..3322b58 100644 --- a/crates/wasmi-plugin-hdk/benches/benchmark.rs +++ b/crates/wasmi-plugin-hdk/benches/benchmark.rs @@ -1,126 +1,170 @@ #[cfg(not(target_arch = "wasm32"))] -use criterion::{Criterion, criterion_group, criterion_main}; -use serde_json::Value; -use std::sync::Arc; -use tokio::runtime::Builder; -use tracing_subscriber::EnvFilter; -use wasmi_plugin_hdk::{ - plugin::{Plugin, PluginId}, - server::HostServer, -}; - -const PLUGIN_WASM: &[u8] = include_bytes!("../../../target/wasm32-wasip1/release/test-plugin.wasm"); - -fn load_plugin_wasm() -> Vec { - PLUGIN_WASM.to_vec() +mod benchmarks { + use criterion::{Criterion, criterion_group, criterion_main}; + use serde_json::Value; + use std::sync::{Arc, Once}; + use tokio::runtime::Builder; + use tracing_subscriber::EnvFilter; + use wasmi_plugin_hdk::{ + plugin::{Plugin, PluginId}, + server::HostServer, + }; + + const PLUGIN_WASM: &[u8] = + include_bytes!("../../../target/wasm32-wasip1/release/test-plugin.wasm"); + + static INIT: Once = Once::new(); + + fn load_plugin_wasm() -> Vec { + PLUGIN_WASM.to_vec() + } + + fn get_host_server() -> HostServer<(Option, ())> { + HostServer::default() + .with_method( + "ping", + |_, _params: ()| async move { Ok("pong".to_string()) }, + ) + .with_method("echo", |_, params: Value| async move { Ok(params) }) + } + + fn setup_logs() { + INIT.call_once(|| { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .with_test_writer() + .init(); + }); + } + + /// Benchmark a single ping request to the wasm module. + pub fn bench_ping(c: &mut Criterion) { + setup_logs(); + + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler.clone()).unwrap(); + + c.bench_function("ping", |b| { + b.iter(|| { + let fut = async { + plugin.call("ping", Value::Null).await.unwrap(); + }; + + rt.block_on(fut); + }) + }); + } + + /// Benchmark the full lifecycle of creating a plugin instance and calling a ping request. + /// Tests the overhead of plugin instantiation. + pub fn bench_lifecycle(c: &mut Criterion) { + setup_logs(); + + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + + c.bench_function("lifecycle", |b| { + b.iter(|| { + let plugin = + Plugin::new("test_plugin", wasm_bytes.clone(), handler.clone()).unwrap(); + let fut = async { + plugin.call("ping", Value::Null).await.unwrap(); + }; + + rt.block_on(fut); + }) + }); + } + + /// Benchmark the prime sieve function with a small input. Primarily tests the overhead + /// of calling into the wasm module. + pub fn bench_prime_sieve_small(c: &mut Criterion) { + setup_logs(); + + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); + + c.bench_function("prime_sieve_small", |b| { + b.iter(|| { + let fut = async { + plugin + .call("prime_sieve", Value::Number(1.into())) + .await + .unwrap(); + }; + + rt.block_on(fut); + }) + }); + } + + /// Benchmark the prime sieve function with a large input. Tests performance within the wasm module. + pub fn bench_prime_sieve_large(c: &mut Criterion) { + setup_logs(); + + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); + + c.bench_function("prime_sieve_large", |b| { + b.iter(|| { + let fut = async { + plugin + .call("prime_sieve", Value::Number(1_000_000.into())) + .await + .unwrap(); + }; + + rt.block_on(fut); + }) + }); + } + + /// Benchmark sending many echo requests to the host, and receiving responses. + pub fn bench_call_many(c: &mut Criterion) { + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); + + c.bench_function("call_many", |b| { + b.iter(|| { + let fut = async { + plugin + .call("call_many", Value::Number(200.into())) + .await + .unwrap(); + }; + + rt.block_on(fut); + }) + }); + } + + criterion_group!( + benches, + bench_ping, + bench_lifecycle, + bench_prime_sieve_small, + bench_prime_sieve_large, + bench_call_many + ); + criterion_main!(benches); } -fn get_host_server() -> HostServer<(Option, ())> { - HostServer::default() - .with_method( - "ping", - |_, _params: ()| async move { Ok("pong".to_string()) }, - ) - .with_method("echo", |_, params: Value| async move { Ok(params) }) -} - -/// Benchmark a single ping request to the wasm module. -pub fn bench_ping(c: &mut Criterion) { - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .with_test_writer() - .init(); - - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler.clone()).unwrap(); - - c.bench_function("ping", |b| { - b.iter(|| { - let fut = async { - plugin.call("ping", Value::Null).await.unwrap(); - }; - - rt.block_on(fut); - }) - }); -} - -/// Benchmark the prime sieve function with a small input. Primarily tests the overhead -/// of calling into the wasm module. -pub fn bench_prime_sieve_small(c: &mut Criterion) { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); - - c.bench_function("prime_sieve_small", |b| { - b.iter(|| { - let fut = async { - plugin - .call("prime_sieve", Value::Number(1.into())) - .await - .unwrap(); - }; - - rt.block_on(fut); - }) - }); -} - -/// Benchmark the prime sieve function with a large input. Tests both the overhead -/// of calling into the wasm module and performance within the wasm module. -pub fn bench_prime_sieve_large(c: &mut Criterion) { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); - - c.bench_function("prime_sieve_large", |b| { - b.iter(|| { - let fut = async { - plugin - .call("prime_sieve", Value::Number(100_0000.into())) - .await - .unwrap(); - }; - - rt.block_on(fut); - }) - }); -} - -/// Benchmark sending many echo requests to the host, and receiving responses. -pub fn bench_call_many(c: &mut Criterion) { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); - - c.bench_function("call_many", |b| { - b.iter(|| { - let fut = async { - plugin - .call("call_many", Value::Number(200.into())) - .await - .unwrap(); - }; - - rt.block_on(fut); - }) - }); -} +#[cfg(not(target_arch = "wasm32"))] +pub use benchmarks::*; -criterion_group!( - benches, - bench_ping, - bench_prime_sieve_small, - bench_prime_sieve_large, - bench_call_many -); -criterion_main!(benches); +#[cfg(target_arch = "wasm32")] +fn main() {} diff --git a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs new file mode 100644 index 0000000..18b65f2 --- /dev/null +++ b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs @@ -0,0 +1,132 @@ +use serde_json::Value; +use std::sync::Arc; +use wasm_bindgen_test::{Criterion, wasm_bindgen_bench}; +use wasmi_plugin_hdk::{ + plugin::{Plugin, PluginId}, + server::HostServer, +}; + +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +const PLUGIN_WASM: &[u8] = include_bytes!("../../../target/wasm32-wasip1/release/test-plugin.wasm"); + +fn load_plugin_wasm() -> Vec { + PLUGIN_WASM.to_vec() +} + +fn get_host_server() -> HostServer<(Option, ())> { + HostServer::default() + .with_method( + "ping", + |_, _params: ()| async move { Ok("pong".to_string()) }, + ) + .with_method("echo", |_, params: Value| async move { Ok(params) }) +} + +#[wasm_bindgen_bench] +async fn bench_ping_wasm(c: &mut Criterion) { + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Arc::new(Plugin::new("test_plugin", wasm_bytes, handler).unwrap()); + + c.bench_async_function("ping", |b| { + let plugin = plugin.clone(); + Box::pin(b.iter_future(move || { + let plugin = plugin.clone(); + async move { + plugin.call("ping", serde_json::Value::Null).await.unwrap(); + } + })) + }) + .await; +} + +/// Benchmark the full lifecycle of creating a plugin instance and calling a ping request. +/// Tests the overhead of plugin instantiation. +#[wasm_bindgen_bench] +async fn bench_lifecycle(c: &mut Criterion) { + let wasm_bytes = Arc::new(load_plugin_wasm()); + let handler = Arc::new(get_host_server()); + + c.bench_async_function("lifecycle", |b| { + let wasm_bytes = wasm_bytes.clone(); + let handler = handler.clone(); + Box::pin(b.iter_future(move || { + let wasm_bytes = wasm_bytes.clone(); + let handler = handler.clone(); + async move { + let plugin = + Plugin::new("test_plugin", wasm_bytes.as_ref().clone(), handler).unwrap(); + plugin.call("ping", serde_json::Value::Null).await.unwrap(); + } + })) + }) + .await; +} + +/// Benchmark the prime sieve function with a small input. Primarily tests the overhead +/// of calling into the wasm module. +#[wasm_bindgen_bench] +async fn bench_prime_sieve_small(c: &mut Criterion) { + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Arc::new(Plugin::new("test_plugin", wasm_bytes, handler).unwrap()); + + c.bench_async_function("prime_sieve_small", |b| { + let plugin = plugin.clone(); + Box::pin(b.iter_future(move || { + let plugin = plugin.clone(); + async move { + plugin + .call("prime_sieve", Value::Number(1.into())) + .await + .unwrap(); + } + })) + }) + .await; +} + +/// Benchmark the prime sieve function with a large input. Tests performance within the wasm module. +#[wasm_bindgen_bench] +async fn bench_prime_sieve_large(c: &mut Criterion) { + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Arc::new(Plugin::new("test_plugin", wasm_bytes, handler).unwrap()); + + c.bench_async_function("prime_sieve_large", |b| { + let plugin = plugin.clone(); + Box::pin(b.iter_future(move || { + let plugin = plugin.clone(); + async move { + plugin + .call("prime_sieve", Value::Number(1_000_000.into())) + .await + .unwrap(); + } + })) + }) + .await; +} + +/// Benchmark sending many echo requests to the host, and receiving responses. +#[wasm_bindgen_bench] +async fn bench_call_many(c: &mut Criterion) { + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Arc::new(Plugin::new("test_plugin", wasm_bytes, handler).unwrap()); + + c.bench_async_function("call_many", |b| { + let plugin = plugin.clone(); + Box::pin(b.iter_future(move || { + let plugin = plugin.clone(); + async move { + plugin + .call("call_many", Value::Number(200.into())) + .await + .unwrap(); + } + })) + }) + .await; +} diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index 5f3f565..724bdf8 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -1,8 +1,6 @@ use serde_json::Value; use std::sync::Arc; use tracing::info; -#[cfg(target_family = "wasm")] -use tracing_wasm::{ConsoleConfig, WASMLayerConfigBuilder}; use wasmi_plugin_hdk::{plugin::Plugin, server::HostServer}; use web_time::{Instant, SystemTime}; @@ -34,14 +32,6 @@ fn get_host_server() -> HostServer<()> { #[cfg_attr(not(target_family = "wasm"), tokio::test)] #[cfg_attr(not(target_family = "wasm"), tracing_test::traced_test)] async fn test_plugin() { - #[cfg(target_family = "wasm")] - { - let config = WASMLayerConfigBuilder::new() - .set_console_config(ConsoleConfig::ReportWithoutConsoleColor) - .build(); - tracing_wasm::set_as_global_default_with_config(config); - } - info!("Starting test_plugin..."); let wasm_bytes = load_plugin_wasm(); @@ -91,7 +81,7 @@ async fn test_get_time() { } // TODO: Fix this for `wasm-pack test --headless --firefox ./crates/wasmi-plugin-hdk` -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] async fn test_sleep() { info!("Starting sleep test..."); diff --git a/crates/wasmi-plugin-pdk/src/server.rs b/crates/wasmi-plugin-pdk/src/server.rs index 1e08d3d..31a77c0 100644 --- a/crates/wasmi-plugin-pdk/src/server.rs +++ b/crates/wasmi-plugin-pdk/src/server.rs @@ -18,9 +18,6 @@ pub struct PluginServer { #[derive(Debug, Error)] enum PluginServerError { - #[error("Missing Request")] - MissingRequest, - #[error("Invalid Message: {0:?}")] InvalidMessage(RpcMessage), diff --git a/flakes/wasm-bindgen-cli.nix b/flakes/wasm-bindgen-cli.nix new file mode 100644 index 0000000..dac40d2 --- /dev/null +++ b/flakes/wasm-bindgen-cli.nix @@ -0,0 +1,19 @@ +{ + buildWasmBindgenCli, + fetchCrate, + rustPlatform, +}: + +buildWasmBindgenCli rec { + src = fetchCrate { + pname = "wasm-bindgen-cli"; + version = "0.2.106"; + hash = "sha256-M6WuGl7EruNopHZbqBpucu4RWz44/MSdv6f0zkYw+44="; + }; + + cargoDeps = rustPlatform.fetchCargoVendor { + inherit src; + inherit (src) pname version; + hash = "sha256-ElDatyOwdKwHg3bNH/1pcxKI7LXkhsotlDPQjiLHBwA="; + }; +} diff --git a/makefile b/makefile new file mode 100644 index 0000000..3649d38 --- /dev/null +++ b/makefile @@ -0,0 +1,30 @@ +# === Configuration === +HDK_PATH := crates/wasmi-plugin-hdk +WORKER_PATH := crates/worker +PLUGIN_NAME := test-plugin +BROWSER := --firefox + +.PHONY: help build-plugin build-worker build-all bench-wasm + +help: + @echo "Usage:" + @echo " make bench-wasm - Build all and run Wasm benchmarks" + @echo " make build-all - Build plugin and worker WASM" + +build-plugin: + @echo "--- Building Plugin WASM ---" + cargo build -p $(PLUGIN_NAME) --target wasm32-wasip1 --release + +build-worker: + @echo "--- Building Worker Assets ---" + cd $(WORKER_PATH) && wasm-pack build --target no-modules --out-dir ./pkg --release + + +build-all: build-plugin build-worker + +bench-wasm: build-all + @echo "--- Running Wasm Benchmarks ---" + @cd $(HDK_PATH) && \ + export WASM_BINDGEN_TEST_TIMEOUT=300 && \ + CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner && \ + cargo bench -p wasmi-plugin-hdk --target wasm32-unknown-unknown \ No newline at end of file diff --git a/shell.nix b/shell.nix index 080288b..dfe1ccf 100644 --- a/shell.nix +++ b/shell.nix @@ -27,5 +27,6 @@ pkgs.mkShell { rust-bin.stable.latest.rust-analyzer wasm-pack geckodriver + wasm-bindgen-cli_0_2_106 ]; } From 4db136060e64b75afca4ce390107084990a5d098 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Wed, 24 Dec 2025 01:21:08 -0500 Subject: [PATCH 08/70] feat: update ci --- .github/workflows/ci.yml | 73 +++++-------------- crates/wasmi-plugin-hdk/benches/wasm_bench.rs | 1 + crates/wasmi-plugin-hdk/src/plugin.rs | 8 +- crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs | 3 +- crates/wasmi-plugin-pdk/src/server.rs | 1 + crates/wasmi-plugin-pdk/src/transport.rs | 8 +- makefile | 19 ++++- shell.nix | 5 ++ 8 files changed, 52 insertions(+), 66 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 157645b..583a17e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,72 +14,33 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v14 + - name: Magic Nix Cache + uses: DeterminateSystems/magic-nix-cache-action@v8 - - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-wasip1 - - - uses: Swatinem/rust-cache@v2 - - - name: Build plugin - run: cargo build --target wasm32-wasip1 -p test-plugin --release - - - name: Optimize Wasm - uses: NiklasEi/wasm-opt-action@v2 - with: - file: target/wasm32-wasip1/release/*.wasm - optimize_all: true - options: -O3 - - - run: cargo build - - run: cargo test - - run: cargo bench + - run: nix-shell --run "make test-native" + - run: nix-shell --run "make bench-native" test-wasm: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v14 + - name: Magic Nix Cache + uses: DeterminateSystems/magic-nix-cache-action@v8 - - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-wasip1, wasm32-unknown-unknown - - - uses: Swatinem/rust-cache@v2 - - - name: Build plugin - run: cargo build --target wasm32-wasip1 -p test-plugin --release - - - name: Optimize Wasm - uses: NiklasEi/wasm-opt-action@v2 - with: - file: target/wasm32-wasip1/release/*.wasm - optimize_all: true - options: -O3 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - uses: jetli/wasm-bindgen-action@v0.2.0 - - - run: cargo build - - run: cargo test - - run: cargo bench + - run: nix-shell --run "make test-wasm" + - run: nix-shell --run "make bench-wasm" clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v14 + - name: Magic Nix Cache + uses: DeterminateSystems/magic-nix-cache-action@v8 - - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-wasip1 - - - uses: Swatinem/rust-cache@v2 - - - name: Build plugin - run: cargo build --target wasm32-wasip1 -p test-plugin --release - - - name: Run Clippy - run: cargo clippy --workspace --all-targets -- -D warnings \ No newline at end of file + - run: nix-shell --run "make clippy" \ No newline at end of file diff --git a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs index 18b65f2..bc23731 100644 --- a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs +++ b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs @@ -1,3 +1,4 @@ +#![cfg(target_arch = "wasm32")] use serde_json::Value; use std::sync::Arc; use wasm_bindgen_test::{Criterion, wasm_bindgen_bench}; diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index 318b716..ad0fd4e 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -156,15 +156,16 @@ impl Plugin { futures::select! { res = transport_task => { bridge.terminate(); - return Ok(res?); + Ok(res?) }, _ = timeout => { bridge.terminate(); - return Err(PluginError::PluginDied); + Err(PluginError::PluginDied) } - }; + } } + #[allow(clippy::type_complexity)] #[cfg(not(target_arch = "wasm32"))] fn create_bridge( &self, @@ -182,6 +183,7 @@ impl Plugin { Ok((Box::new(bridge), Box::new(stdin), Box::new(stdout))) } + #[allow(clippy::type_complexity)] #[cfg(target_arch = "wasm32")] fn create_bridge( &self, diff --git a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs index e40cb7e..4c8f8a9 100644 --- a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs +++ b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs @@ -90,6 +90,7 @@ impl WasiCtx { self } + #[allow(clippy::result_large_err)] pub fn into_fn( self, mut store: &mut wasmer::Store, @@ -115,7 +116,7 @@ impl WasiCtx { } }; - let instance = wasmer::Instance::new(&mut store, &module, &imports)?; + let instance = wasmer::Instance::new(&mut store, module, &imports)?; let memory = instance.exports.get_memory("memory")?; ctx.as_mut(&mut store).set_memory(memory.clone()); diff --git a/crates/wasmi-plugin-pdk/src/server.rs b/crates/wasmi-plugin-pdk/src/server.rs index 31a77c0..cb07249 100644 --- a/crates/wasmi-plugin-pdk/src/server.rs +++ b/crates/wasmi-plugin-pdk/src/server.rs @@ -31,6 +31,7 @@ enum PluginServerError { Rpc(#[from] RpcError), } +#[allow(clippy::new_without_default)] impl PluginServer { pub fn new() -> Self { let router = Router::new(); diff --git a/crates/wasmi-plugin-pdk/src/transport.rs b/crates/wasmi-plugin-pdk/src/transport.rs index 443bcd2..fd0510c 100644 --- a/crates/wasmi-plugin-pdk/src/transport.rs +++ b/crates/wasmi-plugin-pdk/src/transport.rs @@ -142,7 +142,7 @@ impl Transport { continue; } Err(e) => { - return Err(RpcError::custom(&format!( + return Err(RpcError::custom(format!( "Failed to read from transport: {}", e ))); @@ -150,7 +150,7 @@ impl Transport { } } - Ok(serde_json::from_str(&line).map_err(to_rpc_err)?) + serde_json::from_str(&line).map_err(to_rpc_err) } /// Inserts a response message into the pending map. If the message is a request, @@ -259,7 +259,7 @@ impl Transport { continue; } Err(e) => { - return Err(RpcError::custom(&format!( + return Err(RpcError::custom(format!( "Failed to read from transport: {}", e ))); @@ -267,6 +267,6 @@ impl Transport { } } - Ok(serde_json::from_str(&line).map_err(to_rpc_err)?) + serde_json::from_str(&line).map_err(to_rpc_err) } } diff --git a/makefile b/makefile index 3649d38..9d01f35 100644 --- a/makefile +++ b/makefile @@ -22,9 +22,24 @@ build-worker: build-all: build-plugin build-worker +clippy: build-all + @echo "--- Running Clippy Lints ---" + cargo clippy -- -D warnings + +test-native: build-plugin + @echo "--- Running Plugin Tests ---" + cargo test + +bench-native: build-plugin + @echo "--- Running Plugin Benchmarks ---" + cargo bench + +test-wasm: build-all + @echo "--- Running Wasm Tests ---" + @cd $(HDK_PATH) && \ + cargo test -p wasmi-plugin-hdk --target wasm32-unknown-unknown + bench-wasm: build-all @echo "--- Running Wasm Benchmarks ---" @cd $(HDK_PATH) && \ - export WASM_BINDGEN_TEST_TIMEOUT=300 && \ - CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner && \ cargo bench -p wasmi-plugin-hdk --target wasm32-unknown-unknown \ No newline at end of file diff --git a/shell.nix b/shell.nix index dfe1ccf..e975b5c 100644 --- a/shell.nix +++ b/shell.nix @@ -29,4 +29,9 @@ pkgs.mkShell { geckodriver wasm-bindgen-cli_0_2_106 ]; + + shellHook = '' + export CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner + export WASM_BINDGEN_TEST_TIMEOUT=300 + ''; } From 75ac57a4567550867481bb831ce7b7950ffd9a40 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Wed, 24 Dec 2025 01:50:05 -0500 Subject: [PATCH 09/70] fix: native benchmark --- crates/wasmi-plugin-hdk/benches/benchmark.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/wasmi-plugin-hdk/benches/benchmark.rs b/crates/wasmi-plugin-hdk/benches/benchmark.rs index 3322b58..80b1cd2 100644 --- a/crates/wasmi-plugin-hdk/benches/benchmark.rs +++ b/crates/wasmi-plugin-hdk/benches/benchmark.rs @@ -1,6 +1,6 @@ #[cfg(not(target_arch = "wasm32"))] mod benchmarks { - use criterion::{Criterion, criterion_group, criterion_main}; + use criterion::{Criterion, criterion_group}; use serde_json::Value; use std::sync::{Arc, Once}; use tokio::runtime::Builder; @@ -160,11 +160,10 @@ mod benchmarks { bench_prime_sieve_large, bench_call_many ); - criterion_main!(benches); } #[cfg(not(target_arch = "wasm32"))] -pub use benchmarks::*; +criterion::criterion_main!(benchmarks::benches); #[cfg(target_arch = "wasm32")] fn main() {} From d9a4dff59bd771936e0135d05c93f45b9ab2c797 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Wed, 24 Dec 2025 01:50:28 -0500 Subject: [PATCH 10/70] add dummy assets to CI --- .github/workflows/ci.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 583a17e..712706e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,12 @@ jobs: - name: Magic Nix Cache uses: DeterminateSystems/magic-nix-cache-action@v8 + - name: Create dummy assets to satisfy compiler + run: | + mkdir -p crates/worker/pkg + touch crates/worker/pkg/worker.js + touch crates/worker/pkg/worker_bg.wasm + - run: nix-shell --run "make test-wasm" - run: nix-shell --run "make bench-wasm" @@ -43,4 +49,10 @@ jobs: - name: Magic Nix Cache uses: DeterminateSystems/magic-nix-cache-action@v8 + - name: Create dummy assets to satisfy compiler + run: | + mkdir -p crates/worker/pkg + touch crates/worker/pkg/worker.js + touch crates/worker/pkg/worker_bg.wasm + - run: nix-shell --run "make clippy" \ No newline at end of file From 98595ba35f84f253c525fa1bce57e28bf6e6ce65 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Wed, 24 Dec 2025 14:58:32 -0500 Subject: [PATCH 11/70] feat: make separate fully-async host transport --- Cargo.lock | 1 + crates/wasmi-plugin-hdk/Cargo.toml | 1 + crates/wasmi-plugin-hdk/src/bridge/mod.rs | 2 +- .../src/bridge/native_bridge.rs | 9 +- .../src/bridge/non_blocking_pipe.rs | 28 +++- .../src/bridge/shared_pipe.rs | 149 ++++++++++++++++-- .../src/bridge/worker_bridge.rs | 9 +- crates/wasmi-plugin-hdk/src/lib.rs | 1 + crates/wasmi-plugin-hdk/src/plugin.rs | 48 ++---- .../wasmi-plugin-hdk/src/plugin_instance.rs | 60 ------- crates/wasmi-plugin-hdk/src/transport.rs | 130 +++++++++++++++ crates/wasmi-plugin-hdk/tests/integration.rs | 25 ++- crates/wasmi-plugin-pdk/src/rpc_message.rs | 9 ++ 13 files changed, 352 insertions(+), 120 deletions(-) delete mode 100644 crates/wasmi-plugin-hdk/src/plugin_instance.rs create mode 100644 crates/wasmi-plugin-hdk/src/transport.rs diff --git a/Cargo.lock b/Cargo.lock index 79367ac..2802d3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2383,6 +2383,7 @@ dependencies = [ "tracing-test", "tracing-wasm", "uuid", + "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", "wasmer", diff --git a/crates/wasmi-plugin-hdk/Cargo.toml b/crates/wasmi-plugin-hdk/Cargo.toml index 5d6c65c..5b02c6c 100644 --- a/crates/wasmi-plugin-hdk/Cargo.toml +++ b/crates/wasmi-plugin-hdk/Cargo.toml @@ -37,6 +37,7 @@ tracing-wasm = { workspace = true } serde-wasm-bindgen = { workspace = true } wasmer = { workspace = true, features = ["js-default"] } web-sys = { workspace = true, features=["Worker", "MessageEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } +wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } wasmer-types = { workspace = true, features=["enable-serde"] } diff --git a/crates/wasmi-plugin-hdk/src/bridge/mod.rs b/crates/wasmi-plugin-hdk/src/bridge/mod.rs index 6a1e4a4..04ce5d7 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/mod.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/mod.rs @@ -15,5 +15,5 @@ pub mod shared_pipe; pub mod worker_protocol; pub trait Bridge { - fn terminate(self: Box); + fn terminate(self); } diff --git a/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs b/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs index 10869fa..6fd0960 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs @@ -1,8 +1,7 @@ //! Native wasmer bridge, running the Wasm module in a separate thread. -use futures::AsyncBufReadExt; use futures::io::BufReader; -use std::io::{Read, Write}; +use futures::{AsyncBufReadExt, AsyncRead, AsyncWrite}; use thiserror::Error; use tokio::spawn; use tokio::task::{JoinHandle, spawn_blocking}; @@ -36,8 +35,8 @@ impl NativeBridge { ) -> Result< ( Self, - impl Write + Send + Sync + 'static, - impl Read + Send + Sync + 'static, + impl AsyncWrite + Send + Sync + 'static, + impl AsyncRead + Send + Sync + 'static, ), NativeBridgeError, > { @@ -109,7 +108,7 @@ impl NativeBridge { } impl Bridge for NativeBridge { - fn terminate(self: Box) { + fn terminate(self) { self.stderr_handle.abort(); self.wasm_handle.abort(); diff --git a/crates/wasmi-plugin-hdk/src/bridge/non_blocking_pipe.rs b/crates/wasmi-plugin-hdk/src/bridge/non_blocking_pipe.rs index 21023e9..54d684e 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/non_blocking_pipe.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/non_blocking_pipe.rs @@ -10,7 +10,7 @@ use std::pin::Pin; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll, Waker}; -use futures::AsyncRead; +use futures::{AsyncRead, AsyncWrite}; use tracing::info; struct Inner { @@ -116,6 +116,32 @@ impl Write for NonBlockingPipeWriter { } } +impl AsyncWrite for NonBlockingPipeWriter { + fn poll_write( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let mut inner = self.inner.lock().unwrap(); + if inner.closed { + return Poll::Ready(Err(io::ErrorKind::BrokenPipe.into())); + } + inner.buf.extend(buf); + if let Some(waker) = inner.waker.take() { + waker.wake(); + } + Poll::Ready(Ok(buf.len())) + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + impl Drop for NonBlockingPipeWriter { fn drop(&mut self) { let mut inner = self.inner.lock().unwrap(); diff --git a/crates/wasmi-plugin-hdk/src/bridge/shared_pipe.rs b/crates/wasmi-plugin-hdk/src/bridge/shared_pipe.rs index ed58e11..239aa1e 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/shared_pipe.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/shared_pipe.rs @@ -1,10 +1,16 @@ //! Shared pipe implementation for worker bridge. -use std::io::{Read, Write}; +use std::{ + io::{Read, Write}, + pin::Pin, + task::{Context, Poll}, +}; +use futures::{AsyncRead, AsyncWrite}; +use wasm_bindgen_futures::JsFuture; use web_sys::{ - js_sys::{self, Int32Array, SharedArrayBuffer, Uint8Array}, - wasm_bindgen::JsCast, + js_sys::{self, Atomics, Int32Array, SharedArrayBuffer, Uint8Array}, + wasm_bindgen::{JsCast, prelude::wasm_bindgen}, }; // 4xi32 header: (16 bytes) @@ -18,6 +24,21 @@ pub struct SharedPipe { header: Int32Array, data: Uint8Array, capacity: u32, + + pending_read: Option, + pending_write: Option, +} + +#[wasm_bindgen] +extern "C" { + /// The object returned by Atomics.waitAsync + pub type WaitAsyncResult; + + #[wasm_bindgen(method, getter, js_name = async)] + pub fn is_async(this: &WaitAsyncResult) -> bool; + + #[wasm_bindgen(method, getter)] + pub fn value(this: &WaitAsyncResult) -> wasm_bindgen::JsValue; } unsafe impl Send for SharedPipe {} @@ -34,6 +55,8 @@ impl SharedPipe { header: status, data, capacity, + pending_read: None, + pending_write: None, } } @@ -43,32 +66,32 @@ impl SharedPipe { } fn is_closed(&self) -> bool { - js_sys::Atomics::load(&self.header, CLOSED_IDX).unwrap() == 1 + Atomics::load(&self.header, CLOSED_IDX).unwrap() == 1 } pub fn close(&self) { - js_sys::Atomics::store(&self.header, CLOSED_IDX, 1).unwrap(); + Atomics::store(&self.header, CLOSED_IDX, 1).unwrap(); // Notify both pointers to wake up anyone waiting on either end - js_sys::Atomics::notify(&self.header, READ_IDX).unwrap(); - js_sys::Atomics::notify(&self.header, WRITE_IDX).unwrap(); + Atomics::notify(&self.header, READ_IDX).unwrap(); + Atomics::notify(&self.header, WRITE_IDX).unwrap(); } fn read_idx(&self) -> u32 { - js_sys::Atomics::load(&self.header, READ_IDX).unwrap() as u32 + Atomics::load(&self.header, READ_IDX).unwrap() as u32 } fn set_read(&self, value: u32) { - js_sys::Atomics::store(&self.header, READ_IDX, value as i32).unwrap(); - js_sys::Atomics::notify(&self.header, READ_IDX).unwrap(); + Atomics::store(&self.header, READ_IDX, value as i32).unwrap(); + Atomics::notify(&self.header, READ_IDX).unwrap(); } fn write_idx(&self) -> u32 { - js_sys::Atomics::load(&self.header, WRITE_IDX).unwrap() as u32 + Atomics::load(&self.header, WRITE_IDX).unwrap() as u32 } fn set_write(&self, value: u32) { - js_sys::Atomics::store(&self.header, WRITE_IDX, value as i32).unwrap(); - js_sys::Atomics::notify(&self.header, WRITE_IDX).unwrap(); + Atomics::store(&self.header, WRITE_IDX, value as i32).unwrap(); + Atomics::notify(&self.header, WRITE_IDX).unwrap(); } pub fn buffer(&self) -> SharedArrayBuffer { @@ -82,6 +105,8 @@ impl Read for SharedPipe { let w = self.write_idx(); if r == w { + //? Only check if closed when empty, so we can drain remaining data + // after close if self.is_closed() { return Ok(0); } @@ -156,6 +181,104 @@ impl Write for SharedPipe { } } +impl AsyncRead for SharedPipe { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + //? Check for existing promise + if let Some(mut fut) = self.pending_read.take() { + if Pin::new(&mut fut).poll(cx).is_pending() { + self.pending_read = Some(fut); + return Poll::Pending; + } + } + + //? Try sync read + match self.read(buf) { + Ok(n) => Poll::Ready(Ok(n)), + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + //? Setup async wait for WRITE_IDX to change + let current_w = self.write_idx() as i32; + let res: WaitAsyncResult = Atomics::wait_async(&self.header, WRITE_IDX, current_w) + .expect("Atomics.waitAsync should be supported") + .unchecked_into(); + + if !res.is_async() { + // res was "not-equal", meaning another thread changed WRITE_IDX + // already. So wake the caller. + cx.waker().wake_by_ref(); + return Poll::Pending; + } + + let promise: js_sys::Promise = res.value().unchecked_into(); + let mut fut = JsFuture::from(promise); + + if Pin::new(&mut fut).poll(cx).is_pending() { + self.pending_read = Some(fut); + return Poll::Pending; + } + + // Future resolved, meaning WRITE_IDX changed, try read again + cx.waker().wake_by_ref(); + return Poll::Pending; + } + Err(e) => Poll::Ready(Err(e)), + } + } +} + +impl AsyncWrite for SharedPipe { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + if let Some(mut fut) = self.pending_write.take() { + if Pin::new(&mut fut).poll(cx).is_pending() { + self.pending_write = Some(fut); + return Poll::Pending; + } + } + + match self.write(buf) { + Ok(n) => Poll::Ready(Ok(n)), + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // Writer waits for READ_IDX to change (meaning space freed up) + let current_r = self.read_idx() as i32; + let res: WaitAsyncResult = Atomics::wait_async(&self.header, READ_IDX, current_r) + .expect("waitAsync support required") + .unchecked_into(); + + if !res.is_async() { + cx.waker().wake_by_ref(); + return Poll::Pending; + } + + let mut fut = JsFuture::from(res.value().unchecked_into::()); + if Pin::new(&mut fut).poll(cx).is_pending() { + self.pending_write = Some(fut); + return Poll::Pending; + } + + cx.waker().wake_by_ref(); + Poll::Pending + } + Err(e) => Poll::Ready(Err(e)), + } + } + + fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { + self.close(); + Poll::Ready(Ok(())) + } +} + #[cfg(test)] mod tests { diff --git a/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs b/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs index ddff9d0..ba7152f 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs @@ -1,8 +1,9 @@ //! Web Worker wasmer bridge, running the Wasm module in a Web Worker. -use std::io::{BufRead, BufReader, Read, Write}; +use std::io::{BufRead, BufReader}; use futures::channel::oneshot::{Receiver, Sender}; +use futures::{AsyncRead, AsyncWrite}; use thiserror::Error; use tracing::{error, info}; use wasm_bindgen_futures::spawn_local; @@ -49,8 +50,8 @@ impl WorkerBridge { ) -> Result< ( Self, - impl Write + Send + Sync + 'static, - impl Read + Send + Sync + 'static, + impl AsyncWrite + Send + Sync + 'static, + impl AsyncRead + Send + Sync + 'static, ), WorkerBridgeError, > { @@ -245,7 +246,7 @@ async fn load( } impl Bridge for WorkerBridge { - fn terminate(self: Box) { + fn terminate(self) { self.worker.terminate(); } } diff --git a/crates/wasmi-plugin-hdk/src/lib.rs b/crates/wasmi-plugin-hdk/src/lib.rs index 181a967..90a5fe5 100644 --- a/crates/wasmi-plugin-hdk/src/lib.rs +++ b/crates/wasmi-plugin-hdk/src/lib.rs @@ -4,4 +4,5 @@ pub mod host_handler; pub mod plugin; pub mod server; mod time; +mod transport; pub mod wasi; diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index ad0fd4e..12ca4ba 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -1,7 +1,7 @@ -use std::io::{Read, Write}; use std::{fmt::Display, sync::Arc, time::Duration}; use futures::FutureExt; +use futures::{AsyncRead, AsyncWrite}; use serde::{Deserialize, Serialize}; use serde_json::Value; use thiserror::Error; @@ -11,13 +11,13 @@ use wasmi_plugin_pdk::{ api::RequestHandler, router::BoxFuture, rpc_message::{RpcError, RpcResponse}, - transport::Transport, }; use crate::bridge::{self, Bridge}; use crate::compile::Compiled; use crate::host_handler::HostHandler; use crate::time::sleep; +use crate::transport::{Transport, TransportError}; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct PluginId(Uuid); @@ -70,8 +70,8 @@ pub struct Plugin { pub enum PluginError { // #[error("spawn error")] // SpawnError(#[from] SpawnError), - #[error("transport error")] - RpcError(#[from] RpcError), + #[error("Transport error")] + TransportError(#[from] TransportError), #[error("plugin died")] PluginDied, } @@ -117,38 +117,18 @@ impl Plugin { } } -impl PluginError { - pub fn as_rpc_code(&self) -> RpcError { - match self { - PluginError::RpcError(code) => code.clone(), - _ => RpcError::Custom(self.to_string()), - } - } -} - -impl From for RpcError { - fn from(err: PluginError) -> Self { - match err { - PluginError::RpcError(code) => code, - _ => RpcError::Custom(err.to_string()), - } - } -} - impl Plugin { pub async fn call(&self, method: &str, params: Value) -> Result { //? Construct URL from worker bytes let (bridge, stdin_writer, stdout_reader) = self.create_bridge().unwrap(); - let transport = Transport::new(stdout_reader, stdin_writer); let handler = PluginCallback { handler: self.handler.clone(), uuid: self.id, }; + let transport = Transport::new(stdout_reader, stdin_writer); - let transport_task = transport - .async_call_with_handler(method, params, &handler) - .fuse(); + let transport_task = transport.call(method, params, Some(handler)).fuse(); let timeout = sleep(Duration::from_secs(60)).fuse(); futures::pin_mut!(transport_task, timeout); @@ -171,16 +151,16 @@ impl Plugin { &self, ) -> Result< ( - Box, - Box, - Box, + impl Bridge + Send + Sync + 'static, + impl AsyncWrite + Send + Sync + 'static, + impl AsyncRead + Send + Sync + 'static, ), PluginError, > { let (bridge, stdin, stdout) = bridge::NativeBridge::new(self.compiled.clone(), &self.wasm_bytes) .map_err(|_| PluginError::PluginDied)?; - Ok((Box::new(bridge), Box::new(stdin), Box::new(stdout))) + Ok((bridge, stdin, stdout)) } #[allow(clippy::type_complexity)] @@ -189,16 +169,16 @@ impl Plugin { &self, ) -> Result< ( - Box, - Box, - Box, + impl Bridge + Send + Sync + 'static, + impl AsyncWrite + Send + Sync + 'static, + impl AsyncRead + Send + Sync + 'static, ), PluginError, > { let (bridge, stdin, stdout) = bridge::WorkerBridge::new(self.compiled.clone(), &self.wasm_bytes) .map_err(|_| PluginError::PluginDied)?; - Ok((Box::new(bridge), Box::new(stdin), Box::new(stdout))) + Ok((bridge, stdin, stdout)) } } diff --git a/crates/wasmi-plugin-hdk/src/plugin_instance.rs b/crates/wasmi-plugin-hdk/src/plugin_instance.rs deleted file mode 100644 index 6b1bba1..0000000 --- a/crates/wasmi-plugin-hdk/src/plugin_instance.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::sync::{Arc, atomic::AtomicBool}; - -use crate::wasi::{ - non_blocking_pipe::{NonBlockingPipeReader, NonBlockingPipeWriter, non_blocking_pipe}, - wasi_ctx::{WasiCtx, add_to_linker}, - wasmi::spawn_wasm, -}; -use thiserror::Error; -use wasmi::{Engine, Linker, Module, Store}; - -#[derive(Error, Debug)] -pub enum SpawnError { - #[error("io error")] - IoError(#[from] std::io::Error), - #[error("start not found")] - StartNotFound, - #[error("wasmi error")] - WasmiError(#[from] wasmi::Error), -} - -/// Creates the plugin pipes and task. -pub fn spawn_plugin( - engine: &Engine, - module: &Module, - max_fuel: Option, -) -> Result< - ( - NonBlockingPipeWriter, - NonBlockingPipeReader, - NonBlockingPipeReader, - Arc, - impl Future, - ), - SpawnError, -> { - let is_running = Arc::new(AtomicBool::new(true)); - - // Setup pipes - let (stdin_reader, stdin_writer) = non_blocking_pipe(); - let (stdout_reader, stdout_writer) = non_blocking_pipe(); - let (stderr_reader, stderr_writer) = non_blocking_pipe(); - - let mut linker = Linker::new(engine); - let wasi = WasiCtx::new() - .set_stdin(stdin_reader) - .set_stdout(stdout_writer) - .set_stderr(stderr_writer); - - let mut store = Store::new(engine, wasi); - add_to_linker(&mut linker)?; - - let instance = linker.instantiate_and_start(&mut store, module)?; - let start_func = instance - .get_func(&store, "_start") - .ok_or(SpawnError::StartNotFound)?; - - let fut = spawn_wasm(store, start_func, is_running.clone(), max_fuel); - - Ok((stdin_writer, stdout_reader, stderr_reader, is_running, fut)) -} diff --git a/crates/wasmi-plugin-hdk/src/transport.rs b/crates/wasmi-plugin-hdk/src/transport.rs new file mode 100644 index 0000000..71c50dd --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/transport.rs @@ -0,0 +1,130 @@ +use std::collections::HashMap; + +use futures::{ + AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, channel::oneshot, io::BufReader, +}; +use serde_json::Value; +use thiserror::Error; +use tracing::info; +use wasmi_plugin_pdk::{ + api::RequestHandler, + rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcRequest, RpcResponse}, +}; + +pub struct Transport { + reader: BufReader, + writer: W, + pending: HashMap>>, + next_id: u64, +} + +#[derive(Debug, Error)] +pub enum TransportError { + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + #[error("Serde error: {0}")] + Serde(#[from] serde_json::Error), + + #[error("EOF")] + EOF, + + #[error("Error Response: {0:?}")] + ErrorResponse(RpcErrorResponse), + + #[error("Cancelled")] + Cancelled(#[from] oneshot::Canceled), +} + +impl Transport { + pub fn new(reader: R, writer: W) -> Self { + Transport { + reader: BufReader::new(reader), + writer, + pending: HashMap::new(), + next_id: 0, + } + } + + pub async fn call( + mut self, + method: &str, + params: Value, + handler: Option>, + ) -> Result { + let id = self.next_id(); + let request = RpcMessage::request(id, method.to_string(), params); + self.write_message(request).await?; + + let (resp_tx, mut resp_rx) = oneshot::channel(); + self.pending.insert(id, resp_tx); + + let mut line = String::new(); + loop { + match self.reader.read_line(&mut line).await { + Ok(0) => return Err(TransportError::EOF), + Ok(_) => {} + Err(e) => return Err(TransportError::Io(e)), + } + + let msg: RpcMessage = serde_json::from_str(&line)?; + line.clear(); + self.handle_incoming(msg, &handler).await?; + + if let Some(res) = resp_rx.try_recv()? { + let resp = res.map_err(TransportError::ErrorResponse)?; + return Ok(resp); + } + } + } + + async fn write_message(&mut self, msg: RpcMessage) -> Result<(), TransportError> { + let msg: String = serde_json::to_string(&msg)?; + let msg = format!("{}\n", msg); + self.writer.write_all(msg.as_bytes()).await?; + self.writer.flush().await?; + + Ok(()) + } + + async fn handle_incoming( + &mut self, + msg: RpcMessage, + handler: &Option>, + ) -> Result<(), TransportError> { + match msg { + RpcMessage::RpcResponse(resp) => { + self.pending.remove(&resp.id).map(|tx| tx.send(Ok(resp))); + } + RpcMessage::RpcErrorResponse(err) => { + self.pending.remove(&err.id).map(|tx| tx.send(Err(err))); + } + RpcMessage::RpcRequest(RpcRequest { + id, method, params, .. + }) => { + let Some(handler) = handler else { + info!("No handler, request ignored"); + return Ok(()); + }; + + match handler.handle(&method, params).await { + Ok(result) => { + self.write_message(RpcMessage::response(id, result)).await?; + } + Err(error) => { + self.write_message(RpcMessage::error_response(id, error)) + .await?; + } + } + } + } + + Ok(()) + } + + fn next_id(&mut self) -> u64 { + let id = self.next_id; + self.next_id += 1; + id + } +} diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index 724bdf8..8e7cab5 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -1,5 +1,5 @@ use serde_json::Value; -use std::sync::Arc; +use std::sync::{Arc, Once}; use tracing::info; use wasmi_plugin_hdk::{plugin::Plugin, server::HostServer}; use web_time::{Instant, SystemTime}; @@ -12,6 +12,8 @@ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); const PLUGIN_WASM: &[u8] = include_bytes!("../../../target/wasm32-wasip1/release/test-plugin.wasm"); +static INIT: Once = Once::new(); + fn load_plugin_wasm() -> Vec { PLUGIN_WASM.to_vec() } @@ -28,10 +30,22 @@ fn get_host_server() -> HostServer<()> { }) } +fn setup_logs() { + #[cfg(not(target_family = "wasm"))] + { + INIT.call_once(|| { + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .with_test_writer() + .init(); + }); + } +} + #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] -#[cfg_attr(not(target_family = "wasm"), tracing_test::traced_test)] async fn test_plugin() { + setup_logs(); info!("Starting test_plugin..."); let wasm_bytes = load_plugin_wasm(); @@ -45,6 +59,7 @@ async fn test_plugin() { #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] async fn test_get_random_number() { + setup_logs(); info!("Starting get_random_number test..."); let wasm_bytes = load_plugin_wasm(); @@ -60,6 +75,7 @@ async fn test_get_random_number() { #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] async fn test_get_time() { + setup_logs(); info!("Starting get_time test..."); let wasm_bytes = load_plugin_wasm(); @@ -84,6 +100,7 @@ async fn test_get_time() { // #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] async fn test_sleep() { + setup_logs(); info!("Starting sleep test..."); let wasm_bytes = load_plugin_wasm(); @@ -109,6 +126,7 @@ async fn test_sleep() { #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] async fn test_call() { + setup_logs(); info!("Starting call test..."); let wasm_bytes = load_plugin_wasm(); @@ -123,6 +141,7 @@ async fn test_call() { #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] async fn test_call_many() { + setup_logs(); info!("Starting call_many test..."); let wasm_bytes = load_plugin_wasm(); @@ -138,6 +157,7 @@ async fn test_call_many() { #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] async fn test_async_call() { + setup_logs(); info!("Starting async_call test..."); let wasm_bytes = load_plugin_wasm(); @@ -151,6 +171,7 @@ async fn test_async_call() { #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] async fn test_prime_sieve() { + setup_logs(); info!("Starting prime sieve test..."); let wasm_bytes = load_plugin_wasm(); diff --git a/crates/wasmi-plugin-pdk/src/rpc_message.rs b/crates/wasmi-plugin-pdk/src/rpc_message.rs index 0b625b5..9028d9b 100644 --- a/crates/wasmi-plugin-pdk/src/rpc_message.rs +++ b/crates/wasmi-plugin-pdk/src/rpc_message.rs @@ -49,6 +49,15 @@ pub enum RpcError { } impl RpcMessage { + pub fn request(id: u64, method: String, params: Value) -> Self { + RpcMessage::RpcRequest(RpcRequest { + jsonrpc: "2.0".to_string(), + id, + method, + params, + }) + } + pub fn response(id: u64, result: Value) -> Self { RpcMessage::RpcResponse(RpcResponse { jsonrpc: "2.0".to_string(), From fb6b8b28e38e34ae03c0f3e4819bd5b732bbe0bc Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Wed, 24 Dec 2025 16:43:06 -0500 Subject: [PATCH 12/70] feat: sync-capable reactor transport for wasi guests --- crates/test-plugin/src/main.rs | 6 +- crates/wasmi-plugin-hdk/tests/integration.rs | 6 +- crates/wasmi-plugin-pdk/src/lib.rs | 1 + crates/wasmi-plugin-pdk/src/server.rs | 35 ++- crates/wasmi-plugin-pdk/src/transport.rs | 273 ++---------------- .../wasmi-plugin-pdk/src/transport_driver.rs | 243 ++++++++++++++++ 6 files changed, 302 insertions(+), 262 deletions(-) create mode 100644 crates/wasmi-plugin-pdk/src/transport_driver.rs diff --git a/crates/test-plugin/src/main.rs b/crates/test-plugin/src/main.rs index 9f116f5..a757d32 100644 --- a/crates/test-plugin/src/main.rs +++ b/crates/test-plugin/src/main.rs @@ -53,8 +53,8 @@ async fn call_many(transport: Transport, limit: u64) -> Result<(), RpcError> { Ok(()) } -async fn async_call(transport: Transport, _: ()) -> Result { - let resp = transport.async_call("ping", Value::Null).await?; +async fn call_async(transport: Transport, _: ()) -> Result { + let resp = transport.call_async("ping", Value::Null).await?; Ok(resp.result) } @@ -105,7 +105,7 @@ fn main() { .with_method("sleep", sleep) .with_method("call", call) .with_method("call_many", call_many) - .with_method("async_call", async_call) + .with_method("call_async", call_async) .with_method("prime_sieve", prime_sieve) .run(); } diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index 8e7cab5..b8d4f62 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -156,15 +156,15 @@ async fn test_call_many() { #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_async_call() { +async fn test_call_async() { setup_logs(); - info!("Starting async_call test..."); + info!("Starting call_async test..."); let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - let resp = plugin.call("async_call", Value::Null).await.unwrap(); + let resp = plugin.call("call_async", Value::Null).await.unwrap(); assert_eq!(resp.result.as_str().unwrap(), "pong"); } diff --git a/crates/wasmi-plugin-pdk/src/lib.rs b/crates/wasmi-plugin-pdk/src/lib.rs index d9b98cc..d0afbc3 100644 --- a/crates/wasmi-plugin-pdk/src/lib.rs +++ b/crates/wasmi-plugin-pdk/src/lib.rs @@ -3,3 +3,4 @@ pub mod router; pub mod rpc_message; pub mod server; pub mod transport; +mod transport_driver; diff --git a/crates/wasmi-plugin-pdk/src/server.rs b/crates/wasmi-plugin-pdk/src/server.rs index cb07249..7ba5c90 100644 --- a/crates/wasmi-plugin-pdk/src/server.rs +++ b/crates/wasmi-plugin-pdk/src/server.rs @@ -1,12 +1,13 @@ -use std::io::Write; +use std::io::{BufRead, BufReader, Write}; +use futures::{FutureExt, select}; use serde::{Serialize, de::DeserializeOwned}; use thiserror::Error; use tracing::info; use crate::{ router::{MaybeSend, Router}, - rpc_message::{RpcError, RpcMessage}, + rpc_message::{RpcError, RpcMessage, RpcRequest}, transport::Transport, }; @@ -61,18 +62,19 @@ impl PluginServer { fn try_run(&self) -> Result<(), PluginServerError> { //? Read request - let transport = Transport::new(std::io::stdin(), std::io::stdout()); - let request = transport.read()?; - let RpcMessage::RpcRequest(request) = request else { - return Err(PluginServerError::InvalidMessage(request)); - }; - + let request = self.read_request()?; info!("PluginServer: Received request: {:?}", request); + let (transport, driver) = Transport::new(std::io::stdin(), std::io::stdout()); let resp = futures::executor::block_on(async move { - self.router - .handle_with_state(transport, &request.method, request.params) - .await + select! { + res = self.router.handle_with_state(transport, &request.method, request.params).fuse() => { + res + }, + drive_err = driver.run().fuse() => { + panic!("Transport driver exited unexpectedly: {:?}", drive_err); + } + } }); //? Send response @@ -89,4 +91,15 @@ impl PluginServer { Ok(()) } + + fn read_request(&self) -> Result { + let mut reader = BufReader::new(std::io::stdin()); + let mut line = String::new(); + reader.read_line(&mut line)?; + let msg: RpcMessage = serde_json::from_str(&line)?; + let RpcMessage::RpcRequest(msg) = msg else { + return Err(PluginServerError::InvalidMessage(msg)); + }; + Ok(msg) + } } diff --git a/crates/wasmi-plugin-pdk/src/transport.rs b/crates/wasmi-plugin-pdk/src/transport.rs index fd0510c..e908f56 100644 --- a/crates/wasmi-plugin-pdk/src/transport.rs +++ b/crates/wasmi-plugin-pdk/src/transport.rs @@ -1,272 +1,55 @@ -use std::{ - collections::{HashMap, HashSet}, - io::{BufRead, BufReader, Read, Write}, - sync::{ - Arc, Mutex, - atomic::{AtomicU64, Ordering}, - }, -}; +use std::io::{Read, Write}; use serde_json::Value; -use tracing::info; -use wasmi_plugin_rt::yield_now; use crate::{ - api::RequestHandler, - rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcRequest, RpcResponse, to_rpc_err}, + rpc_message::{RpcErrorResponse, RpcResponse}, + transport_driver::{TransportDriver, TransportError}, }; -/// Single-threaded, concurrent-safe json-rpc transport layer -/// -/// TODO: Create async version that accepts async readers/writers and yields properly -/// For the client-side, considering setting up a centralized "reactor" to handle all reads. -/// Then we can have the transport send requests to the reactor and await responses via channels. -/// If the main executor notices that everything but the reactor is blocked, it can yield via -/// poll_oneoff to free up the web worker and not mindlessly spin. -#[derive(Clone)] -pub struct Transport { - id: Arc, - reader: Arc>>, - writer: Arc>>, - - pending: Arc>>>, +pub struct Transport { + driver: TransportDriver, } -impl Transport { - pub fn new( - reader: impl Read + Send + Sync + 'static, - writer: impl Write + Send + Sync + 'static, - ) -> Self { +impl Clone for Transport { + fn clone(&self) -> Self { Self { - id: Arc::new(AtomicU64::new(0)), - pending: Arc::new(Mutex::new(HashMap::new())), - reader: Arc::new(Mutex::new(Box::new(BufReader::new(reader)))), - writer: Arc::new(Mutex::new(Box::new(writer))), + driver: self.driver.clone(), } } } -impl Transport { - /// Send a single call and wait for the response. - pub fn call(&self, method: &str, params: Value) -> Result { - let responses = self.call_many(std::iter::once((method, params)))?; - let resp = responses - .into_iter() - .next() - .ok_or(RpcError::custom("Response does not exist"))?; - - Ok(resp) - } - - /// Send multiple calls and wait for all responses. This allows async-like - /// behavior because the host will process requests concurrently. - /// - /// The order of responses matches the order of requests. If any call fails, - /// the entire batch will fail. - pub fn call_many(&self, calls: I) -> Result, RpcError> - where - I: IntoIterator, - S: Into, - { - let ids: Result, RpcError> = calls - .into_iter() - .map(|(method, params)| { - let id = self.next_id(); - self.write(id, &method.into(), params)?; - Ok(id) - }) - .collect(); - let ids = ids?; - - let mut results = Vec::with_capacity(ids.len()); - let mut remaining: HashSet = ids.iter().copied().collect(); - - while !remaining.is_empty() { - let message = self.read()?; - self.insert_response(message); - - //? Retrieve any fulfilled requests - let mut fulfilled = Vec::new(); - for &id in &remaining { - if let Some(result) = self.pending.lock().unwrap().remove(&id) { - results.push(result.map_err(|e| to_rpc_err(e.error))?); - fulfilled.push(id); - } - } +impl Transport { + pub fn new(reader: R, writer: W) -> (Self, TransportDriver) { + let driver = TransportDriver::new(reader, writer); - for id in fulfilled { - remaining.remove(&id); - } - } - - results.sort_by_key(|r| r.id); - - Ok(results) - } - - fn write(&self, id: u64, method: &str, params: Value) -> Result<(), RpcError> { - let request = RpcMessage::RpcRequest(crate::rpc_message::RpcRequest { - jsonrpc: "2.0".into(), - id, - method: method.into(), - params, - }); - - self.write_message(&request) - } - - fn write_message(&self, message: &RpcMessage) -> Result<(), RpcError> { - let message_str = serde_json::to_string(message).map_err(to_rpc_err)?; - let msg = format!("{}\n", message_str); - { - let mut writer = self.writer.lock().unwrap(); - writer.write_all(msg.as_bytes()).map_err(to_rpc_err)?; - writer.flush().map_err(to_rpc_err)?; - } + let transport = Self { + driver: driver.clone(), + }; - Ok(()) + (transport, driver) } - /// Blocking read of a single message - pub fn read(&self) -> Result { - let mut line = String::new(); - - //? Loop until we successfully read a line or hit EOF, continuing on WouldBlock - //? errors. - loop { - let res = self.reader.lock().unwrap().read_line(&mut line); - match res { - Ok(0) => return Err(RpcError::custom("EOF reached while reading from transport")), - Ok(_) => break, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - continue; - } - Err(e) => { - return Err(RpcError::custom(format!( - "Failed to read from transport: {}", - e - ))); - } - } - } - - serde_json::from_str(&line).map_err(to_rpc_err) + pub fn call(&self, method: &str, params: Value) -> Result { + self.driver.call(method, params) } - /// Inserts a response message into the pending map. If the message is a request, - /// it is ignored. - fn insert_response(&self, message: RpcMessage) { - match message { - RpcMessage::RpcResponse(resp) => { - self.pending.lock().unwrap().insert(resp.id, Ok(resp)); - } - RpcMessage::RpcErrorResponse(err_resp) => { - self.pending - .lock() - .unwrap() - .insert(err_resp.id, Err(err_resp)); - } - RpcMessage::RpcRequest(_) => { - info!("Ignoring request message"); - } - } - } - - fn next_id(&self) -> u64 { - self.id.fetch_add(1, Ordering::Relaxed) - } -} - -impl Transport { - /// Asynchronously send a single call and wait for the response. This allows - /// other async tasks to run while waiting for the response. - pub async fn async_call(&self, method: &str, params: Value) -> Result { - self.async_call_impl(method, params, None).await - } - - /// Asynchronously send a single call and wait for the response, while also handling - /// incoming requests using the provided handler. - /// - /// Used primarily by hosts to handle requests from guests while waiting for responses. - /// - /// ```text - /// host -> guest: balance() - /// host <- guest: balanceOf(1) - /// host ->> guest: 42 - /// host <- guest: balanceOf(2) - /// host ->> guest: 100 - /// host <<- guest: (42, 100) - /// ``` - pub async fn async_call_with_handler( + pub fn call_many( &self, - method: &str, - params: Value, - handler: &dyn RequestHandler, - ) -> Result { - self.async_call_impl(method, params, Some(handler)).await + calls: I, + ) -> Result>, TransportError> + where + I: IntoIterator, + S: Into, + { + self.driver.call_many(calls) } - async fn async_call_impl( + pub async fn call_async( &self, method: &str, params: Value, - handler: Option<&dyn RequestHandler>, - ) -> Result { - let id = self.next_id(); - self.write(id, method, params)?; - - loop { - let message = self.async_read().await?; - match message { - RpcMessage::RpcRequest(RpcRequest { - id, method, params, .. - }) => { - let Some(handler) = handler else { - info!("Ignoring request message as no handler is provided"); - continue; - }; - - match handler.handle(&method, params).await { - Ok(result) => { - self.write_message(&RpcMessage::response(id, result))?; - } - Err(error) => { - self.write_message(&RpcMessage::error_response(id, error))?; - } - } - } - _ => self.insert_response(message), - } - - //? If our request has been fulfilled, return the result - if let Some(result) = self.pending.lock().unwrap().remove(&id) { - return result.map_err(|e| to_rpc_err(e.error)); - } - } - } - - /// Async read of a single message, yielding when the reader would block - async fn async_read(&self) -> Result { - let mut line = String::new(); - - loop { - let res = self.reader.lock().unwrap().read_line(&mut line); - match res { - Ok(0) => return Err(RpcError::custom("EOF reached while reading from transport")), - Ok(_) => break, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - yield_now().await; - continue; - } - Err(e) => { - return Err(RpcError::custom(format!( - "Failed to read from transport: {}", - e - ))); - } - } - } - - serde_json::from_str(&line).map_err(to_rpc_err) + ) -> Result { + self.driver.call_async(method, params).await } } diff --git a/crates/wasmi-plugin-pdk/src/transport_driver.rs b/crates/wasmi-plugin-pdk/src/transport_driver.rs new file mode 100644 index 0000000..1766e0d --- /dev/null +++ b/crates/wasmi-plugin-pdk/src/transport_driver.rs @@ -0,0 +1,243 @@ +use std::{ + collections::HashMap, + io::{BufRead, BufReader, Read, Write}, + sync::{Arc, Mutex, atomic::AtomicU64}, +}; + +use futures::channel::oneshot; +use serde_json::Value; +use thiserror::Error; +use tracing::info; +use wasmi_plugin_rt::yield_now; + +use crate::rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcResponse}; + +pub struct TransportDriver { + reader: Arc>>, + writer: Arc>, + pending: Arc>>>>, + next_id: Arc, +} + +impl Clone for TransportDriver { + fn clone(&self) -> Self { + Self { + reader: self.reader.clone(), + writer: self.writer.clone(), + pending: self.pending.clone(), + next_id: self.next_id.clone(), + } + } +} + +#[derive(Debug, Error)] +pub enum TransportError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("serde_json error: {0}")] + Serde(#[from] serde_json::Error), + + #[error("oneshot send error")] + OneshotSend, + + #[error("oneshot Canceled")] + OneshotCanceled(#[from] oneshot::Canceled), + + #[error("EOF")] + EOF, + + #[error("Unmatched response ID")] + UnmatchedResponseId, + + #[error("Error Response: {0:?}")] + ErrorResponse(RpcErrorResponse), + + #[error("Missing Response")] + MissingResponse, +} + +impl From for RpcError { + fn from(value: TransportError) -> Self { + RpcError::custom(value.to_string()) + } +} + +impl TransportDriver { + pub fn new(reader: R, writer: W) -> Self { + Self { + reader: Arc::new(Mutex::new(BufReader::new(reader))), + writer: Arc::new(Mutex::new(writer)), + pending: Arc::new(Mutex::new(HashMap::new())), + next_id: Arc::new(AtomicU64::new(0)), + } + } + + pub async fn run(self) -> Result<(), TransportError> { + let mut line = String::new(); + + loop { + self.step(&mut self.reader.lock().unwrap(), &mut line)?; + yield_now().await; + } + } + + /// Asynchronous call that sends a request and waits for the response. + pub async fn call_async( + &self, + method: &str, + params: Value, + ) -> Result { + let id = self.next_id(); + let request = RpcMessage::request(id, method.to_string(), params); + + let (recv_tx, revc_rx) = oneshot::channel(); + self.pending.lock().unwrap().insert(id, recv_tx); + self.write_message(&request)?; + + // Await the response + let res = revc_rx.await?; + let res = res.map_err(TransportError::ErrorResponse)?; + Ok(res) + } + + /// Synchronous call that sends a request and waits for the response. Handles any + /// unrelated incoming messages while waiting. + pub fn call(&self, method: &str, params: Value) -> Result { + let resps = self.call_many(std::iter::once((method, params)))?; + let resp = resps + .into_iter() + .next() + .ok_or(TransportError::MissingResponse)?; + let resp = resp.map_err(TransportError::ErrorResponse)?; + Ok(resp) + } + + /// Synchronous call that sends multiple requests and waits for all responses. Handles + /// any unrelated incoming messages while waiting. + pub fn call_many( + &self, + calls: I, + ) -> Result>, TransportError> + where + I: IntoIterator, + S: Into, + { + //? Send all requests & collect their IDs + let mut receivers = Vec::new(); + let mut ids = Vec::new(); + for (method, params) in calls { + let id = self.next_id(); + let (tx, rx) = oneshot::channel(); + + self.pending.lock().unwrap().insert(id, tx); + self.write_message(&RpcMessage::request(id, method.into(), params))?; + + receivers.push(rx); + ids.push(id); + } + + let mut results = Vec::new(); + let mut reader = self.reader.lock().unwrap(); + let mut line = String::new(); + + //? Drive IO while any responses are still missing + while !receivers.is_empty() { + self.step(&mut reader, &mut line)?; + + //? Check for completed responses and collect them, dropping the + //? corresponding receivers + for i in (0..receivers.len()).rev() { + let rx = &mut receivers[i]; + match rx.try_recv() { + Ok(Some(res)) => { + results.push(res); + let _ = receivers.swap_remove(i); + } + Ok(None) => {} + Err(e) => { + return Err(TransportError::OneshotCanceled(e)); + } + } + } + } + + //? Sort into original order + results.sort_by_key(|r| match r { + Ok(res) => res.id, + Err(err) => err.id, + }); + + Ok(results) + } + + /// Perform a single read step, processing one incoming message if available. + /// Returns immediately if no message is available. + fn step( + &self, + reader: &mut std::sync::MutexGuard<'_, BufReader>, + line: &mut String, + ) -> Result<(), TransportError> { + match reader.read_line(line) { + Ok(0) => return Err(TransportError::EOF), + Ok(_) => {} + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(()), + Err(e) => return Err(TransportError::Io(e)), + } + + let msg: RpcMessage = serde_json::from_str(line)?; + line.clear(); + + self.insert_response(msg)?; + Ok(()) + } + + fn write_message(&self, message: &RpcMessage) -> Result<(), TransportError> { + let message_str = serde_json::to_string(message)?; + let msg = format!("{}\n", message_str); + { + let mut writer = self.writer.lock().unwrap(); + writer.write_all(msg.as_bytes())?; + writer.flush()?; + } + + Ok(()) + } + + fn insert_response(&self, message: RpcMessage) -> Result<(), TransportError> { + match message { + RpcMessage::RpcResponse(res) => { + let sender = self.pending.lock().unwrap().remove(&res.id); + let Some(sender) = sender else { + info!("Ignoring unmatched response ID: {}", res.id); + return Ok(()); + }; + + sender + .send(Ok(res)) + .map_err(|_| TransportError::OneshotSend)?; + } + RpcMessage::RpcErrorResponse(res) => { + let sender = self.pending.lock().unwrap().remove(&res.id); + let Some(sender) = sender else { + info!("Ignoring unmatched response ID: {}", res.id); + return Ok(()); + }; + + sender + .send(Err(res)) + .map_err(|_| TransportError::OneshotSend)?; + } + RpcMessage::RpcRequest(_) => { + info!("Ignoring request message"); + } + } + + Ok(()) + } + + fn next_id(&self) -> u64 { + self.next_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + } +} From ee76e3e18393b6c78ac04c222b21c51eb974eb19 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Wed, 24 Dec 2025 17:23:05 -0500 Subject: [PATCH 13/70] feat: pre-compile and transfer Module to workers --- crates/wasmi-plugin-hdk/Cargo.toml | 2 +- .../src/bridge/native_bridge.rs | 1 - .../src/bridge/worker_bridge.rs | 20 +++++++++++-------- .../src/bridge/worker_protocol.rs | 3 ++- crates/wasmi-plugin-hdk/src/compile.rs | 6 ++++-- crates/wasmi-plugin-hdk/src/plugin.rs | 14 +++++-------- crates/wasmi-plugin-hdk/tests/integration.rs | 11 ++++++++++ crates/worker/Cargo.toml | 2 +- crates/worker/src/lib.rs | 12 +++++------ 9 files changed, 42 insertions(+), 29 deletions(-) diff --git a/crates/wasmi-plugin-hdk/Cargo.toml b/crates/wasmi-plugin-hdk/Cargo.toml index 5b02c6c..24536bc 100644 --- a/crates/wasmi-plugin-hdk/Cargo.toml +++ b/crates/wasmi-plugin-hdk/Cargo.toml @@ -35,7 +35,7 @@ wasmer = { workspace = true, default-features = true} gloo-timers = { workspace = true } tracing-wasm = { workspace = true } serde-wasm-bindgen = { workspace = true } -wasmer = { workspace = true, features = ["js-default"] } +wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } web-sys = { workspace = true, features=["Worker", "MessageEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } diff --git a/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs b/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs index 6fd0960..f3a0e7c 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs @@ -31,7 +31,6 @@ pub enum NativeBridgeError { impl NativeBridge { pub fn new( compiled: Compiled, - _wasm_bytes: &[u8], ) -> Result< ( Self, diff --git a/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs b/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs index ba7152f..a68e6d5 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs @@ -29,10 +29,12 @@ struct SendWorker(Worker); #[derive(Debug, Error)] pub enum WorkerBridgeError { - #[error("JavaScript error: {0:?}")] + #[error("JavaScript error")] JsValue(JsValue), #[error("Serde serialization error: {0}")] Serde(#[from] serde_wasm_bindgen::Error), + #[error("Module serialization error: {0}")] + ModuleSerialization(#[from] wasmer::SerializeError), #[error("Receiver error: {0}")] Receiver(#[from] futures::channel::oneshot::Canceled), } @@ -46,7 +48,6 @@ unsafe impl Sync for WorkerBridge {} impl WorkerBridge { pub fn new( compiled: Compiled, - wasm_bytes: &[u8], ) -> Result< ( Self, @@ -55,7 +56,7 @@ impl WorkerBridge { ), WorkerBridgeError, > { - let name = compiled.name; + let name = compiled.name.clone(); info!("Creating WorkerBridge for plugin: {}", &name); let (ready_tx, ready_rx) = futures::channel::oneshot::channel::<()>(); @@ -76,14 +77,13 @@ impl WorkerBridge { //? Send all stdin data to worker let worker_inner = worker.clone(); - let wasm = wasm_bytes.to_vec(); let stdin_buf = stdin.buffer(); let stdout_buf = stdout.buffer(); let stderr_buf = stderr.buffer(); spawn_local(async move { match load( ready_rx, - wasm, + compiled, stdin_buf, stdout_buf, stderr_buf, @@ -213,7 +213,7 @@ fn on_message(ready_tx: Sender<()>) -> Box { /// Sends the Load message to the worker, transferring the Wasm bytes and pipes async fn load( ready_rx: Receiver<()>, - wasm: Vec, + compiled: Compiled, stdin: SharedArrayBuffer, stdout: SharedArrayBuffer, stderr: SharedArrayBuffer, @@ -222,13 +222,17 @@ async fn load( ready_rx.await?; // Create Load message - let msg = serde_wasm_bindgen::to_value(&WorkerMessage::Load { wasm })?; + let wasm_module = compiled.module.serialize()?; + let msg = serde_wasm_bindgen::to_value(&WorkerMessage::Load { + wasm_module: wasm_module.to_vec(), + })?; Reflect::set(&msg, &"stdin".into(), &stdin).map_err(WorkerBridgeError::JsValue)?; Reflect::set(&msg, &"stdout".into(), &stdout).map_err(WorkerBridgeError::JsValue)?; Reflect::set(&msg, &"stderr".into(), &stderr).map_err(WorkerBridgeError::JsValue)?; // Extract the wasm as Uint8Array for transfer - let payload = js_sys::Reflect::get(&msg, &"wasm".into()).map_err(WorkerBridgeError::JsValue)?; + let payload = + js_sys::Reflect::get(&msg, &"wasm_module".into()).map_err(WorkerBridgeError::JsValue)?; let uint8_array = payload .dyn_into::() .map_err(WorkerBridgeError::JsValue)?; diff --git a/crates/wasmi-plugin-hdk/src/bridge/worker_protocol.rs b/crates/wasmi-plugin-hdk/src/bridge/worker_protocol.rs index 413fd86..e9c24f9 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/worker_protocol.rs +++ b/crates/wasmi-plugin-hdk/src/bridge/worker_protocol.rs @@ -7,8 +7,9 @@ pub enum WorkerMessage { /// Load wasm bytes into the worker. Also has attached stdin/stdout/stderr /// pipes, which are SharedArrayBuffers but can't be serialized. Load { + /// Serialized wasm module. #[serde(with = "serde_bytes")] - wasm: Vec, + wasm_module: Vec, }, Log { message: String, diff --git a/crates/wasmi-plugin-hdk/src/compile.rs b/crates/wasmi-plugin-hdk/src/compile.rs index c17f57d..11bd9ae 100644 --- a/crates/wasmi-plugin-hdk/src/compile.rs +++ b/crates/wasmi-plugin-hdk/src/compile.rs @@ -3,7 +3,6 @@ pub struct Compiled { pub name: String, #[cfg(not(target_arch = "wasm32"))] pub engine: wasmer::Engine, - #[cfg(not(target_arch = "wasm32"))] pub module: wasmer::Module, } @@ -21,9 +20,12 @@ impl Compiled { } #[cfg(target_arch = "wasm32")] - pub fn new(name: &str, _wasm_bytes: &[u8]) -> Result { + pub fn new(name: &str, wasm_bytes: &[u8]) -> Result { + let engine = wasmer::Engine::default(); + let module = wasmer::Module::new(&engine, wasm_bytes)?; Ok(Compiled { name: name.to_string(), + module, }) } } diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index 12ca4ba..91d0ad4 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -60,7 +60,6 @@ pub struct Plugin { handler: Arc, logger: Logger, compiled: Compiled, - wasm_bytes: Vec, #[allow(dead_code)] // TODO: Use fuel to terminate native plugins max_fuel: Option, @@ -89,7 +88,6 @@ impl Plugin { logger: Box::new(default_plugin_logger), max_fuel: None, compiled: Compiled::new(name, &wasm_bytes)?, - wasm_bytes, }) } @@ -129,7 +127,7 @@ impl Plugin { let transport = Transport::new(stdout_reader, stdin_writer); let transport_task = transport.call(method, params, Some(handler)).fuse(); - let timeout = sleep(Duration::from_secs(60)).fuse(); + let timeout = sleep(Duration::from_secs(1)).fuse(); futures::pin_mut!(transport_task, timeout); info!("Plugin: Waiting for call to complete or timeout..."); @@ -157,9 +155,8 @@ impl Plugin { ), PluginError, > { - let (bridge, stdin, stdout) = - bridge::NativeBridge::new(self.compiled.clone(), &self.wasm_bytes) - .map_err(|_| PluginError::PluginDied)?; + let (bridge, stdin, stdout) = bridge::NativeBridge::new(self.compiled.clone()) + .map_err(|_| PluginError::PluginDied)?; Ok((bridge, stdin, stdout)) } @@ -175,9 +172,8 @@ impl Plugin { ), PluginError, > { - let (bridge, stdin, stdout) = - bridge::WorkerBridge::new(self.compiled.clone(), &self.wasm_bytes) - .map_err(|_| PluginError::PluginDied)?; + let (bridge, stdin, stdout) = bridge::WorkerBridge::new(self.compiled.clone()) + .map_err(|_| PluginError::PluginDied)?; Ok((bridge, stdin, stdout)) } } diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index b8d4f62..d94fe90 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -40,6 +40,17 @@ fn setup_logs() { .init(); }); } + + #[cfg(target_family = "wasm")] + { + INIT.call_once(|| { + tracing_wasm::set_as_global_default_with_config( + tracing_wasm::WASMLayerConfigBuilder::new() + .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) + .build(), + ); + }); + } } #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml index 07f8577..e820671 100644 --- a/crates/worker/Cargo.toml +++ b/crates/worker/Cargo.toml @@ -26,7 +26,7 @@ wasmi-plugin-pdk = { workspace = true } wasmi-plugin-rt = { workspace = true } web-time = { workspace = true } wasmer-types = { workspace = true } -wasmer = { workspace = true, features = ["js-default"] } +wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } web-sys = { workspace = true, features=["Worker", "DedicatedWorkerGlobalScope", "MessageEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index bb66f46..bd81988 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -16,8 +16,8 @@ use web_sys::{ #[derive(Debug, Error)] enum WorkerError { - #[error("Compile Error: {0}")] - CompileError(#[from] wasmer::CompileError), + #[error("Deserialization Error: {0}")] + DeserializeError(#[from] wasmer::DeserializeError), #[error("WASI Error: {0}")] WasiError(#[from] wasi_ctx::WasiError), #[error("Runtime Error: {0}")] @@ -46,7 +46,7 @@ fn on_message(e: MessageEvent) { return; }; - let WorkerMessage::Load { wasm } = msg else { + let WorkerMessage::Load { wasm_module } = msg else { error("Worker: Received unexpected message type"); return; }; @@ -70,7 +70,7 @@ fn on_message(e: MessageEvent) { return; }; - if let Err(err) = run_instance(&wasm, stdin, stdout, stderr) { + if let Err(err) = run_instance(&wasm_module, stdin, stdout, stderr) { error(&format!("Worker: Error running instance: {:?}", err)); } else { log("Worker: Instance finished execution"); @@ -84,7 +84,7 @@ fn get_sab(obj: &JsValue, key: &str) -> Option { } fn run_instance( - wasm_bytes: &[u8], + wasm_module: &[u8], stdin: SharedArrayBuffer, stdout: SharedArrayBuffer, stderr: SharedArrayBuffer, @@ -94,7 +94,7 @@ fn run_instance( let stderr = SharedPipe::new(&stderr); let mut store = wasmer::Store::default(); - let module = wasmer::Module::new(&store, wasm_bytes)?; + let module = unsafe { wasmer::Module::deserialize(&store, wasm_module) }?; let wasi_ctx = WasiCtx::new() .set_stdin(stdin) From bfc6d621d00ba40d3d7839e2a136ca178b5dea93 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Wed, 24 Dec 2025 17:27:57 -0500 Subject: [PATCH 14/70] fix: native_sleep --- crates/wasmi-plugin-hdk/src/plugin.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index 91d0ad4..0e942bd 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -60,6 +60,7 @@ pub struct Plugin { handler: Arc, logger: Logger, compiled: Compiled, + timeout: Duration, #[allow(dead_code)] // TODO: Use fuel to terminate native plugins max_fuel: Option, @@ -87,6 +88,7 @@ impl Plugin { handler, logger: Box::new(default_plugin_logger), max_fuel: None, + timeout: Duration::from_secs(10), compiled: Compiled::new(name, &wasm_bytes)?, }) } @@ -106,6 +108,13 @@ impl Plugin { self } + /// Sets a timeout duration for plugin calls. If a call takes longer than + /// this duration, it will be terminated and an error will be returned. + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } + pub fn name(&self) -> &str { &self.name } @@ -127,7 +136,7 @@ impl Plugin { let transport = Transport::new(stdout_reader, stdin_writer); let transport_task = transport.call(method, params, Some(handler)).fuse(); - let timeout = sleep(Duration::from_secs(1)).fuse(); + let timeout = sleep(self.timeout).fuse(); futures::pin_mut!(transport_task, timeout); info!("Plugin: Waiting for call to complete or timeout..."); From e97e4eb808d56a7c6701287c6f063520dbf0c150 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Wed, 24 Dec 2025 23:56:18 -0500 Subject: [PATCH 15/70] feat: impl worker pool Rename bridge to Runtime Create proper runtime lifecycle Refactor wasm runtime --- crates/wasmi-plugin-hdk/src/bridge/mod.rs | 19 -- .../src/bridge/worker_bridge.rs | 270 ------------------ crates/wasmi-plugin-hdk/src/lib.rs | 2 +- crates/wasmi-plugin-hdk/src/plugin.rs | 65 ++--- crates/wasmi-plugin-hdk/src/runtime/mod.rs | 40 +++ .../native_runtime.rs} | 138 +++++---- .../{bridge => runtime}/non_blocking_pipe.rs | 0 .../src/{bridge => runtime}/shared_pipe.rs | 0 .../src/runtime/worker_loader.js | 15 + .../src/runtime/worker_pool.rs | 264 +++++++++++++++++ .../{bridge => runtime}/worker_protocol.rs | 4 +- .../src/runtime/worker_runtime.rs | 62 ++++ crates/worker/src/lib.rs | 2 +- 13 files changed, 494 insertions(+), 387 deletions(-) delete mode 100644 crates/wasmi-plugin-hdk/src/bridge/mod.rs delete mode 100644 crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs create mode 100644 crates/wasmi-plugin-hdk/src/runtime/mod.rs rename crates/wasmi-plugin-hdk/src/{bridge/native_bridge.rs => runtime/native_runtime.rs} (55%) rename crates/wasmi-plugin-hdk/src/{bridge => runtime}/non_blocking_pipe.rs (100%) rename crates/wasmi-plugin-hdk/src/{bridge => runtime}/shared_pipe.rs (100%) create mode 100644 crates/wasmi-plugin-hdk/src/runtime/worker_loader.js create mode 100644 crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs rename crates/wasmi-plugin-hdk/src/{bridge => runtime}/worker_protocol.rs (91%) create mode 100644 crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs diff --git a/crates/wasmi-plugin-hdk/src/bridge/mod.rs b/crates/wasmi-plugin-hdk/src/bridge/mod.rs deleted file mode 100644 index 04ce5d7..0000000 --- a/crates/wasmi-plugin-hdk/src/bridge/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -#[cfg(not(target_arch = "wasm32"))] -mod native_bridge; -#[cfg(not(target_arch = "wasm32"))] -pub use native_bridge::NativeBridge; -#[cfg(not(target_arch = "wasm32"))] -mod non_blocking_pipe; - -#[cfg(target_arch = "wasm32")] -mod worker_bridge; -#[cfg(target_arch = "wasm32")] -pub use worker_bridge::WorkerBridge; -#[cfg(target_arch = "wasm32")] -pub mod shared_pipe; -#[cfg(target_arch = "wasm32")] -pub mod worker_protocol; - -pub trait Bridge { - fn terminate(self); -} diff --git a/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs b/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs deleted file mode 100644 index a68e6d5..0000000 --- a/crates/wasmi-plugin-hdk/src/bridge/worker_bridge.rs +++ /dev/null @@ -1,270 +0,0 @@ -//! Web Worker wasmer bridge, running the Wasm module in a Web Worker. - -use std::io::{BufRead, BufReader}; - -use futures::channel::oneshot::{Receiver, Sender}; -use futures::{AsyncRead, AsyncWrite}; -use thiserror::Error; -use tracing::{error, info}; -use wasm_bindgen_futures::spawn_local; -use web_sys::Worker; -use web_sys::js_sys::{Reflect, SharedArrayBuffer}; -use web_sys::{MessageEvent, js_sys, wasm_bindgen::prelude::Closure}; - -use web_sys::wasm_bindgen::{JsCast, JsValue}; - -use crate::bridge::Bridge; -use crate::bridge::shared_pipe::SharedPipe; -use crate::bridge::worker_protocol::WorkerMessage; -use crate::compile::Compiled; - -pub struct WorkerBridge { - worker: SendWorker, - // Keep this to ensure the JS callback isn't garbage collected - _onmessage_closure: Closure, -} - -#[derive(Clone)] -struct SendWorker(Worker); - -#[derive(Debug, Error)] -pub enum WorkerBridgeError { - #[error("JavaScript error")] - JsValue(JsValue), - #[error("Serde serialization error: {0}")] - Serde(#[from] serde_wasm_bindgen::Error), - #[error("Module serialization error: {0}")] - ModuleSerialization(#[from] wasmer::SerializeError), - #[error("Receiver error: {0}")] - Receiver(#[from] futures::channel::oneshot::Canceled), -} - -const WORKER_JS_GLUE: &str = include_str!("../../../worker/pkg/worker.js"); -const WORKER_WASM_BYTES: &[u8] = include_bytes!("../../../worker/pkg/worker_bg.wasm"); - -unsafe impl Send for WorkerBridge {} -unsafe impl Sync for WorkerBridge {} - -impl WorkerBridge { - pub fn new( - compiled: Compiled, - ) -> Result< - ( - Self, - impl AsyncWrite + Send + Sync + 'static, - impl AsyncRead + Send + Sync + 'static, - ), - WorkerBridgeError, - > { - let name = compiled.name.clone(); - info!("Creating WorkerBridge for plugin: {}", &name); - - let (ready_tx, ready_rx) = futures::channel::oneshot::channel::<()>(); - let stdin = SharedPipe::new_with_capacity(16384); - let stdout = SharedPipe::new_with_capacity(16384); - let stderr = SharedPipe::new_with_capacity(16384); - - //? Setup and send worker - let worker_url = worker_script()?; - let worker = - SendWorker(Worker::new(&worker_url).map_err(|e| WorkerBridgeError::JsValue(e))?); - - //? Worker onmessage callback - let onmessage_callback = Closure::wrap(on_message(ready_tx)); - worker - .0 - .set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref())); - - //? Send all stdin data to worker - let worker_inner = worker.clone(); - let stdin_buf = stdin.buffer(); - let stdout_buf = stdout.buffer(); - let stderr_buf = stderr.buffer(); - spawn_local(async move { - match load( - ready_rx, - compiled, - stdin_buf, - stdout_buf, - stderr_buf, - &worker_inner, - ) - .await - { - Ok(_) => {} - Err(e) => { - error!("[WorkerBridge] Failed to load Wasm: {}", e); - } - } - }); - - //? Log messages from stderr as plugin logs - spawn_local(async move { - let mut stderr = BufReader::new(stderr); - let mut buffer = String::new(); - loop { - buffer.clear(); - match stderr.read_line(&mut buffer) { - Ok(0) => { - break; - } - Ok(_) => { - info!("[plugin] [{}] {}", name, buffer.trim_end()); - } - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - gloo_timers::future::TimeoutFuture::new(0).await; - continue; - } - Err(e) => { - error!("[WorkerBridge] Error reading from stderr: {}", e); - break; - } - } - } - }); - - let bridge = WorkerBridge { - worker, - _onmessage_closure: onmessage_callback, - }; - Ok((bridge, stdin, stdout)) - } -} - -/// Creates a blob URL containing the worker script that lodas and executes the -/// wasm module. -fn worker_script() -> Result { - let options = web_sys::BlobPropertyBag::new(); - options.set_type("application/wasm"); - let wasm_blob = web_sys::Blob::new_with_u8_array_sequence_and_options( - &js_sys::Array::of1(&js_sys::Uint8Array::from(WORKER_WASM_BYTES)), - &options, - ) - .map_err(WorkerBridgeError::JsValue)?; - let wasm_url = web_sys::Url::create_object_url_with_blob(&wasm_blob) - .map_err(WorkerBridgeError::JsValue)?; - - // 2. Create a script that initializes the glue - let loader_script = format!( - r#" - console.log = function(...args) {{ - self.postMessage({{ type: 'Log', message: args.join(' ') }}); - }}; - console.error = function(...args) {{ - self.postMessage({{ type: 'Error', message: args.join(' ') }}); - }}; - - console.log("Worker Shim: Script starting..."); - - {0} // WORKER_JS_GLUE - - wasm_bindgen('{1}').then(() => {{ - console.log("Worker Shim: Rust Wasm Initialized"); - wasm_bindgen.start_worker(); - }}).catch(err => console.error("Worker Init Failed:", err)); - "#, - WORKER_JS_GLUE, wasm_url - ); - - let options = web_sys::BlobPropertyBag::new(); - options.set_type("text/javascript"); - let blob = web_sys::Blob::new_with_u8_array_sequence_and_options( - &js_sys::Array::of1(&js_sys::JsString::from(loader_script)), - &options, - ) - .map_err(WorkerBridgeError::JsValue)?; - - web_sys::Url::create_object_url_with_blob(&blob).map_err(WorkerBridgeError::JsValue) -} - -// Callback for worker onmessage events, when the host receives a message from the worker. -fn on_message(ready_tx: Sender<()>) -> Box { - let mut ready_tx = Some(ready_tx); - Box::new(move |e: MessageEvent| { - let Ok(msg) = serde_wasm_bindgen::from_value::(e.data()) else { - error!( - "WorkerBridge: Received unknown message from worker: {:?}", - e.data() - ); - return; - }; - - match msg { - WorkerMessage::Log { message } => { - info!("[WORKER] {}", message); - } - WorkerMessage::Error { message } => { - error!("[WORKER ERROR] {}", message); - } - WorkerMessage::Ready => { - info!("[WorkerBridge] Worker is ready"); - - if let Some(tx) = ready_tx.take() { - let _ = tx.send(()); - } - } - _ => { - error!("[WorkerBridge] Received unexpected message type from worker"); - } - } - }) -} - -/// Sends the Load message to the worker, transferring the Wasm bytes and pipes -async fn load( - ready_rx: Receiver<()>, - compiled: Compiled, - stdin: SharedArrayBuffer, - stdout: SharedArrayBuffer, - stderr: SharedArrayBuffer, - worker_inner: &SendWorker, -) -> Result<(), WorkerBridgeError> { - ready_rx.await?; - - // Create Load message - let wasm_module = compiled.module.serialize()?; - let msg = serde_wasm_bindgen::to_value(&WorkerMessage::Load { - wasm_module: wasm_module.to_vec(), - })?; - Reflect::set(&msg, &"stdin".into(), &stdin).map_err(WorkerBridgeError::JsValue)?; - Reflect::set(&msg, &"stdout".into(), &stdout).map_err(WorkerBridgeError::JsValue)?; - Reflect::set(&msg, &"stderr".into(), &stderr).map_err(WorkerBridgeError::JsValue)?; - - // Extract the wasm as Uint8Array for transfer - let payload = - js_sys::Reflect::get(&msg, &"wasm_module".into()).map_err(WorkerBridgeError::JsValue)?; - let uint8_array = payload - .dyn_into::() - .map_err(WorkerBridgeError::JsValue)?; - let buffer = uint8_array.buffer(); - - // Post the message with transfer - //? Transfer will more efficiently move the Wasm bytes to the worker by - //? moving the underlying ArrayBuffer instead of copying it. This will - //? invalidate the original buffer in the main thread (zero it). - let transfer_list = js_sys::Array::of1(&buffer); - worker_inner - .post_message_with_transfer(&msg, &transfer_list) - .map_err(WorkerBridgeError::JsValue)?; - Ok(()) -} - -impl Bridge for WorkerBridge { - fn terminate(self) { - self.worker.terminate(); - } -} - -impl SendWorker { - pub fn post_message_with_transfer( - &self, - msg: &JsValue, - transfer: &js_sys::Array, - ) -> Result<(), JsValue> { - self.0.post_message_with_transfer(msg, transfer) - } - - fn terminate(&self) { - self.0.terminate(); - } -} diff --git a/crates/wasmi-plugin-hdk/src/lib.rs b/crates/wasmi-plugin-hdk/src/lib.rs index 90a5fe5..12c4060 100644 --- a/crates/wasmi-plugin-hdk/src/lib.rs +++ b/crates/wasmi-plugin-hdk/src/lib.rs @@ -1,7 +1,7 @@ -pub mod bridge; mod compile; pub mod host_handler; pub mod plugin; +pub mod runtime; pub mod server; mod time; mod transport; diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index 0e942bd..a34339e 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -1,7 +1,6 @@ use std::{fmt::Display, sync::Arc, time::Duration}; use futures::FutureExt; -use futures::{AsyncRead, AsyncWrite}; use serde::{Deserialize, Serialize}; use serde_json::Value; use thiserror::Error; @@ -13,9 +12,10 @@ use wasmi_plugin_pdk::{ rpc_message::{RpcError, RpcResponse}, }; -use crate::bridge::{self, Bridge}; use crate::compile::Compiled; use crate::host_handler::HostHandler; +use crate::runtime; +use crate::runtime::Runtime; use crate::time::sleep; use crate::transport::{Transport, TransportError}; @@ -54,10 +54,10 @@ impl Display for PluginId { type Logger = Box; /// Plugin is an async-capable instance of a plugin -pub struct Plugin { +pub struct Plugin { name: String, id: PluginId, - handler: Arc, + handler: Arc, logger: Logger, compiled: Compiled, timeout: Duration, @@ -68,19 +68,19 @@ pub struct Plugin { #[derive(Debug, Error)] pub enum PluginError { - // #[error("spawn error")] - // SpawnError(#[from] SpawnError), #[error("Transport error")] TransportError(#[from] TransportError), - #[error("plugin died")] - PluginDied, + #[error("Bridge Error")] + BridgeError(#[from] Box), + #[error("Plugin timeout")] + PluginTimeout, } -impl Plugin { +impl Plugin { pub fn new( name: &str, wasm_bytes: Vec, - handler: Arc, + handler: Arc, ) -> Result { Ok(Plugin { name: name.to_string(), @@ -124,10 +124,13 @@ impl Plugin { } } -impl Plugin { +impl Plugin { pub async fn call(&self, method: &str, params: Value) -> Result { - //? Construct URL from worker bytes - let (bridge, stdin_writer, stdout_reader) = self.create_bridge().unwrap(); + let runtime = self.create_runtime(); + let (id, stdin_writer, stdout_reader) = runtime + .spawn(self.compiled.clone()) + .await + .map_err(|e| PluginError::BridgeError(e.into()))?; let handler = PluginCallback { handler: self.handler.clone(), @@ -142,48 +145,26 @@ impl Plugin { info!("Plugin: Waiting for call to complete or timeout..."); futures::select! { res = transport_task => { - bridge.terminate(); + runtime.terminate(id).await; Ok(res?) }, _ = timeout => { - bridge.terminate(); - Err(PluginError::PluginDied) + runtime.terminate(id).await; + Err(PluginError::PluginTimeout) } } } #[allow(clippy::type_complexity)] #[cfg(not(target_arch = "wasm32"))] - fn create_bridge( - &self, - ) -> Result< - ( - impl Bridge + Send + Sync + 'static, - impl AsyncWrite + Send + Sync + 'static, - impl AsyncRead + Send + Sync + 'static, - ), - PluginError, - > { - let (bridge, stdin, stdout) = bridge::NativeBridge::new(self.compiled.clone()) - .map_err(|_| PluginError::PluginDied)?; - Ok((bridge, stdin, stdout)) + fn create_runtime(&self) -> impl Runtime + Send + Sync + 'static { + runtime::NativeRuntime::new() } #[allow(clippy::type_complexity)] #[cfg(target_arch = "wasm32")] - fn create_bridge( - &self, - ) -> Result< - ( - impl Bridge + Send + Sync + 'static, - impl AsyncWrite + Send + Sync + 'static, - impl AsyncRead + Send + Sync + 'static, - ), - PluginError, - > { - let (bridge, stdin, stdout) = bridge::WorkerBridge::new(self.compiled.clone()) - .map_err(|_| PluginError::PluginDied)?; - Ok((bridge, stdin, stdout)) + fn create_runtime(&self) -> impl Runtime + Send + Sync + 'static { + runtime::WorkerRuntime::new() } } diff --git a/crates/wasmi-plugin-hdk/src/runtime/mod.rs b/crates/wasmi-plugin-hdk/src/runtime/mod.rs new file mode 100644 index 0000000..428dd0a --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/mod.rs @@ -0,0 +1,40 @@ +#[cfg(not(target_arch = "wasm32"))] +mod native_runtime; +use futures::{AsyncRead, AsyncWrite}; +#[cfg(not(target_arch = "wasm32"))] +pub use native_runtime::NativeRuntime; +#[cfg(not(target_arch = "wasm32"))] +mod non_blocking_pipe; + +#[cfg(target_arch = "wasm32")] +mod worker_runtime; +#[cfg(target_arch = "wasm32")] +pub use worker_runtime::WorkerRuntime; + +#[cfg(target_arch = "wasm32")] +pub mod shared_pipe; +#[cfg(target_arch = "wasm32")] +mod worker_pool; +#[cfg(target_arch = "wasm32")] +pub mod worker_protocol; + +use crate::compile::Compiled; + +pub trait Runtime { + type Error: Into>; + + fn spawn( + &self, + compiled: Compiled, + ) -> impl std::future::Future< + Output = Result< + ( + u64, + impl AsyncWrite + Unpin + Send + Sync + 'static, + impl AsyncRead + Unpin + Send + Sync + 'static, + ), + Self::Error, + >, + >; + fn terminate(self, instance_id: u64) -> impl std::future::Future; +} diff --git a/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs b/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs similarity index 55% rename from crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs rename to crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs index f3a0e7c..eb0f0a0 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/native_bridge.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs @@ -1,6 +1,11 @@ -//! Native wasmer bridge, running the Wasm module in a separate thread. +//! Native wasmer runtime, running the Wasm module in a separate thread. + +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; use futures::io::BufReader; +use futures::lock::Mutex; use futures::{AsyncBufReadExt, AsyncRead, AsyncWrite}; use thiserror::Error; use tokio::spawn; @@ -8,81 +13,82 @@ use tokio::task::{JoinHandle, spawn_blocking}; use tracing::{error, info}; use wasmer::Store; -use crate::bridge::Bridge; -use crate::bridge::non_blocking_pipe::{ +use crate::compile::Compiled; +use crate::runtime::Runtime; +use crate::runtime::non_blocking_pipe::{ NonBlockingPipeReader, NonBlockingPipeWriter, non_blocking_pipe, }; -use crate::compile::Compiled; use crate::wasi::wasi_ctx::WasiCtx; -pub struct NativeBridge { +pub struct NativeRuntime { + sessions: Arc>>, + next_id: Arc, +} + +struct NativeSession { wasm_handle: JoinHandle<()>, stderr_handle: JoinHandle<()>, } #[derive(Debug, Error)] -pub enum NativeBridgeError { +pub enum NativeRuntimeError { #[error("Wasmer runtime error: {0}")] WasmerError(#[from] wasmer::RuntimeError), #[error("IO error: {0}")] IOError(#[from] std::io::Error), } -impl NativeBridge { - pub fn new( +impl NativeRuntime { + pub fn new() -> Self { + NativeRuntime { + sessions: Arc::new(Mutex::new(HashMap::default())), + next_id: Arc::new(AtomicU64::new(1)), + } + } +} + +impl Runtime for NativeRuntime { + type Error = NativeRuntimeError; + + async fn spawn( + &self, compiled: Compiled, ) -> Result< ( - Self, - impl AsyncWrite + Send + Sync + 'static, - impl AsyncRead + Send + Sync + 'static, + u64, + impl AsyncWrite + Unpin + Send + Sync + 'static, + impl AsyncRead + Unpin + Send + Sync + 'static, ), - NativeBridgeError, + Self::Error, > { + let id = self.next_id.fetch_add(1, Ordering::Relaxed); let name = compiled.name.clone(); let (stdin_reader, stdin_writer) = non_blocking_pipe(); let (stdout_reader, stdout_writer) = non_blocking_pipe(); let (stderr_reader, stderr_writer) = non_blocking_pipe(); - let wasm_handle = spawn_blocking(move || { - let result = Self::run_instance(compiled, stdin_reader, stdout_writer, stderr_writer); - if let Err(e) = result { - eprintln!("Plugin execution error: {:?}", e); - } - }); - - let stderr_handle = spawn(async move { - let mut stderr = BufReader::new(stderr_reader); - let mut buffer = String::new(); - loop { - buffer.clear(); - match stderr.read_line(&mut buffer).await { - Ok(0) => { - break; - } - Ok(_) => { - info!("[plugin] [{}] {}", name, buffer.trim_end()); - } - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - tokio::task::yield_now().await; - continue; - } - Err(e) => { - error!("[WorkerBridge] Error reading from stderr: {}", e); - break; - } - } - } - }); + let wasm_handle = Self::wasm_handle(compiled, stdin_reader, stdout_writer, stderr_writer); + let stderr_handle = Self::stderr_handle(name, stderr_reader); - let bridge = NativeBridge { + let session = NativeSession { wasm_handle, stderr_handle, }; - Ok((bridge, stdin_writer, stdout_reader)) + self.sessions.lock().await.insert(id, session); + Ok((id, stdin_writer, stdout_reader)) + } + + async fn terminate(self, instance_id: u64) -> () { + if let Some(session) = self.sessions.lock().await.remove(&instance_id) { + // TODO: Use gas metering to force terminate the wasm runtime + session.wasm_handle.abort(); + session.stderr_handle.abort(); + } } +} +impl NativeRuntime { fn run_instance( compiled: Compiled, stdin: NonBlockingPipeReader, @@ -104,14 +110,44 @@ impl NativeBridge { Ok(()) } -} -impl Bridge for NativeBridge { - fn terminate(self) { - self.stderr_handle.abort(); - self.wasm_handle.abort(); + fn wasm_handle( + compiled: Compiled, + stdin_reader: NonBlockingPipeReader, + stdout_writer: NonBlockingPipeWriter, + stderr_writer: NonBlockingPipeWriter, + ) -> JoinHandle<()> { + spawn_blocking(move || { + let result = Self::run_instance(compiled, stdin_reader, stdout_writer, stderr_writer); + if let Err(e) = result { + eprintln!("Plugin execution error: {:?}", e); + } + }) + } - // Currently just drops the handle to stop the thread - // TODO: Trigger out of gas termination + fn stderr_handle(name: String, mut stderr_reader: NonBlockingPipeReader) -> JoinHandle<()> { + spawn(async move { + let mut stderr = BufReader::new(&mut stderr_reader); + let mut buffer = String::new(); + loop { + buffer.clear(); + match stderr.read_line(&mut buffer).await { + Ok(0) => { + break; + } + Ok(_) => { + info!("[plugin] [{}] {}", name, buffer.trim_end()); + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + tokio::task::yield_now().await; + continue; + } + Err(e) => { + error!("[WorkerBridge] Error reading from stderr: {}", e); + break; + } + } + } + }) } } diff --git a/crates/wasmi-plugin-hdk/src/bridge/non_blocking_pipe.rs b/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs similarity index 100% rename from crates/wasmi-plugin-hdk/src/bridge/non_blocking_pipe.rs rename to crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs diff --git a/crates/wasmi-plugin-hdk/src/bridge/shared_pipe.rs b/crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs similarity index 100% rename from crates/wasmi-plugin-hdk/src/bridge/shared_pipe.rs rename to crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_loader.js b/crates/wasmi-plugin-hdk/src/runtime/worker_loader.js new file mode 100644 index 0000000..ff08796 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_loader.js @@ -0,0 +1,15 @@ +console.log = function (...args) { + self.postMessage({ type: 'Log', message: args.join(' ') }); +}; +console.error = function (...args) { + self.postMessage({ type: 'Log', message: args.join(' ') }); +}; + +console.log("Worker Shim: Script starting..."); + +{ WORKER_JS_GLUE } + +wasm_bindgen('{ WASM_URL }').then(() => { + console.log("Worker Shim: Rust Wasm Initialized"); + wasm_bindgen.start_worker(); +}).catch(err => console.error("Worker Init Failed:", err)); \ No newline at end of file diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs new file mode 100644 index 0000000..becc47e --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs @@ -0,0 +1,264 @@ +use std::sync::{ + Arc, Mutex, + atomic::{AtomicU64, Ordering}, +}; + +use futures::channel::oneshot::{self}; +use thiserror::Error; +use tracing::{error, info}; +use wasm_bindgen::{JsCast, JsValue, prelude::Closure}; +use web_sys::{ + MessageEvent, + js_sys::{self, Reflect, Uint8Array}, +}; + +use crate::{ + compile::Compiled, + runtime::{shared_pipe::SharedPipe, worker_protocol::WorkerMessage}, +}; + +pub type WorkerId = u32; + +pub struct WorkerPool { + workers: Vec>, + next_id: AtomicU64, + script_url: String, +} + +pub struct WorkerSession { + pub id: WorkerId, + handle: Arc, +} + +struct WorkerHandle { + id: WorkerId, + state: Arc>, + inner: web_sys::Worker, + + //? Store handle to prevent javascript garbage collection + _on_message: Closure, +} + +#[derive(Debug, Error)] +pub enum PoolError { + #[error("JavaScript error")] + JsValue(JsValue), + #[error("Serde serialization error: {0}")] + Serde(#[from] serde_wasm_bindgen::Error), + #[error("Module serialization error: {0}")] + ModuleSerialization(#[from] wasmer::SerializeError), + #[error("Receiver error: {0}")] + Receiver(#[from] futures::channel::oneshot::Canceled), +} + +#[derive(Clone, Copy, PartialEq)] +enum WorkerState { + Idle, + Busy, + Terminated, +} + +const WORKER_LOADER_JS: &str = include_str!("worker_loader.js"); +const WORKER_JS_GLUE: &str = include_str!("../../../worker/pkg/worker.js"); +const WORKER_WASM_BYTES: &[u8] = include_bytes!("../../../worker/pkg/worker_bg.wasm"); + +impl WorkerPool { + pub fn new() -> Result { + let pool = WorkerPool { + workers: Vec::new(), + next_id: AtomicU64::new(0), + script_url: create_worker_script()?, + }; + Ok(pool) + } + + pub async fn run( + &mut self, + compiled: Compiled, + ) -> Result<(WorkerSession, SharedPipe, SharedPipe), PoolError> { + let worker = self.get_or_create_worker().await?; + + let stdin = SharedPipe::new_with_capacity(16384); + let stdout = SharedPipe::new_with_capacity(16384); + let stderr = SharedPipe::new_with_capacity(16384); + + worker.run(compiled, &stdin, &stdout, &stderr)?; + + Ok(( + WorkerSession { + id: worker.id, + + handle: worker, + }, + stdin, + stdout, + )) + } + + async fn get_or_create_worker(&mut self) -> Result, PoolError> { + self.workers + .retain(|w| w.state() != WorkerState::Terminated); + + //? Look for an idle worker + if let Some(idle) = self.workers.iter().find(|w| w.state() == WorkerState::Idle) { + idle.set_state(WorkerState::Busy); + return Ok(idle.clone()); + } + + //? If none found, create a new worker + let id = self.next_id.fetch_add(1, Ordering::Relaxed) as WorkerId; + let worker = WorkerHandle::new(id, &self.script_url).await?; + let worker = Arc::new(worker); + worker.set_state(WorkerState::Busy); + self.workers.push(worker.clone()); + + Ok(worker) + } +} + +impl WorkerSession { + pub fn terminate(&self) { + self.handle.terminate(); + } +} + +unsafe impl Send for WorkerHandle {} +unsafe impl Sync for WorkerHandle {} + +impl WorkerHandle { + pub async fn new(id: WorkerId, script_url: &str) -> Result { + let worker = web_sys::Worker::new(script_url).map_err(PoolError::JsValue)?; + let state = Arc::new(Mutex::new(WorkerState::Idle)); + + let (ready_tx, ready_rx) = oneshot::channel(); + let on_message = Closure::wrap(WorkerHandle::on_message_handler( + ready_tx, + id, + state.clone(), + )); + worker.set_onmessage(Some(on_message.as_ref().unchecked_ref())); + + //? Wait for the worker to signal readiness + ready_rx.await?; + + Ok(WorkerHandle { + id, + state, + inner: worker, + _on_message: on_message, + }) + } + + pub fn state(&self) -> WorkerState { + self.state.lock().unwrap().clone() + } + + pub fn set_state(&self, state: WorkerState) { + let mut guard = self.state.lock().unwrap(); + *guard = state; + } + + pub fn run( + &self, + compiled: Compiled, + stdin: &SharedPipe, + stdout: &SharedPipe, + stderr: &SharedPipe, + ) -> Result<(), PoolError> { + self.set_state(WorkerState::Busy); + + let wasm_bytes = compiled.module.serialize()?; + let msg = serde_wasm_bindgen::to_value(&WorkerMessage::Load { + wasm_module: wasm_bytes.to_vec(), + })?; + + // Attach SharedArrayBuffers via Reflect + Reflect::set(&msg, &"stdin".into(), &stdin.buffer()).map_err(PoolError::JsValue)?; + Reflect::set(&msg, &"stdout".into(), &stdout.buffer()).map_err(PoolError::JsValue)?; + Reflect::set(&msg, &"stderr".into(), &stderr.buffer()).map_err(PoolError::JsValue)?; + + // Transfer WASM buffer to worker + let uint8_array = Uint8Array::new( + &Reflect::get(&msg, &"wasm_module".into()).map_err(PoolError::JsValue)?, + ); + let transfer_list = js_sys::Array::of1(&uint8_array.buffer()); + + self.inner + .post_message_with_transfer(&msg, &transfer_list) + .map_err(PoolError::JsValue)?; + + Ok(()) + } + + pub fn terminate(&self) { + self.inner.terminate(); + self.set_state(WorkerState::Terminated); + } + + fn on_message_handler( + ready_tx: oneshot::Sender<()>, + id: WorkerId, + state: Arc>, + ) -> Box { + let mut ready_tx = Some(ready_tx); + Box::new(move |e: MessageEvent| { + let Ok(msg) = serde_wasm_bindgen::from_value::(e.data()) else { + error!( + "WorkerBridge: Received unknown message from worker: {:?}", + e.data() + ); + return; + }; + + match msg { + WorkerMessage::Log { message } => { + info!("[WORKER_{}] {}", id, message); + } + WorkerMessage::Ready => { + if let Some(tx) = ready_tx.take() { + info!("[WORKER_{}] Ready", id); + let _ = tx.send(()); + } + } + WorkerMessage::Idle => { + let mut guard = state.lock().unwrap(); + // Only transition to Idle if currently Busy + if *guard == WorkerState::Busy { + *guard = WorkerState::Idle; + info!("[WORKER_{}] Idle", id); + } + } + _ => { + error!("[WORKER_{}] Unexpected message", id); + } + } + }) + } +} + +/// Creates a blob URL containing the worker script that lodas and executes the +/// wasm module. +fn create_worker_script() -> Result { + //? Create a blob URL for the worker WASM module + let options = web_sys::BlobPropertyBag::new(); + options.set_type("application/wasm"); + let wasm_blob = web_sys::Blob::new_with_u8_array_sequence_and_options( + &js_sys::Array::of1(&js_sys::Uint8Array::from(WORKER_WASM_BYTES)), + &options, + )?; + let wasm_url = web_sys::Url::create_object_url_with_blob(&wasm_blob)?; + + //? + let loader_script = WORKER_LOADER_JS + .replace("{ WORKER_JS_GLUE }", WORKER_JS_GLUE) + .replace("{ WASM_URL }", &wasm_url); + + let options = web_sys::BlobPropertyBag::new(); + options.set_type("text/javascript"); + let blob = web_sys::Blob::new_with_u8_array_sequence_and_options( + &js_sys::Array::of1(&js_sys::JsString::from(loader_script)), + &options, + )?; + + web_sys::Url::create_object_url_with_blob(&blob) +} diff --git a/crates/wasmi-plugin-hdk/src/bridge/worker_protocol.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_protocol.rs similarity index 91% rename from crates/wasmi-plugin-hdk/src/bridge/worker_protocol.rs rename to crates/wasmi-plugin-hdk/src/runtime/worker_protocol.rs index e9c24f9..33bed0f 100644 --- a/crates/wasmi-plugin-hdk/src/bridge/worker_protocol.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_protocol.rs @@ -14,7 +14,5 @@ pub enum WorkerMessage { Log { message: String, }, - Error { - message: String, - }, + Idle, } diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs new file mode 100644 index 0000000..35860e3 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -0,0 +1,62 @@ +//! Web Worker runtime bridge, running the Wasm module in a Web Worker. + +use std::collections::HashMap; +use std::sync::{Arc, OnceLock}; + +use futures::lock::Mutex; +use futures::{AsyncRead, AsyncWrite}; + +use crate::compile::Compiled; +use crate::runtime::Runtime; +use crate::runtime::worker_pool::{PoolError, WorkerId, WorkerPool, WorkerSession}; + +static POOL: OnceLock> = OnceLock::new(); + +pub fn get_pool() -> &'static Mutex { + POOL.get_or_init(|| Mutex::new(WorkerPool::new().expect("Failed to init global worker pool"))) +} + +pub struct WorkerRuntime { + active_sessions: Arc>>, +} + +impl WorkerRuntime { + pub fn new() -> Self { + WorkerRuntime { + active_sessions: Arc::new(Mutex::new(HashMap::new())), + } + } +} + +impl Runtime for WorkerRuntime { + type Error = PoolError; + + async fn spawn( + &self, + compiled: Compiled, + ) -> Result< + ( + u64, + impl AsyncWrite + Unpin + Send + Sync + 'static, + impl AsyncRead + Unpin + Send + Sync + 'static, + ), + Self::Error, + > { + let (session, stdin, stdout) = get_pool().lock().await.run(compiled).await?; + + let instance_id = session.id; + self.active_sessions + .lock() + .await + .insert(instance_id, session); + + Ok((instance_id as u64, stdin, stdout)) + } + + async fn terminate(self, instance_id: u64) { + let instance_id = instance_id as WorkerId; + if let Some(session) = self.active_sessions.lock().await.remove(&instance_id) { + session.terminate(); + } + } +} diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index bd81988..eed1801 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -6,7 +6,7 @@ use wasm_bindgen::{ prelude::{Closure, wasm_bindgen}, }; use wasmi_plugin_hdk::{ - bridge::{shared_pipe::SharedPipe, worker_protocol::WorkerMessage}, + runtime::{shared_pipe::SharedPipe, worker_protocol::WorkerMessage}, wasi::wasi_ctx::{self, WasiCtx}, }; use web_sys::{ From 650be5a82f262bd27fc532398f252b064ac8b964 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Thu, 25 Dec 2025 00:39:39 -0500 Subject: [PATCH 16/70] fix: wasi transport race condition --- crates/wasmi-plugin-hdk/src/plugin.rs | 2 +- .../src/runtime/native_runtime.rs | 45 +++++++++---------- .../src/runtime/non_blocking_pipe.rs | 3 -- crates/wasmi-plugin-pdk/src/server.rs | 16 +++++-- crates/wasmi-plugin-pdk/src/transport.rs | 8 ++-- .../wasmi-plugin-pdk/src/transport_driver.rs | 38 ++++++++-------- 6 files changed, 58 insertions(+), 54 deletions(-) diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index a34339e..79939db 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -88,7 +88,7 @@ impl Plugin { handler, logger: Box::new(default_plugin_logger), max_fuel: None, - timeout: Duration::from_secs(10), + timeout: Duration::from_secs(5), compiled: Compiled::new(name, &wasm_bytes)?, }) } diff --git a/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs index eb0f0a0..55bab6b 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs @@ -83,34 +83,11 @@ impl Runtime for NativeRuntime { if let Some(session) = self.sessions.lock().await.remove(&instance_id) { // TODO: Use gas metering to force terminate the wasm runtime session.wasm_handle.abort(); - session.stderr_handle.abort(); } } } impl NativeRuntime { - fn run_instance( - compiled: Compiled, - stdin: NonBlockingPipeReader, - stdout: NonBlockingPipeWriter, - stderr: NonBlockingPipeWriter, - ) -> Result<(), Box> { - let engine = compiled.engine; - let module = compiled.module; - - let mut store = Store::new(engine); - - let wasi_ctx = WasiCtx::new() - .set_stdin(stdin) - .set_stdout(stdout) - .set_stderr(stderr); - - let start = wasi_ctx.into_fn(&mut store, &module)?; - start.call(&mut store, &[])?; - - Ok(()) - } - fn wasm_handle( compiled: Compiled, stdin_reader: NonBlockingPipeReader, @@ -150,4 +127,26 @@ impl NativeRuntime { } }) } + + fn run_instance( + compiled: Compiled, + stdin: NonBlockingPipeReader, + stdout: NonBlockingPipeWriter, + stderr: NonBlockingPipeWriter, + ) -> Result<(), Box> { + let engine = compiled.engine; + let module = compiled.module; + + let mut store = Store::new(engine); + + let wasi_ctx = WasiCtx::new() + .set_stdin(stdin) + .set_stdout(stdout) + .set_stderr(stderr); + + let start = wasi_ctx.into_fn(&mut store, &module)?; + start.call(&mut store, &[])?; + + Ok(()) + } } diff --git a/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs b/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs index 54d684e..16b2e4d 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs @@ -11,7 +11,6 @@ use std::sync::{Arc, Mutex}; use std::task::{Context, Poll, Waker}; use futures::{AsyncRead, AsyncWrite}; -use tracing::info; struct Inner { buf: VecDeque, @@ -145,9 +144,7 @@ impl AsyncWrite for NonBlockingPipeWriter { impl Drop for NonBlockingPipeWriter { fn drop(&mut self) { let mut inner = self.inner.lock().unwrap(); - info!("Pipe writer dropped, closing pipe"); inner.closed = true; - inner.waker = None; if let Some(waker) = inner.waker.take() { waker.wake(); } diff --git a/crates/wasmi-plugin-pdk/src/server.rs b/crates/wasmi-plugin-pdk/src/server.rs index 7ba5c90..f598de8 100644 --- a/crates/wasmi-plugin-pdk/src/server.rs +++ b/crates/wasmi-plugin-pdk/src/server.rs @@ -3,7 +3,7 @@ use std::io::{BufRead, BufReader, Write}; use futures::{FutureExt, select}; use serde::{Serialize, de::DeserializeOwned}; use thiserror::Error; -use tracing::info; +use tracing::{error, info}; use crate::{ router::{MaybeSend, Router}, @@ -54,7 +54,7 @@ impl PluginServer { match self.try_run() { Ok(()) => {} Err(e) => { - eprintln!("Plugin server error: {}", e); + error!("PluginServer encountered an error: {:?}", e); std::process::exit(1); } } @@ -95,7 +95,17 @@ impl PluginServer { fn read_request(&self) -> Result { let mut reader = BufReader::new(std::io::stdin()); let mut line = String::new(); - reader.read_line(&mut line)?; + loop { + match reader.read_line(&mut line) { + Ok(_) if !line.is_empty() => break, + Ok(_) => continue, // EOF or empty read + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + continue; + } + Err(e) => return Err(e.into()), + } + } + let msg: RpcMessage = serde_json::from_str(&line)?; let RpcMessage::RpcRequest(msg) = msg else { return Err(PluginServerError::InvalidMessage(msg)); diff --git a/crates/wasmi-plugin-pdk/src/transport.rs b/crates/wasmi-plugin-pdk/src/transport.rs index e908f56..b281f38 100644 --- a/crates/wasmi-plugin-pdk/src/transport.rs +++ b/crates/wasmi-plugin-pdk/src/transport.rs @@ -4,7 +4,7 @@ use serde_json::Value; use crate::{ rpc_message::{RpcErrorResponse, RpcResponse}, - transport_driver::{TransportDriver, TransportError}, + transport_driver::{DriverError, TransportDriver}, }; pub struct Transport { @@ -30,14 +30,14 @@ impl Transport { (transport, driver) } - pub fn call(&self, method: &str, params: Value) -> Result { + pub fn call(&self, method: &str, params: Value) -> Result { self.driver.call(method, params) } pub fn call_many( &self, calls: I, - ) -> Result>, TransportError> + ) -> Result>, DriverError> where I: IntoIterator, S: Into, @@ -49,7 +49,7 @@ impl Transport { &self, method: &str, params: Value, - ) -> Result { + ) -> Result { self.driver.call_async(method, params).await } } diff --git a/crates/wasmi-plugin-pdk/src/transport_driver.rs b/crates/wasmi-plugin-pdk/src/transport_driver.rs index 1766e0d..f6091b9 100644 --- a/crates/wasmi-plugin-pdk/src/transport_driver.rs +++ b/crates/wasmi-plugin-pdk/src/transport_driver.rs @@ -31,7 +31,7 @@ impl Clone for TransportDriver { } #[derive(Debug, Error)] -pub enum TransportError { +pub enum DriverError { #[error("IO error: {0}")] Io(#[from] std::io::Error), @@ -57,8 +57,8 @@ pub enum TransportError { MissingResponse, } -impl From for RpcError { - fn from(value: TransportError) -> Self { +impl From for RpcError { + fn from(value: DriverError) -> Self { RpcError::custom(value.to_string()) } } @@ -73,7 +73,7 @@ impl TransportDriver { } } - pub async fn run(self) -> Result<(), TransportError> { + pub async fn run(self) -> Result<(), DriverError> { let mut line = String::new(); loop { @@ -87,7 +87,7 @@ impl TransportDriver { &self, method: &str, params: Value, - ) -> Result { + ) -> Result { let id = self.next_id(); let request = RpcMessage::request(id, method.to_string(), params); @@ -97,19 +97,19 @@ impl TransportDriver { // Await the response let res = revc_rx.await?; - let res = res.map_err(TransportError::ErrorResponse)?; + let res = res.map_err(DriverError::ErrorResponse)?; Ok(res) } /// Synchronous call that sends a request and waits for the response. Handles any /// unrelated incoming messages while waiting. - pub fn call(&self, method: &str, params: Value) -> Result { + pub fn call(&self, method: &str, params: Value) -> Result { let resps = self.call_many(std::iter::once((method, params)))?; let resp = resps .into_iter() .next() - .ok_or(TransportError::MissingResponse)?; - let resp = resp.map_err(TransportError::ErrorResponse)?; + .ok_or(DriverError::MissingResponse)?; + let resp = resp.map_err(DriverError::ErrorResponse)?; Ok(resp) } @@ -118,7 +118,7 @@ impl TransportDriver { pub fn call_many( &self, calls: I, - ) -> Result>, TransportError> + ) -> Result>, DriverError> where I: IntoIterator, S: Into, @@ -156,7 +156,7 @@ impl TransportDriver { } Ok(None) => {} Err(e) => { - return Err(TransportError::OneshotCanceled(e)); + return Err(DriverError::OneshotCanceled(e)); } } } @@ -177,12 +177,12 @@ impl TransportDriver { &self, reader: &mut std::sync::MutexGuard<'_, BufReader>, line: &mut String, - ) -> Result<(), TransportError> { + ) -> Result<(), DriverError> { match reader.read_line(line) { - Ok(0) => return Err(TransportError::EOF), + Ok(0) => return Err(DriverError::EOF), Ok(_) => {} Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(()), - Err(e) => return Err(TransportError::Io(e)), + Err(e) => return Err(DriverError::Io(e)), } let msg: RpcMessage = serde_json::from_str(line)?; @@ -192,7 +192,7 @@ impl TransportDriver { Ok(()) } - fn write_message(&self, message: &RpcMessage) -> Result<(), TransportError> { + fn write_message(&self, message: &RpcMessage) -> Result<(), DriverError> { let message_str = serde_json::to_string(message)?; let msg = format!("{}\n", message_str); { @@ -204,7 +204,7 @@ impl TransportDriver { Ok(()) } - fn insert_response(&self, message: RpcMessage) -> Result<(), TransportError> { + fn insert_response(&self, message: RpcMessage) -> Result<(), DriverError> { match message { RpcMessage::RpcResponse(res) => { let sender = self.pending.lock().unwrap().remove(&res.id); @@ -213,9 +213,7 @@ impl TransportDriver { return Ok(()); }; - sender - .send(Ok(res)) - .map_err(|_| TransportError::OneshotSend)?; + sender.send(Ok(res)).map_err(|_| DriverError::OneshotSend)?; } RpcMessage::RpcErrorResponse(res) => { let sender = self.pending.lock().unwrap().remove(&res.id); @@ -226,7 +224,7 @@ impl TransportDriver { sender .send(Err(res)) - .map_err(|_| TransportError::OneshotSend)?; + .map_err(|_| DriverError::OneshotSend)?; } RpcMessage::RpcRequest(_) => { info!("Ignoring request message"); From e28dd5bdfae70994c6dfb5ab3c867211780331fa Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Thu, 25 Dec 2025 00:50:27 -0500 Subject: [PATCH 17/70] remove call_many from transport --- Cargo.lock | 1 + crates/test-plugin/Cargo.toml | 1 + crates/test-plugin/src/main.rs | 26 ++++--- crates/wasmi-plugin-pdk/src/transport.rs | 11 --- .../wasmi-plugin-pdk/src/transport_driver.rs | 71 +++++-------------- 5 files changed, 34 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2802d3d..27295be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1832,6 +1832,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" name = "test-plugin" version = "0.1.0" dependencies = [ + "futures", "rand", "serde_json", "tracing", diff --git a/crates/test-plugin/Cargo.toml b/crates/test-plugin/Cargo.toml index 8b19bfa..87abc82 100644 --- a/crates/test-plugin/Cargo.toml +++ b/crates/test-plugin/Cargo.toml @@ -14,6 +14,7 @@ exclude.workspace = true publish = false [dependencies] +futures = { workspace = true } rand = { workspace = true } serde_json = { workspace = true } tracing = { workspace = true } diff --git a/crates/test-plugin/src/main.rs b/crates/test-plugin/src/main.rs index a757d32..4ff643e 100644 --- a/crates/test-plugin/src/main.rs +++ b/crates/test-plugin/src/main.rs @@ -38,16 +38,22 @@ async fn call(transport: Transport, _: ()) -> Result { } async fn call_many(transport: Transport, limit: u64) -> Result<(), RpcError> { - let calls = (0..limit).map(|_| ("ping", Value::Null)); - let resp = transport.call_many(calls)?; - - if resp.len() as u64 != limit { - error!( - "Incorrect number of responses: expected {}, got {}", - limit, - resp.len() - ); - return Err(RpcError::InternalError); + let mut tasks = vec![]; + for i in 0..limit { + let transport_clone = transport.clone(); + let task = async move { + let resp = transport_clone.call("ping", Value::Null)?; + info!("Call {} got response: {:?}", i, resp.result); + Ok::<(), RpcError>(()) + }; + tasks.push(task); + } + + let resps: Vec> = + futures::future::join_all(tasks).await.into_iter().collect(); + + for resp in resps { + resp?; } Ok(()) diff --git a/crates/wasmi-plugin-pdk/src/transport.rs b/crates/wasmi-plugin-pdk/src/transport.rs index b281f38..887bb90 100644 --- a/crates/wasmi-plugin-pdk/src/transport.rs +++ b/crates/wasmi-plugin-pdk/src/transport.rs @@ -34,17 +34,6 @@ impl Transport { self.driver.call(method, params) } - pub fn call_many( - &self, - calls: I, - ) -> Result>, DriverError> - where - I: IntoIterator, - S: Into, - { - self.driver.call_many(calls) - } - pub async fn call_async( &self, method: &str, diff --git a/crates/wasmi-plugin-pdk/src/transport_driver.rs b/crates/wasmi-plugin-pdk/src/transport_driver.rs index f6091b9..ccb4652 100644 --- a/crates/wasmi-plugin-pdk/src/transport_driver.rs +++ b/crates/wasmi-plugin-pdk/src/transport_driver.rs @@ -104,71 +104,32 @@ impl TransportDriver { /// Synchronous call that sends a request and waits for the response. Handles any /// unrelated incoming messages while waiting. pub fn call(&self, method: &str, params: Value) -> Result { - let resps = self.call_many(std::iter::once((method, params)))?; - let resp = resps - .into_iter() - .next() - .ok_or(DriverError::MissingResponse)?; - let resp = resp.map_err(DriverError::ErrorResponse)?; - Ok(resp) - } + let id = self.next_id(); + let request = RpcMessage::request(id, method.to_string(), params); - /// Synchronous call that sends multiple requests and waits for all responses. Handles - /// any unrelated incoming messages while waiting. - pub fn call_many( - &self, - calls: I, - ) -> Result>, DriverError> - where - I: IntoIterator, - S: Into, - { - //? Send all requests & collect their IDs - let mut receivers = Vec::new(); - let mut ids = Vec::new(); - for (method, params) in calls { - let id = self.next_id(); - let (tx, rx) = oneshot::channel(); - - self.pending.lock().unwrap().insert(id, tx); - self.write_message(&RpcMessage::request(id, method.into(), params))?; - - receivers.push(rx); - ids.push(id); - } + let (res_tx, mut res_rx) = oneshot::channel(); + self.pending.lock().unwrap().insert(id, res_tx); + self.write_message(&request)?; - let mut results = Vec::new(); let mut reader = self.reader.lock().unwrap(); let mut line = String::new(); - //? Drive IO while any responses are still missing - while !receivers.is_empty() { + // Drive IO until we get the response + loop { self.step(&mut reader, &mut line)?; - //? Check for completed responses and collect them, dropping the - //? corresponding receivers - for i in (0..receivers.len()).rev() { - let rx = &mut receivers[i]; - match rx.try_recv() { - Ok(Some(res)) => { - results.push(res); - let _ = receivers.swap_remove(i); - } - Ok(None) => {} - Err(e) => { - return Err(DriverError::OneshotCanceled(e)); - } + // Check if we have received the response + match res_rx.try_recv() { + Ok(Some(res)) => { + let res = res.map_err(DriverError::ErrorResponse)?; + return Ok(res); + } + Ok(None) => {} + Err(e) => { + return Err(DriverError::OneshotCanceled(e)); } } } - - //? Sort into original order - results.sort_by_key(|r| match r { - Ok(res) => res.id, - Err(err) => err.id, - }); - - Ok(results) } /// Perform a single read step, processing one incoming message if available. From bf86d6bb7362a37c29ea8901f6baf45e394d04cf Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Thu, 25 Dec 2025 01:27:47 -0500 Subject: [PATCH 18/70] feat: re-use SABs across all runs --- .../src/runtime/shared_pipe.rs | 10 +- .../src/runtime/worker_pool.rs | 95 ++++++++++++------- .../src/runtime/worker_protocol.rs | 13 ++- .../wasmi-plugin-pdk/src/transport_driver.rs | 1 + crates/worker/src/lib.rs | 86 +++++++++++------ 5 files changed, 137 insertions(+), 68 deletions(-) diff --git a/crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs b/crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs index 239aa1e..e8811b8 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs @@ -65,8 +65,10 @@ impl SharedPipe { Self::new(&sab) } - fn is_closed(&self) -> bool { - Atomics::load(&self.header, CLOSED_IDX).unwrap() == 1 + pub fn reset(&self) { + Atomics::store(&self.header, READ_IDX, 0).unwrap(); + Atomics::store(&self.header, WRITE_IDX, 0).unwrap(); + Atomics::store(&self.header, CLOSED_IDX, 0).unwrap(); } pub fn close(&self) { @@ -76,6 +78,10 @@ impl SharedPipe { Atomics::notify(&self.header, WRITE_IDX).unwrap(); } + fn is_closed(&self) -> bool { + Atomics::load(&self.header, CLOSED_IDX).unwrap() == 1 + } + fn read_idx(&self) -> u32 { Atomics::load(&self.header, READ_IDX).unwrap() as u32 } diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs index becc47e..c815f4f 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs @@ -9,7 +9,7 @@ use tracing::{error, info}; use wasm_bindgen::{JsCast, JsValue, prelude::Closure}; use web_sys::{ MessageEvent, - js_sys::{self, Reflect, Uint8Array}, + js_sys::{self, Reflect, SharedArrayBuffer, Uint8Array}, }; use crate::{ @@ -34,6 +34,9 @@ struct WorkerHandle { id: WorkerId, state: Arc>, inner: web_sys::Worker, + stdin: SharedArrayBuffer, + stdout: SharedArrayBuffer, + stderr: SharedArrayBuffer, //? Store handle to prevent javascript garbage collection _on_message: Closure, @@ -58,6 +61,7 @@ enum WorkerState { Terminated, } +const SHARED_PIPE_CAPACITY: u32 = 16384; const WORKER_LOADER_JS: &str = include_str!("worker_loader.js"); const WORKER_JS_GLUE: &str = include_str!("../../../worker/pkg/worker.js"); const WORKER_WASM_BYTES: &[u8] = include_bytes!("../../../worker/pkg/worker_bg.wasm"); @@ -78,11 +82,16 @@ impl WorkerPool { ) -> Result<(WorkerSession, SharedPipe, SharedPipe), PoolError> { let worker = self.get_or_create_worker().await?; - let stdin = SharedPipe::new_with_capacity(16384); - let stdout = SharedPipe::new_with_capacity(16384); - let stderr = SharedPipe::new_with_capacity(16384); + let stdin = SharedPipe::new(&worker.stdin); + let stdout = SharedPipe::new(&worker.stdout); + let stderr = SharedPipe::new(&worker.stderr); - worker.run(compiled, &stdin, &stdout, &stderr)?; + // Reset the header (read/write pointers) in the shared memory + stdin.reset(); + stdout.reset(); + stderr.reset(); + + worker.run(compiled)?; Ok(( WorkerSession { @@ -127,26 +136,38 @@ unsafe impl Sync for WorkerHandle {} impl WorkerHandle { pub async fn new(id: WorkerId, script_url: &str) -> Result { - let worker = web_sys::Worker::new(script_url).map_err(PoolError::JsValue)?; + let inner = web_sys::Worker::new(script_url).map_err(PoolError::JsValue)?; let state = Arc::new(Mutex::new(WorkerState::Idle)); - let (ready_tx, ready_rx) = oneshot::channel(); + let stdin = SharedPipe::new_with_capacity(SHARED_PIPE_CAPACITY).buffer(); + let stdout = SharedPipe::new_with_capacity(SHARED_PIPE_CAPACITY).buffer(); + let stderr = SharedPipe::new_with_capacity(SHARED_PIPE_CAPACITY).buffer(); + + let (booted_tx, booted_rx) = oneshot::channel(); + let (init_tx, init_rx) = oneshot::channel(); let on_message = Closure::wrap(WorkerHandle::on_message_handler( - ready_tx, + booted_tx, + init_tx, id, state.clone(), )); - worker.set_onmessage(Some(on_message.as_ref().unchecked_ref())); - - //? Wait for the worker to signal readiness - ready_rx.await?; + inner.set_onmessage(Some(on_message.as_ref().unchecked_ref())); - Ok(WorkerHandle { + let worker = WorkerHandle { id, state, - inner: worker, + inner, + stdin, + stdout, + stderr, _on_message: on_message, - }) + }; + + booted_rx.await?; + worker.initalize()?; + init_rx.await?; + + Ok(worker) } pub fn state(&self) -> WorkerState { @@ -158,13 +179,7 @@ impl WorkerHandle { *guard = state; } - pub fn run( - &self, - compiled: Compiled, - stdin: &SharedPipe, - stdout: &SharedPipe, - stderr: &SharedPipe, - ) -> Result<(), PoolError> { + pub fn run(&self, compiled: Compiled) -> Result<(), PoolError> { self.set_state(WorkerState::Busy); let wasm_bytes = compiled.module.serialize()?; @@ -172,11 +187,6 @@ impl WorkerHandle { wasm_module: wasm_bytes.to_vec(), })?; - // Attach SharedArrayBuffers via Reflect - Reflect::set(&msg, &"stdin".into(), &stdin.buffer()).map_err(PoolError::JsValue)?; - Reflect::set(&msg, &"stdout".into(), &stdout.buffer()).map_err(PoolError::JsValue)?; - Reflect::set(&msg, &"stderr".into(), &stderr.buffer()).map_err(PoolError::JsValue)?; - // Transfer WASM buffer to worker let uint8_array = Uint8Array::new( &Reflect::get(&msg, &"wasm_module".into()).map_err(PoolError::JsValue)?, @@ -196,11 +206,13 @@ impl WorkerHandle { } fn on_message_handler( - ready_tx: oneshot::Sender<()>, + booted_tx: oneshot::Sender<()>, + init_tx: oneshot::Sender<()>, id: WorkerId, state: Arc>, ) -> Box { - let mut ready_tx = Some(ready_tx); + let mut booted_tx = Some(booted_tx); + let mut init_tx = Some(init_tx); Box::new(move |e: MessageEvent| { let Ok(msg) = serde_wasm_bindgen::from_value::(e.data()) else { error!( @@ -214,9 +226,15 @@ impl WorkerHandle { WorkerMessage::Log { message } => { info!("[WORKER_{}] {}", id, message); } - WorkerMessage::Ready => { - if let Some(tx) = ready_tx.take() { - info!("[WORKER_{}] Ready", id); + WorkerMessage::Booted => { + if let Some(tx) = booted_tx.take() { + info!("[WORKER_{}] Booted", id); + let _ = tx.send(()); + } + } + WorkerMessage::Initialized => { + if let Some(tx) = init_tx.take() { + info!("[WORKER_{}] Initialized", id); let _ = tx.send(()); } } @@ -234,6 +252,19 @@ impl WorkerHandle { } }) } + + fn initalize(&self) -> Result<(), PoolError> { + let init_msg = serde_wasm_bindgen::to_value(&WorkerMessage::Initialize)?; + + Reflect::set(&init_msg, &"stdin".into(), &self.stdin).map_err(PoolError::JsValue)?; + Reflect::set(&init_msg, &"stdout".into(), &self.stdout).map_err(PoolError::JsValue)?; + Reflect::set(&init_msg, &"stderr".into(), &self.stderr).map_err(PoolError::JsValue)?; + + self.inner + .post_message(&init_msg) + .map_err(PoolError::JsValue)?; + Ok(()) + } } /// Creates a blob URL containing the worker script that lodas and executes the diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_protocol.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_protocol.rs index 33bed0f..ae98067 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_protocol.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_protocol.rs @@ -3,11 +3,16 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] #[serde(tag = "type")] pub enum WorkerMessage { - Ready, - /// Load wasm bytes into the worker. Also has attached stdin/stdout/stderr - /// pipes, which are SharedArrayBuffers but can't be serialized. + /// Send by the worker when it has booted into WASM and is listening for messages + Booted, + /// Send by the host to initialize the worker with necessary SABs. Attaches the + /// shared memory pipes for stdin, stdout, and stderr (can't be sent via serde + /// for reasons). + Initialize, + /// Send by the worker when it has initialized + Initialized, Load { - /// Serialized wasm module. + /// Serialized wasm module. Deserialize with `wasmer::Module::deserialize` #[serde(with = "serde_bytes")] wasm_module: Vec, }, diff --git a/crates/wasmi-plugin-pdk/src/transport_driver.rs b/crates/wasmi-plugin-pdk/src/transport_driver.rs index ccb4652..54921d3 100644 --- a/crates/wasmi-plugin-pdk/src/transport_driver.rs +++ b/crates/wasmi-plugin-pdk/src/transport_driver.rs @@ -153,6 +153,7 @@ impl TransportDriver { Ok(()) } + // TODO: Catch WouldBlock and retry later fn write_message(&self, message: &RpcMessage) -> Result<(), DriverError> { let message_str = serde_json::to_string(message)?; let msg = format!("{}\n", message_str); diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index eed1801..2c89f50 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -1,5 +1,7 @@ #![cfg(target_arch = "wasm32")] +use std::sync::OnceLock; + use thiserror::Error; use wasm_bindgen::{ JsCast, JsValue, @@ -14,6 +16,13 @@ use web_sys::{ js_sys::{self, SharedArrayBuffer}, }; +static BUFFERS: OnceLock<(SAB, SAB, SAB)> = OnceLock::new(); + +struct SAB(SharedArrayBuffer); + +unsafe impl Send for SAB {} +unsafe impl Sync for SAB {} + #[derive(Debug, Error)] enum WorkerError { #[error("Deserialization Error: {0}")] @@ -35,46 +44,63 @@ pub fn start_worker() { onmessage.forget(); // Send ready - let ready_msg = WorkerMessage::Ready; - let ready_msg = serde_wasm_bindgen::to_value(&ready_msg).unwrap(); - global.post_message(&ready_msg).unwrap(); + let booted_msg = serde_wasm_bindgen::to_value(&WorkerMessage::Booted).unwrap(); + global.post_message(&booted_msg).unwrap(); } fn on_message(e: MessageEvent) { - let Ok(msg) = serde_wasm_bindgen::from_value::(e.data()) else { - log("Worker: Failed to deserialize message from Host"); - return; - }; + let global = js_sys::global().unchecked_into::(); + let data = e.data(); - let WorkerMessage::Load { wasm_module } = msg else { - error("Worker: Received unexpected message type"); + // Use serde to parse the variant. + // If it's "Initialize", it won't have fields in the enum, so we use Reflect. + let Ok(msg) = serde_wasm_bindgen::from_value::(data.clone()) else { + error("Worker: Failed to deserialize message"); return; }; - let stdin_buf = get_sab(&e.data(), "stdin"); - let stdout_buf = get_sab(&e.data(), "stdout"); - let stderr_buf = get_sab(&e.data(), "stderr"); + match msg { + WorkerMessage::Initialize => { + on_initialize(&global, data); + } - let Some(stdin) = stdin_buf else { - error("Load message missing stdin SharedArrayBuffer"); - return; - }; + WorkerMessage::Load { wasm_module } => { + on_load(global, wasm_module); + } + _ => {} + } +} - let Some(stdout) = stdout_buf else { - error("Load message missing stdout SharedArrayBuffer"); +fn on_initialize(global: &DedicatedWorkerGlobalScope, data: JsValue) { + let stdin_buf = get_sab(&data, "stdin"); + let stdout_buf = get_sab(&data, "stdout"); + let stderr_buf = get_sab(&data, "stderr"); + + let (Some(si), Some(so), Some(se)) = (stdin_buf, stdout_buf, stderr_buf) else { + error("Worker: Initialize message missing SharedArrayBuffers"); return; }; - let Some(stderr) = stderr_buf else { - error("Load message missing stderr SharedArrayBuffer"); + if BUFFERS.set((SAB(si), SAB(so), SAB(se))).is_err() { + log("Worker: Warning - Attempted to re-initialize buffers"); + } + + let msg = serde_wasm_bindgen::to_value(&WorkerMessage::Initialized).unwrap(); + global.post_message(&msg).unwrap(); +} + +fn on_load(global: DedicatedWorkerGlobalScope, wasm_module: Vec) { + let Some((stdin, stdout, stderr)) = BUFFERS.get() else { + error("Worker: Received Load message before pipes were initialized"); return; }; - if let Err(err) = run_instance(&wasm_module, stdin, stdout, stderr) { - error(&format!("Worker: Error running instance: {:?}", err)); - } else { - log("Worker: Instance finished execution"); + if let Err(err) = run_instance(&wasm_module, &stdin.0, &stdout.0, &stderr.0) { + error(&format!("Worker: Run error: {:?}", err)); } + + let idle_msg = serde_wasm_bindgen::to_value(&WorkerMessage::Idle).unwrap(); + global.post_message(&idle_msg).unwrap(); } fn get_sab(obj: &JsValue, key: &str) -> Option { @@ -85,13 +111,13 @@ fn get_sab(obj: &JsValue, key: &str) -> Option { fn run_instance( wasm_module: &[u8], - stdin: SharedArrayBuffer, - stdout: SharedArrayBuffer, - stderr: SharedArrayBuffer, + stdin: &SharedArrayBuffer, + stdout: &SharedArrayBuffer, + stderr: &SharedArrayBuffer, ) -> Result<(), WorkerError> { - let stdin = SharedPipe::new(&stdin); - let stdout = SharedPipe::new(&stdout); - let stderr = SharedPipe::new(&stderr); + let stdin = SharedPipe::new(stdin); + let stdout = SharedPipe::new(stdout); + let stderr = SharedPipe::new(stderr); let mut store = wasmer::Store::default(); let module = unsafe { wasmer::Module::deserialize(&store, wasm_module) }?; From 424c4d970ffee7977df37b579af73bad12a53bc4 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Thu, 25 Dec 2025 01:31:10 -0500 Subject: [PATCH 19/70] clippy --- .../src/runtime/native_runtime.rs | 18 ++++++++++-------- .../src/runtime/worker_runtime.rs | 6 ++++++ crates/wasmi-plugin-hdk/src/transport.rs | 4 ++-- crates/wasmi-plugin-pdk/src/transport.rs | 2 +- .../wasmi-plugin-pdk/src/transport_driver.rs | 5 +++-- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs index 55bab6b..5cbe294 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs @@ -27,7 +27,6 @@ pub struct NativeRuntime { struct NativeSession { wasm_handle: JoinHandle<()>, - stderr_handle: JoinHandle<()>, } #[derive(Debug, Error)] @@ -47,6 +46,12 @@ impl NativeRuntime { } } +impl Default for NativeRuntime { + fn default() -> Self { + Self::new() + } +} + impl Runtime for NativeRuntime { type Error = NativeRuntimeError; @@ -68,12 +73,9 @@ impl Runtime for NativeRuntime { let (stderr_reader, stderr_writer) = non_blocking_pipe(); let wasm_handle = Self::wasm_handle(compiled, stdin_reader, stdout_writer, stderr_writer); - let stderr_handle = Self::stderr_handle(name, stderr_reader); + Self::spawn_stderr(name, stderr_reader); - let session = NativeSession { - wasm_handle, - stderr_handle, - }; + let session = NativeSession { wasm_handle }; self.sessions.lock().await.insert(id, session); Ok((id, stdin_writer, stdout_reader)) @@ -102,7 +104,7 @@ impl NativeRuntime { }) } - fn stderr_handle(name: String, mut stderr_reader: NonBlockingPipeReader) -> JoinHandle<()> { + fn spawn_stderr(name: String, mut stderr_reader: NonBlockingPipeReader) { spawn(async move { let mut stderr = BufReader::new(&mut stderr_reader); let mut buffer = String::new(); @@ -125,7 +127,7 @@ impl NativeRuntime { } } } - }) + }); } fn run_instance( diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index 35860e3..7496f02 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -28,6 +28,12 @@ impl WorkerRuntime { } } +impl Default for WorkerRuntime { + fn default() -> Self { + Self::new() + } +} + impl Runtime for WorkerRuntime { type Error = PoolError; diff --git a/crates/wasmi-plugin-hdk/src/transport.rs b/crates/wasmi-plugin-hdk/src/transport.rs index 71c50dd..fe29bfe 100644 --- a/crates/wasmi-plugin-hdk/src/transport.rs +++ b/crates/wasmi-plugin-hdk/src/transport.rs @@ -27,7 +27,7 @@ pub enum TransportError { Serde(#[from] serde_json::Error), #[error("EOF")] - EOF, + Eof, #[error("Error Response: {0:?}")] ErrorResponse(RpcErrorResponse), @@ -62,7 +62,7 @@ impl Transport { let mut line = String::new(); loop { match self.reader.read_line(&mut line).await { - Ok(0) => return Err(TransportError::EOF), + Ok(0) => return Err(TransportError::Eof), Ok(_) => {} Err(e) => return Err(TransportError::Io(e)), } diff --git a/crates/wasmi-plugin-pdk/src/transport.rs b/crates/wasmi-plugin-pdk/src/transport.rs index 887bb90..4182776 100644 --- a/crates/wasmi-plugin-pdk/src/transport.rs +++ b/crates/wasmi-plugin-pdk/src/transport.rs @@ -3,7 +3,7 @@ use std::io::{Read, Write}; use serde_json::Value; use crate::{ - rpc_message::{RpcErrorResponse, RpcResponse}, + rpc_message::RpcResponse, transport_driver::{DriverError, TransportDriver}, }; diff --git a/crates/wasmi-plugin-pdk/src/transport_driver.rs b/crates/wasmi-plugin-pdk/src/transport_driver.rs index 54921d3..acf1e37 100644 --- a/crates/wasmi-plugin-pdk/src/transport_driver.rs +++ b/crates/wasmi-plugin-pdk/src/transport_driver.rs @@ -15,6 +15,7 @@ use crate::rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcResponse}; pub struct TransportDriver { reader: Arc>>, writer: Arc>, + #[allow(clippy::type_complexity)] pending: Arc>>>>, next_id: Arc, } @@ -45,7 +46,7 @@ pub enum DriverError { OneshotCanceled(#[from] oneshot::Canceled), #[error("EOF")] - EOF, + Eof, #[error("Unmatched response ID")] UnmatchedResponseId, @@ -140,7 +141,7 @@ impl TransportDriver { line: &mut String, ) -> Result<(), DriverError> { match reader.read_line(line) { - Ok(0) => return Err(DriverError::EOF), + Ok(0) => return Err(DriverError::Eof), Ok(_) => {} Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(()), Err(e) => return Err(DriverError::Io(e)), From d482f17b0b3ff901f05d85c775ffeb5a27273b2c Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Thu, 25 Dec 2025 01:36:26 -0500 Subject: [PATCH 20/70] add stderr to worker_runtime --- .../src/runtime/worker_pool.rs | 4 +- .../src/runtime/worker_runtime.rs | 40 ++++++++++++++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs index c815f4f..b7ca5c6 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs @@ -79,7 +79,7 @@ impl WorkerPool { pub async fn run( &mut self, compiled: Compiled, - ) -> Result<(WorkerSession, SharedPipe, SharedPipe), PoolError> { + ) -> Result<(WorkerSession, SharedPipe, SharedPipe, SharedPipe), PoolError> { let worker = self.get_or_create_worker().await?; let stdin = SharedPipe::new(&worker.stdin); @@ -96,11 +96,11 @@ impl WorkerPool { Ok(( WorkerSession { id: worker.id, - handle: worker, }, stdin, stdout, + stderr, )) } diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index 7496f02..e27402b 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -3,11 +3,16 @@ use std::collections::HashMap; use std::sync::{Arc, OnceLock}; +use futures::io::BufReader; use futures::lock::Mutex; -use futures::{AsyncRead, AsyncWrite}; +use futures::{AsyncBufReadExt, AsyncRead, AsyncWrite}; +use tracing::{error, info}; +use wasm_bindgen_futures::spawn_local; +use wasmi_plugin_rt::yield_now; use crate::compile::Compiled; use crate::runtime::Runtime; +use crate::runtime::shared_pipe::SharedPipe; use crate::runtime::worker_pool::{PoolError, WorkerId, WorkerPool, WorkerSession}; static POOL: OnceLock> = OnceLock::new(); @@ -48,7 +53,8 @@ impl Runtime for WorkerRuntime { ), Self::Error, > { - let (session, stdin, stdout) = get_pool().lock().await.run(compiled).await?; + let name = compiled.name.clone(); + let (session, stdin, stdout, stderr) = get_pool().lock().await.run(compiled).await?; let instance_id = session.id; self.active_sessions @@ -56,6 +62,8 @@ impl Runtime for WorkerRuntime { .await .insert(instance_id, session); + Self::spawn_stderr(name, stderr); + Ok((instance_id as u64, stdin, stdout)) } @@ -66,3 +74,31 @@ impl Runtime for WorkerRuntime { } } } + +impl WorkerRuntime { + fn spawn_stderr(name: String, mut stderr_reader: SharedPipe) { + spawn_local(async move { + let mut stderr = BufReader::new(&mut stderr_reader); + let mut buffer = String::new(); + loop { + buffer.clear(); + match stderr.read_line(&mut buffer).await { + Ok(0) => { + break; + } + Ok(_) => { + info!("[plugin] [{}] {}", name, buffer.trim_end()); + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + yield_now().await; + continue; + } + Err(e) => { + error!("[WorkerBridge] Error reading from stderr: {}", e); + break; + } + } + } + }); + } +} From da9e16892deba559a7b2fc72e7b79cf34aa0780f Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Thu, 25 Dec 2025 01:36:37 -0500 Subject: [PATCH 21/70] re-enable test-sleep for wasm --- crates/wasmi-plugin-hdk/tests/integration.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index d94fe90..857e403 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -107,8 +107,7 @@ async fn test_get_time() { assert!(diff < 2, "Timestamp difference too large: {}", diff); } -// TODO: Fix this for `wasm-pack test --headless --firefox ./crates/wasmi-plugin-hdk` -// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] async fn test_sleep() { setup_logs(); From bb06383f4bb96e203d42901b061b65d33db8cddb Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Thu, 25 Dec 2025 01:37:20 -0500 Subject: [PATCH 22/70] disable test_sleep for wasm --- crates/wasmi-plugin-hdk/tests/integration.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index 857e403..d94fe90 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -107,7 +107,8 @@ async fn test_get_time() { assert!(diff < 2, "Timestamp difference too large: {}", diff); } -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// TODO: Fix this for `wasm-pack test --headless --firefox ./crates/wasmi-plugin-hdk` +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] async fn test_sleep() { setup_logs(); From 4d45c8f80f6420bbeb4a3a34df80b9523c46340f Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Thu, 25 Dec 2025 01:45:01 -0500 Subject: [PATCH 23/70] change default timeout to 10s --- crates/wasmi-plugin-hdk/src/plugin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index 79939db..a34339e 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -88,7 +88,7 @@ impl Plugin { handler, logger: Box::new(default_plugin_logger), max_fuel: None, - timeout: Duration::from_secs(5), + timeout: Duration::from_secs(10), compiled: Compiled::new(name, &wasm_bytes)?, }) } From 97bb31d826fa41025c7b2f37d9b32f94f8cc6eef Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Fri, 26 Dec 2025 02:05:34 -0500 Subject: [PATCH 24/70] feat: possibly fix worker shared pipe race condition --- Cargo.lock | 108 ++++++ Cargo.toml | 1 + crates/test-plugin/src/main.rs | 23 ++ crates/wasmi-plugin-hdk/benches/benchmark.rs | 4 +- crates/wasmi-plugin-hdk/benches/wasm_bench.rs | 59 +++- crates/wasmi-plugin-hdk/src/plugin.rs | 1 - .../src/runtime/non_blocking_pipe.rs | 14 + .../src/runtime/shared_pipe.rs | 314 ++++++++++++------ .../src/runtime/worker_loader.js | 21 +- .../src/runtime/worker_pool.rs | 28 +- .../src/runtime/worker_protocol.rs | 4 + .../src/runtime/worker_runtime.rs | 40 +-- crates/wasmi-plugin-hdk/src/transport.rs | 109 +++--- crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs | 298 +++++++++++------ crates/wasmi-plugin-hdk/tests/integration.rs | 33 +- crates/wasmi-plugin-pdk/src/lib.rs | 1 + crates/wasmi-plugin-pdk/src/poll_oneoff.rs | 22 ++ crates/wasmi-plugin-pdk/src/server.rs | 15 +- .../wasmi-plugin-pdk/src/transport_driver.rs | 26 +- crates/worker/Cargo.toml | 3 + crates/worker/src/lib.rs | 57 ++-- crates/worker/src/message_writer.rs | 37 +++ 22 files changed, 847 insertions(+), 371 deletions(-) create mode 100644 crates/wasmi-plugin-pdk/src/poll_oneoff.rs create mode 100644 crates/worker/src/message_writer.rs diff --git a/Cargo.lock b/Cargo.lock index 27295be..200ec25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,15 @@ dependencies = [ "cc", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -232,6 +241,19 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -313,6 +335,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "corosensei" version = "0.3.2" @@ -921,6 +949,30 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -2510,12 +2562,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2691,11 +2796,14 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" name = "worker" version = "0.1.0" dependencies = [ + "chrono", "futures", "serde", "serde-wasm-bindgen", "serde_json", "thiserror 2.0.17", + "tracing", + "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", "wasmer", diff --git a/Cargo.toml b/Cargo.toml index af05c59..f8e8aee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ exclude = ["tests", "benches"] [workspace.dependencies] async-trait = "0.1" +chrono = { version = "0.4", features = ["wasmbind"] } futures = "0.3" getrandom = { version = "0.3.4", features = ["wasm_js"] } gloo-timers = { version = "0.3", features = ["futures"] } diff --git a/crates/test-plugin/src/main.rs b/crates/test-plugin/src/main.rs index 4ff643e..bebfa53 100644 --- a/crates/test-plugin/src/main.rs +++ b/crates/test-plugin/src/main.rs @@ -64,6 +64,28 @@ async fn call_async(transport: Transport, _: ()) -> Result { Ok(resp.result) } +async fn call_many_async(transport: Transport, limit: u64) -> Result<(), RpcError> { + let mut tasks = vec![]; + for i in 0..limit { + let transport_clone = transport.clone(); + let task = async move { + let resp = transport_clone.call_async("ping", Value::Null).await?; + info!("Call {} got response: {:?}", i, resp.result); + Ok::<(), RpcError>(()) + }; + tasks.push(task); + } + + let resps: Vec> = + futures::future::join_all(tasks).await.into_iter().collect(); + + for resp in resps { + resp?; + } + + Ok(()) +} + async fn prime_sieve(_transport: Transport, limit: u64) -> Result { let limit = limit as usize; let primes = sieve_of_eratosthenes(limit); @@ -112,6 +134,7 @@ fn main() { .with_method("call", call) .with_method("call_many", call_many) .with_method("call_async", call_async) + .with_method("call_many_async", call_many_async) .with_method("prime_sieve", prime_sieve) .run(); } diff --git a/crates/wasmi-plugin-hdk/benches/benchmark.rs b/crates/wasmi-plugin-hdk/benches/benchmark.rs index 80b1cd2..b6e889e 100644 --- a/crates/wasmi-plugin-hdk/benches/benchmark.rs +++ b/crates/wasmi-plugin-hdk/benches/benchmark.rs @@ -31,7 +31,9 @@ mod benchmarks { fn setup_logs() { INIT.call_once(|| { tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) + .compact() + .with_ansi(false) + .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE) .with_test_writer() .init(); }); diff --git a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs index bc23731..722821f 100644 --- a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs +++ b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs @@ -1,6 +1,10 @@ #![cfg(target_arch = "wasm32")] use serde_json::Value; -use std::sync::Arc; +use std::{ + sync::{Arc, Once}, + time::Duration, +}; +use tracing::info; use wasm_bindgen_test::{Criterion, wasm_bindgen_bench}; use wasmi_plugin_hdk::{ plugin::{Plugin, PluginId}, @@ -11,6 +15,8 @@ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); const PLUGIN_WASM: &[u8] = include_bytes!("../../../target/wasm32-wasip1/release/test-plugin.wasm"); +static INIT: Once = Once::new(); + fn load_plugin_wasm() -> Vec { PLUGIN_WASM.to_vec() } @@ -24,8 +30,19 @@ fn get_host_server() -> HostServer<(Option, ())> { .with_method("echo", |_, params: Value| async move { Ok(params) }) } +fn setup_logs() { + INIT.call_once(|| { + // tracing_wasm::set_as_global_default_with_config( + // tracing_wasm::WASMLayerConfigBuilder::new() + // .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) + // .build(), + // ); + }); +} + #[wasm_bindgen_bench] async fn bench_ping_wasm(c: &mut Criterion) { + setup_logs(); let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); let plugin = Arc::new(Plugin::new("test_plugin", wasm_bytes, handler).unwrap()); @@ -46,6 +63,7 @@ async fn bench_ping_wasm(c: &mut Criterion) { /// Tests the overhead of plugin instantiation. #[wasm_bindgen_bench] async fn bench_lifecycle(c: &mut Criterion) { + setup_logs(); let wasm_bytes = Arc::new(load_plugin_wasm()); let handler = Arc::new(get_host_server()); @@ -69,6 +87,7 @@ async fn bench_lifecycle(c: &mut Criterion) { /// of calling into the wasm module. #[wasm_bindgen_bench] async fn bench_prime_sieve_small(c: &mut Criterion) { + setup_logs(); let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); let plugin = Arc::new(Plugin::new("test_plugin", wasm_bytes, handler).unwrap()); @@ -91,6 +110,7 @@ async fn bench_prime_sieve_small(c: &mut Criterion) { /// Benchmark the prime sieve function with a large input. Tests performance within the wasm module. #[wasm_bindgen_bench] async fn bench_prime_sieve_large(c: &mut Criterion) { + setup_logs(); let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); let plugin = Arc::new(Plugin::new("test_plugin", wasm_bytes, handler).unwrap()); @@ -113,15 +133,22 @@ async fn bench_prime_sieve_large(c: &mut Criterion) { /// Benchmark sending many echo requests to the host, and receiving responses. #[wasm_bindgen_bench] async fn bench_call_many(c: &mut Criterion) { + setup_logs(); + let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); - let plugin = Arc::new(Plugin::new("test_plugin", wasm_bytes, handler).unwrap()); + let plugin = Arc::new( + Plugin::new("test_plugin", wasm_bytes, handler) + .unwrap() + .with_timeout(Duration::from_secs(5)), + ); c.bench_async_function("call_many", |b| { let plugin = plugin.clone(); Box::pin(b.iter_future(move || { let plugin = plugin.clone(); async move { + info!("Starting call_many benchmark iteration..."); plugin .call("call_many", Value::Number(200.into())) .await @@ -131,3 +158,31 @@ async fn bench_call_many(c: &mut Criterion) { }) .await; } + +#[wasm_bindgen_bench] +async fn bench_call_many_async(c: &mut Criterion) { + setup_logs(); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Arc::new( + Plugin::new("test_plugin", wasm_bytes, handler) + .unwrap() + .with_timeout(Duration::from_secs(5)), + ); + + c.bench_async_function("call_many_async", |b| { + let plugin = plugin.clone(); + Box::pin(b.iter_future(move || { + let plugin = plugin.clone(); + async move { + info!("Starting call_many_async benchmark iteration..."); + plugin + .call("call_many_async", Value::Number(200.into())) + .await + .unwrap(); + } + })) + }) + .await; +} diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index a34339e..7bd9235 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -142,7 +142,6 @@ impl Plugin { let timeout = sleep(self.timeout).fuse(); futures::pin_mut!(transport_task, timeout); - info!("Plugin: Waiting for call to complete or timeout..."); futures::select! { res = transport_task => { runtime.terminate(id).await; diff --git a/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs b/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs index 16b2e4d..b34bb22 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs @@ -12,6 +12,8 @@ use std::task::{Context, Poll, Waker}; use futures::{AsyncRead, AsyncWrite}; +use crate::wasi::wasi_ctx::WasiReader; + struct Inner { buf: VecDeque, closed: bool, @@ -65,6 +67,18 @@ impl Read for NonBlockingPipeReader { } } +impl WasiReader for NonBlockingPipeReader { + fn is_ready(&self) -> bool { + let inner = self.inner.lock().unwrap(); + !inner.buf.is_empty() || inner.closed + } + + fn wait_ready(&self, _timeout: Option) { + // TODO: Make native-compatible version of this? + // No-op + } +} + impl AsyncRead for NonBlockingPipeReader { fn poll_read( self: Pin<&mut Self>, diff --git a/crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs b/crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs index e8811b8..70c58f8 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs @@ -1,18 +1,21 @@ //! Shared pipe implementation for worker bridge. use std::{ + f64, io::{Read, Write}, pin::Pin, task::{Context, Poll}, }; use futures::{AsyncRead, AsyncWrite}; +use tracing::info; use wasm_bindgen_futures::JsFuture; use web_sys::{ js_sys::{self, Atomics, Int32Array, SharedArrayBuffer, Uint8Array}, wasm_bindgen::{JsCast, prelude::wasm_bindgen}, }; +use crate::wasi::wasi_ctx::WasiReader; // 4xi32 header: (16 bytes) const CLOSED_IDX: u32 = 0; // 0 = open, 1 = closed const WRITE_IDX: u32 = 1; @@ -25,6 +28,7 @@ pub struct SharedPipe { data: Uint8Array, capacity: u32, + name: String, pending_read: Option, pending_write: Option, } @@ -45,7 +49,7 @@ unsafe impl Send for SharedPipe {} unsafe impl Sync for SharedPipe {} impl SharedPipe { - pub fn new(sab: &SharedArrayBuffer) -> Self { + pub fn new(name: &str, sab: &SharedArrayBuffer) -> Self { let status = js_sys::Int32Array::new_with_byte_offset_and_length(&sab, 0, 4); let data_len = sab.byte_length() - DATA_OFFSET; let data = js_sys::Uint8Array::new_with_byte_offset_and_length(&sab, DATA_OFFSET, data_len); @@ -55,14 +59,15 @@ impl SharedPipe { header: status, data, capacity, + name: name.to_string(), pending_read: None, pending_write: None, } } - pub fn new_with_capacity(capacity: u32) -> Self { + pub fn new_with_capacity(name: &str, capacity: u32) -> Self { let sab = SharedArrayBuffer::new((DATA_OFFSET + capacity) as u32); - Self::new(&sab) + Self::new(name, &sab) } pub fn reset(&self) { @@ -78,8 +83,49 @@ impl SharedPipe { Atomics::notify(&self.header, WRITE_IDX).unwrap(); } + pub fn buffer(&self) -> SharedArrayBuffer { + self.header.buffer().unchecked_into::() + } + + pub fn dump(&self) { + //? Dumps internal state to logs for debugging + let closed = self.is_closed(); + let r = self.read_idx(); + let w = self.write_idx(); + let occupied = self.get_occupied(r, w); + + let last_byte_count = occupied.min(50); + let last_bytes = if occupied == 0 { + vec![] + } else { + let mut bytes = vec![0u8; last_byte_count as usize]; + let start_idx = if w >= last_byte_count { + w - last_byte_count + } else { + self.capacity + w - last_byte_count + }; + for i in 0..last_byte_count { + let idx = (start_idx + i) % self.capacity; + bytes[i as usize] = self.data.get_index(idx); + } + bytes + }; + + // Interpret as UTF-8 if possible + let last_bytes_str = match String::from_utf8(last_bytes.clone()) { + Ok(s) => s, + Err(_) => format!("{:?}", last_bytes), + }; + + info!( + "SharedPipe[{}] dump: closed={} read_idx={} write_idx={} occupied={} last_bytes={}", + self.name, closed, r, w, occupied, last_bytes_str + ); + } + fn is_closed(&self) -> bool { - Atomics::load(&self.header, CLOSED_IDX).unwrap() == 1 + let closed = Atomics::load(&self.header, CLOSED_IDX).unwrap(); + closed == 1 } fn read_idx(&self) -> u32 { @@ -96,90 +142,97 @@ impl SharedPipe { } fn set_write(&self, value: u32) { - Atomics::store(&self.header, WRITE_IDX, value as i32).unwrap(); + let old = Atomics::exchange(&self.header, WRITE_IDX, value as i32).unwrap(); + if old == value as i32 { + panic!("set_write called with same value: {}", value); + } Atomics::notify(&self.header, WRITE_IDX).unwrap(); } - pub fn buffer(&self) -> SharedArrayBuffer { - self.header.buffer().unchecked_into::() + fn get_occupied(&self, r: u32, w: u32) -> u32 { + if w >= r { w - r } else { self.capacity - r + w } } -} - -impl Read for SharedPipe { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let r = self.read_idx(); - let w = self.write_idx(); + fn try_read(&self, r: u32, w: u32, buf: &mut [u8]) -> Option { if r == w { - //? Only check if closed when empty, so we can drain remaining data - // after close if self.is_closed() { - return Ok(0); + return Some(0); // EOF } - return Err(std::io::ErrorKind::WouldBlock.into()); + return None; } - let buf_len = buf.len() as u32; - let occupied = if w >= r { w - r } else { self.capacity - r + w }; - let amount = occupied.min(buf_len); + let occupied = self.get_occupied(r, w); + let amount = (buf.len() as u32).min(occupied); - // Chunk 1: From R to (End of Buffer or W) - let first_chunk_len = if w >= r { - amount - } else { - (self.capacity - r).min(amount) - }; - self.data - .subarray(r, r + first_chunk_len) - .copy_to(&mut buf[..first_chunk_len as usize]); - - // Chunk 2: From 0 to remaining (only if we wrapped) - if amount > first_chunk_len { - let second_chunk_len = amount - first_chunk_len; - self.data - .subarray(0, second_chunk_len) - .copy_to(&mut buf[first_chunk_len as usize..amount as usize]); + for i in 0..amount { + let idx = (r + i) % self.capacity; + buf[i as usize] = self.data.get_index(idx); } - self.set_read((r + amount) % self.capacity); - Ok(amount as usize) + // info!( + // "SharedPipe[{}] try_read: r={} first_bytes={:?}", + // self.name, + // r, + // &buf[..(amount as usize).min(50)] + // ); + Some(amount as usize) } -} -impl Write for SharedPipe { - fn write(&mut self, buf: &[u8]) -> std::io::Result { + fn try_write(&self, r: u32, w: u32, buf: &[u8]) -> Option { if self.is_closed() { - return Err(std::io::ErrorKind::BrokenPipe.into()); + return None; } - let r = self.read_idx(); - let w = self.write_idx(); - - let occupied = if w >= r { w - r } else { self.capacity - r + w }; - let available = self.capacity - occupied - 1; // N-1 rule + let occupied = self.get_occupied(r, w); + //? The buffer is full if occupied == capacity - 1 + let available = self.capacity.saturating_sub(occupied).saturating_sub(1); if available == 0 { - return Err(std::io::ErrorKind::WouldBlock.into()); + return None; } let n = (buf.len() as u32).min(available); - // Chunk 1: From W to (End of Buffer or R-1) - let first_chunk_len = if r > w { n } else { (self.capacity - w).min(n) }; - self.data.set( - &js_sys::Uint8Array::from(&buf[..first_chunk_len as usize]), - w, - ); + for i in 0..n { + let idx = (w + i) % self.capacity; + self.data.set_index(idx, buf[i as usize]); + } + + // info!( + // "SharedPipe[{}] try_write: w={} first_bytes={:?}", + // self.name, + // w, + // &buf[..buf.len().min(50)] + // ); + Some(n as usize) + } +} - // Chunk 2: From 0 to remaining (only if we wrap) - if n > first_chunk_len { - let second_chunk_len = n - first_chunk_len; - let src = &buf[first_chunk_len as usize..(first_chunk_len + second_chunk_len) as usize]; - self.data.set(&js_sys::Uint8Array::from(src), 0); +impl Read for SharedPipe { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let r = self.read_idx(); + let w = self.write_idx(); + + if let Some(n) = self.try_read(r, w, buf) { + self.set_read((r + n as u32) % self.capacity); + return Ok(n); } - self.set_write((w + n) % self.capacity); - Ok(n as usize) + Err(std::io::ErrorKind::WouldBlock.into()) + } +} + +impl Write for SharedPipe { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let r = self.read_idx(); + let w = self.write_idx(); + + if let Some(n) = self.try_write(r, w, buf) { + self.set_write((w + n as u32) % self.capacity); + return Ok(n); + } + + Err(std::io::ErrorKind::WouldBlock.into()) } fn flush(&mut self) -> std::io::Result<()> { @@ -187,6 +240,40 @@ impl Write for SharedPipe { } } +impl WasiReader for SharedPipe { + fn is_ready(&self) -> bool { + let r = self.read_idx(); + let w = self.write_idx(); + + r != w || self.is_closed() + } + + fn wait_ready(&self, timeout: Option) { + let timeout_ms = timeout + .map(|d| d.as_millis() as f64) + .unwrap_or(f64::INFINITY); + + loop { + let (r, w) = (self.read_idx(), self.write_idx()); + + if r != w || self.is_closed() { + return; + } + + match Atomics::wait_with_timeout(&self.header, WRITE_IDX, w as i32, timeout_ms) { + Ok(s) if s == "not-equal" => { + //? WRITE_IDX changed, re-check + continue; + } + _ => { + //? Resolved + return; + } + } + } + } +} + impl AsyncRead for SharedPipe { fn poll_read( mut self: Pin<&mut Self>, @@ -202,35 +289,39 @@ impl AsyncRead for SharedPipe { } //? Try sync read - match self.read(buf) { - Ok(n) => Poll::Ready(Ok(n)), - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - //? Setup async wait for WRITE_IDX to change - let current_w = self.write_idx() as i32; - let res: WaitAsyncResult = Atomics::wait_async(&self.header, WRITE_IDX, current_w) - .expect("Atomics.waitAsync should be supported") - .unchecked_into(); - - if !res.is_async() { - // res was "not-equal", meaning another thread changed WRITE_IDX - // already. So wake the caller. - cx.waker().wake_by_ref(); - return Poll::Pending; - } + loop { + let (r, w) = (self.read_idx(), self.write_idx()); - let promise: js_sys::Promise = res.value().unchecked_into(); - let mut fut = JsFuture::from(promise); + if let Some(n) = self.try_read(r, w, buf) { + self.set_read((r + n as u32) % self.capacity); + return Poll::Ready(Ok(n)); + } - if Pin::new(&mut fut).poll(cx).is_pending() { - self.pending_read = Some(fut); - return Poll::Pending; - } + //? Wait for WRITE_IDX to change + let res: WaitAsyncResult = Atomics::wait_async(&self.header, WRITE_IDX, w as i32) + .expect("waitAsync support") + .unchecked_into(); - // Future resolved, meaning WRITE_IDX changed, try read again - cx.waker().wake_by_ref(); - return Poll::Pending; + if !res.is_async() { + //? WRITE_IDX changed, re-check + continue; + } + + let current_w = self.write_idx(); + if current_w != w { + continue; } - Err(e) => Poll::Ready(Err(e)), + + let promise: js_sys::Promise = res.value().unchecked_into(); + let mut fut = JsFuture::from(promise); + + if let Poll::Ready(_) = Pin::new(&mut fut).poll(cx) { + //? Resolved immediately, re-check + continue; + } + + self.pending_read = Some(fut); + return Poll::Pending; } } } @@ -241,6 +332,7 @@ impl AsyncWrite for SharedPipe { cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { + //? Drain existing promise if let Some(mut fut) = self.pending_write.take() { if Pin::new(&mut fut).poll(cx).is_pending() { self.pending_write = Some(fut); @@ -248,30 +340,34 @@ impl AsyncWrite for SharedPipe { } } - match self.write(buf) { - Ok(n) => Poll::Ready(Ok(n)), - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - // Writer waits for READ_IDX to change (meaning space freed up) - let current_r = self.read_idx() as i32; - let res: WaitAsyncResult = Atomics::wait_async(&self.header, READ_IDX, current_r) - .expect("waitAsync support required") - .unchecked_into(); - - if !res.is_async() { - cx.waker().wake_by_ref(); - return Poll::Pending; - } + loop { + let (r, w) = (self.read_idx(), self.write_idx()); - let mut fut = JsFuture::from(res.value().unchecked_into::()); - if Pin::new(&mut fut).poll(cx).is_pending() { - self.pending_write = Some(fut); - return Poll::Pending; - } + if let Some(n) = self.try_write(r, w, buf) { + self.set_write((w + n as u32) % self.capacity); + return Poll::Ready(Ok(n)); + } - cx.waker().wake_by_ref(); - Poll::Pending + //? Wait for READ_IDX to change + let res: WaitAsyncResult = Atomics::wait_async(&self.header, READ_IDX, r as i32) + .expect("waitAsync support required") + .unchecked_into(); + + if !res.is_async() { + //? READ_IDX changed, re-check + continue; } - Err(e) => Poll::Ready(Err(e)), + + let promise = res.value().unchecked_into::(); + let mut fut = JsFuture::from(promise); + + if let Poll::Ready(_) = Pin::new(&mut fut).poll(cx) { + //? Resolved immediately, re-check + continue; + } + + self.pending_write = Some(fut); + return Poll::Pending; } } @@ -296,7 +392,7 @@ mod tests { #[wasm_bindgen_test] fn test_basic_read_write() { - let mut pipe = SharedPipe::new_with_capacity(10); + let mut pipe = SharedPipe::new_with_capacity("test", 10); let input = b"hello"; let written = pipe.write(input).expect("Write failed"); @@ -321,7 +417,7 @@ mod tests { #[wasm_bindgen_test] fn test_wrap_around_saturation() { - let mut pipe = SharedPipe::new_with_capacity(10); + let mut pipe = SharedPipe::new_with_capacity("test", 10); // 1. Fill 7 bytes pipe.write(b"hello_w").unwrap(); diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_loader.js b/crates/wasmi-plugin-hdk/src/runtime/worker_loader.js index ff08796..7850b40 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_loader.js +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_loader.js @@ -1,8 +1,25 @@ +function getTimestamp() { + const now = new Date(); + + const YYYY = now.getFullYear(); + const MM = String(now.getMonth() + 1).padStart(2, '0'); + const DD = String(now.getDate()).padStart(2, '0'); + const hh = String(now.getHours()).padStart(2, '0'); + const mm = String(now.getMinutes()).padStart(2, '0'); + const ss = String(now.getSeconds()).padStart(2, '0'); + const ms = String(now.getMilliseconds()).padStart(3, '0'); + + return `${YYYY}-${MM}-${DD} ${hh}:${mm}:${ss}.${ms}`; +} + console.log = function (...args) { - self.postMessage({ type: 'Log', message: args.join(' ') }); + const prefix = `[${getTimestamp()}]`; + self.postMessage({ type: 'Log', message: `${prefix} ${args.join(' ')}` }); }; + console.error = function (...args) { - self.postMessage({ type: 'Log', message: args.join(' ') }); + const prefix = `[${getTimestamp()}] [ERROR]`; + self.postMessage({ type: 'Log', message: `${prefix} ${args.join(' ')}` }); }; console.log("Worker Shim: Script starting..."); diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs index b7ca5c6..40693cb 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs @@ -36,7 +36,6 @@ struct WorkerHandle { inner: web_sys::Worker, stdin: SharedArrayBuffer, stdout: SharedArrayBuffer, - stderr: SharedArrayBuffer, //? Store handle to prevent javascript garbage collection _on_message: Closure, @@ -79,17 +78,15 @@ impl WorkerPool { pub async fn run( &mut self, compiled: Compiled, - ) -> Result<(WorkerSession, SharedPipe, SharedPipe, SharedPipe), PoolError> { + ) -> Result<(WorkerSession, SharedPipe, SharedPipe), PoolError> { let worker = self.get_or_create_worker().await?; - let stdin = SharedPipe::new(&worker.stdin); - let stdout = SharedPipe::new(&worker.stdout); - let stderr = SharedPipe::new(&worker.stderr); + let stdin = SharedPipe::new("HOST_STDIN", &worker.stdin); + let stdout = SharedPipe::new("HOST_STDOUT", &worker.stdout); // Reset the header (read/write pointers) in the shared memory stdin.reset(); stdout.reset(); - stderr.reset(); worker.run(compiled)?; @@ -100,7 +97,6 @@ impl WorkerPool { }, stdin, stdout, - stderr, )) } @@ -139,9 +135,8 @@ impl WorkerHandle { let inner = web_sys::Worker::new(script_url).map_err(PoolError::JsValue)?; let state = Arc::new(Mutex::new(WorkerState::Idle)); - let stdin = SharedPipe::new_with_capacity(SHARED_PIPE_CAPACITY).buffer(); - let stdout = SharedPipe::new_with_capacity(SHARED_PIPE_CAPACITY).buffer(); - let stderr = SharedPipe::new_with_capacity(SHARED_PIPE_CAPACITY).buffer(); + let stdin = SharedPipe::new_with_capacity("HOST_STDIN", SHARED_PIPE_CAPACITY).buffer(); + let stdout = SharedPipe::new_with_capacity("HOST_STDOUT", SHARED_PIPE_CAPACITY).buffer(); let (booted_tx, booted_rx) = oneshot::channel(); let (init_tx, init_rx) = oneshot::channel(); @@ -159,7 +154,6 @@ impl WorkerHandle { inner, stdin, stdout, - stderr, _on_message: on_message, }; @@ -185,6 +179,7 @@ impl WorkerHandle { let wasm_bytes = compiled.module.serialize()?; let msg = serde_wasm_bindgen::to_value(&WorkerMessage::Load { wasm_module: wasm_bytes.to_vec(), + name: compiled.name.clone(), })?; // Transfer WASM buffer to worker @@ -201,6 +196,13 @@ impl WorkerHandle { } pub fn terminate(&self) { + //? Create temp SharedPipe instances to dump their state to logs + // let stdin = SharedPipe::new("DUMP_STDIN", &self.stdin); + // let stdout = SharedPipe::new("DUMP_STDOUT", &self.stdout); + + // stdin.dump(); + // stdout.dump(); + self.inner.terminate(); self.set_state(WorkerState::Terminated); } @@ -226,6 +228,9 @@ impl WorkerHandle { WorkerMessage::Log { message } => { info!("[WORKER_{}] {}", id, message); } + WorkerMessage::PluginLog { message } => { + info!("[PLUGIN_{}] {}", id, message); + } WorkerMessage::Booted => { if let Some(tx) = booted_tx.take() { info!("[WORKER_{}] Booted", id); @@ -258,7 +263,6 @@ impl WorkerHandle { Reflect::set(&init_msg, &"stdin".into(), &self.stdin).map_err(PoolError::JsValue)?; Reflect::set(&init_msg, &"stdout".into(), &self.stdout).map_err(PoolError::JsValue)?; - Reflect::set(&init_msg, &"stderr".into(), &self.stderr).map_err(PoolError::JsValue)?; self.inner .post_message(&init_msg) diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_protocol.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_protocol.rs index ae98067..cdda608 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_protocol.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_protocol.rs @@ -15,9 +15,13 @@ pub enum WorkerMessage { /// Serialized wasm module. Deserialize with `wasmer::Module::deserialize` #[serde(with = "serde_bytes")] wasm_module: Vec, + name: String, }, Log { message: String, }, + PluginLog { + message: String, + }, Idle, } diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index e27402b..7496f02 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -3,16 +3,11 @@ use std::collections::HashMap; use std::sync::{Arc, OnceLock}; -use futures::io::BufReader; use futures::lock::Mutex; -use futures::{AsyncBufReadExt, AsyncRead, AsyncWrite}; -use tracing::{error, info}; -use wasm_bindgen_futures::spawn_local; -use wasmi_plugin_rt::yield_now; +use futures::{AsyncRead, AsyncWrite}; use crate::compile::Compiled; use crate::runtime::Runtime; -use crate::runtime::shared_pipe::SharedPipe; use crate::runtime::worker_pool::{PoolError, WorkerId, WorkerPool, WorkerSession}; static POOL: OnceLock> = OnceLock::new(); @@ -53,8 +48,7 @@ impl Runtime for WorkerRuntime { ), Self::Error, > { - let name = compiled.name.clone(); - let (session, stdin, stdout, stderr) = get_pool().lock().await.run(compiled).await?; + let (session, stdin, stdout) = get_pool().lock().await.run(compiled).await?; let instance_id = session.id; self.active_sessions @@ -62,8 +56,6 @@ impl Runtime for WorkerRuntime { .await .insert(instance_id, session); - Self::spawn_stderr(name, stderr); - Ok((instance_id as u64, stdin, stdout)) } @@ -74,31 +66,3 @@ impl Runtime for WorkerRuntime { } } } - -impl WorkerRuntime { - fn spawn_stderr(name: String, mut stderr_reader: SharedPipe) { - spawn_local(async move { - let mut stderr = BufReader::new(&mut stderr_reader); - let mut buffer = String::new(); - loop { - buffer.clear(); - match stderr.read_line(&mut buffer).await { - Ok(0) => { - break; - } - Ok(_) => { - info!("[plugin] [{}] {}", name, buffer.trim_end()); - } - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - yield_now().await; - continue; - } - Err(e) => { - error!("[WorkerBridge] Error reading from stderr: {}", e); - break; - } - } - } - }); - } -} diff --git a/crates/wasmi-plugin-hdk/src/transport.rs b/crates/wasmi-plugin-hdk/src/transport.rs index fe29bfe..f25bb8e 100644 --- a/crates/wasmi-plugin-hdk/src/transport.rs +++ b/crates/wasmi-plugin-hdk/src/transport.rs @@ -1,21 +1,16 @@ -use std::collections::HashMap; - -use futures::{ - AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, channel::oneshot, io::BufReader, -}; +use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use serde_json::Value; use thiserror::Error; use tracing::info; use wasmi_plugin_pdk::{ api::RequestHandler, - rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcRequest, RpcResponse}, + rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcResponse}, }; +use wasmi_plugin_rt::yield_now; pub struct Transport { - reader: BufReader, + reader: R, writer: W, - pending: HashMap>>, - next_id: u64, } #[derive(Debug, Error)] @@ -31,19 +26,11 @@ pub enum TransportError { #[error("Error Response: {0:?}")] ErrorResponse(RpcErrorResponse), - - #[error("Cancelled")] - Cancelled(#[from] oneshot::Canceled), } impl Transport { pub fn new(reader: R, writer: W) -> Self { - Transport { - reader: BufReader::new(reader), - writer, - pending: HashMap::new(), - next_id: 0, - } + Transport { reader, writer } } pub async fn call( @@ -52,28 +39,44 @@ impl Transport { params: Value, handler: Option>, ) -> Result { - let id = self.next_id(); + let id = 1; let request = RpcMessage::request(id, method.to_string(), params); self.write_message(request).await?; - let (resp_tx, mut resp_rx) = oneshot::channel(); - self.pending.insert(id, resp_tx); + let mut buffer = Vec::new(); + let mut temp = [0u8; 256]; - let mut line = String::new(); loop { - match self.reader.read_line(&mut line).await { + match self.reader.read(&mut temp).await { Ok(0) => return Err(TransportError::Eof), - Ok(_) => {} + Ok(n) => { + buffer.extend_from_slice(&temp[..n]); + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + yield_now().await; + continue; + } Err(e) => return Err(TransportError::Io(e)), } - let msg: RpcMessage = serde_json::from_str(&line)?; - line.clear(); - self.handle_incoming(msg, &handler).await?; + // Process complete lines + while let Some(pos) = buffer.iter().position(|&b| b == b'\n') { + let line = String::from_utf8_lossy(&buffer[..pos]).to_string(); + buffer.drain(..=pos); + + let msg: RpcMessage = serde_json::from_str(&line)?; - if let Some(res) = resp_rx.try_recv()? { - let resp = res.map_err(TransportError::ErrorResponse)?; - return Ok(resp); + match msg { + RpcMessage::RpcResponse(resp) if resp.id == id => return Ok(resp), + RpcMessage::RpcErrorResponse(err) if err.id == id => { + return Err(TransportError::ErrorResponse(err)); + } + RpcMessage::RpcRequest(req) => { + self.handle_guest_req(req.id, &req.method, req.params, &handler) + .await?; + } + _ => {} + } } } } @@ -87,44 +90,28 @@ impl Transport { Ok(()) } - async fn handle_incoming( + async fn handle_guest_req( &mut self, - msg: RpcMessage, + id: u64, + method: &str, + params: Value, handler: &Option>, ) -> Result<(), TransportError> { - match msg { - RpcMessage::RpcResponse(resp) => { - self.pending.remove(&resp.id).map(|tx| tx.send(Ok(resp))); + let Some(handler) = handler else { + info!("No handler, request ignored"); + return Ok(()); + }; + + match handler.handle(method, params).await { + Ok(result) => { + self.write_message(RpcMessage::response(id, result)).await?; } - RpcMessage::RpcErrorResponse(err) => { - self.pending.remove(&err.id).map(|tx| tx.send(Err(err))); - } - RpcMessage::RpcRequest(RpcRequest { - id, method, params, .. - }) => { - let Some(handler) = handler else { - info!("No handler, request ignored"); - return Ok(()); - }; - - match handler.handle(&method, params).await { - Ok(result) => { - self.write_message(RpcMessage::response(id, result)).await?; - } - Err(error) => { - self.write_message(RpcMessage::error_response(id, error)) - .await?; - } - } + Err(error) => { + self.write_message(RpcMessage::error_response(id, error)) + .await?; } } Ok(()) } - - fn next_id(&mut self) -> u64 { - let id = self.next_id; - self.next_id += 1; - id - } } diff --git a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs index 4c8f8a9..58c9563 100644 --- a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs +++ b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs @@ -1,4 +1,4 @@ -use std::io::{BufRead, Read, Write}; +use std::io::{BufReader, Read, Write}; use thiserror::Error; use tracing::{error, info, trace, warn}; use wasmer::{FunctionEnvMut, RuntimeError}; @@ -6,6 +6,14 @@ use web_time::{Duration, Instant, SystemTime}; use crate::time::blocking_sleep; +pub trait WasiReader: Read + Send + Sync { + /// Blocks the current thread until the reader is ready to read, or the timeout is reached. + fn wait_ready(&self, timeout: Option); + + /// Returns true if the reader is ready to read without blocking. + fn is_ready(&self) -> bool; +} + /// A WASI context that can be attached to a wasmi instance. Attaches /// a subset of WASI syscalls to the instance, allowing it to /// get args, env vars, read/write to stdin/stdout/stderr, get time, and @@ -18,7 +26,7 @@ use crate::time::blocking_sleep; pub struct WasiCtx { args: Vec, env: Vec, - stdin_reader: Option>, + stdin_reader: Option>>, stdout_writer: Option>, stderr_writer: Option>, @@ -74,7 +82,7 @@ impl WasiCtx { self } - pub fn set_stdin(mut self, reader: R) -> Self { + pub fn set_stdin(mut self, reader: R) -> Self { let reader = std::io::BufReader::new(reader); self.stdin_reader = Some(Box::new(reader)); self @@ -371,6 +379,10 @@ pub fn fd_write( break; // partial write, stop } } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // Spin until writable + continue; + } Err(_) => return Errno::Io as i32, } } @@ -499,6 +511,57 @@ pub fn random_get(mut env: FunctionEnvMut, buf_ptr: i32, buf_len: i32) } const CLOCK_EVENT_TYPE: u8 = 0; +const FD_READ_EVENT_TYPE: u8 = 1; +#[allow(dead_code)] +const FD_WRITE_EVENT_TYPE: u8 = 2; + +#[repr(C, packed)] +#[derive(Copy, Clone)] +struct WasiSubscription { + userdata: u64, + type_: u8, + _padding: [u8; 7], + union: WasiSubscriptionUnion, +} + +#[repr(C, packed)] +#[derive(Copy, Clone)] +union WasiSubscriptionUnion { + clock: WasiSubscriptionClock, + fd_readwrite: WasiSubscriptionFdReadWrite, +} + +#[repr(C, packed)] +#[derive(Copy, Clone)] +struct WasiSubscriptionClock { + id: u32, + _padding: [u8; 4], + timeout: u64, + precision: u64, + flags: u16, +} + +#[repr(C, packed)] +#[derive(Copy, Clone)] +struct WasiSubscriptionFdReadWrite { + fd: u32, +} + +#[repr(C, packed)] +struct WasiEvent { + userdata: u64, + error: u16, + type_: u8, + _padding: [u8; 5], + fd_readwrite: WasiEventFdReadWrite, +} + +#[repr(C, packed)] +struct WasiEventFdReadWrite { + nbytes: u64, + flags: u16, + _padding: [u8; 6], +} pub fn poll_oneoff( mut env: FunctionEnvMut, @@ -521,117 +584,141 @@ pub fn poll_oneoff( let memory = ctx.memory.as_ref().expect("Memory not initialized"); let view = memory.view(&store); - let mut clock_subscriptions = Vec::new(); + //? Currently only supports a single clock / stdin subscription, and doesn't + //? support stdout. Takes the earliest clock and the last stdin subscription. + + // TODO: Add stdout support (via similar wait_ready mechanism as stdin) + // TODO: Support multiple file descriptors concurrently. Perhaps change + // the wait_* mechanism to async and select across multiple futures? + + let mut stdin_sub: Option = None; + let mut stdin_userdata: u64 = 0; + + // Time at which earliest clock sub will resolve + let mut earliest_clock_sub_time: Option = None; + let mut earliest_clock_sub: Option = None; + let mut clock_userdata: u64 = 0; for i in 0..nsubscriptions { // Each subscription is 48 bytes let sub_offset = (subs_ptr as usize) + (i as usize * 48); - - // Read the subscription struct - // subscription layout (48 bytes total): - // - userdata: u64 at offset 0 - // - type: u8 at offset 8 (eventtype: 0=clock, 1=fd_read, 2=fd_write) - // - padding: 7 bytes - // - union data: 32 bytes at offset 16 - // - // For clock subscription (subscription_clock at offset 16): - // - id: u32 (clockid) at offset 16 - // - padding: 4 bytes - // - timeout: u64 (timestamp) at offset 24 - // - precision: u64 (timestamp) at offset 32 - // - flags: u16 (subclockflags) at offset 40 - let mut sub_bytes = [0u8; 48]; view.read(sub_offset as u64, &mut sub_bytes).unwrap(); - let userdata = u64::from_le_bytes(sub_bytes[0..8].try_into().unwrap()); - let sub_type = sub_bytes[8]; - - if sub_type != CLOCK_EVENT_TYPE { - warn!( - "poll_oneoff: only clock subscriptions supported, got type {}", - sub_type - ); - continue; - } - - let clock_id = u32::from_le_bytes(sub_bytes[16..20].try_into().unwrap()); - let timeout_ns = u64::from_le_bytes(sub_bytes[24..32].try_into().unwrap()); - let precision_ns = u64::from_le_bytes(sub_bytes[32..40].try_into().unwrap()); - let flags = u16::from_le_bytes(sub_bytes[40..42].try_into().unwrap()); + // Cast bytes to struct + let sub = unsafe { &*(sub_bytes.as_ptr() as *const WasiSubscription) }; + + match sub.type_ { + CLOCK_EVENT_TYPE => { + let clock = unsafe { sub.union.clock }; + let is_absolute = (clock.flags & 1) != 0; + + //? Convert to absolute time if required + let timeout_ns = if is_absolute { + clock.timeout + } else { + match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { + Ok(dur) => dur.as_nanos() as u64 + clock.timeout, + Err(_) => return Errno::Inval as i32, // time before epoch shouldn't happen + } + }; + + //? Keep the earliest clock subscription + if earliest_clock_sub_time.is_none() + || timeout_ns < earliest_clock_sub_time.unwrap() + { + earliest_clock_sub_time = Some(timeout_ns); + earliest_clock_sub = Some(clock); + clock_userdata = sub.userdata; + } + } + FD_READ_EVENT_TYPE => { + let fd_read = unsafe { sub.union.fd_readwrite }; - if clock_id > 1 { - // Only realtime (0) and monotonic (1) - warn!("poll_oneoff: unsupported clock_id {}", clock_id); - continue; + // Only support stdin (fd 0) for now + if fd_read.fd != 0 { + warn!("poll_oneoff: only fd_read on stdin (fd 0) is supported"); + continue; + } + stdin_sub = Some(fd_read); + stdin_userdata = sub.userdata; + } + _ => { + warn!("poll_oneoff: unsupported subscription type {}", sub.type_); + continue; + } } - - let is_absolute = (flags & 1) != 0; - trace!( - "poll_oneoff: clock_id={}, timeout={}, precision={}, flags={:#x}, is_absolute={}", - clock_id, timeout_ns, precision_ns, flags, is_absolute - ); - - clock_subscriptions.push((userdata, clock_id, timeout_ns, is_absolute)); } - info!( - "poll_oneoff: {} clock subscriptions", - clock_subscriptions.len() - ); - - if clock_subscriptions.is_empty() { - warn!("poll_oneoff: no valid clock subscriptions found"); + //? If no subscriptions were found, return error + if stdin_sub.is_none() && earliest_clock_sub.is_none() { + warn!("poll_oneoff: no valid subscriptions found"); return Errno::Inval as i32; } - // Convert all relative timeouts to absolute deadlines - let now_ns = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { - Ok(dur) => dur.as_nanos() as u64, - Err(_) => return Errno::Inval as i32, // time before epoch shouldn't happen - }; - - clock_subscriptions.iter_mut().for_each(|sub| { - if !sub.3 { - sub.2 = now_ns.saturating_add(sub.2); + let clock_duration = earliest_clock_sub_time.map(|deadline| { + let now_ns = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + if deadline > now_ns { + Duration::from_nanos(deadline - now_ns) + } else { + Duration::from_nanos(0) } }); - // Find the earliest deadline - let (earliest_userdata, _earliest_clock_id, earliest_deadline_ns, _) = - clock_subscriptions.iter().min_by_key(|sub| sub.2).unwrap(); - - // Just blocking sleep until the earliest deadline is hit. - if earliest_deadline_ns > &now_ns { - let wait_duration = Duration::from_nanos(earliest_deadline_ns - now_ns); - - blocking_sleep(wait_duration); + //? If there's a stdin subscription, wait on that with the clock as timeout. Otherwise just wait on the clock. + if stdin_sub.is_some() + && let Some(reader) = &ctx.stdin_reader + { + trace!("poll_oneoff: waiting for stdin or clock timeout..."); + let reader = reader.get_ref(); + reader.wait_ready(clock_duration); + } else if let Some(duration) = clock_duration { + trace!("poll_oneoff: waiting for clock timeout..."); + blocking_sleep(duration); } - // Write the event for the earliest deadline - // size: 32 - // - userdata: at offset 0 - // - error: at offset 8 - // - type: at offset 10 - // - fd_readwrite: at offset 16 (empty for clock events) - let mut event_buf = [0u8; 32]; - event_buf[0..8].copy_from_slice(&earliest_userdata.to_le_bytes()); - event_buf[8..10].copy_from_slice(&(Errno::Success as u16).to_le_bytes()); - event_buf[10] = CLOCK_EVENT_TYPE; - // fd_readwrite is empty for clock events - - info!( - "Writing event: userdata={}, error={}, type={}", - earliest_userdata, - Errno::Success as u16, - CLOCK_EVENT_TYPE - ); - info!("Event buffer: {:?}", &event_buf[..16]); + let mut events_written = 0; + + // 1. Check if Stdin is ready (Data available or Pipe Closed) + if let Some(_sub) = stdin_sub { + if let Some(reader) = &ctx.stdin_reader { + if reader.get_ref().is_ready() { + write_event( + &view, + events_ptr, + events_written, + stdin_userdata, + FD_READ_EVENT_TYPE, + ); + events_written += 1; + } + } + } - let event_offset = events_ptr as usize; - view.write(event_offset as u64, &event_buf).unwrap(); + // 2. Check if Clock has expired + if let Some(deadline_ns) = earliest_clock_sub_time { + let now_ns = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + + if now_ns >= deadline_ns { + write_event( + &view, + events_ptr, + events_written, + clock_userdata, + CLOCK_EVENT_TYPE, + ); + events_written += 1; + } + } - // Write number of events (1) into result_ptr - view.write(result_ptr as u64, &1u32.to_le_bytes()).unwrap(); + // Write number of events triggered to result_ptr + view.write(result_ptr as u64, &events_written.to_le_bytes()) + .unwrap(); Errno::Success as i32 } @@ -662,6 +749,29 @@ pub fn proc_exit(_: FunctionEnvMut, status: i32) -> Result<(), RuntimeE Err(RuntimeError::new(format!("exit: {}", status))) } +fn write_event(view: &wasmer::MemoryView, events_ptr: i32, index: u32, userdata: u64, type_: u8) { + let event = WasiEvent { + userdata, + error: 0, // Errno::Success + type_, + _padding: [0; 5], + fd_readwrite: WasiEventFdReadWrite { + nbytes: 0, + flags: 0, + _padding: [0; 6], + }, + }; + + let offset = (events_ptr as u64) + (index as u64 * 32); + let bytes: [u8; 32] = unsafe { std::mem::transmute(event) }; + view.write(offset, &bytes).unwrap(); + + trace!( + "poll_oneoff: Wrote event type {} for userdata {}", + type_, userdata + ); +} + fn write_pointer_and_string( view: &wasmer::MemoryView, table_ptr: &mut u64, diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index d94fe90..14d80f3 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -2,7 +2,6 @@ use serde_json::Value; use std::sync::{Arc, Once}; use tracing::info; use wasmi_plugin_hdk::{plugin::Plugin, server::HostServer}; -use web_time::{Instant, SystemTime}; #[cfg(target_family = "wasm")] use wasm_bindgen_test::*; @@ -35,7 +34,9 @@ fn setup_logs() { { INIT.call_once(|| { tracing_subscriber::fmt() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .compact() + .with_ansi(false) + .with_max_level(tracing_subscriber::filter::LevelFilter::INFO) .with_test_writer() .init(); }); @@ -98,7 +99,7 @@ async fn test_get_time() { info!("Get time response: {:?}", response); let timestamp = response.result.as_u64().unwrap(); - let now = SystemTime::now() + let now = web_time::SystemTime::now() .duration_since(web_time::UNIX_EPOCH) .unwrap() .as_secs(); @@ -120,7 +121,7 @@ async fn test_sleep() { let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); let sleep_duration = 1500; // milliseconds - let start = Instant::now(); + let start = web_time::Instant::now(); plugin .call("sleep", Value::Number(sleep_duration.into())) .await @@ -158,9 +159,11 @@ async fn test_call_many() { let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + let plugin = Plugin::new("test_plugin", wasm_bytes, handler) + .unwrap() + .with_timeout(web_time::Duration::from_secs(2)); plugin - .call("call_many", Value::Number(200.into())) + .call("call_many", Value::Number(500.into())) .await .unwrap(); } @@ -179,6 +182,24 @@ async fn test_call_async() { assert_eq!(resp.result.as_str().unwrap(), "pong"); } +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_call_many_async() { + setup_logs(); + info!("Starting call_many_async test..."); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + + let plugin = Plugin::new("test_plugin", wasm_bytes, handler) + .unwrap() + .with_timeout(web_time::Duration::from_secs(2)); + plugin + .call("call_many_async", Value::Number(500.into())) + .await + .unwrap(); +} + #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] #[cfg_attr(not(target_family = "wasm"), tokio::test)] async fn test_prime_sieve() { diff --git a/crates/wasmi-plugin-pdk/src/lib.rs b/crates/wasmi-plugin-pdk/src/lib.rs index d0afbc3..c828391 100644 --- a/crates/wasmi-plugin-pdk/src/lib.rs +++ b/crates/wasmi-plugin-pdk/src/lib.rs @@ -1,4 +1,5 @@ pub mod api; +mod poll_oneoff; pub mod router; pub mod rpc_message; pub mod server; diff --git a/crates/wasmi-plugin-pdk/src/poll_oneoff.rs b/crates/wasmi-plugin-pdk/src/poll_oneoff.rs new file mode 100644 index 0000000..f170b1b --- /dev/null +++ b/crates/wasmi-plugin-pdk/src/poll_oneoff.rs @@ -0,0 +1,22 @@ +#[link(wasm_import_module = "wasi_snapshot_preview1")] +unsafe extern "C" { + fn poll_oneoff(subs: *const u8, events: *mut u8, nsubs: usize, nevents: *mut usize) -> u16; +} + +/// A helper that uses WASI poll_oneoff to sleep the thread until STDIN is ready +pub fn wait_for_stdin() { + let mut sub_bytes = [0u8; 48]; + let mut event_bytes = [0u8; 32]; + let mut nevents: usize = 0; + + sub_bytes[8] = 1; + + unsafe { + poll_oneoff( + sub_bytes.as_ptr(), + event_bytes.as_mut_ptr(), + 1, + &mut nevents, + ); + } +} diff --git a/crates/wasmi-plugin-pdk/src/server.rs b/crates/wasmi-plugin-pdk/src/server.rs index f598de8..fb5ad82 100644 --- a/crates/wasmi-plugin-pdk/src/server.rs +++ b/crates/wasmi-plugin-pdk/src/server.rs @@ -1,4 +1,4 @@ -use std::io::{BufRead, BufReader, Write}; +use std::io::{BufRead, BufReader}; use futures::{FutureExt, select}; use serde::{Serialize, de::DeserializeOwned}; @@ -30,6 +30,9 @@ enum PluginServerError { #[error("RPC error: {0}")] Rpc(#[from] RpcError), + + #[error("Transport driver error: {0}")] + TransportDriver(#[from] crate::transport_driver::DriverError), } #[allow(clippy::new_without_default)] @@ -65,8 +68,8 @@ impl PluginServer { let request = self.read_request()?; info!("PluginServer: Received request: {:?}", request); - let (transport, driver) = Transport::new(std::io::stdin(), std::io::stdout()); - let resp = futures::executor::block_on(async move { + let (transport, mut driver) = Transport::new(std::io::stdin(), std::io::stdout()); + let resp = futures::executor::block_on(async { select! { res = self.router.handle_with_state(transport, &request.method, request.params).fuse() => { res @@ -83,11 +86,7 @@ impl PluginServer { Err(error) => RpcMessage::error_response(request.id, error), }; info!("PluginServer: Sending response: {:?}", resp); - - let serialized = serde_json::to_string(&resp)?; - let msg = format!("{}\n", serialized); - std::io::stdout().write_all(msg.as_bytes())?; - std::io::stdout().flush()?; + driver.write_message(&resp)?; Ok(()) } diff --git a/crates/wasmi-plugin-pdk/src/transport_driver.rs b/crates/wasmi-plugin-pdk/src/transport_driver.rs index acf1e37..be24de5 100644 --- a/crates/wasmi-plugin-pdk/src/transport_driver.rs +++ b/crates/wasmi-plugin-pdk/src/transport_driver.rs @@ -10,7 +10,10 @@ use thiserror::Error; use tracing::info; use wasmi_plugin_rt::yield_now; -use crate::rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcResponse}; +use crate::{ + poll_oneoff::wait_for_stdin, + rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcResponse}, +}; pub struct TransportDriver { reader: Arc>>, @@ -74,7 +77,7 @@ impl TransportDriver { } } - pub async fn run(self) -> Result<(), DriverError> { + pub async fn run(&mut self) -> Result<(), DriverError> { let mut line = String::new(); loop { @@ -117,7 +120,7 @@ impl TransportDriver { // Drive IO until we get the response loop { - self.step(&mut reader, &mut line)?; + let would_block = self.step(&mut reader, &mut line)?; // Check if we have received the response match res_rx.try_recv() { @@ -125,7 +128,11 @@ impl TransportDriver { let res = res.map_err(DriverError::ErrorResponse)?; return Ok(res); } - Ok(None) => {} + Ok(None) => { + if would_block { + wait_for_stdin(); + } + } Err(e) => { return Err(DriverError::OneshotCanceled(e)); } @@ -134,16 +141,17 @@ impl TransportDriver { } /// Perform a single read step, processing one incoming message if available. - /// Returns immediately if no message is available. + /// Returns Ok(true) if no message was available (WouldBlock) or Ok(false) + /// if a message was processed. fn step( &self, reader: &mut std::sync::MutexGuard<'_, BufReader>, line: &mut String, - ) -> Result<(), DriverError> { + ) -> Result { match reader.read_line(line) { Ok(0) => return Err(DriverError::Eof), Ok(_) => {} - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(true), Err(e) => return Err(DriverError::Io(e)), } @@ -151,11 +159,11 @@ impl TransportDriver { line.clear(); self.insert_response(msg)?; - Ok(()) + Ok(false) } // TODO: Catch WouldBlock and retry later - fn write_message(&self, message: &RpcMessage) -> Result<(), DriverError> { + pub fn write_message(&self, message: &RpcMessage) -> Result<(), DriverError> { let message_str = serde_json::to_string(message)?; let msg = format!("{}\n", message_str); { diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml index e820671..2846e6a 100644 --- a/crates/worker/Cargo.toml +++ b/crates/worker/Cargo.toml @@ -16,6 +16,9 @@ exclude.workspace = true crate-type = ["cdylib"] [target.'cfg(target_arch = "wasm32")'.dependencies] +chrono = { workspace = true, features = ["wasmbind"] } +tracing-wasm = { workspace = true } +tracing = { workspace = true } futures = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index 2c89f50..e03da7f 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -3,6 +3,7 @@ use std::sync::OnceLock; use thiserror::Error; +use tracing::{Level, error, info}; use wasm_bindgen::{ JsCast, JsValue, prelude::{Closure, wasm_bindgen}, @@ -12,11 +13,13 @@ use wasmi_plugin_hdk::{ wasi::wasi_ctx::{self, WasiCtx}, }; use web_sys::{ - DedicatedWorkerGlobalScope, MessageEvent, console, + DedicatedWorkerGlobalScope, MessageEvent, js_sys::{self, SharedArrayBuffer}, }; -static BUFFERS: OnceLock<(SAB, SAB, SAB)> = OnceLock::new(); +mod message_writer; + +static BUFFERS: OnceLock<(SAB, SAB)> = OnceLock::new(); struct SAB(SharedArrayBuffer); @@ -35,7 +38,14 @@ enum WorkerError { #[wasm_bindgen] pub fn start_worker() { - log("Worker: Starting up..."); + tracing_wasm::set_as_global_default_with_config( + tracing_wasm::WASMLayerConfigBuilder::new() + .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) + .set_max_level(Level::INFO) + .build(), + ); + + info!("Worker: Starting up"); let global = js_sys::global().unchecked_into::(); // Register onmessage with channels @@ -55,7 +65,7 @@ fn on_message(e: MessageEvent) { // Use serde to parse the variant. // If it's "Initialize", it won't have fields in the enum, so we use Reflect. let Ok(msg) = serde_wasm_bindgen::from_value::(data.clone()) else { - error("Worker: Failed to deserialize message"); + error!("Worker: Failed to deserialize message"); return; }; @@ -64,8 +74,8 @@ fn on_message(e: MessageEvent) { on_initialize(&global, data); } - WorkerMessage::Load { wasm_module } => { - on_load(global, wasm_module); + WorkerMessage::Load { wasm_module, name } => { + on_load(global, wasm_module, name); } _ => {} } @@ -74,29 +84,28 @@ fn on_message(e: MessageEvent) { fn on_initialize(global: &DedicatedWorkerGlobalScope, data: JsValue) { let stdin_buf = get_sab(&data, "stdin"); let stdout_buf = get_sab(&data, "stdout"); - let stderr_buf = get_sab(&data, "stderr"); - let (Some(si), Some(so), Some(se)) = (stdin_buf, stdout_buf, stderr_buf) else { - error("Worker: Initialize message missing SharedArrayBuffers"); + let (Some(si), Some(so)) = (stdin_buf, stdout_buf) else { + error!("Worker: Initialize message missing SharedArrayBuffers"); return; }; - if BUFFERS.set((SAB(si), SAB(so), SAB(se))).is_err() { - log("Worker: Warning - Attempted to re-initialize buffers"); + if BUFFERS.set((SAB(si), SAB(so))).is_err() { + info!("Worker: Warning - Attempted to re-initialize buffers"); } let msg = serde_wasm_bindgen::to_value(&WorkerMessage::Initialized).unwrap(); global.post_message(&msg).unwrap(); } -fn on_load(global: DedicatedWorkerGlobalScope, wasm_module: Vec) { - let Some((stdin, stdout, stderr)) = BUFFERS.get() else { - error("Worker: Received Load message before pipes were initialized"); +fn on_load(global: DedicatedWorkerGlobalScope, wasm_module: Vec, name: String) { + let Some((stdin, stdout)) = BUFFERS.get() else { + error!("Worker: Received Load message before pipes were initialized"); return; }; - if let Err(err) = run_instance(&wasm_module, &stdin.0, &stdout.0, &stderr.0) { - error(&format!("Worker: Run error: {:?}", err)); + if let Err(err) = run_instance(&wasm_module, name, &stdin.0, &stdout.0) { + error!("Worker: Run error: {:?}", err); } let idle_msg = serde_wasm_bindgen::to_value(&WorkerMessage::Idle).unwrap(); @@ -111,13 +120,13 @@ fn get_sab(obj: &JsValue, key: &str) -> Option { fn run_instance( wasm_module: &[u8], + name: String, stdin: &SharedArrayBuffer, stdout: &SharedArrayBuffer, - stderr: &SharedArrayBuffer, ) -> Result<(), WorkerError> { - let stdin = SharedPipe::new(stdin); - let stdout = SharedPipe::new(stdout); - let stderr = SharedPipe::new(stderr); + let stdin = SharedPipe::new("WORKER_STDIN", stdin); + let stdout = SharedPipe::new("WORKER_STDOUT", stdout); + let stderr = message_writer::MessageWriter::new(name); let mut store = wasmer::Store::default(); let module = unsafe { wasmer::Module::deserialize(&store, wasm_module) }?; @@ -131,11 +140,3 @@ fn run_instance( Ok(()) } - -fn log(s: &str) { - console::log_1(&s.into()); -} - -fn error(s: &str) { - console::error_1(&s.into()); -} diff --git a/crates/worker/src/message_writer.rs b/crates/worker/src/message_writer.rs new file mode 100644 index 0000000..e9fd33f --- /dev/null +++ b/crates/worker/src/message_writer.rs @@ -0,0 +1,37 @@ +use std::io::Write; + +use wasm_bindgen::JsCast; +use wasmi_plugin_hdk::runtime::worker_protocol::WorkerMessage; +use web_sys::js_sys; + +pub struct MessageWriter { + name: String, + global: web_sys::DedicatedWorkerGlobalScope, +} + +unsafe impl Send for MessageWriter {} +unsafe impl Sync for MessageWriter {} + +impl MessageWriter { + pub fn new(name: String) -> Self { + let global = js_sys::global().unchecked_into::(); + Self { name, global } + } +} + +impl Write for MessageWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let msg = String::from_utf8_lossy(buf).to_string(); + let now = chrono::Local::now(); + let timestamp = now.format("%Y-%m-%d %H:%M:%S%.3f").to_string(); + let msg = format!("[{}] [{}] {}", timestamp, &self.name, msg); + let msg = serde_wasm_bindgen::to_value(&WorkerMessage::PluginLog { message: msg }).unwrap(); + + self.global.post_message(&msg).unwrap(); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} From adf6a80d725bbf5362c5fe498caeb65ce11ce4d7 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Fri, 26 Dec 2025 02:07:47 -0500 Subject: [PATCH 25/70] todo --- crates/worker/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index e03da7f..0654414 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -126,6 +126,7 @@ fn run_instance( ) -> Result<(), WorkerError> { let stdin = SharedPipe::new("WORKER_STDIN", stdin); let stdout = SharedPipe::new("WORKER_STDOUT", stdout); + // TODO: Switch this back to SharedPipe for potentially increased performance? let stderr = message_writer::MessageWriter::new(name); let mut store = wasmer::Store::default(); From 1ba053c1018819e1a64066c4e24ac2a9b45cdd31 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Fri, 26 Dec 2025 11:13:31 -0500 Subject: [PATCH 26/70] fix: remove bench logs --- crates/wasmi-plugin-hdk/benches/benchmark.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/wasmi-plugin-hdk/benches/benchmark.rs b/crates/wasmi-plugin-hdk/benches/benchmark.rs index b6e889e..8bd6b83 100644 --- a/crates/wasmi-plugin-hdk/benches/benchmark.rs +++ b/crates/wasmi-plugin-hdk/benches/benchmark.rs @@ -30,12 +30,12 @@ mod benchmarks { fn setup_logs() { INIT.call_once(|| { - tracing_subscriber::fmt() - .compact() - .with_ansi(false) - .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE) - .with_test_writer() - .init(); + // tracing_subscriber::fmt() + // .compact() + // .with_ansi(false) + // .with_max_level(tracing_subscriber::filter::LevelFilter::INFO) + // .with_test_writer() + // .init(); }); } From 73d71b184c8a9d8ffa48cd9e537353ae0e3481f3 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Fri, 26 Dec 2025 11:17:01 -0500 Subject: [PATCH 27/70] clippy --- crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs | 25 ++++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs index 58c9563..5ce4497 100644 --- a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs +++ b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs @@ -682,19 +682,18 @@ pub fn poll_oneoff( let mut events_written = 0; // 1. Check if Stdin is ready (Data available or Pipe Closed) - if let Some(_sub) = stdin_sub { - if let Some(reader) = &ctx.stdin_reader { - if reader.get_ref().is_ready() { - write_event( - &view, - events_ptr, - events_written, - stdin_userdata, - FD_READ_EVENT_TYPE, - ); - events_written += 1; - } - } + if let Some(_sub) = stdin_sub + && let Some(reader) = &ctx.stdin_reader + && reader.get_ref().is_ready() + { + write_event( + &view, + events_ptr, + events_written, + stdin_userdata, + FD_READ_EVENT_TYPE, + ); + events_written += 1; } // 2. Check if Clock has expired From b595108b0ae1f7687fa5e6b7daaa91f4b8faf065 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Fri, 26 Dec 2025 22:20:43 -0500 Subject: [PATCH 28/70] feat: working worker pool --- .cargo/config.toml | 17 +- Cargo.lock | 133 ------ Cargo.toml | 2 - crates/test-plugin/Cargo.toml | 3 +- crates/wasmi-plugin-hdk/Cargo.toml | 6 +- crates/wasmi-plugin-hdk/benches/wasm_bench.rs | 29 +- crates/wasmi-plugin-hdk/src/compile.rs | 22 +- crates/wasmi-plugin-hdk/src/lib.rs | 2 +- crates/wasmi-plugin-hdk/src/plugin.rs | 6 +- crates/wasmi-plugin-hdk/src/runtime/mod.rs | 19 +- .../src/runtime/native_runtime.rs | 110 +++-- .../src/runtime/shared_pipe.rs | 104 ++--- .../src/runtime/thread_worker.rs | 14 + crates/wasmi-plugin-hdk/src/runtime/worker.js | 42 ++ .../src/runtime/worker_loader.js | 32 -- .../src/runtime/worker_message.rs | 13 + .../src/runtime/worker_pool.rs | 411 ++++++++---------- .../src/runtime/worker_protocol.rs | 27 -- .../src/runtime/worker_runtime.rs | 70 ++- crates/wasmi-plugin-hdk/tests/integration.rs | 290 ++++++------ crates/wasmi-plugin-hdk/tests/worker_test.rs | 41 ++ crates/worker/Cargo.toml | 35 -- crates/worker/src/lib.rs | 143 ------ crates/worker/src/message_writer.rs | 37 -- makefile | 29 +- shell.nix | 26 +- todo.md | 47 ++ 27 files changed, 731 insertions(+), 979 deletions(-) create mode 100644 crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs create mode 100644 crates/wasmi-plugin-hdk/src/runtime/worker.js delete mode 100644 crates/wasmi-plugin-hdk/src/runtime/worker_loader.js create mode 100644 crates/wasmi-plugin-hdk/src/runtime/worker_message.rs delete mode 100644 crates/wasmi-plugin-hdk/src/runtime/worker_protocol.rs create mode 100644 crates/wasmi-plugin-hdk/tests/worker_test.rs delete mode 100644 crates/worker/Cargo.toml delete mode 100644 crates/worker/src/lib.rs delete mode 100644 crates/worker/src/message_writer.rs create mode 100644 todo.md diff --git a/.cargo/config.toml b/.cargo/config.toml index a7dc7ad..850378a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,16 @@ [target.wasm32-unknown-unknown] -rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] -runner = 'wasm-bindgen-test-runner' \ No newline at end of file +rustflags = [ + "--cfg", "getrandom_backend=\"wasm_js\"", + "-C", "target-feature=+atomics,+bulk-memory", + "-C", "link-arg=--shared-memory", + "-C", "link-arg=--max-memory=1073741824", + "-C", "link-arg=--import-memory", + "-C", "link-arg=--export=__wasm_init_tls", + "-C", "link-arg=--export=__tls_size", + "-C", "link-arg=--export=__tls_align", + "-C", "link-arg=--export=__tls_base" +] +runner = "wasm-bindgen-test-runner" + +[unstable] +build-std = ["std", "panic_abort"] \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 200ec25..bb4e3ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,15 +47,6 @@ dependencies = [ "cc", ] -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anes" version = "0.1.6" @@ -241,19 +232,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - [[package]] name = "ciborium" version = "0.2.2" @@ -335,12 +313,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "corosensei" version = "0.3.2" @@ -949,30 +921,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -987,8 +935,6 @@ checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown 0.16.1", - "serde", - "serde_core", ] [[package]] @@ -2380,8 +2326,6 @@ dependencies = [ "indexmap", "more-asserts", "rkyv", - "serde", - "serde_bytes", "sha2", "target-lexicon", "thiserror 1.0.69", @@ -2440,7 +2384,6 @@ dependencies = [ "wasm-bindgen-futures", "wasm-bindgen-test", "wasmer", - "wasmer-types", "wasmi-plugin-pdk", "wasmi-plugin-rt", "web-sys", @@ -2562,65 +2505,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -2792,29 +2682,6 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" -[[package]] -name = "worker" -version = "0.1.0" -dependencies = [ - "chrono", - "futures", - "serde", - "serde-wasm-bindgen", - "serde_json", - "thiserror 2.0.17", - "tracing", - "tracing-wasm", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasmer", - "wasmer-types", - "wasmi-plugin-hdk", - "wasmi-plugin-pdk", - "wasmi-plugin-rt", - "web-sys", - "web-time", -] - [[package]] name = "xattr" version = "1.6.1" diff --git a/Cargo.toml b/Cargo.toml index f8e8aee..2ad46af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ members = [ "crates/wasmi-plugin-hdk", "crates/wasmi-plugin-pdk", "crates/wasmi-plugin-rt", - "crates/worker", ] resolver = "2" @@ -41,7 +40,6 @@ uuid = { version = "1", features = ["v4", "serde", "js"] } wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" wasmer = { version = "6", default-features = false } -wasmer-types = "6" web-sys = "0.3" wasmi-plugin-hdk = { version = "0.1.0", path = "./crates/wasmi-plugin-hdk" } wasmi-plugin-pdk = { version = "0.1.0", path = "./crates/wasmi-plugin-pdk" } diff --git a/crates/test-plugin/Cargo.toml b/crates/test-plugin/Cargo.toml index 87abc82..118b565 100644 --- a/crates/test-plugin/Cargo.toml +++ b/crates/test-plugin/Cargo.toml @@ -25,5 +25,4 @@ wasmi-plugin-pdk = { workspace = true } opt-level = 3 lto = "fat" codegen-units = 1 -strip = true -panic = "abort" \ No newline at end of file +strip = true \ No newline at end of file diff --git a/crates/wasmi-plugin-hdk/Cargo.toml b/crates/wasmi-plugin-hdk/Cargo.toml index 24536bc..6c0d980 100644 --- a/crates/wasmi-plugin-hdk/Cargo.toml +++ b/crates/wasmi-plugin-hdk/Cargo.toml @@ -25,7 +25,6 @@ uuid = { workspace = true } wasmi-plugin-pdk = { workspace = true } wasmi-plugin-rt = { workspace = true } web-time = { workspace = true } -wasmer-types = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { workspace = true } @@ -33,13 +32,11 @@ wasmer = { workspace = true, default-features = true} [target.'cfg(target_arch = "wasm32")'.dependencies] gloo-timers = { workspace = true } -tracing-wasm = { workspace = true } serde-wasm-bindgen = { workspace = true } wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } -web-sys = { workspace = true, features=["Worker", "MessageEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } +web-sys = { workspace = true, features=["Worker", "WorkerOptions", "WorkerType", "MessageEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } -wasmer-types = { workspace = true, features=["enable-serde"] } [dev-dependencies] tracing-subscriber = { workspace = true } @@ -49,6 +46,7 @@ tracing-test = { version = "0.2.5", features = ["no-env-filter"] } criterion = "0.8.0" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] +tracing-wasm = { workspace = true } wasm-bindgen-test = "0.3" web-sys = { version = "0.3", features = ["Performance", "Window"] } diff --git a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs index 722821f..bb6099a 100644 --- a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs +++ b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs @@ -45,7 +45,11 @@ async fn bench_ping_wasm(c: &mut Criterion) { setup_logs(); let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); - let plugin = Arc::new(Plugin::new("test_plugin", wasm_bytes, handler).unwrap()); + let plugin = Arc::new( + Plugin::new("test_plugin", &wasm_bytes, handler) + .await + .unwrap(), + ); c.bench_async_function("ping", |b| { let plugin = plugin.clone(); @@ -74,8 +78,9 @@ async fn bench_lifecycle(c: &mut Criterion) { let wasm_bytes = wasm_bytes.clone(); let handler = handler.clone(); async move { - let plugin = - Plugin::new("test_plugin", wasm_bytes.as_ref().clone(), handler).unwrap(); + let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) + .await + .unwrap(); plugin.call("ping", serde_json::Value::Null).await.unwrap(); } })) @@ -90,7 +95,11 @@ async fn bench_prime_sieve_small(c: &mut Criterion) { setup_logs(); let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); - let plugin = Arc::new(Plugin::new("test_plugin", wasm_bytes, handler).unwrap()); + let plugin = Arc::new( + Plugin::new("test_plugin", &wasm_bytes, handler) + .await + .unwrap(), + ); c.bench_async_function("prime_sieve_small", |b| { let plugin = plugin.clone(); @@ -113,7 +122,11 @@ async fn bench_prime_sieve_large(c: &mut Criterion) { setup_logs(); let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); - let plugin = Arc::new(Plugin::new("test_plugin", wasm_bytes, handler).unwrap()); + let plugin = Arc::new( + Plugin::new("test_plugin", &wasm_bytes, handler) + .await + .unwrap(), + ); c.bench_async_function("prime_sieve_large", |b| { let plugin = plugin.clone(); @@ -138,7 +151,8 @@ async fn bench_call_many(c: &mut Criterion) { let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); let plugin = Arc::new( - Plugin::new("test_plugin", wasm_bytes, handler) + Plugin::new("test_plugin", &wasm_bytes, handler) + .await .unwrap() .with_timeout(Duration::from_secs(5)), ); @@ -166,7 +180,8 @@ async fn bench_call_many_async(c: &mut Criterion) { let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); let plugin = Arc::new( - Plugin::new("test_plugin", wasm_bytes, handler) + Plugin::new("test_plugin", &wasm_bytes, handler) + .await .unwrap() .with_timeout(Duration::from_secs(5)), ); diff --git a/crates/wasmi-plugin-hdk/src/compile.rs b/crates/wasmi-plugin-hdk/src/compile.rs index 11bd9ae..724b0db 100644 --- a/crates/wasmi-plugin-hdk/src/compile.rs +++ b/crates/wasmi-plugin-hdk/src/compile.rs @@ -8,7 +8,7 @@ pub struct Compiled { impl Compiled { #[cfg(not(target_arch = "wasm32"))] - pub fn new(name: &str, wasm_bytes: &[u8]) -> Result { + pub async fn new(name: &str, wasm_bytes: &[u8]) -> Result { let engine = wasmer::Engine::default(); let module = wasmer::Module::new(&engine, wasm_bytes)?; @@ -20,9 +20,23 @@ impl Compiled { } #[cfg(target_arch = "wasm32")] - pub fn new(name: &str, wasm_bytes: &[u8]) -> Result { - let engine = wasmer::Engine::default(); - let module = wasmer::Module::new(&engine, wasm_bytes)?; + pub async fn new(name: &str, wasm_bytes: &[u8]) -> Result { + use wasm_bindgen::JsCast; + + //? Because we're in wasm32-unknown-unknown AND we want to have Shared Memory + //? (target-feature=+atomics,+bulk-memory), + //? we have to use the WebAssembly.compile async function and pass the compiled + //? native mmodule to wasmer. Wasmer only supports compiling via `WebAssembly::new()`, + //? which is illegal for use with Shared Memory. + let js_bytes = web_sys::js_sys::Uint8Array::from(wasm_bytes); + let promise = web_sys::js_sys::WebAssembly::compile(&js_bytes.into()); + let js_module_value = wasm_bindgen_futures::JsFuture::from(promise).await.unwrap(); + let js_module = js_module_value + .dyn_into::() + .unwrap(); + + let module: wasmer::Module = (js_module, wasm_bytes).into(); + Ok(Compiled { name: name.to_string(), module, diff --git a/crates/wasmi-plugin-hdk/src/lib.rs b/crates/wasmi-plugin-hdk/src/lib.rs index 12c4060..660c22f 100644 --- a/crates/wasmi-plugin-hdk/src/lib.rs +++ b/crates/wasmi-plugin-hdk/src/lib.rs @@ -1,4 +1,4 @@ -mod compile; +pub mod compile; pub mod host_handler; pub mod plugin; pub mod runtime; diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index 7bd9235..709164f 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -77,9 +77,9 @@ pub enum PluginError { } impl Plugin { - pub fn new( + pub async fn new( name: &str, - wasm_bytes: Vec, + wasm_bytes: &[u8], handler: Arc, ) -> Result { Ok(Plugin { @@ -89,7 +89,7 @@ impl Plugin { logger: Box::new(default_plugin_logger), max_fuel: None, timeout: Duration::from_secs(10), - compiled: Compiled::new(name, &wasm_bytes)?, + compiled: Compiled::new(name, wasm_bytes).await?, }) } diff --git a/crates/wasmi-plugin-hdk/src/runtime/mod.rs b/crates/wasmi-plugin-hdk/src/runtime/mod.rs index 428dd0a..4fe26e4 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/mod.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/mod.rs @@ -1,24 +1,25 @@ -#[cfg(not(target_arch = "wasm32"))] -mod native_runtime; -use futures::{AsyncRead, AsyncWrite}; +pub mod non_blocking_pipe; + #[cfg(not(target_arch = "wasm32"))] pub use native_runtime::NativeRuntime; #[cfg(not(target_arch = "wasm32"))] -mod non_blocking_pipe; +mod native_runtime; -#[cfg(target_arch = "wasm32")] -mod worker_runtime; #[cfg(target_arch = "wasm32")] pub use worker_runtime::WorkerRuntime; - #[cfg(target_arch = "wasm32")] pub mod shared_pipe; #[cfg(target_arch = "wasm32")] -mod worker_pool; +pub mod thread_worker; +#[cfg(target_arch = "wasm32")] +pub mod worker_message; #[cfg(target_arch = "wasm32")] -pub mod worker_protocol; +pub mod worker_pool; +#[cfg(target_arch = "wasm32")] +pub mod worker_runtime; use crate::compile::Compiled; +use futures::{AsyncRead, AsyncWrite}; pub trait Runtime { type Error: Into>; diff --git a/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs index 5cbe294..717f4ca 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs @@ -72,8 +72,8 @@ impl Runtime for NativeRuntime { let (stdout_reader, stdout_writer) = non_blocking_pipe(); let (stderr_reader, stderr_writer) = non_blocking_pipe(); - let wasm_handle = Self::wasm_handle(compiled, stdin_reader, stdout_writer, stderr_writer); - Self::spawn_stderr(name, stderr_reader); + let wasm_handle = wasm_handle(compiled, stdin_reader, stdout_writer, stderr_writer); + spawn_stderr(name, stderr_reader); let session = NativeSession { wasm_handle }; @@ -89,66 +89,64 @@ impl Runtime for NativeRuntime { } } -impl NativeRuntime { - fn wasm_handle( - compiled: Compiled, - stdin_reader: NonBlockingPipeReader, - stdout_writer: NonBlockingPipeWriter, - stderr_writer: NonBlockingPipeWriter, - ) -> JoinHandle<()> { - spawn_blocking(move || { - let result = Self::run_instance(compiled, stdin_reader, stdout_writer, stderr_writer); - if let Err(e) = result { - eprintln!("Plugin execution error: {:?}", e); - } - }) - } +fn wasm_handle( + compiled: Compiled, + stdin_reader: NonBlockingPipeReader, + stdout_writer: NonBlockingPipeWriter, + stderr_writer: NonBlockingPipeWriter, +) -> JoinHandle<()> { + spawn_blocking(move || { + let result = run_instance(compiled, stdin_reader, stdout_writer, stderr_writer); + if let Err(e) = result { + eprintln!("Plugin execution error: {:?}", e); + } + }) +} - fn spawn_stderr(name: String, mut stderr_reader: NonBlockingPipeReader) { - spawn(async move { - let mut stderr = BufReader::new(&mut stderr_reader); - let mut buffer = String::new(); - loop { - buffer.clear(); - match stderr.read_line(&mut buffer).await { - Ok(0) => { - break; - } - Ok(_) => { - info!("[plugin] [{}] {}", name, buffer.trim_end()); - } - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - tokio::task::yield_now().await; - continue; - } - Err(e) => { - error!("[WorkerBridge] Error reading from stderr: {}", e); - break; - } +fn spawn_stderr(name: String, mut stderr_reader: NonBlockingPipeReader) { + spawn(async move { + let mut stderr = BufReader::new(&mut stderr_reader); + let mut buffer = String::new(); + loop { + buffer.clear(); + match stderr.read_line(&mut buffer).await { + Ok(0) => { + break; + } + Ok(_) => { + info!("[plugin] [{}] {}", name, buffer.trim_end()); + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + tokio::task::yield_now().await; + continue; + } + Err(e) => { + error!("[WorkerBridge] Error reading from stderr: {}", e); + break; } } - }); - } + } + }); +} - fn run_instance( - compiled: Compiled, - stdin: NonBlockingPipeReader, - stdout: NonBlockingPipeWriter, - stderr: NonBlockingPipeWriter, - ) -> Result<(), Box> { - let engine = compiled.engine; - let module = compiled.module; +fn run_instance( + compiled: Compiled, + stdin: NonBlockingPipeReader, + stdout: NonBlockingPipeWriter, + stderr: NonBlockingPipeWriter, +) -> Result<(), Box> { + let engine = compiled.engine; + let module = compiled.module; - let mut store = Store::new(engine); + let mut store = Store::new(engine); - let wasi_ctx = WasiCtx::new() - .set_stdin(stdin) - .set_stdout(stdout) - .set_stderr(stderr); + let wasi_ctx = WasiCtx::new() + .set_stdin(stdin) + .set_stdout(stdout) + .set_stderr(stderr); - let start = wasi_ctx.into_fn(&mut store, &module)?; - start.call(&mut store, &[])?; + let start = wasi_ctx.into_fn(&mut store, &module)?; + start.call(&mut store, &[])?; - Ok(()) - } + Ok(()) } diff --git a/crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs b/crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs index 70c58f8..dea6ee1 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs @@ -118,7 +118,7 @@ impl SharedPipe { }; info!( - "SharedPipe[{}] dump: closed={} read_idx={} write_idx={} occupied={} last_bytes={}", + "SharedPipe[{}] terminated: closed={} read_idx={} write_idx={} occupied={} last_bytes={}", self.name, closed, r, w, occupied, last_bytes_str ); } @@ -153,6 +153,8 @@ impl SharedPipe { if w >= r { w - r } else { self.capacity - r + w } } + /// Attempts to read from the pipe, returning the number of bytes read, or None if + /// wouldBlock. fn try_read(&self, r: u32, w: u32, buf: &mut [u8]) -> Option { if r == w { if self.is_closed() { @@ -178,6 +180,8 @@ impl SharedPipe { Some(amount as usize) } + /// Attempts to write to the pipe, returning the number of bytes written, or None if + /// wouldBlock. fn try_write(&self, r: u32, w: u32, buf: &[u8]) -> Option { if self.is_closed() { return None; @@ -381,67 +385,67 @@ impl AsyncWrite for SharedPipe { } } -#[cfg(test)] -mod tests { +// #[cfg(test)] +// mod tests { - #[cfg(target_family = "wasm")] - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); +// #[cfg(target_family = "wasm")] +// wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - use super::*; - use wasm_bindgen_test::wasm_bindgen_test; +// use super::*; +// use wasm_bindgen_test::wasm_bindgen_test; - #[wasm_bindgen_test] - fn test_basic_read_write() { - let mut pipe = SharedPipe::new_with_capacity("test", 10); +// #[wasm_bindgen_test] +// fn test_basic_read_write() { +// let mut pipe = SharedPipe::new_with_capacity("test", 10); - let input = b"hello"; - let written = pipe.write(input).expect("Write failed"); - assert_eq!(written, 5); +// let input = b"hello"; +// let written = pipe.write(input).expect("Write failed"); +// assert_eq!(written, 5); - let mut output = [0u8; 5]; - let read = pipe.read(&mut output).expect("Read failed"); - assert_eq!(read, 5); - assert_eq!(&output, b"hello"); +// let mut output = [0u8; 5]; +// let read = pipe.read(&mut output).expect("Read failed"); +// assert_eq!(read, 5); +// assert_eq!(&output, b"hello"); - // Should now be WouldBlock - let res = pipe.read(&mut output); - assert!(res.is_err()); +// // Should now be WouldBlock +// let res = pipe.read(&mut output); +// assert!(res.is_err()); - pipe.close(); - let res = pipe.write(b"more"); - assert!(res.is_err()); +// pipe.close(); +// let res = pipe.write(b"more"); +// assert!(res.is_err()); - let res = pipe.read(&mut output); - assert_eq!(res.ok(), Some(0)); // EOF - } +// let res = pipe.read(&mut output); +// assert_eq!(res.ok(), Some(0)); // EOF +// } - #[wasm_bindgen_test] - fn test_wrap_around_saturation() { - let mut pipe = SharedPipe::new_with_capacity("test", 10); +// #[wasm_bindgen_test] +// fn test_wrap_around_saturation() { +// let mut pipe = SharedPipe::new_with_capacity("test", 10); - // 1. Fill 7 bytes - pipe.write(b"hello_w").unwrap(); +// // 1. Fill 7 bytes +// pipe.write(b"hello_w").unwrap(); - // 2. Read 4 bytes (read_idx moves to 4, write_idx is at 7) - let mut tmp = [0u8; 4]; - pipe.read(&mut tmp).unwrap(); - assert_eq!(&tmp, b"hell"); +// // 2. Read 4 bytes (read_idx moves to 4, write_idx is at 7) +// let mut tmp = [0u8; 4]; +// pipe.read(&mut tmp).unwrap(); +// assert_eq!(&tmp, b"hell"); - // 3. Write 5 more bytes. - // Current state: r=4, w=7. Space available: 10 - (7-4) - 1 = 6. - // This write will wrap! (7, 8, 9, then 0, 1) - let written = pipe.write(b"orld_").unwrap(); - assert_eq!(written, 5); +// // 3. Write 5 more bytes. +// // Current state: r=4, w=7. Space available: 10 - (7-4) - 1 = 6. +// // This write will wrap! (7, 8, 9, then 0, 1) +// let written = pipe.write(b"orld_").unwrap(); +// assert_eq!(written, 5); - // 4. Read all available data (should be "efg" + "hijkl" = 8 bytes) - // This read MUST saturate the output buffer across the wrap point. - let mut output = [0u8; 8]; - let read = pipe.read(&mut output).expect("Wrap read failed"); +// // 4. Read all available data (should be "efg" + "hijkl" = 8 bytes) +// // This read MUST saturate the output buffer across the wrap point. +// let mut output = [0u8; 8]; +// let read = pipe.read(&mut output).expect("Wrap read failed"); - assert_eq!(read, 8); - assert_eq!(&output, b"o_world_"); +// assert_eq!(read, 8); +// assert_eq!(&output, b"o_world_"); - // Verify pointers are back in sync - assert_eq!(pipe.read_idx(), pipe.write_idx()); - } -} +// // Verify pointers are back in sync +// assert_eq!(pipe.read_idx(), pipe.write_idx()); +// } +// } diff --git a/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs b/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs new file mode 100644 index 0000000..731651f --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs @@ -0,0 +1,14 @@ +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen] +pub async fn execute_worker_task(ptr: u32) { + //* SAFETY: We transfer ownership of the closure from the caller's `spawn` + //* function to this web worker. The pointer must be a FnOnce closure + //* allocated on the heap. + let closure = unsafe { + let raw = ptr as *mut Box; + Box::from_raw(raw) + }; + + (*closure)(); +} diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker.js b/crates/wasmi-plugin-hdk/src/runtime/worker.js new file mode 100644 index 0000000..cfd8433 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/worker.js @@ -0,0 +1,42 @@ +["log", "info", "warn", "error"].forEach(level => { + globalThis.console[level] = (...args) => { + globalThis.postMessage({ + type: "Log", + level: level, + message: args.map(String).join(" "), + ts: globalThis.performance.now() + }); + }; +}); + +console.log("Worker started"); +let wasmPkg = null; + +globalThis.onmessage = async ev => { + try { + if (ev.data.type === "init") { + const { memory, module, sdkUrl } = ev.data; + + wasmPkg = await import(sdkUrl); + const init = wasmPkg.default || wasmPkg.init; + + if (typeof init !== 'function') { + throw new Error("Wasm init is not a function"); + } + + await init(module, memory); + globalThis.postMessage({ type: "Ready" }); + } else if (ev.data.type === "run") { + const { taskPtr } = ev.data; + try { + await wasmPkg.execute_worker_task(taskPtr); + } finally { + globalThis.postMessage({ type: "Idle" }); + } + } else { + console.error(`Unknown message type: ${type}`); + } + } catch (e) { + console.error(`Error in worker: ${e.message}`); + } +}; \ No newline at end of file diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_loader.js b/crates/wasmi-plugin-hdk/src/runtime/worker_loader.js deleted file mode 100644 index 7850b40..0000000 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_loader.js +++ /dev/null @@ -1,32 +0,0 @@ -function getTimestamp() { - const now = new Date(); - - const YYYY = now.getFullYear(); - const MM = String(now.getMonth() + 1).padStart(2, '0'); - const DD = String(now.getDate()).padStart(2, '0'); - const hh = String(now.getHours()).padStart(2, '0'); - const mm = String(now.getMinutes()).padStart(2, '0'); - const ss = String(now.getSeconds()).padStart(2, '0'); - const ms = String(now.getMilliseconds()).padStart(3, '0'); - - return `${YYYY}-${MM}-${DD} ${hh}:${mm}:${ss}.${ms}`; -} - -console.log = function (...args) { - const prefix = `[${getTimestamp()}]`; - self.postMessage({ type: 'Log', message: `${prefix} ${args.join(' ')}` }); -}; - -console.error = function (...args) { - const prefix = `[${getTimestamp()}] [ERROR]`; - self.postMessage({ type: 'Log', message: `${prefix} ${args.join(' ')}` }); -}; - -console.log("Worker Shim: Script starting..."); - -{ WORKER_JS_GLUE } - -wasm_bindgen('{ WASM_URL }').then(() => { - console.log("Worker Shim: Rust Wasm Initialized"); - wasm_bindgen.start_worker(); -}).catch(err => console.error("Worker Init Failed:", err)); \ No newline at end of file diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_message.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_message.rs new file mode 100644 index 0000000..aea3aa6 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_message.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum WorkerMessage { + Log { + message: String, + level: String, + ts: f64, + }, + Ready, + Idle, +} diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs index 40693cb..d62dd30 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs @@ -1,299 +1,232 @@ -use std::sync::{ - Arc, Mutex, - atomic::{AtomicU64, Ordering}, -}; +use std::sync::{Arc, Mutex, OnceLock}; -use futures::channel::oneshot::{self}; +use futures::channel::oneshot; use thiserror::Error; -use tracing::{error, info}; -use wasm_bindgen::{JsCast, JsValue, prelude::Closure}; +use tracing::{error, info, warn}; +use wasm_bindgen::{ + JsCast, JsValue, + prelude::{Closure, wasm_bindgen}, +}; use web_sys::{ - MessageEvent, - js_sys::{self, Reflect, SharedArrayBuffer, Uint8Array}, + Blob, Url, Worker, WorkerOptions, WorkerType, + js_sys::{self, Array, Reflect, Uint8Array}, }; -use crate::{ - compile::Compiled, - runtime::{shared_pipe::SharedPipe, worker_protocol::WorkerMessage}, -}; +use crate::runtime::worker_message::WorkerMessage; -pub type WorkerId = u32; +pub type WorkerId = u64; pub struct WorkerPool { + state: Arc>, +} + +struct PoolState { workers: Vec>, - next_id: AtomicU64, - script_url: String, } -pub struct WorkerSession { - pub id: WorkerId, - handle: Arc, +enum WorkerState { + Idle, + Busy, + Terminated, } -struct WorkerHandle { - id: WorkerId, +pub struct WorkerHandle { + pub id: WorkerId, state: Arc>, - inner: web_sys::Worker, - stdin: SharedArrayBuffer, - stdout: SharedArrayBuffer, - - //? Store handle to prevent javascript garbage collection - _on_message: Closure, + worker: web_sys::Worker, + _on_message: wasm_bindgen::prelude::Closure, } #[derive(Debug, Error)] -pub enum PoolError { - #[error("JavaScript error")] - JsValue(JsValue), - #[error("Serde serialization error: {0}")] - Serde(#[from] serde_wasm_bindgen::Error), - #[error("Module serialization error: {0}")] - ModuleSerialization(#[from] wasmer::SerializeError), - #[error("Receiver error: {0}")] - Receiver(#[from] futures::channel::oneshot::Canceled), +pub enum SpawnError { + #[error("JavaScript error: {0:?}")] + JsError(JsValue), } -#[derive(Clone, Copy, PartialEq)] -enum WorkerState { - Idle, - Busy, - Terminated, -} +static GLOBAL_POOL: OnceLock = OnceLock::new(); -const SHARED_PIPE_CAPACITY: u32 = 16384; -const WORKER_LOADER_JS: &str = include_str!("worker_loader.js"); -const WORKER_JS_GLUE: &str = include_str!("../../../worker/pkg/worker.js"); -const WORKER_WASM_BYTES: &[u8] = include_bytes!("../../../worker/pkg/worker_bg.wasm"); +unsafe impl Send for WorkerHandle {} +unsafe impl Sync for WorkerHandle {} impl WorkerPool { - pub fn new() -> Result { - let pool = WorkerPool { - workers: Vec::new(), - next_id: AtomicU64::new(0), - script_url: create_worker_script()?, - }; - Ok(pool) + pub fn global() -> &'static WorkerPool { + GLOBAL_POOL.get_or_init(|| WorkerPool { + state: Arc::new(Mutex::new(PoolState { workers: vec![] })), + }) } - pub async fn run( - &mut self, - compiled: Compiled, - ) -> Result<(WorkerSession, SharedPipe, SharedPipe), PoolError> { - let worker = self.get_or_create_worker().await?; - - let stdin = SharedPipe::new("HOST_STDIN", &worker.stdin); - let stdout = SharedPipe::new("HOST_STDOUT", &worker.stdout); - - // Reset the header (read/write pointers) in the shared memory - stdin.reset(); - stdout.reset(); - - worker.run(compiled)?; - - Ok(( - WorkerSession { - id: worker.id, - handle: worker, - }, - stdin, - stdout, - )) + pub async fn run(&self, f: F) -> Result, SpawnError> + where + F: FnOnce() + Send + 'static, + { + let handle = self.get_or_spawn_worker().await?; + handle.run(f)?; + Ok(handle) } - async fn get_or_create_worker(&mut self) -> Result, PoolError> { - self.workers - .retain(|w| w.state() != WorkerState::Terminated); - - //? Look for an idle worker - if let Some(idle) = self.workers.iter().find(|w| w.state() == WorkerState::Idle) { - idle.set_state(WorkerState::Busy); - return Ok(idle.clone()); + pub fn terminate(&self, worker_id: WorkerId) { + let mut state = self.state.lock().unwrap(); + if let Some(pos) = state.workers.iter().position(|w| w.id == worker_id) { + let handle = state.workers.remove(pos); + handle.terminate(); } - - //? If none found, create a new worker - let id = self.next_id.fetch_add(1, Ordering::Relaxed) as WorkerId; - let worker = WorkerHandle::new(id, &self.script_url).await?; - let worker = Arc::new(worker); - worker.set_state(WorkerState::Busy); - self.workers.push(worker.clone()); - - Ok(worker) } -} -impl WorkerSession { - pub fn terminate(&self) { - self.handle.terminate(); + async fn get_or_spawn_worker(&self) -> Result, SpawnError> { + // Remove any that are terminated + self.state.lock().unwrap().workers.retain(|w| { + let worker_state = w.state.lock().unwrap(); + !matches!(*worker_state, WorkerState::Terminated) + }); + + // Try to find an idle worker + for worker in self.state.lock().unwrap().workers.iter() { + let mut worker_state = worker.state.lock().unwrap(); + if matches!(*worker_state, WorkerState::Idle) { + *worker_state = WorkerState::Busy; + return Ok(worker.clone()); + } + } + + // Otherwise, spawn a new one + let id = self.state.lock().unwrap().workers.len() as u64; + let handle = WorkerHandle::spawn(id).await?; + let handle = Arc::new(handle); + self.state.lock().unwrap().workers.push(handle.clone()); + Ok(handle) } } -unsafe impl Send for WorkerHandle {} -unsafe impl Sync for WorkerHandle {} - impl WorkerHandle { - pub async fn new(id: WorkerId, script_url: &str) -> Result { - let inner = web_sys::Worker::new(script_url).map_err(PoolError::JsValue)?; - let state = Arc::new(Mutex::new(WorkerState::Idle)); + pub async fn spawn(id: u64) -> Result { + let name = format!("worker-{}", id); - let stdin = SharedPipe::new_with_capacity("HOST_STDIN", SHARED_PIPE_CAPACITY).buffer(); - let stdout = SharedPipe::new_with_capacity("HOST_STDOUT", SHARED_PIPE_CAPACITY).buffer(); + // 2. Create the worker + let options = WorkerOptions::new(); + options.set_name(&name); + options.set_type(WorkerType::Module); + let worker = Worker::new_with_options(&worker_url(), &options).unwrap(); - let (booted_tx, booted_rx) = oneshot::channel(); + // 3. Set up onmessage handler let (init_tx, init_rx) = oneshot::channel(); - let on_message = Closure::wrap(WorkerHandle::on_message_handler( - booted_tx, - init_tx, - id, - state.clone(), - )); - inner.set_onmessage(Some(on_message.as_ref().unchecked_ref())); - - let worker = WorkerHandle { + let state = Arc::new(Mutex::new(WorkerState::Busy)); + let on_message = on_message_handler(init_tx, state.clone(), name.clone()); + let on_message = Closure::wrap(on_message); + worker.set_onmessage(Some(on_message.as_ref().unchecked_ref())); + + // 4. Send the "init" message with memory and module + let msg = js_sys::Object::new(); + Reflect::set(&msg, &"type".into(), &"init".into()).unwrap(); + Reflect::set(&msg, &"memory".into(), &wasm_bindgen::memory()).unwrap(); + Reflect::set(&msg, &"module".into(), ¤t_module()).unwrap(); + Reflect::set(&msg, &"sdkUrl".into(), &sdk_url().into()).unwrap(); + worker.post_message(&msg).unwrap(); + + // 5. Wait for initialization + init_rx.await.unwrap(); + + Ok(WorkerHandle { id, state, - inner, - stdin, - stdout, + worker, _on_message: on_message, - }; - - booted_rx.await?; - worker.initalize()?; - init_rx.await?; - - Ok(worker) - } - - pub fn state(&self) -> WorkerState { - self.state.lock().unwrap().clone() - } - - pub fn set_state(&self, state: WorkerState) { - let mut guard = self.state.lock().unwrap(); - *guard = state; + }) } - pub fn run(&self, compiled: Compiled) -> Result<(), PoolError> { - self.set_state(WorkerState::Busy); - - let wasm_bytes = compiled.module.serialize()?; - let msg = serde_wasm_bindgen::to_value(&WorkerMessage::Load { - wasm_module: wasm_bytes.to_vec(), - name: compiled.name.clone(), - })?; + fn run(&self, f: impl FnOnce() + Send + 'static) -> Result<(), SpawnError> { + // 1. Box the function and get a raw pointer + let box_f: Box> = Box::new(Box::new(f)); + let ptr = Box::into_raw(box_f) as u32; - // Transfer WASM buffer to worker - let uint8_array = Uint8Array::new( - &Reflect::get(&msg, &"wasm_module".into()).map_err(PoolError::JsValue)?, - ); - let transfer_list = js_sys::Array::of1(&uint8_array.buffer()); + // 2. Load my worker + let worker = &self.worker; - self.inner - .post_message_with_transfer(&msg, &transfer_list) - .map_err(PoolError::JsValue)?; + // 3. Send the "run" message with raw ptr + let msg = js_sys::Object::new(); + Reflect::set(&msg, &"type".into(), &"run".into()).unwrap(); + Reflect::set(&msg, &"taskPtr".into(), &ptr.into()).unwrap(); + worker.post_message(&msg).unwrap(); Ok(()) } pub fn terminate(&self) { - //? Create temp SharedPipe instances to dump their state to logs - // let stdin = SharedPipe::new("DUMP_STDIN", &self.stdin); - // let stdout = SharedPipe::new("DUMP_STDOUT", &self.stdout); - - // stdin.dump(); - // stdout.dump(); - - self.inner.terminate(); - self.set_state(WorkerState::Terminated); + self.worker.terminate(); + let mut state = self.state.lock().unwrap(); + *state = WorkerState::Terminated; } +} - fn on_message_handler( - booted_tx: oneshot::Sender<()>, - init_tx: oneshot::Sender<()>, - id: WorkerId, - state: Arc>, - ) -> Box { - let mut booted_tx = Some(booted_tx); - let mut init_tx = Some(init_tx); - Box::new(move |e: MessageEvent| { - let Ok(msg) = serde_wasm_bindgen::from_value::(e.data()) else { - error!( - "WorkerBridge: Received unknown message from worker: {:?}", - e.data() - ); - return; - }; - - match msg { - WorkerMessage::Log { message } => { - info!("[WORKER_{}] {}", id, message); - } - WorkerMessage::PluginLog { message } => { - info!("[PLUGIN_{}] {}", id, message); - } - WorkerMessage::Booted => { - if let Some(tx) = booted_tx.take() { - info!("[WORKER_{}] Booted", id); - let _ = tx.send(()); - } - } - WorkerMessage::Initialized => { - if let Some(tx) = init_tx.take() { - info!("[WORKER_{}] Initialized", id); - let _ = tx.send(()); - } - } - WorkerMessage::Idle => { - let mut guard = state.lock().unwrap(); - // Only transition to Idle if currently Busy - if *guard == WorkerState::Busy { - *guard = WorkerState::Idle; - info!("[WORKER_{}] Idle", id); - } - } - _ => { - error!("[WORKER_{}] Unexpected message", id); - } - } - }) - } +fn worker_url() -> String { + let script = include_str!("./worker.js"); - fn initalize(&self) -> Result<(), PoolError> { - let init_msg = serde_wasm_bindgen::to_value(&WorkerMessage::Initialize)?; + let options = web_sys::BlobPropertyBag::new(); + options.set_type("application/javascript"); + let blob = Blob::new_with_u8_array_sequence_and_options( + Array::from_iter([Uint8Array::from(script.as_bytes())]).as_ref(), + &options, + ) + .unwrap(); - Reflect::set(&init_msg, &"stdin".into(), &self.stdin).map_err(PoolError::JsValue)?; - Reflect::set(&init_msg, &"stdout".into(), &self.stdout).map_err(PoolError::JsValue)?; + Url::create_object_url_with_blob(&blob).unwrap() +} - self.inner - .post_message(&init_msg) - .map_err(PoolError::JsValue)?; - Ok(()) +fn sdk_url() -> String { + #[wasm_bindgen] + extern "C" { + // Adding the thread_local_v2 attribute satisfies the deprecation warning + #[wasm_bindgen(thread_local_v2, js_namespace = ["import", "meta"], js_name = url)] + static IMPORT_META_URL: String; } + + IMPORT_META_URL.with(|url| url.clone()) } -/// Creates a blob URL containing the worker script that lodas and executes the -/// wasm module. -fn create_worker_script() -> Result { - //? Create a blob URL for the worker WASM module - let options = web_sys::BlobPropertyBag::new(); - options.set_type("application/wasm"); - let wasm_blob = web_sys::Blob::new_with_u8_array_sequence_and_options( - &js_sys::Array::of1(&js_sys::Uint8Array::from(WORKER_WASM_BYTES)), - &options, - )?; - let wasm_url = web_sys::Url::create_object_url_with_blob(&wasm_blob)?; +fn current_module() -> JsValue { + wasm_bindgen::module().dyn_into().unwrap() +} - //? - let loader_script = WORKER_LOADER_JS - .replace("{ WORKER_JS_GLUE }", WORKER_JS_GLUE) - .replace("{ WASM_URL }", &wasm_url); +fn on_message_handler( + init_tx: oneshot::Sender<()>, + state: Arc>, + name: String, +) -> Box { + let mut init_tx = Some(init_tx); + + Box::new(move |e: web_sys::MessageEvent| { + let Ok(msg) = serde_wasm_bindgen::from_value::(e.data()) else { + error!( + "WorkerBridge: Received unknown message from worker: {:?}", + e.data() + ); + return; + }; - let options = web_sys::BlobPropertyBag::new(); - options.set_type("text/javascript"); - let blob = web_sys::Blob::new_with_u8_array_sequence_and_options( - &js_sys::Array::of1(&js_sys::JsString::from(loader_script)), - &options, - )?; + match msg { + WorkerMessage::Log { message, level, ts } => { + log_worker_message(&name, message, level, ts); + } + WorkerMessage::Ready => { + info!(target: "WORKER", "[{}] Ready", name); + if let Some(tx) = init_tx.take() { + let _ = tx.send(()); + } + } + WorkerMessage::Idle => { + info!(target: "WORKER", "[{}] Idle", name); + let mut worker_state = state.lock().unwrap(); + *worker_state = WorkerState::Idle; + } + } + }) +} - web_sys::Url::create_object_url_with_blob(&blob) +fn log_worker_message(name: &str, msg: String, level: String, ts: f64) { + let _ = ts; + + match level.as_str() { + "warn" => warn!(target: "WORKER", "[{}] {}", name, msg), + "error" => error!(target: "WORKER", "[{}] {}", name, msg), + _ => info!(target: "WORKER", "[{}] {}", name, msg), + } } diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_protocol.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_protocol.rs deleted file mode 100644 index cdda608..0000000 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_protocol.rs +++ /dev/null @@ -1,27 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum WorkerMessage { - /// Send by the worker when it has booted into WASM and is listening for messages - Booted, - /// Send by the host to initialize the worker with necessary SABs. Attaches the - /// shared memory pipes for stdin, stdout, and stderr (can't be sent via serde - /// for reasons). - Initialize, - /// Send by the worker when it has initialized - Initialized, - Load { - /// Serialized wasm module. Deserialize with `wasmer::Module::deserialize` - #[serde(with = "serde_bytes")] - wasm_module: Vec, - name: String, - }, - Log { - message: String, - }, - PluginLog { - message: String, - }, - Idle, -} diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index 7496f02..9d77f85 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -1,23 +1,31 @@ //! Web Worker runtime bridge, running the Wasm module in a Web Worker. use std::collections::HashMap; -use std::sync::{Arc, OnceLock}; +use std::sync::Arc; use futures::lock::Mutex; use futures::{AsyncRead, AsyncWrite}; +use thiserror::Error; +use tracing::error; use crate::compile::Compiled; use crate::runtime::Runtime; -use crate::runtime::worker_pool::{PoolError, WorkerId, WorkerPool, WorkerSession}; +use crate::runtime::non_blocking_pipe::{ + NonBlockingPipeReader, NonBlockingPipeWriter, non_blocking_pipe, +}; +use crate::runtime::worker_pool::{SpawnError, WorkerHandle, WorkerId, WorkerPool}; +use crate::wasi::wasi_ctx; -static POOL: OnceLock> = OnceLock::new(); - -pub fn get_pool() -> &'static Mutex { - POOL.get_or_init(|| Mutex::new(WorkerPool::new().expect("Failed to init global worker pool"))) +pub struct WorkerRuntime { + active_sessions: Arc>>>, } -pub struct WorkerRuntime { - active_sessions: Arc>>, +#[derive(Debug, Error)] +enum RunError { + #[error("WASI Error: {0}")] + WasiError(#[from] wasi_ctx::WasiError), + #[error("Runtime Error: {0}")] + RuntimeError(#[from] wasmer::RuntimeError), } impl WorkerRuntime { @@ -35,7 +43,7 @@ impl Default for WorkerRuntime { } impl Runtime for WorkerRuntime { - type Error = PoolError; + type Error = SpawnError; async fn spawn( &self, @@ -48,15 +56,26 @@ impl Runtime for WorkerRuntime { ), Self::Error, > { - let (session, stdin, stdout) = get_pool().lock().await.run(compiled).await?; + let (stdin_reader, stdin_writer) = non_blocking_pipe(); + let (stdout_reader, stdout_writer) = non_blocking_pipe(); + let (stderr_reader, stderr_writer) = non_blocking_pipe(); + + let pool = WorkerPool::global(); + let handle = pool + .run(move || { + let res = run_instance(compiled, stdin_reader, stdout_writer, stderr_writer); + if let Err(e) = res { + error!("Error running Wasm instance: {}", e); + } + }) + .await?; + + let _ = stderr_reader; // Currently unused - let instance_id = session.id; - self.active_sessions - .lock() - .await - .insert(instance_id, session); + let id = handle.id; + self.active_sessions.lock().await.insert(handle.id, handle); - Ok((instance_id as u64, stdin, stdout)) + return Ok((id, stdin_writer, stdout_reader)); } async fn terminate(self, instance_id: u64) { @@ -66,3 +85,22 @@ impl Runtime for WorkerRuntime { } } } + +fn run_instance( + compiled: Compiled, + stdin: NonBlockingPipeReader, + stdout: NonBlockingPipeWriter, + stderr: NonBlockingPipeWriter, +) -> Result<(), RunError> { + let mut store = wasmer::Store::default(); + let module = compiled.module; + let wasi_ctx = wasi_ctx::WasiCtx::new() + .set_stdin(stdin) + .set_stdout(stdout) + .set_stderr(stderr); + + let start = wasi_ctx.into_fn(&mut store, &module)?; + start.call(&mut store, &[])?; + + Ok(()) +} diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index 14d80f3..020bbf8 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -54,169 +54,171 @@ fn setup_logs() { } } -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_plugin() { - setup_logs(); - info!("Starting test_plugin..."); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - let resp = plugin.call("ping", Value::Null).await.unwrap(); - assert_eq!(resp.result.as_str().unwrap(), "pong"); -} - -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_get_random_number() { - setup_logs(); - info!("Starting get_random_number test..."); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - let response = plugin.call("get_random_number", Value::Null).await.unwrap(); - - info!("Random number response: {:?}", response); - response.result.as_u64().unwrap(); -} +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// #[cfg_attr(not(target_family = "wasm"), tokio::test)] +// async fn test_plugin() { +// setup_logs(); +// info!("Starting test_plugin..."); -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_get_time() { - setup_logs(); - info!("Starting get_time test..."); +// let wasm_bytes = load_plugin_wasm(); +// let handler = Arc::new(get_host_server()); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); +// let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) +// .await +// .unwrap(); +// let resp = plugin.call("ping", Value::Null).await.unwrap(); +// assert_eq!(resp.result.as_str().unwrap(), "pong"); +// } - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - let response = plugin.call("get_time", Value::Null).await.unwrap(); +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// #[cfg_attr(not(target_family = "wasm"), tokio::test)] +// async fn test_get_random_number() { +// setup_logs(); +// info!("Starting get_random_number test..."); - info!("Get time response: {:?}", response); +// let wasm_bytes = load_plugin_wasm(); +// let handler = Arc::new(get_host_server()); - let timestamp = response.result.as_u64().unwrap(); - let now = web_time::SystemTime::now() - .duration_since(web_time::UNIX_EPOCH) - .unwrap() - .as_secs(); +// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); +// let response = plugin.call("get_random_number", Value::Null).await.unwrap(); - let diff = timestamp.abs_diff(now); - assert!(diff < 2, "Timestamp difference too large: {}", diff); -} +// info!("Random number response: {:?}", response); +// response.result.as_u64().unwrap(); +// } -// TODO: Fix this for `wasm-pack test --headless --firefox ./crates/wasmi-plugin-hdk` // #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_sleep() { - setup_logs(); - info!("Starting sleep test..."); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - - let sleep_duration = 1500; // milliseconds - let start = web_time::Instant::now(); - plugin - .call("sleep", Value::Number(sleep_duration.into())) - .await - .unwrap(); - let elapsed = start.elapsed().as_millis(); - - assert!( - elapsed >= sleep_duration as u128, - "Sleep duration too short: {} ms", - elapsed - ); -} +// #[cfg_attr(not(target_family = "wasm"), tokio::test)] +// async fn test_get_time() { +// setup_logs(); +// info!("Starting get_time test..."); + +// let wasm_bytes = load_plugin_wasm(); +// let handler = Arc::new(get_host_server()); + +// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); +// let response = plugin.call("get_time", Value::Null).await.unwrap(); + +// info!("Get time response: {:?}", response); + +// let timestamp = response.result.as_u64().unwrap(); +// let now = web_time::SystemTime::now() +// .duration_since(web_time::UNIX_EPOCH) +// .unwrap() +// .as_secs(); + +// let diff = timestamp.abs_diff(now); +// assert!(diff < 2, "Timestamp difference too large: {}", diff); +// } + +// // TODO: Fix this for `wasm-pack test --headless --firefox ./crates/wasmi-plugin-hdk` +// // #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// #[cfg_attr(not(target_family = "wasm"), tokio::test)] +// async fn test_sleep() { +// setup_logs(); +// info!("Starting sleep test..."); + +// let wasm_bytes = load_plugin_wasm(); +// let handler = Arc::new(get_host_server()); + +// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + +// let sleep_duration = 1500; // milliseconds +// let start = web_time::Instant::now(); +// plugin +// .call("sleep", Value::Number(sleep_duration.into())) +// .await +// .unwrap(); +// let elapsed = start.elapsed().as_millis(); + +// assert!( +// elapsed >= sleep_duration as u128, +// "Sleep duration too short: {} ms", +// elapsed +// ); +// } -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_call() { - setup_logs(); - info!("Starting call test..."); +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// #[cfg_attr(not(target_family = "wasm"), tokio::test)] +// async fn test_call() { +// setup_logs(); +// info!("Starting call test..."); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); +// let wasm_bytes = load_plugin_wasm(); +// let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - let resp = plugin.call("call", Value::Null).await.unwrap(); +// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); +// let resp = plugin.call("call", Value::Null).await.unwrap(); - assert_eq!(resp.result.as_str().unwrap(), "pong"); -} +// assert_eq!(resp.result.as_str().unwrap(), "pong"); +// } -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_call_many() { - setup_logs(); - info!("Starting call_many test..."); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - - let plugin = Plugin::new("test_plugin", wasm_bytes, handler) - .unwrap() - .with_timeout(web_time::Duration::from_secs(2)); - plugin - .call("call_many", Value::Number(500.into())) - .await - .unwrap(); -} +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// #[cfg_attr(not(target_family = "wasm"), tokio::test)] +// async fn test_call_many() { +// setup_logs(); +// info!("Starting call_many test..."); + +// let wasm_bytes = load_plugin_wasm(); +// let handler = Arc::new(get_host_server()); + +// let plugin = Plugin::new("test_plugin", wasm_bytes, handler) +// .unwrap() +// .with_timeout(web_time::Duration::from_secs(2)); +// plugin +// .call("call_many", Value::Number(500.into())) +// .await +// .unwrap(); +// } -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_call_async() { - setup_logs(); - info!("Starting call_async test..."); +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// #[cfg_attr(not(target_family = "wasm"), tokio::test)] +// async fn test_call_async() { +// setup_logs(); +// info!("Starting call_async test..."); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); +// let wasm_bytes = load_plugin_wasm(); +// let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - let resp = plugin.call("call_async", Value::Null).await.unwrap(); - assert_eq!(resp.result.as_str().unwrap(), "pong"); -} +// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); +// let resp = plugin.call("call_async", Value::Null).await.unwrap(); +// assert_eq!(resp.result.as_str().unwrap(), "pong"); +// } -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_call_many_async() { - setup_logs(); - info!("Starting call_many_async test..."); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - - let plugin = Plugin::new("test_plugin", wasm_bytes, handler) - .unwrap() - .with_timeout(web_time::Duration::from_secs(2)); - plugin - .call("call_many_async", Value::Number(500.into())) - .await - .unwrap(); -} +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// #[cfg_attr(not(target_family = "wasm"), tokio::test)] +// async fn test_call_many_async() { +// setup_logs(); +// info!("Starting call_many_async test..."); + +// let wasm_bytes = load_plugin_wasm(); +// let handler = Arc::new(get_host_server()); + +// let plugin = Plugin::new("test_plugin", wasm_bytes, handler) +// .unwrap() +// .with_timeout(web_time::Duration::from_secs(2)); +// plugin +// .call("call_many_async", Value::Number(500.into())) +// .await +// .unwrap(); +// } -#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -#[cfg_attr(not(target_family = "wasm"), tokio::test)] -async fn test_prime_sieve() { - setup_logs(); - info!("Starting prime sieve test..."); +// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +// #[cfg_attr(not(target_family = "wasm"), tokio::test)] +// async fn test_prime_sieve() { +// setup_logs(); +// info!("Starting prime sieve test..."); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); +// let wasm_bytes = load_plugin_wasm(); +// let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); +// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - let response = plugin - .call("prime_sieve", Value::Number(1000.into())) - .await - .unwrap(); +// let response = plugin +// .call("prime_sieve", Value::Number(1000.into())) +// .await +// .unwrap(); - info!("Prime sieve response: {:?}", response); - let count = response.result["count"].as_u64().unwrap(); - assert_eq!(count, 168); -} +// info!("Prime sieve response: {:?}", response); +// let count = response.result["count"].as_u64().unwrap(); +// assert_eq!(count, 168); +// } diff --git a/crates/wasmi-plugin-hdk/tests/worker_test.rs b/crates/wasmi-plugin-hdk/tests/worker_test.rs new file mode 100644 index 0000000..fef30a3 --- /dev/null +++ b/crates/wasmi-plugin-hdk/tests/worker_test.rs @@ -0,0 +1,41 @@ +use std::sync::Once; +use tracing::info; +use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; +use wasmi_plugin_hdk::runtime::worker_pool::{WorkerHandle, WorkerPool}; + +wasm_bindgen_test_configure!(run_in_browser); + +static INIT: Once = Once::new(); + +fn setup_logs() { + INIT.call_once(|| { + tracing_wasm::set_as_global_default_with_config( + tracing_wasm::WASMLayerConfigBuilder::new() + .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) + .build(), + ); + }); +} + +#[wasm_bindgen_test] +async fn test_worker_with_channel() { + setup_logs(); + + let (tx, rx) = futures::channel::oneshot::channel::(); + + let task = move || { + info!("Worker started"); + tx.send(true).unwrap(); + }; + + info!("Spawning worker..."); + let pool = WorkerPool::global(); + pool.run(task).await.expect("Failed to spawn worker"); + + info!("Waiting for worker response..."); + let result = rx + .await + .expect("Worker dropped the sender without responding"); + + assert!(result); +} diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml deleted file mode 100644 index 2846e6a..0000000 --- a/crates/worker/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "worker" -version.workspace = true -rust-version.workspace = true -description = "Lightweight web worker used to run WASIX" -authors.workspace = true -repository.workspace = true -edition.workspace = true -readme.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -exclude.workspace = true - -[lib] -crate-type = ["cdylib"] - -[target.'cfg(target_arch = "wasm32")'.dependencies] -chrono = { workspace = true, features = ["wasmbind"] } -tracing-wasm = { workspace = true } -tracing = { workspace = true } -futures = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -serde-wasm-bindgen = { workspace = true } -thiserror = { workspace = true } -wasmi-plugin-hdk = { workspace = true } -wasmi-plugin-pdk = { workspace = true } -wasmi-plugin-rt = { workspace = true } -web-time = { workspace = true } -wasmer-types = { workspace = true } -wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } -web-sys = { workspace = true, features=["Worker", "DedicatedWorkerGlobalScope", "MessageEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } -wasm-bindgen = { workspace = true } -wasm-bindgen-futures = { workspace = true } diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs deleted file mode 100644 index 0654414..0000000 --- a/crates/worker/src/lib.rs +++ /dev/null @@ -1,143 +0,0 @@ -#![cfg(target_arch = "wasm32")] - -use std::sync::OnceLock; - -use thiserror::Error; -use tracing::{Level, error, info}; -use wasm_bindgen::{ - JsCast, JsValue, - prelude::{Closure, wasm_bindgen}, -}; -use wasmi_plugin_hdk::{ - runtime::{shared_pipe::SharedPipe, worker_protocol::WorkerMessage}, - wasi::wasi_ctx::{self, WasiCtx}, -}; -use web_sys::{ - DedicatedWorkerGlobalScope, MessageEvent, - js_sys::{self, SharedArrayBuffer}, -}; - -mod message_writer; - -static BUFFERS: OnceLock<(SAB, SAB)> = OnceLock::new(); - -struct SAB(SharedArrayBuffer); - -unsafe impl Send for SAB {} -unsafe impl Sync for SAB {} - -#[derive(Debug, Error)] -enum WorkerError { - #[error("Deserialization Error: {0}")] - DeserializeError(#[from] wasmer::DeserializeError), - #[error("WASI Error: {0}")] - WasiError(#[from] wasi_ctx::WasiError), - #[error("Runtime Error: {0}")] - RuntimeError(#[from] wasmer::RuntimeError), -} - -#[wasm_bindgen] -pub fn start_worker() { - tracing_wasm::set_as_global_default_with_config( - tracing_wasm::WASMLayerConfigBuilder::new() - .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) - .set_max_level(Level::INFO) - .build(), - ); - - info!("Worker: Starting up"); - let global = js_sys::global().unchecked_into::(); - - // Register onmessage with channels - let onmessage: Closure = Closure::wrap(Box::new(on_message)); - global.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); - onmessage.forget(); - - // Send ready - let booted_msg = serde_wasm_bindgen::to_value(&WorkerMessage::Booted).unwrap(); - global.post_message(&booted_msg).unwrap(); -} - -fn on_message(e: MessageEvent) { - let global = js_sys::global().unchecked_into::(); - let data = e.data(); - - // Use serde to parse the variant. - // If it's "Initialize", it won't have fields in the enum, so we use Reflect. - let Ok(msg) = serde_wasm_bindgen::from_value::(data.clone()) else { - error!("Worker: Failed to deserialize message"); - return; - }; - - match msg { - WorkerMessage::Initialize => { - on_initialize(&global, data); - } - - WorkerMessage::Load { wasm_module, name } => { - on_load(global, wasm_module, name); - } - _ => {} - } -} - -fn on_initialize(global: &DedicatedWorkerGlobalScope, data: JsValue) { - let stdin_buf = get_sab(&data, "stdin"); - let stdout_buf = get_sab(&data, "stdout"); - - let (Some(si), Some(so)) = (stdin_buf, stdout_buf) else { - error!("Worker: Initialize message missing SharedArrayBuffers"); - return; - }; - - if BUFFERS.set((SAB(si), SAB(so))).is_err() { - info!("Worker: Warning - Attempted to re-initialize buffers"); - } - - let msg = serde_wasm_bindgen::to_value(&WorkerMessage::Initialized).unwrap(); - global.post_message(&msg).unwrap(); -} - -fn on_load(global: DedicatedWorkerGlobalScope, wasm_module: Vec, name: String) { - let Some((stdin, stdout)) = BUFFERS.get() else { - error!("Worker: Received Load message before pipes were initialized"); - return; - }; - - if let Err(err) = run_instance(&wasm_module, name, &stdin.0, &stdout.0) { - error!("Worker: Run error: {:?}", err); - } - - let idle_msg = serde_wasm_bindgen::to_value(&WorkerMessage::Idle).unwrap(); - global.post_message(&idle_msg).unwrap(); -} - -fn get_sab(obj: &JsValue, key: &str) -> Option { - js_sys::Reflect::get(obj, &key.into()) - .ok() - .and_then(|v| v.dyn_into::().ok()) -} - -fn run_instance( - wasm_module: &[u8], - name: String, - stdin: &SharedArrayBuffer, - stdout: &SharedArrayBuffer, -) -> Result<(), WorkerError> { - let stdin = SharedPipe::new("WORKER_STDIN", stdin); - let stdout = SharedPipe::new("WORKER_STDOUT", stdout); - // TODO: Switch this back to SharedPipe for potentially increased performance? - let stderr = message_writer::MessageWriter::new(name); - - let mut store = wasmer::Store::default(); - let module = unsafe { wasmer::Module::deserialize(&store, wasm_module) }?; - - let wasi_ctx = WasiCtx::new() - .set_stdin(stdin) - .set_stdout(stdout) - .set_stderr(stderr); - let start = wasi_ctx.into_fn(&mut store, &module)?; - start.call(&mut store, &[])?; - - Ok(()) -} diff --git a/crates/worker/src/message_writer.rs b/crates/worker/src/message_writer.rs deleted file mode 100644 index e9fd33f..0000000 --- a/crates/worker/src/message_writer.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::io::Write; - -use wasm_bindgen::JsCast; -use wasmi_plugin_hdk::runtime::worker_protocol::WorkerMessage; -use web_sys::js_sys; - -pub struct MessageWriter { - name: String, - global: web_sys::DedicatedWorkerGlobalScope, -} - -unsafe impl Send for MessageWriter {} -unsafe impl Sync for MessageWriter {} - -impl MessageWriter { - pub fn new(name: String) -> Self { - let global = js_sys::global().unchecked_into::(); - Self { name, global } - } -} - -impl Write for MessageWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - let msg = String::from_utf8_lossy(buf).to_string(); - let now = chrono::Local::now(); - let timestamp = now.format("%Y-%m-%d %H:%M:%S%.3f").to_string(); - let msg = format!("[{}] [{}] {}", timestamp, &self.name, msg); - let msg = serde_wasm_bindgen::to_value(&WorkerMessage::PluginLog { message: msg }).unwrap(); - - self.global.post_message(&msg).unwrap(); - Ok(buf.len()) - } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } -} diff --git a/makefile b/makefile index 9d01f35..744392e 100644 --- a/makefile +++ b/makefile @@ -1,26 +1,13 @@ # === Configuration === -HDK_PATH := crates/wasmi-plugin-hdk -WORKER_PATH := crates/worker -PLUGIN_NAME := test-plugin BROWSER := --firefox .PHONY: help build-plugin build-worker build-all bench-wasm -help: - @echo "Usage:" - @echo " make bench-wasm - Build all and run Wasm benchmarks" - @echo " make build-all - Build plugin and worker WASM" - build-plugin: @echo "--- Building Plugin WASM ---" - cargo build -p $(PLUGIN_NAME) --target wasm32-wasip1 --release - -build-worker: - @echo "--- Building Worker Assets ---" - cd $(WORKER_PATH) && wasm-pack build --target no-modules --out-dir ./pkg --release - + cargo build --target wasm32-wasip1 -p test-plugin --release -build-all: build-plugin build-worker +build-all: build-plugin clippy: build-all @echo "--- Running Clippy Lints ---" @@ -36,10 +23,14 @@ bench-native: build-plugin test-wasm: build-all @echo "--- Running Wasm Tests ---" - @cd $(HDK_PATH) && \ - cargo test -p wasmi-plugin-hdk --target wasm32-unknown-unknown + cargo test \ + -Z build-std=std,panic_abort \ + -p wasmi-plugin-hdk \ + --target wasm32-unknown-unknown bench-wasm: build-all @echo "--- Running Wasm Benchmarks ---" - @cd $(HDK_PATH) && \ - cargo bench -p wasmi-plugin-hdk --target wasm32-unknown-unknown \ No newline at end of file + cargo bench \ + -Z build-std=std,panic_abort \ + -p wasmi-plugin-hdk \ + --target wasm32-unknown-unknown \ No newline at end of file diff --git a/shell.nix b/shell.nix index e975b5c..c2c6663 100644 --- a/shell.nix +++ b/shell.nix @@ -4,34 +4,32 @@ let (import (builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz")) ]; }; - unstable = import { }; + rustToolchain = pkgs.rust-bin.nightly.latest.default.override { + extensions = [ "rust-src" ]; + targets = [ + "wasm32-wasip1" + ]; + }; wasm-bindgen-cli_0_2_106 = pkgs.callPackage ./flakes/wasm-bindgen-cli.nix { }; in pkgs.mkShell { packages = with pkgs; [ - # Rust toolchain with WASI target - (rust-bin.stable.latest.default.override { - extensions = [ "rust-src" ]; - targets = [ - "wasm32-wasip1" - "wasm32-unknown-unknown" - ]; - }) - - cargo + rustToolchain cargo-sort cargo-machete samply - rustfmt binaryen - rust-bin.stable.latest.rust-analyzer wasm-pack geckodriver wasm-bindgen-cli_0_2_106 + wasm-tools + wabt ]; shellHook = '' + export WASM_BINDGEN_THREADS_HEADERS=1 export CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner - export WASM_BINDGEN_TEST_TIMEOUT=300 + export WASM_BINDGEN_TEST_TIMEOUT=10 + export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library" ''; } diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..21cd847 --- /dev/null +++ b/todo.md @@ -0,0 +1,47 @@ +[PLUGIN_0] [50.278] [test_plugin] INFO test_plugin: Starting plugin... +[WORKER_0] [50.279] INFO /runtime/shared_pipe.rs:152 SharedPipe_WORKER_STDIN::read called, buf len=8192 +[PLUGIN_0] [50.279] [test_plugin] INFO wasmi_plugin_pdk::server: PluginServer: Received request: RpcRequest { jsonrpc: "2.0", id: 1, method: "call_many", params: Number(200) } + +#? These are logs from a background read loop running in the plugin to enable async calls. But they're not relevant right now +[PLUGIN_0] [50.279] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver_Background: Awaiting incoming message... +[WORKER_0] [50.279] INFO /runtime/shared_pipe.rs:152 SharedPipe_WORKER_STDIN::read called, buf len=8192 +[WORKER_0] [50.279] INFO /runtime/shared_pipe.rs:168 SharedPipe_WORKER_STDIN::read WouldBlock +[PLUGIN_0] [50.279] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver_Background: Message processed, yielding... + +[PLUGIN_0] [50.279] [test_plugin] INFO test_plugin: Making call 0 +[PLUGIN_0] [50.280] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Sending request ID 0 +[WORKER_0] [50.280] INFO /runtime/shared_pipe.rs:187 SharedPipe_WORKER_STDOUT::write called, buf len=55 +[WORKER_0] [50.280] INFO /runtime/shared_pipe.rs:215 SharedPipe_WORKER_STDOUT::write wrote 55, new_w=55 +[PLUGIN_0] [50.280] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Locking reader... +[PLUGIN_0] [50.280] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Waiting for response id=0 +[WORKER_0] [50.280] INFO /runtime/shared_pipe.rs:152 SharedPipe_WORKER_STDIN::read called, buf len=8192 +[WORKER_0] [50.280] INFO /runtime/shared_pipe.rs:168 SharedPipe_WORKER_STDIN::read WouldBlock +[PLUGIN_0] [50.280] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: WouldBlock, yielding... id=0 +[WORKER_0] [50.280] INFO wasi/wasi_ctx.rs:569 wasi poll_oneoff(1046552, 1046600, 1, 1046636) +[WORKER_0] [50.280] INFO wasi/wasi_ctx.rs:670 poll_oneoff: waiting for stdin or clock timeout... +[WORKER_0] [50.280] INFO /runtime/shared_pipe.rs:241 SharedPipe_WORKER_STDIN::wait_ready called, timeout=inf +[WORKER_0] [50.280] INFO /runtime/shared_pipe.rs:250 SharedPipe_WORKER_STDIN::wait_ready checking r=59, current_w=59 +[WORKER_0] [50.281] INFO /runtime/shared_pipe.rs:277 SharedPipe_WORKER_STDIN::wait_ready resolved +[PLUGIN_0] [50.281] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Resuming... id=0 +[WORKER_0] [50.281] INFO /runtime/shared_pipe.rs:152 SharedPipe_WORKER_STDIN::read called, buf len=8192 +[PLUGIN_0] [50.281] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Received message: RpcResponse(RpcResponse { jsonrpc: "2.0", id: 0, result: String("pong") }) +[PLUGIN_0] [50.281] [test_plugin] INFO test_plugin: Call 0 got response: String("pong") + +[PLUGIN_0] [50.281] [test_plugin] INFO test_plugin: Making call 1 +[PLUGIN_0] [50.281] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Sending request ID 1 +[WORKER_0] [50.281] INFO /runtime/shared_pipe.rs:187 SharedPipe_WORKER_STDOUT::write called, buf len=55 +[WORKER_0] [50.281] INFO /runtime/shared_pipe.rs:215 SharedPipe_WORKER_STDOUT::write wrote 55, new_w=110 +[PLUGIN_0] [50.281] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Locking reader... +[PLUGIN_0] [50.281] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Waiting for response id=1 +[WORKER_0] [50.281] INFO /runtime/shared_pipe.rs:152 SharedPipe_WORKER_STDIN::read called, buf len=8192 +[WORKER_0] [50.281] INFO /runtime/shared_pipe.rs:168 SharedPipe_WORKER_STDIN::read WouldBlock +[PLUGIN_0] [50.281] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: WouldBlock, yielding... id=1 +[WORKER_0] [50.281] INFO wasi/wasi_ctx.rs:569 wasi poll_oneoff(1046552, 1046600, 1, 1046636) +[WORKER_0] [50.281] INFO wasi/wasi_ctx.rs:670 poll_oneoff: waiting for stdin or clock timeout... +[WORKER_0] [50.281] INFO /runtime/shared_pipe.rs:241 SharedPipe_WORKER_STDIN::wait_ready called, timeout=inf +[WORKER_0] [50.281] INFO /runtime/shared_pipe.rs:250 SharedPipe_WORKER_STDIN::wait_ready checking r=100, current_w=100 +[WORKER_0] [50.282] INFO /runtime/shared_pipe.rs:277 SharedPipe_WORKER_STDIN::wait_ready resolved +[PLUGIN_0] [50.282] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Resuming... id=1 +[WORKER_0] [50.282] INFO /runtime/shared_pipe.rs:152 SharedPipe_WORKER_STDIN::read called, buf len=8192 +[PLUGIN_0] [50.282] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Received message: RpcResponse(RpcResponse { jsonrpc: "2.0", id: 1, result: String("pong") }) +[PLUGIN_0] [50.282] [test_plugin] INFO test_plugin: Call 1 got response: String("pong") \ No newline at end of file From d55b64e749f7f859ce20fe27bc94519f67ca49c7 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sat, 27 Dec 2025 00:27:56 -0500 Subject: [PATCH 29/70] feat: wasi over shared worker pool --- crates/wasmi-plugin-hdk/Cargo.toml | 2 +- crates/wasmi-plugin-hdk/src/compile.rs | 35 +- .../src/runtime/message_writer.rs | 35 ++ crates/wasmi-plugin-hdk/src/runtime/mod.rs | 8 +- .../src/runtime/non_blocking_pipe.rs | 1 + .../src/runtime/shared_pipe.rs | 451 ------------------ .../src/runtime/thread_worker.rs | 33 ++ .../src/runtime/worker_message.rs | 3 + .../src/runtime/worker_pool.rs | 39 +- .../src/runtime/worker_runtime.rs | 41 +- crates/wasmi-plugin-hdk/src/transport.rs | 60 +-- crates/wasmi-plugin-hdk/tests/integration.rs | 284 ++++++----- crates/wasmi-plugin-hdk/tests/worker_test.rs | 81 +++- shell.nix | 2 +- 14 files changed, 408 insertions(+), 667 deletions(-) create mode 100644 crates/wasmi-plugin-hdk/src/runtime/message_writer.rs delete mode 100644 crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs diff --git a/crates/wasmi-plugin-hdk/Cargo.toml b/crates/wasmi-plugin-hdk/Cargo.toml index 6c0d980..d37a4f6 100644 --- a/crates/wasmi-plugin-hdk/Cargo.toml +++ b/crates/wasmi-plugin-hdk/Cargo.toml @@ -34,7 +34,7 @@ wasmer = { workspace = true, default-features = true} gloo-timers = { workspace = true } serde-wasm-bindgen = { workspace = true } wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } -web-sys = { workspace = true, features=["Worker", "WorkerOptions", "WorkerType", "MessageEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } +web-sys = { workspace = true, features=["Worker", "WorkerOptions", "DedicatedWorkerGlobalScope", "WorkerType", "MessageEvent", "ErrorEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } diff --git a/crates/wasmi-plugin-hdk/src/compile.rs b/crates/wasmi-plugin-hdk/src/compile.rs index 724b0db..f035c63 100644 --- a/crates/wasmi-plugin-hdk/src/compile.rs +++ b/crates/wasmi-plugin-hdk/src/compile.rs @@ -1,11 +1,26 @@ +#[cfg(target_arch = "wasm32")] +use std::sync::Arc; + #[derive(Clone)] pub struct Compiled { pub name: String, #[cfg(not(target_arch = "wasm32"))] pub engine: wasmer::Engine, + #[cfg(not(target_arch = "wasm32"))] pub module: wasmer::Module, + + // #[cfg(target_arch = "wasm32")] + // pub js_module: web_sys::js_sys::WebAssembly::Module, + #[cfg(target_arch = "wasm32")] + pub wasm_bytes: Arc<[u8]>, } +// SAFETY: WebAssembly.Module is one of the few JS objects that is +// "thread-safe" in the browser because it is immutable and +// can be transferred/cloned across workers without issues. +unsafe impl Send for Compiled {} +unsafe impl Sync for Compiled {} + impl Compiled { #[cfg(not(target_arch = "wasm32"))] pub async fn new(name: &str, wasm_bytes: &[u8]) -> Result { @@ -21,25 +36,25 @@ impl Compiled { #[cfg(target_arch = "wasm32")] pub async fn new(name: &str, wasm_bytes: &[u8]) -> Result { - use wasm_bindgen::JsCast; + // use wasm_bindgen::JsCast; + // TODO: Compile module once and cache it instead of re-compiling for every call. //? Because we're in wasm32-unknown-unknown AND we want to have Shared Memory //? (target-feature=+atomics,+bulk-memory), //? we have to use the WebAssembly.compile async function and pass the compiled //? native mmodule to wasmer. Wasmer only supports compiling via `WebAssembly::new()`, //? which is illegal for use with Shared Memory. - let js_bytes = web_sys::js_sys::Uint8Array::from(wasm_bytes); - let promise = web_sys::js_sys::WebAssembly::compile(&js_bytes.into()); - let js_module_value = wasm_bindgen_futures::JsFuture::from(promise).await.unwrap(); - let js_module = js_module_value - .dyn_into::() - .unwrap(); - - let module: wasmer::Module = (js_module, wasm_bytes).into(); + // let js_bytes = web_sys::js_sys::Uint8Array::from(wasm_bytes); + // let promise = web_sys::js_sys::WebAssembly::compile(&js_bytes.into()); + // let js_module_value = wasm_bindgen_futures::JsFuture::from(promise).await.unwrap(); + // let js_module = js_module_value + // .dyn_into::() + // .unwrap(); Ok(Compiled { name: name.to_string(), - module, + // js_module, + wasm_bytes: Arc::from(wasm_bytes), }) } } diff --git a/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs b/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs new file mode 100644 index 0000000..e64ff30 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs @@ -0,0 +1,35 @@ +use std::io::Write; + +use wasm_bindgen::JsCast; +use web_sys::js_sys; + +use crate::runtime::worker_message::WorkerMessage; + +pub struct MessageWriter { + name: String, + global: web_sys::DedicatedWorkerGlobalScope, +} + +unsafe impl Send for MessageWriter {} +unsafe impl Sync for MessageWriter {} + +impl MessageWriter { + pub fn new(name: String) -> Self { + let global = js_sys::global().unchecked_into::(); + Self { name, global } + } +} + +impl Write for MessageWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let msg = String::from_utf8_lossy(buf).to_string(); + let msg = serde_wasm_bindgen::to_value(&WorkerMessage::PluginLog { message: msg }).unwrap(); + + self.global.post_message(&msg).unwrap(); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} diff --git a/crates/wasmi-plugin-hdk/src/runtime/mod.rs b/crates/wasmi-plugin-hdk/src/runtime/mod.rs index 4fe26e4..62dea9d 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/mod.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/mod.rs @@ -8,15 +8,15 @@ mod native_runtime; #[cfg(target_arch = "wasm32")] pub use worker_runtime::WorkerRuntime; #[cfg(target_arch = "wasm32")] -pub mod shared_pipe; +mod message_writer; #[cfg(target_arch = "wasm32")] -pub mod thread_worker; +mod thread_worker; #[cfg(target_arch = "wasm32")] -pub mod worker_message; +mod worker_message; #[cfg(target_arch = "wasm32")] pub mod worker_pool; #[cfg(target_arch = "wasm32")] -pub mod worker_runtime; +mod worker_runtime; use crate::compile::Compiled; use futures::{AsyncRead, AsyncWrite}; diff --git a/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs b/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs index b34bb22..1582a22 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs @@ -76,6 +76,7 @@ impl WasiReader for NonBlockingPipeReader { fn wait_ready(&self, _timeout: Option) { // TODO: Make native-compatible version of this? // No-op +std::hint::spin_loop(); } } diff --git a/crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs b/crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs deleted file mode 100644 index dea6ee1..0000000 --- a/crates/wasmi-plugin-hdk/src/runtime/shared_pipe.rs +++ /dev/null @@ -1,451 +0,0 @@ -//! Shared pipe implementation for worker bridge. - -use std::{ - f64, - io::{Read, Write}, - pin::Pin, - task::{Context, Poll}, -}; - -use futures::{AsyncRead, AsyncWrite}; -use tracing::info; -use wasm_bindgen_futures::JsFuture; -use web_sys::{ - js_sys::{self, Atomics, Int32Array, SharedArrayBuffer, Uint8Array}, - wasm_bindgen::{JsCast, prelude::wasm_bindgen}, -}; - -use crate::wasi::wasi_ctx::WasiReader; -// 4xi32 header: (16 bytes) -const CLOSED_IDX: u32 = 0; // 0 = open, 1 = closed -const WRITE_IDX: u32 = 1; -const READ_IDX: u32 = 2; -const DATA_OFFSET: u32 = 16; - -// TODO: Impl AsyncRead so we don't need to busy-wait with WouldBlock -pub struct SharedPipe { - header: Int32Array, - data: Uint8Array, - capacity: u32, - - name: String, - pending_read: Option, - pending_write: Option, -} - -#[wasm_bindgen] -extern "C" { - /// The object returned by Atomics.waitAsync - pub type WaitAsyncResult; - - #[wasm_bindgen(method, getter, js_name = async)] - pub fn is_async(this: &WaitAsyncResult) -> bool; - - #[wasm_bindgen(method, getter)] - pub fn value(this: &WaitAsyncResult) -> wasm_bindgen::JsValue; -} - -unsafe impl Send for SharedPipe {} -unsafe impl Sync for SharedPipe {} - -impl SharedPipe { - pub fn new(name: &str, sab: &SharedArrayBuffer) -> Self { - let status = js_sys::Int32Array::new_with_byte_offset_and_length(&sab, 0, 4); - let data_len = sab.byte_length() - DATA_OFFSET; - let data = js_sys::Uint8Array::new_with_byte_offset_and_length(&sab, DATA_OFFSET, data_len); - let capacity = data_len; - - Self { - header: status, - data, - capacity, - name: name.to_string(), - pending_read: None, - pending_write: None, - } - } - - pub fn new_with_capacity(name: &str, capacity: u32) -> Self { - let sab = SharedArrayBuffer::new((DATA_OFFSET + capacity) as u32); - Self::new(name, &sab) - } - - pub fn reset(&self) { - Atomics::store(&self.header, READ_IDX, 0).unwrap(); - Atomics::store(&self.header, WRITE_IDX, 0).unwrap(); - Atomics::store(&self.header, CLOSED_IDX, 0).unwrap(); - } - - pub fn close(&self) { - Atomics::store(&self.header, CLOSED_IDX, 1).unwrap(); - // Notify both pointers to wake up anyone waiting on either end - Atomics::notify(&self.header, READ_IDX).unwrap(); - Atomics::notify(&self.header, WRITE_IDX).unwrap(); - } - - pub fn buffer(&self) -> SharedArrayBuffer { - self.header.buffer().unchecked_into::() - } - - pub fn dump(&self) { - //? Dumps internal state to logs for debugging - let closed = self.is_closed(); - let r = self.read_idx(); - let w = self.write_idx(); - let occupied = self.get_occupied(r, w); - - let last_byte_count = occupied.min(50); - let last_bytes = if occupied == 0 { - vec![] - } else { - let mut bytes = vec![0u8; last_byte_count as usize]; - let start_idx = if w >= last_byte_count { - w - last_byte_count - } else { - self.capacity + w - last_byte_count - }; - for i in 0..last_byte_count { - let idx = (start_idx + i) % self.capacity; - bytes[i as usize] = self.data.get_index(idx); - } - bytes - }; - - // Interpret as UTF-8 if possible - let last_bytes_str = match String::from_utf8(last_bytes.clone()) { - Ok(s) => s, - Err(_) => format!("{:?}", last_bytes), - }; - - info!( - "SharedPipe[{}] terminated: closed={} read_idx={} write_idx={} occupied={} last_bytes={}", - self.name, closed, r, w, occupied, last_bytes_str - ); - } - - fn is_closed(&self) -> bool { - let closed = Atomics::load(&self.header, CLOSED_IDX).unwrap(); - closed == 1 - } - - fn read_idx(&self) -> u32 { - Atomics::load(&self.header, READ_IDX).unwrap() as u32 - } - - fn set_read(&self, value: u32) { - Atomics::store(&self.header, READ_IDX, value as i32).unwrap(); - Atomics::notify(&self.header, READ_IDX).unwrap(); - } - - fn write_idx(&self) -> u32 { - Atomics::load(&self.header, WRITE_IDX).unwrap() as u32 - } - - fn set_write(&self, value: u32) { - let old = Atomics::exchange(&self.header, WRITE_IDX, value as i32).unwrap(); - if old == value as i32 { - panic!("set_write called with same value: {}", value); - } - Atomics::notify(&self.header, WRITE_IDX).unwrap(); - } - - fn get_occupied(&self, r: u32, w: u32) -> u32 { - if w >= r { w - r } else { self.capacity - r + w } - } - - /// Attempts to read from the pipe, returning the number of bytes read, or None if - /// wouldBlock. - fn try_read(&self, r: u32, w: u32, buf: &mut [u8]) -> Option { - if r == w { - if self.is_closed() { - return Some(0); // EOF - } - return None; - } - - let occupied = self.get_occupied(r, w); - let amount = (buf.len() as u32).min(occupied); - - for i in 0..amount { - let idx = (r + i) % self.capacity; - buf[i as usize] = self.data.get_index(idx); - } - - // info!( - // "SharedPipe[{}] try_read: r={} first_bytes={:?}", - // self.name, - // r, - // &buf[..(amount as usize).min(50)] - // ); - Some(amount as usize) - } - - /// Attempts to write to the pipe, returning the number of bytes written, or None if - /// wouldBlock. - fn try_write(&self, r: u32, w: u32, buf: &[u8]) -> Option { - if self.is_closed() { - return None; - } - - let occupied = self.get_occupied(r, w); - //? The buffer is full if occupied == capacity - 1 - let available = self.capacity.saturating_sub(occupied).saturating_sub(1); - - if available == 0 { - return None; - } - - let n = (buf.len() as u32).min(available); - - for i in 0..n { - let idx = (w + i) % self.capacity; - self.data.set_index(idx, buf[i as usize]); - } - - // info!( - // "SharedPipe[{}] try_write: w={} first_bytes={:?}", - // self.name, - // w, - // &buf[..buf.len().min(50)] - // ); - Some(n as usize) - } -} - -impl Read for SharedPipe { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let r = self.read_idx(); - let w = self.write_idx(); - - if let Some(n) = self.try_read(r, w, buf) { - self.set_read((r + n as u32) % self.capacity); - return Ok(n); - } - - Err(std::io::ErrorKind::WouldBlock.into()) - } -} - -impl Write for SharedPipe { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - let r = self.read_idx(); - let w = self.write_idx(); - - if let Some(n) = self.try_write(r, w, buf) { - self.set_write((w + n as u32) % self.capacity); - return Ok(n); - } - - Err(std::io::ErrorKind::WouldBlock.into()) - } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } -} - -impl WasiReader for SharedPipe { - fn is_ready(&self) -> bool { - let r = self.read_idx(); - let w = self.write_idx(); - - r != w || self.is_closed() - } - - fn wait_ready(&self, timeout: Option) { - let timeout_ms = timeout - .map(|d| d.as_millis() as f64) - .unwrap_or(f64::INFINITY); - - loop { - let (r, w) = (self.read_idx(), self.write_idx()); - - if r != w || self.is_closed() { - return; - } - - match Atomics::wait_with_timeout(&self.header, WRITE_IDX, w as i32, timeout_ms) { - Ok(s) if s == "not-equal" => { - //? WRITE_IDX changed, re-check - continue; - } - _ => { - //? Resolved - return; - } - } - } - } -} - -impl AsyncRead for SharedPipe { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut [u8], - ) -> Poll> { - //? Check for existing promise - if let Some(mut fut) = self.pending_read.take() { - if Pin::new(&mut fut).poll(cx).is_pending() { - self.pending_read = Some(fut); - return Poll::Pending; - } - } - - //? Try sync read - loop { - let (r, w) = (self.read_idx(), self.write_idx()); - - if let Some(n) = self.try_read(r, w, buf) { - self.set_read((r + n as u32) % self.capacity); - return Poll::Ready(Ok(n)); - } - - //? Wait for WRITE_IDX to change - let res: WaitAsyncResult = Atomics::wait_async(&self.header, WRITE_IDX, w as i32) - .expect("waitAsync support") - .unchecked_into(); - - if !res.is_async() { - //? WRITE_IDX changed, re-check - continue; - } - - let current_w = self.write_idx(); - if current_w != w { - continue; - } - - let promise: js_sys::Promise = res.value().unchecked_into(); - let mut fut = JsFuture::from(promise); - - if let Poll::Ready(_) = Pin::new(&mut fut).poll(cx) { - //? Resolved immediately, re-check - continue; - } - - self.pending_read = Some(fut); - return Poll::Pending; - } - } -} - -impl AsyncWrite for SharedPipe { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - //? Drain existing promise - if let Some(mut fut) = self.pending_write.take() { - if Pin::new(&mut fut).poll(cx).is_pending() { - self.pending_write = Some(fut); - return Poll::Pending; - } - } - - loop { - let (r, w) = (self.read_idx(), self.write_idx()); - - if let Some(n) = self.try_write(r, w, buf) { - self.set_write((w + n as u32) % self.capacity); - return Poll::Ready(Ok(n)); - } - - //? Wait for READ_IDX to change - let res: WaitAsyncResult = Atomics::wait_async(&self.header, READ_IDX, r as i32) - .expect("waitAsync support required") - .unchecked_into(); - - if !res.is_async() { - //? READ_IDX changed, re-check - continue; - } - - let promise = res.value().unchecked_into::(); - let mut fut = JsFuture::from(promise); - - if let Poll::Ready(_) = Pin::new(&mut fut).poll(cx) { - //? Resolved immediately, re-check - continue; - } - - self.pending_write = Some(fut); - return Poll::Pending; - } - } - - fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { - self.close(); - Poll::Ready(Ok(())) - } -} - -// #[cfg(test)] -// mod tests { - -// #[cfg(target_family = "wasm")] -// wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - -// use super::*; -// use wasm_bindgen_test::wasm_bindgen_test; - -// #[wasm_bindgen_test] -// fn test_basic_read_write() { -// let mut pipe = SharedPipe::new_with_capacity("test", 10); - -// let input = b"hello"; -// let written = pipe.write(input).expect("Write failed"); -// assert_eq!(written, 5); - -// let mut output = [0u8; 5]; -// let read = pipe.read(&mut output).expect("Read failed"); -// assert_eq!(read, 5); -// assert_eq!(&output, b"hello"); - -// // Should now be WouldBlock -// let res = pipe.read(&mut output); -// assert!(res.is_err()); - -// pipe.close(); -// let res = pipe.write(b"more"); -// assert!(res.is_err()); - -// let res = pipe.read(&mut output); -// assert_eq!(res.ok(), Some(0)); // EOF -// } - -// #[wasm_bindgen_test] -// fn test_wrap_around_saturation() { -// let mut pipe = SharedPipe::new_with_capacity("test", 10); - -// // 1. Fill 7 bytes -// pipe.write(b"hello_w").unwrap(); - -// // 2. Read 4 bytes (read_idx moves to 4, write_idx is at 7) -// let mut tmp = [0u8; 4]; -// pipe.read(&mut tmp).unwrap(); -// assert_eq!(&tmp, b"hell"); - -// // 3. Write 5 more bytes. -// // Current state: r=4, w=7. Space available: 10 - (7-4) - 1 = 6. -// // This write will wrap! (7, 8, 9, then 0, 1) -// let written = pipe.write(b"orld_").unwrap(); -// assert_eq!(written, 5); - -// // 4. Read all available data (should be "efg" + "hijkl" = 8 bytes) -// // This read MUST saturate the output buffer across the wrap point. -// let mut output = [0u8; 8]; -// let read = pipe.read(&mut output).expect("Wrap read failed"); - -// assert_eq!(read, 8); -// assert_eq!(&output, b"o_world_"); - -// // Verify pointers are back in sync -// assert_eq!(pipe.read_idx(), pipe.write_idx()); -// } -// } diff --git a/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs b/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs index 731651f..6e95373 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs @@ -1,7 +1,12 @@ +use std::panic; + +use tracing::error; use wasm_bindgen::prelude::wasm_bindgen; #[wasm_bindgen] pub async fn execute_worker_task(ptr: u32) { + set_panic_hook(); + //* SAFETY: We transfer ownership of the closure from the caller's `spawn` //* function to this web worker. The pointer must be a FnOnce closure //* allocated on the heap. @@ -12,3 +17,31 @@ pub async fn execute_worker_task(ptr: u32) { (*closure)(); } + +fn set_panic_hook() { + panic::set_hook(Box::new(|panic_info| { + // 1. Try to get the message as a &str (for panic!("msg")) + // 2. Or as a String (for panic!("format {}", arg)) + let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() { + s.to_string() + } else if let Some(s) = panic_info.payload().downcast_ref::() { + s.clone() + } else { + "Unknown panic payload".to_string() + }; + + // Get location information + let location = panic_info + .location() + .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column())) + .unwrap_or_else(|| "unknown location".to_string()); + + // Log via tracing + error!( + target: "panic", + panic_message = %message, + panic_location = %location, + "A panic occurred" + ); + })); +} diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_message.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_message.rs index aea3aa6..e525540 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_message.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_message.rs @@ -8,6 +8,9 @@ pub enum WorkerMessage { level: String, ts: f64, }, + PluginLog { + message: String, + }, Ready, Idle, } diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs index d62dd30..2443cf8 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs @@ -16,6 +16,9 @@ use crate::runtime::worker_message::WorkerMessage; pub type WorkerId = u64; +/// Pool that manages a set of web workers for executing tasks +/// asynchronously. Workers are reused for multiple tasks to minimize +/// the overhead of spawning new workers. pub struct WorkerPool { state: Arc>, } @@ -35,6 +38,7 @@ pub struct WorkerHandle { state: Arc>, worker: web_sys::Worker, _on_message: wasm_bindgen::prelude::Closure, + _on_error: wasm_bindgen::prelude::Closure, } #[derive(Debug, Error)] @@ -55,12 +59,19 @@ impl WorkerPool { }) } - pub async fn run(&self, f: F) -> Result, SpawnError> + pub async fn run(&self, f: F) -> Result, SpawnError> where - F: FnOnce() + Send + 'static, + F: FnOnce() -> Fut + Send + 'static, + Fut: Future + 'static, { let handle = self.get_or_spawn_worker().await?; - handle.run(f)?; + + let wrapper = move || { + wasm_bindgen_futures::spawn_local(async move { + f().await; + }); + }; + handle.run(wrapper)?; Ok(handle) } @@ -114,6 +125,11 @@ impl WorkerHandle { let on_message = Closure::wrap(on_message); worker.set_onmessage(Some(on_message.as_ref().unchecked_ref())); + // Set up onerror handler + let on_error = on_error_handler(name.clone()); + let on_error = Closure::wrap(on_error); + worker.set_onerror(Some(on_error.as_ref().unchecked_ref())); + // 4. Send the "init" message with memory and module let msg = js_sys::Object::new(); Reflect::set(&msg, &"type".into(), &"init".into()).unwrap(); @@ -130,6 +146,7 @@ impl WorkerHandle { state, worker, _on_message: on_message, + _on_error: on_error, }) } @@ -206,6 +223,9 @@ fn on_message_handler( WorkerMessage::Log { message, level, ts } => { log_worker_message(&name, message, level, ts); } + WorkerMessage::PluginLog { message } => { + info!(target: "WORKER", "[{}] {}", name, message); + } WorkerMessage::Ready => { info!(target: "WORKER", "[{}] Ready", name); if let Some(tx) = init_tx.take() { @@ -230,3 +250,16 @@ fn log_worker_message(name: &str, msg: String, level: String, ts: f64) { _ => info!(target: "WORKER", "[{}] {}", name, msg), } } + +fn on_error_handler(name: String) -> Box { + Box::new(move |e: web_sys::ErrorEvent| { + error!( + target: "WORKER", + "[{}] Worker error: {} (at {}:{})", + name, + e.message(), + e.filename(), + e.lineno() + ); + }) +} diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index 9d77f85..4eb133e 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -1,20 +1,21 @@ //! Web Worker runtime bridge, running the Wasm module in a Web Worker. use std::collections::HashMap; +use std::io::Write; use std::sync::Arc; use futures::lock::Mutex; use futures::{AsyncRead, AsyncWrite}; use thiserror::Error; use tracing::error; +use wasm_bindgen::JsCast; use crate::compile::Compiled; use crate::runtime::Runtime; -use crate::runtime::non_blocking_pipe::{ - NonBlockingPipeReader, NonBlockingPipeWriter, non_blocking_pipe, -}; +use crate::runtime::message_writer::MessageWriter; +use crate::runtime::non_blocking_pipe::non_blocking_pipe; use crate::runtime::worker_pool::{SpawnError, WorkerHandle, WorkerId, WorkerPool}; -use crate::wasi::wasi_ctx; +use crate::wasi::wasi_ctx::{self, WasiReader}; pub struct WorkerRuntime { active_sessions: Arc>>>, @@ -26,6 +27,8 @@ enum RunError { WasiError(#[from] wasi_ctx::WasiError), #[error("Runtime Error: {0}")] RuntimeError(#[from] wasmer::RuntimeError), + #[error("Compile Error: {0}")] + CompileError(#[from] wasmer::CompileError), } impl WorkerRuntime { @@ -58,20 +61,19 @@ impl Runtime for WorkerRuntime { > { let (stdin_reader, stdin_writer) = non_blocking_pipe(); let (stdout_reader, stdout_writer) = non_blocking_pipe(); - let (stderr_reader, stderr_writer) = non_blocking_pipe(); + let stderr_writer = MessageWriter::new("".into()); + // let (stderr_reader, stderr_writer) = non_blocking_pipe(); let pool = WorkerPool::global(); let handle = pool - .run(move || { - let res = run_instance(compiled, stdin_reader, stdout_writer, stderr_writer); + .run(async move || { + let res = run_instance(compiled, stdin_reader, stdout_writer, stderr_writer).await; if let Err(e) = res { error!("Error running Wasm instance: {}", e); } }) .await?; - let _ = stderr_reader; // Currently unused - let id = handle.id; self.active_sessions.lock().await.insert(handle.id, handle); @@ -86,14 +88,25 @@ impl Runtime for WorkerRuntime { } } -fn run_instance( +async fn run_instance( compiled: Compiled, - stdin: NonBlockingPipeReader, - stdout: NonBlockingPipeWriter, - stderr: NonBlockingPipeWriter, + stdin: impl WasiReader + Send + Sync + 'static, + stdout: impl Write + Send + Sync + 'static, + stderr: impl Write + Send + Sync + 'static, ) -> Result<(), RunError> { let mut store = wasmer::Store::default(); - let module = compiled.module; + + // let js_module = compiled.js_module; + let wasm_bytes = compiled.wasm_bytes.as_ref(); + + let js_bytes = web_sys::js_sys::Uint8Array::from(wasm_bytes); + let promise = web_sys::js_sys::WebAssembly::compile(&js_bytes.into()); + let js_module_value = wasm_bindgen_futures::JsFuture::from(promise).await.unwrap(); + let js_module = js_module_value + .dyn_into::() + .unwrap(); + let module = (js_module, wasm_bytes.as_ref()).into(); + let wasi_ctx = wasi_ctx::WasiCtx::new() .set_stdin(stdin) .set_stdout(stdout) diff --git a/crates/wasmi-plugin-hdk/src/transport.rs b/crates/wasmi-plugin-hdk/src/transport.rs index f25bb8e..144de2e 100644 --- a/crates/wasmi-plugin-hdk/src/transport.rs +++ b/crates/wasmi-plugin-hdk/src/transport.rs @@ -1,7 +1,7 @@ -use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use futures::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, io::BufReader}; use serde_json::Value; use thiserror::Error; -use tracing::info; +use tracing::{error, info}; use wasmi_plugin_pdk::{ api::RequestHandler, rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcResponse}, @@ -9,7 +9,7 @@ use wasmi_plugin_pdk::{ use wasmi_plugin_rt::yield_now; pub struct Transport { - reader: R, + reader: BufReader, writer: W, } @@ -18,8 +18,8 @@ pub enum TransportError { #[error("I/O error: {0}")] Io(#[from] std::io::Error), - #[error("Serde error: {0}")] - Serde(#[from] serde_json::Error), + #[error("Serde error: {0}, data: {1}")] + Serde(serde_json::Error, String), #[error("EOF")] Eof, @@ -30,7 +30,10 @@ pub enum TransportError { impl Transport { pub fn new(reader: R, writer: W) -> Self { - Transport { reader, writer } + Transport { + reader: BufReader::new(reader), + writer, + } } pub async fn call( @@ -41,17 +44,16 @@ impl Transport { ) -> Result { let id = 1; let request = RpcMessage::request(id, method.to_string(), params); + info!("Sending request: {:?}", request); self.write_message(request).await?; - let mut buffer = Vec::new(); - let mut temp = [0u8; 256]; + let mut line = String::new(); loop { - match self.reader.read(&mut temp).await { + info!("Waiting for response..."); + match self.reader.read_line(&mut line).await { Ok(0) => return Err(TransportError::Eof), - Ok(n) => { - buffer.extend_from_slice(&temp[..n]); - } + Ok(_) => {} Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { yield_now().await; continue; @@ -60,29 +62,29 @@ impl Transport { } // Process complete lines - while let Some(pos) = buffer.iter().position(|&b| b == b'\n') { - let line = String::from_utf8_lossy(&buffer[..pos]).to_string(); - buffer.drain(..=pos); - - let msg: RpcMessage = serde_json::from_str(&line)?; - - match msg { - RpcMessage::RpcResponse(resp) if resp.id == id => return Ok(resp), - RpcMessage::RpcErrorResponse(err) if err.id == id => { - return Err(TransportError::ErrorResponse(err)); - } - RpcMessage::RpcRequest(req) => { - self.handle_guest_req(req.id, &req.method, req.params, &handler) - .await?; - } - _ => {} + let msg: RpcMessage = + serde_json::from_str(&line).map_err(|e| TransportError::Serde(e, line.clone()))?; + line.clear(); + + match msg { + RpcMessage::RpcResponse(resp) if resp.id == id => return Ok(resp), + RpcMessage::RpcErrorResponse(err) if err.id == id => { + return Err(TransportError::ErrorResponse(err)); + } + RpcMessage::RpcRequest(req) => { + self.handle_guest_req(req.id, &req.method, req.params, &handler) + .await?; } + _ => {} } } } async fn write_message(&mut self, msg: RpcMessage) -> Result<(), TransportError> { - let msg: String = serde_json::to_string(&msg)?; + let msg: String = serde_json::to_string(&msg).map_err(|e| { + error!("Serde error while serializing message: {}", e); + TransportError::Serde(e, "".to_string()) + })?; let msg = format!("{}\n", msg); self.writer.write_all(msg.as_bytes()).await?; self.writer.flush().await?; diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index 020bbf8..fa46aa8 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -1,5 +1,8 @@ use serde_json::Value; -use std::sync::{Arc, Once}; +use std::{ + sync::{Arc, Once}, + time::Duration, +}; use tracing::info; use wasmi_plugin_hdk::{plugin::Plugin, server::HostServer}; @@ -17,6 +20,17 @@ fn load_plugin_wasm() -> Vec { PLUGIN_WASM.to_vec() } +async fn load_plugin() -> Plugin> { + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) + .await + .unwrap() + .with_timeout(Duration::from_secs(2)); + + plugin +} + fn get_host_server() -> HostServer<()> { HostServer::default() .with_method("ping", |_, _params: ()| async move { @@ -54,171 +68,143 @@ fn setup_logs() { } } -// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -// #[cfg_attr(not(target_family = "wasm"), tokio::test)] -// async fn test_plugin() { -// setup_logs(); -// info!("Starting test_plugin..."); - -// let wasm_bytes = load_plugin_wasm(); -// let handler = Arc::new(get_host_server()); +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_plugin() { + setup_logs(); + info!("Starting test_plugin..."); -// let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) -// .await -// .unwrap(); -// let resp = plugin.call("ping", Value::Null).await.unwrap(); -// assert_eq!(resp.result.as_str().unwrap(), "pong"); -// } + let plugin = load_plugin().await; + let resp = plugin.call("ping", Value::Null).await.unwrap(); -// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -// #[cfg_attr(not(target_family = "wasm"), tokio::test)] -// async fn test_get_random_number() { -// setup_logs(); -// info!("Starting get_random_number test..."); - -// let wasm_bytes = load_plugin_wasm(); -// let handler = Arc::new(get_host_server()); + assert_eq!(resp.result.as_str().unwrap(), "pong"); +} -// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); -// let response = plugin.call("get_random_number", Value::Null).await.unwrap(); +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_get_random_number() { + setup_logs(); + info!("Starting get_random_number test..."); -// info!("Random number response: {:?}", response); -// response.result.as_u64().unwrap(); -// } + let plugin = load_plugin().await; + let response = plugin.call("get_random_number", Value::Null).await.unwrap(); -// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -// #[cfg_attr(not(target_family = "wasm"), tokio::test)] -// async fn test_get_time() { -// setup_logs(); -// info!("Starting get_time test..."); - -// let wasm_bytes = load_plugin_wasm(); -// let handler = Arc::new(get_host_server()); - -// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); -// let response = plugin.call("get_time", Value::Null).await.unwrap(); - -// info!("Get time response: {:?}", response); - -// let timestamp = response.result.as_u64().unwrap(); -// let now = web_time::SystemTime::now() -// .duration_since(web_time::UNIX_EPOCH) -// .unwrap() -// .as_secs(); - -// let diff = timestamp.abs_diff(now); -// assert!(diff < 2, "Timestamp difference too large: {}", diff); -// } - -// // TODO: Fix this for `wasm-pack test --headless --firefox ./crates/wasmi-plugin-hdk` -// // #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -// #[cfg_attr(not(target_family = "wasm"), tokio::test)] -// async fn test_sleep() { -// setup_logs(); -// info!("Starting sleep test..."); - -// let wasm_bytes = load_plugin_wasm(); -// let handler = Arc::new(get_host_server()); - -// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); - -// let sleep_duration = 1500; // milliseconds -// let start = web_time::Instant::now(); -// plugin -// .call("sleep", Value::Number(sleep_duration.into())) -// .await -// .unwrap(); -// let elapsed = start.elapsed().as_millis(); - -// assert!( -// elapsed >= sleep_duration as u128, -// "Sleep duration too short: {} ms", -// elapsed -// ); -// } + info!("Random number response: {:?}", response); + response.result.as_u64().unwrap(); +} -// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -// #[cfg_attr(not(target_family = "wasm"), tokio::test)] -// async fn test_call() { -// setup_logs(); -// info!("Starting call test..."); +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_get_time() { + setup_logs(); + info!("Starting get_time test..."); -// let wasm_bytes = load_plugin_wasm(); -// let handler = Arc::new(get_host_server()); + let plugin = load_plugin().await; + let response = plugin.call("get_time", Value::Null).await.unwrap(); -// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); -// let resp = plugin.call("call", Value::Null).await.unwrap(); + info!("Get time response: {:?}", response); -// assert_eq!(resp.result.as_str().unwrap(), "pong"); -// } + let timestamp = response.result.as_u64().unwrap(); + let now = web_time::SystemTime::now() + .duration_since(web_time::UNIX_EPOCH) + .unwrap() + .as_secs(); -// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -// #[cfg_attr(not(target_family = "wasm"), tokio::test)] -// async fn test_call_many() { -// setup_logs(); -// info!("Starting call_many test..."); - -// let wasm_bytes = load_plugin_wasm(); -// let handler = Arc::new(get_host_server()); - -// let plugin = Plugin::new("test_plugin", wasm_bytes, handler) -// .unwrap() -// .with_timeout(web_time::Duration::from_secs(2)); -// plugin -// .call("call_many", Value::Number(500.into())) -// .await -// .unwrap(); -// } + let diff = timestamp.abs_diff(now); + assert!(diff < 2, "Timestamp difference too large: {}", diff); +} +// TODO: Fix this for `wasm-pack test --headless --firefox ./crates/wasmi-plugin-hdk` // #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -// #[cfg_attr(not(target_family = "wasm"), tokio::test)] -// async fn test_call_async() { -// setup_logs(); -// info!("Starting call_async test..."); +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_sleep() { + setup_logs(); + info!("Starting sleep test..."); + + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + + let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) + .await + .unwrap(); + + let sleep_duration = 1500; // milliseconds + let start = web_time::Instant::now(); + plugin + .call("sleep", Value::Number(sleep_duration.into())) + .await + .unwrap(); + let elapsed = start.elapsed().as_millis(); + + assert!( + elapsed >= sleep_duration as u128, + "Sleep duration too short: {} ms", + elapsed + ); +} -// let wasm_bytes = load_plugin_wasm(); -// let handler = Arc::new(get_host_server()); +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_call() { + setup_logs(); + info!("Starting call test..."); -// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); -// let resp = plugin.call("call_async", Value::Null).await.unwrap(); -// assert_eq!(resp.result.as_str().unwrap(), "pong"); -// } + let plugin = load_plugin().await; + let resp = plugin.call("call", Value::Null).await.unwrap(); -// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -// #[cfg_attr(not(target_family = "wasm"), tokio::test)] -// async fn test_call_many_async() { -// setup_logs(); -// info!("Starting call_many_async test..."); - -// let wasm_bytes = load_plugin_wasm(); -// let handler = Arc::new(get_host_server()); - -// let plugin = Plugin::new("test_plugin", wasm_bytes, handler) -// .unwrap() -// .with_timeout(web_time::Duration::from_secs(2)); -// plugin -// .call("call_many_async", Value::Number(500.into())) -// .await -// .unwrap(); -// } + assert_eq!(resp.result.as_str().unwrap(), "pong"); +} -// #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] -// #[cfg_attr(not(target_family = "wasm"), tokio::test)] -// async fn test_prime_sieve() { -// setup_logs(); -// info!("Starting prime sieve test..."); +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_call_many() { + setup_logs(); + info!("Starting call_many test..."); + + let plugin = load_plugin().await; + plugin + .call("call_many", Value::Number(500.into())) + .await + .unwrap(); +} -// let wasm_bytes = load_plugin_wasm(); -// let handler = Arc::new(get_host_server()); +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_call_async() { + setup_logs(); + info!("Starting call_async test..."); -// let plugin = Plugin::new("test_plugin", wasm_bytes, handler).unwrap(); + let plugin = load_plugin().await; + let resp = plugin.call("call_async", Value::Null).await.unwrap(); + assert_eq!(resp.result.as_str().unwrap(), "pong"); +} -// let response = plugin -// .call("prime_sieve", Value::Number(1000.into())) -// .await -// .unwrap(); +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_call_many_async() { + setup_logs(); + info!("Starting call_many_async test..."); + + let plugin = load_plugin().await; + plugin + .call("call_many_async", Value::Number(500.into())) + .await + .unwrap(); +} -// info!("Prime sieve response: {:?}", response); -// let count = response.result["count"].as_u64().unwrap(); -// assert_eq!(count, 168); -// } +#[cfg_attr(target_family = "wasm", wasm_bindgen_test)] +#[cfg_attr(not(target_family = "wasm"), tokio::test)] +async fn test_prime_sieve() { + setup_logs(); + info!("Starting prime sieve test..."); + + let plugin = load_plugin().await; + let response = plugin + .call("prime_sieve", Value::Number(1000.into())) + .await + .unwrap(); + + info!("Prime sieve response: {:?}", response); + let count = response.result["count"].as_u64().unwrap(); + assert_eq!(count, 168); +} diff --git a/crates/wasmi-plugin-hdk/tests/worker_test.rs b/crates/wasmi-plugin-hdk/tests/worker_test.rs index fef30a3..80d455f 100644 --- a/crates/wasmi-plugin-hdk/tests/worker_test.rs +++ b/crates/wasmi-plugin-hdk/tests/worker_test.rs @@ -1,7 +1,14 @@ -use std::sync::Once; +#![cfg(target_arch = "wasm32")] + +use gloo_timers::future::sleep; +use std::{ + io::{BufRead, BufReader, Write}, + sync::Once, + time::Duration, +}; use tracing::info; use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; -use wasmi_plugin_hdk::runtime::worker_pool::{WorkerHandle, WorkerPool}; +use wasmi_plugin_hdk::runtime::{non_blocking_pipe::non_blocking_pipe, worker_pool::WorkerPool}; wasm_bindgen_test_configure!(run_in_browser); @@ -18,13 +25,13 @@ fn setup_logs() { } #[wasm_bindgen_test] -async fn test_worker_with_channel() { +async fn test_worker() { setup_logs(); let (tx, rx) = futures::channel::oneshot::channel::(); - let task = move || { - info!("Worker started"); + let task = async move || { + info!("Hello from inside the worker!"); tx.send(true).unwrap(); }; @@ -39,3 +46,67 @@ async fn test_worker_with_channel() { assert!(result); } + +#[wasm_bindgen_test] +async fn test_worker_with_pipes() { + setup_logs(); + + let (stdin_reader, mut stdin_writer) = non_blocking_pipe(); + let (mut stdout_reader, stdout_writer) = non_blocking_pipe(); + + let task = async move || { + // Wait to recieve a message, then echo it back. + let mut writer = stdout_writer; + let mut reader = BufReader::new(stdin_reader); + let mut line = String::new(); + loop { + match reader.read_line(&mut line) { + Ok(0) => break, + Ok(_) => { + // Echo back the line + writer.write_all(line.as_bytes()).unwrap(); + writer.flush().unwrap(); + break; + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // Would block, try again + continue; + } + Err(e) => { + panic!("Error reading from stdin: {}", e); + } + } + } + }; + + info!("Spawning worker..."); + let pool = WorkerPool::global(); + pool.run(task).await.expect("Failed to spawn worker"); + + // Sleep for a sec to ensure the worker is ready + sleep(Duration::from_millis(10)).await; + + let test_message = "Hello, worker!\n"; + info!("Sending message to worker: {}", test_message.trim()); + stdin_writer.write_all(test_message.as_bytes()).unwrap(); + stdin_writer.flush().unwrap(); + + let mut reader = BufReader::new(&mut stdout_reader); + let mut line = String::new(); + info!("Waiting for worker response..."); + loop { + match reader.read_line(&mut line) { + Ok(0) => panic!("Worker closed the pipe unexpectedly"), + Ok(_) => break, + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // Would block, try again + continue; + } + Err(e) => { + panic!("Error reading from stdout: {}", e); + } + } + } + + assert_eq!(line, test_message); +} diff --git a/shell.nix b/shell.nix index c2c6663..ff00938 100644 --- a/shell.nix +++ b/shell.nix @@ -29,7 +29,7 @@ pkgs.mkShell { shellHook = '' export WASM_BINDGEN_THREADS_HEADERS=1 export CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner - export WASM_BINDGEN_TEST_TIMEOUT=10 + export WASM_BINDGEN_TEST_TIMEOUT=100 export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library" ''; } From 95acd815bd4deb9f52f88b7483810e160e2761d3 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sat, 27 Dec 2025 01:37:07 -0500 Subject: [PATCH 30/70] feat: fixed benchmark --- crates/test-plugin/src/main.rs | 1 + crates/wasmi-plugin-hdk/src/plugin.rs | 2 ++ .../src/runtime/message_writer.rs | 7 ++---- .../src/runtime/thread_worker.rs | 19 ++++++++------ .../src/runtime/worker_pool.rs | 18 ++++++++----- .../src/runtime/worker_runtime.rs | 8 +++--- crates/wasmi-plugin-hdk/src/transport.rs | 7 ++++-- crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs | 25 +++++++++++++------ crates/wasmi-plugin-hdk/tests/integration.rs | 8 +++++- shell.nix | 2 +- 10 files changed, 63 insertions(+), 34 deletions(-) diff --git a/crates/test-plugin/src/main.rs b/crates/test-plugin/src/main.rs index bebfa53..2a5d830 100644 --- a/crates/test-plugin/src/main.rs +++ b/crates/test-plugin/src/main.rs @@ -125,6 +125,7 @@ fn main() { .without_time() .init(); info!("Starting plugin..."); + info!("Test"); PluginServer::new() .with_method("ping", ping) diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index 709164f..a0f8cd9 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -144,6 +144,8 @@ impl Plugin { futures::select! { res = transport_task => { + #[cfg(debug_assertions)] + sleep(Duration::from_millis(100)).await; // Give some time for cleanup runtime.terminate(id).await; Ok(res?) }, diff --git a/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs b/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs index e64ff30..136e13d 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs @@ -1,7 +1,7 @@ use std::io::Write; use wasm_bindgen::JsCast; -use web_sys::js_sys; +use web_sys::{console::log_1, js_sys}; use crate::runtime::worker_message::WorkerMessage; @@ -22,10 +22,7 @@ impl MessageWriter { impl Write for MessageWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { - let msg = String::from_utf8_lossy(buf).to_string(); - let msg = serde_wasm_bindgen::to_value(&WorkerMessage::PluginLog { message: msg }).unwrap(); - - self.global.post_message(&msg).unwrap(); + log_1(js_sys::Uint8Array::from(buf).as_ref()); Ok(buf.len()) } diff --git a/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs b/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs index 6e95373..409e8fa 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs @@ -1,21 +1,24 @@ use std::panic; +use futures::future::LocalBoxFuture; use tracing::error; -use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsValue, prelude::wasm_bindgen}; #[wasm_bindgen] -pub async fn execute_worker_task(ptr: u32) { - set_panic_hook(); +pub async fn execute_worker_task(ptr: u32) -> web_sys::js_sys::Promise { + // set_panic_hook(); //* SAFETY: We transfer ownership of the closure from the caller's `spawn` //* function to this web worker. The pointer must be a FnOnce closure //* allocated on the heap. - let closure = unsafe { - let raw = ptr as *mut Box; - Box::from_raw(raw) - }; + let boxed: Box LocalBoxFuture<'static, ()> + Send>> = + unsafe { Box::from_raw(ptr as *mut _) }; + let fut = boxed(); - (*closure)(); + wasm_bindgen_futures::future_to_promise(async move { + fut.await; + Ok(JsValue::UNDEFINED) + }) } fn set_panic_hook() { diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs index 2443cf8..77e0d63 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs @@ -1,6 +1,6 @@ use std::sync::{Arc, Mutex, OnceLock}; -use futures::channel::oneshot; +use futures::{channel::oneshot, future::LocalBoxFuture}; use thiserror::Error; use tracing::{error, info, warn}; use wasm_bindgen::{ @@ -66,10 +66,10 @@ impl WorkerPool { { let handle = self.get_or_spawn_worker().await?; - let wrapper = move || { - wasm_bindgen_futures::spawn_local(async move { + let wrapper = move || -> LocalBoxFuture<'static, ()> { + Box::pin(async move { f().await; - }); + }) }; handle.run(wrapper)?; Ok(handle) @@ -150,9 +150,14 @@ impl WorkerHandle { }) } - fn run(&self, f: impl FnOnce() + Send + 'static) -> Result<(), SpawnError> { + fn run(&self, f: F) -> Result<(), SpawnError> + where + F: FnOnce() -> Fut + Send + 'static, + Fut: Future + 'static, + { // 1. Box the function and get a raw pointer - let box_f: Box> = Box::new(Box::new(f)); + let box_f: Box LocalBoxFuture<'static, ()> + Send>> = + Box::new(Box::new(move || Box::pin(f()))); let ptr = Box::into_raw(box_f) as u32; // 2. Load my worker @@ -168,6 +173,7 @@ impl WorkerHandle { } pub fn terminate(&self) { + info!("Terminating worker {}", self.id); self.worker.terminate(); let mut state = self.state.lock().unwrap(); *state = WorkerState::Terminated; diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index 4eb133e..2ea122a 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use futures::lock::Mutex; use futures::{AsyncRead, AsyncWrite}; use thiserror::Error; -use tracing::error; +use tracing::{error, info}; use wasm_bindgen::JsCast; use crate::compile::Compiled; @@ -61,7 +61,7 @@ impl Runtime for WorkerRuntime { > { let (stdin_reader, stdin_writer) = non_blocking_pipe(); let (stdout_reader, stdout_writer) = non_blocking_pipe(); - let stderr_writer = MessageWriter::new("".into()); + let stderr_writer: MessageWriter = MessageWriter::new("".into()); // let (stderr_reader, stderr_writer) = non_blocking_pipe(); let pool = WorkerPool::global(); @@ -113,7 +113,9 @@ async fn run_instance( .set_stderr(stderr); let start = wasi_ctx.into_fn(&mut store, &module)?; - start.call(&mut store, &[])?; + info!("Starting Wasm instance"); + let res = start.call(&mut store, &[])?; + info!("Wasm instance finished with result: {:?}", res); Ok(()) } diff --git a/crates/wasmi-plugin-hdk/src/transport.rs b/crates/wasmi-plugin-hdk/src/transport.rs index 144de2e..2854cec 100644 --- a/crates/wasmi-plugin-hdk/src/transport.rs +++ b/crates/wasmi-plugin-hdk/src/transport.rs @@ -1,7 +1,7 @@ use futures::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, io::BufReader}; use serde_json::Value; use thiserror::Error; -use tracing::{error, info}; +use tracing::{error, info, warn}; use wasmi_plugin_pdk::{ api::RequestHandler, rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcResponse}, @@ -52,7 +52,10 @@ impl Transport { loop { info!("Waiting for response..."); match self.reader.read_line(&mut line).await { - Ok(0) => return Err(TransportError::Eof), + Ok(0) => { + warn!("EOF reached while reading from transport"); + return Err(TransportError::Eof); + } Ok(_) => {} Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { yield_now().await; diff --git a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs index 5ce4497..5373e5f 100644 --- a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs +++ b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs @@ -104,6 +104,21 @@ impl WasiCtx { mut store: &mut wasmer::Store, module: &wasmer::Module, ) -> Result { + let (ctx, imports) = self.get_imports(&mut store); + + let instance = wasmer::Instance::new(&mut store, module, &imports)?; + let memory = instance.exports.get_memory("memory")?; + ctx.as_mut(&mut store).set_memory(memory.clone()); + + let start_fn = instance.exports.get_function("_start")?; + + Ok(start_fn.clone()) + } + + pub fn get_imports( + self, + mut store: &mut wasmer::Store, + ) -> (wasmer::FunctionEnv, wasmer::Imports) { let ctx = wasmer::FunctionEnv::new(&mut store, self); let imports = wasmer::imports! { "wasi_snapshot_preview1" => { @@ -124,16 +139,10 @@ impl WasiCtx { } }; - let instance = wasmer::Instance::new(&mut store, module, &imports)?; - let memory = instance.exports.get_memory("memory")?; - ctx.as_mut(&mut store).set_memory(memory.clone()); - - let start_fn = instance.exports.get_function("_start")?; - - Ok(start_fn.clone()) + (ctx, imports) } - fn set_memory(&mut self, memory: wasmer::Memory) { + pub fn set_memory(&mut self, memory: wasmer::Memory) { self.memory = Some(memory); } } diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index fa46aa8..e1720ba 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -74,7 +74,13 @@ async fn test_plugin() { setup_logs(); info!("Starting test_plugin..."); - let plugin = load_plugin().await; + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + + let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) + .await + .unwrap() + .with_timeout(Duration::from_secs(2)); let resp = plugin.call("ping", Value::Null).await.unwrap(); assert_eq!(resp.result.as_str().unwrap(), "pong"); diff --git a/shell.nix b/shell.nix index ff00938..f908cf1 100644 --- a/shell.nix +++ b/shell.nix @@ -29,7 +29,7 @@ pkgs.mkShell { shellHook = '' export WASM_BINDGEN_THREADS_HEADERS=1 export CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner - export WASM_BINDGEN_TEST_TIMEOUT=100 + export WASM_BINDGEN_TEST_TIMEOUT=300 export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library" ''; } From 3b752dac887ea17a79b3c1ca56cf619b43975346 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sat, 27 Dec 2025 11:21:11 -0500 Subject: [PATCH 31/70] fix: disable global console.logs in worker Apparently terribly slow --- crates/wasmi-plugin-hdk/benches/wasm_bench.rs | 15 ++++--- crates/wasmi-plugin-hdk/src/plugin.rs | 3 -- .../src/runtime/message_writer.rs | 13 +++--- .../src/runtime/non_blocking_pipe.rs | 1 - .../src/runtime/thread_worker.rs | 44 +++---------------- crates/wasmi-plugin-hdk/src/runtime/worker.js | 20 ++++----- .../src/runtime/worker_runtime.rs | 5 +-- crates/wasmi-plugin-hdk/src/transport.rs | 1 - crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs | 21 ++++++++- 9 files changed, 51 insertions(+), 72 deletions(-) diff --git a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs index bb6099a..25dd408 100644 --- a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs +++ b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs @@ -32,11 +32,12 @@ fn get_host_server() -> HostServer<(Option, ())> { fn setup_logs() { INIT.call_once(|| { - // tracing_wasm::set_as_global_default_with_config( - // tracing_wasm::WASMLayerConfigBuilder::new() - // .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) - // .build(), - // ); + tracing_wasm::set_as_global_default_with_config( + tracing_wasm::WASMLayerConfigBuilder::new() + .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) + .set_max_level(tracing::Level::INFO) + .build(), + ); }); } @@ -48,7 +49,8 @@ async fn bench_ping_wasm(c: &mut Criterion) { let plugin = Arc::new( Plugin::new("test_plugin", &wasm_bytes, handler) .await - .unwrap(), + .unwrap() + .with_timeout(Duration::from_secs(1)), ); c.bench_async_function("ping", |b| { @@ -57,6 +59,7 @@ async fn bench_ping_wasm(c: &mut Criterion) { let plugin = plugin.clone(); async move { plugin.call("ping", serde_json::Value::Null).await.unwrap(); + info!("ping call completed"); } })) }) diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index a0f8cd9..bb425f2 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -144,9 +144,6 @@ impl Plugin { futures::select! { res = transport_task => { - #[cfg(debug_assertions)] - sleep(Duration::from_millis(100)).await; // Give some time for cleanup - runtime.terminate(id).await; Ok(res?) }, _ = timeout => { diff --git a/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs b/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs index 136e13d..45e60af 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs @@ -1,13 +1,9 @@ use std::io::Write; -use wasm_bindgen::JsCast; use web_sys::{console::log_1, js_sys}; -use crate::runtime::worker_message::WorkerMessage; - pub struct MessageWriter { name: String, - global: web_sys::DedicatedWorkerGlobalScope, } unsafe impl Send for MessageWriter {} @@ -15,14 +11,17 @@ unsafe impl Sync for MessageWriter {} impl MessageWriter { pub fn new(name: String) -> Self { - let global = js_sys::global().unchecked_into::(); - Self { name, global } + Self { name } } } impl Write for MessageWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { - log_1(js_sys::Uint8Array::from(buf).as_ref()); + let mut prefix = format!("[{}] ", self.name).into_bytes(); + prefix.extend_from_slice(buf); + let prefix = prefix.as_slice(); + + log_1(js_sys::Uint8Array::from(prefix).as_ref()); Ok(buf.len()) } diff --git a/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs b/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs index 1582a22..b34bb22 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs @@ -76,7 +76,6 @@ impl WasiReader for NonBlockingPipeReader { fn wait_ready(&self, _timeout: Option) { // TODO: Make native-compatible version of this? // No-op -std::hint::spin_loop(); } } diff --git a/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs b/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs index 409e8fa..622fd60 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs @@ -1,50 +1,18 @@ -use std::panic; - -use futures::future::LocalBoxFuture; +use futures::{FutureExt, future::LocalBoxFuture}; use tracing::error; use wasm_bindgen::{JsValue, prelude::wasm_bindgen}; +use web_sys::js_sys; #[wasm_bindgen] -pub async fn execute_worker_task(ptr: u32) -> web_sys::js_sys::Promise { - // set_panic_hook(); - - //* SAFETY: We transfer ownership of the closure from the caller's `spawn` - //* function to this web worker. The pointer must be a FnOnce closure - //* allocated on the heap. +pub fn execute_worker_task(ptr: u32) -> js_sys::Promise { let boxed: Box LocalBoxFuture<'static, ()> + Send>> = unsafe { Box::from_raw(ptr as *mut _) }; let fut = boxed(); wasm_bindgen_futures::future_to_promise(async move { - fut.await; + if let Err(e) = std::panic::AssertUnwindSafe(fut).catch_unwind().await { + error!("Panic in thread worker: {:?}", e); + } Ok(JsValue::UNDEFINED) }) } - -fn set_panic_hook() { - panic::set_hook(Box::new(|panic_info| { - // 1. Try to get the message as a &str (for panic!("msg")) - // 2. Or as a String (for panic!("format {}", arg)) - let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() { - s.to_string() - } else if let Some(s) = panic_info.payload().downcast_ref::() { - s.clone() - } else { - "Unknown panic payload".to_string() - }; - - // Get location information - let location = panic_info - .location() - .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column())) - .unwrap_or_else(|| "unknown location".to_string()); - - // Log via tracing - error!( - target: "panic", - panic_message = %message, - panic_location = %location, - "A panic occurred" - ); - })); -} diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker.js b/crates/wasmi-plugin-hdk/src/runtime/worker.js index cfd8433..6e2df25 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker.js +++ b/crates/wasmi-plugin-hdk/src/runtime/worker.js @@ -1,13 +1,13 @@ -["log", "info", "warn", "error"].forEach(level => { - globalThis.console[level] = (...args) => { - globalThis.postMessage({ - type: "Log", - level: level, - message: args.map(String).join(" "), - ts: globalThis.performance.now() - }); - }; -}); +// ["log", "info", "warn", "error"].forEach(level => { +// globalThis.console[level] = (...args) => { +// globalThis.postMessage({ +// type: "Log", +// level: level, +// message: args.map(String).join(" "), +// ts: globalThis.performance.now() +// }); +// }; +// }); console.log("Worker started"); let wasmPkg = null; diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index 2ea122a..45ed986 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -96,7 +96,6 @@ async fn run_instance( ) -> Result<(), RunError> { let mut store = wasmer::Store::default(); - // let js_module = compiled.js_module; let wasm_bytes = compiled.wasm_bytes.as_ref(); let js_bytes = web_sys::js_sys::Uint8Array::from(wasm_bytes); @@ -113,9 +112,7 @@ async fn run_instance( .set_stderr(stderr); let start = wasi_ctx.into_fn(&mut store, &module)?; - info!("Starting Wasm instance"); - let res = start.call(&mut store, &[])?; - info!("Wasm instance finished with result: {:?}", res); + start.call(&mut store, &[])?; Ok(()) } diff --git a/crates/wasmi-plugin-hdk/src/transport.rs b/crates/wasmi-plugin-hdk/src/transport.rs index 2854cec..ef84d60 100644 --- a/crates/wasmi-plugin-hdk/src/transport.rs +++ b/crates/wasmi-plugin-hdk/src/transport.rs @@ -50,7 +50,6 @@ impl Transport { let mut line = String::new(); loop { - info!("Waiting for response..."); match self.reader.read_line(&mut line).await { Ok(0) => { warn!("EOF reached while reading from transport"); diff --git a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs index 5373e5f..2aa4be4 100644 --- a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs +++ b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs @@ -379,9 +379,20 @@ pub fn fd_write( return Errno::Fault as i32; } + // Print the data to be copied as a string + info!( + "fd_write: writing {} bytes to fd {}: {:?}", + to_copy, + fd, + String::from_utf8_lossy(&scratch[..to_copy]) + ); match writer.write(&scratch[..to_copy]) { - Ok(0) => break, // Writer closed + Ok(0) => { + info!("fd_write: writer returned 0 bytes written, stopping"); + break; + } Ok(n) => { + info!("fd_write: wrote {} bytes to fd {}", n, fd); written += n; total_written += n as u32; if n < to_copy { @@ -389,19 +400,25 @@ pub fn fd_write( } } Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + info!("fd_write: writer would block"); // Spin until writable continue; } - Err(_) => return Errno::Io as i32, + Err(_) => { + info!("fd_write: writer error"); + return Errno::Io as i32; + } } } //? If we couldn't write the full iovec, stop processing further iovecs if written < buf_len { + info!("fd_write: short write, stopping further iovecs"); break; } } + info!("fd_write: total_written={}", total_written); // Write total_written into nwrite_ptr if view .write(nwrite_ptr as u64, &total_written.to_le_bytes()) From fb3f2d9956b15193d308001a21b62a3c32ce8c0d Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sat, 27 Dec 2025 12:00:35 -0500 Subject: [PATCH 32/70] update test timeouts --- crates/wasmi-plugin-hdk/benches/wasm_bench.rs | 104 +++++++----------- crates/wasmi-plugin-hdk/tests/integration.rs | 13 +-- 2 files changed, 45 insertions(+), 72 deletions(-) diff --git a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs index 25dd408..7e682c5 100644 --- a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs +++ b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs @@ -21,6 +21,17 @@ fn load_plugin_wasm() -> Vec { PLUGIN_WASM.to_vec() } +async fn load_plugin() -> Plugin, ())>> { + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) + .await + .unwrap() + .with_timeout(Duration::from_secs(10)); + + plugin +} + fn get_host_server() -> HostServer<(Option, ())> { HostServer::default() .with_method( @@ -44,14 +55,7 @@ fn setup_logs() { #[wasm_bindgen_bench] async fn bench_ping_wasm(c: &mut Criterion) { setup_logs(); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Arc::new( - Plugin::new("test_plugin", &wasm_bytes, handler) - .await - .unwrap() - .with_timeout(Duration::from_secs(1)), - ); + let plugin = Arc::new(load_plugin().await); c.bench_async_function("ping", |b| { let plugin = plugin.clone(); @@ -66,43 +70,37 @@ async fn bench_ping_wasm(c: &mut Criterion) { .await; } -/// Benchmark the full lifecycle of creating a plugin instance and calling a ping request. -/// Tests the overhead of plugin instantiation. -#[wasm_bindgen_bench] -async fn bench_lifecycle(c: &mut Criterion) { - setup_logs(); - let wasm_bytes = Arc::new(load_plugin_wasm()); - let handler = Arc::new(get_host_server()); - - c.bench_async_function("lifecycle", |b| { - let wasm_bytes = wasm_bytes.clone(); - let handler = handler.clone(); - Box::pin(b.iter_future(move || { - let wasm_bytes = wasm_bytes.clone(); - let handler = handler.clone(); - async move { - let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) - .await - .unwrap(); - plugin.call("ping", serde_json::Value::Null).await.unwrap(); - } - })) - }) - .await; -} +// /// Benchmark the full lifecycle of creating a plugin instance and calling a ping request. +// /// Tests the overhead of plugin instantiation. +// #[wasm_bindgen_bench] +// async fn bench_lifecycle(c: &mut Criterion) { +// setup_logs(); +// let wasm_bytes = Arc::new(load_plugin_wasm()); +// let handler = Arc::new(get_host_server()); + +// c.bench_async_function("lifecycle", |b| { +// let wasm_bytes = wasm_bytes.clone(); +// let handler = handler.clone(); +// Box::pin(b.iter_future(move || { +// let wasm_bytes = wasm_bytes.clone(); +// let handler = handler.clone(); +// async move { +// let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) +// .await +// .unwrap(); +// plugin.call("ping", serde_json::Value::Null).await.unwrap(); +// } +// })) +// }) +// .await; +// } /// Benchmark the prime sieve function with a small input. Primarily tests the overhead /// of calling into the wasm module. #[wasm_bindgen_bench] async fn bench_prime_sieve_small(c: &mut Criterion) { setup_logs(); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Arc::new( - Plugin::new("test_plugin", &wasm_bytes, handler) - .await - .unwrap(), - ); + let plugin = Arc::new(load_plugin().await); c.bench_async_function("prime_sieve_small", |b| { let plugin = plugin.clone(); @@ -123,13 +121,7 @@ async fn bench_prime_sieve_small(c: &mut Criterion) { #[wasm_bindgen_bench] async fn bench_prime_sieve_large(c: &mut Criterion) { setup_logs(); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Arc::new( - Plugin::new("test_plugin", &wasm_bytes, handler) - .await - .unwrap(), - ); + let plugin = Arc::new(load_plugin().await); c.bench_async_function("prime_sieve_large", |b| { let plugin = plugin.clone(); @@ -150,15 +142,7 @@ async fn bench_prime_sieve_large(c: &mut Criterion) { #[wasm_bindgen_bench] async fn bench_call_many(c: &mut Criterion) { setup_logs(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Arc::new( - Plugin::new("test_plugin", &wasm_bytes, handler) - .await - .unwrap() - .with_timeout(Duration::from_secs(5)), - ); + let plugin = Arc::new(load_plugin().await); c.bench_async_function("call_many", |b| { let plugin = plugin.clone(); @@ -179,15 +163,7 @@ async fn bench_call_many(c: &mut Criterion) { #[wasm_bindgen_bench] async fn bench_call_many_async(c: &mut Criterion) { setup_logs(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Arc::new( - Plugin::new("test_plugin", &wasm_bytes, handler) - .await - .unwrap() - .with_timeout(Duration::from_secs(5)), - ); + let plugin = Arc::new(load_plugin().await); c.bench_async_function("call_many_async", |b| { let plugin = plugin.clone(); diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index e1720ba..69211aa 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -33,14 +33,11 @@ async fn load_plugin() -> Plugin> { fn get_host_server() -> HostServer<()> { HostServer::default() - .with_method("ping", |_, _params: ()| async move { - info!("Received ping request, sending pong response"); - Ok("pong".to_string()) - }) - .with_method("echo", |_, params: Value| async move { - info!("Received echo request, returning response"); - Ok(params) - }) + .with_method( + "ping", + |_, _params: ()| async move { Ok("pong".to_string()) }, + ) + .with_method("echo", |_, params: Value| async move { Ok(params) }) } fn setup_logs() { From 279bd947783790d85fa7dde45d05b41a7ddaa541 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sat, 27 Dec 2025 12:01:19 -0500 Subject: [PATCH 33/70] feat: shared pre-compiled modules across workers --- crates/wasmi-plugin-hdk/src/compile.rs | 28 ++++------ .../src/runtime/thread_worker.rs | 25 +++++++-- crates/wasmi-plugin-hdk/src/runtime/worker.js | 8 +++ .../src/runtime/worker_pool.rs | 51 +++++++++++++++++++ .../src/runtime/worker_runtime.rs | 38 +++++++++----- 5 files changed, 116 insertions(+), 34 deletions(-) diff --git a/crates/wasmi-plugin-hdk/src/compile.rs b/crates/wasmi-plugin-hdk/src/compile.rs index f035c63..90fd326 100644 --- a/crates/wasmi-plugin-hdk/src/compile.rs +++ b/crates/wasmi-plugin-hdk/src/compile.rs @@ -9,8 +9,8 @@ pub struct Compiled { #[cfg(not(target_arch = "wasm32"))] pub module: wasmer::Module, - // #[cfg(target_arch = "wasm32")] - // pub js_module: web_sys::js_sys::WebAssembly::Module, + #[cfg(target_arch = "wasm32")] + pub js_module: web_sys::js_sys::WebAssembly::Module, #[cfg(target_arch = "wasm32")] pub wasm_bytes: Arc<[u8]>, } @@ -36,24 +36,18 @@ impl Compiled { #[cfg(target_arch = "wasm32")] pub async fn new(name: &str, wasm_bytes: &[u8]) -> Result { - // use wasm_bindgen::JsCast; - - // TODO: Compile module once and cache it instead of re-compiling for every call. - //? Because we're in wasm32-unknown-unknown AND we want to have Shared Memory - //? (target-feature=+atomics,+bulk-memory), - //? we have to use the WebAssembly.compile async function and pass the compiled - //? native mmodule to wasmer. Wasmer only supports compiling via `WebAssembly::new()`, - //? which is illegal for use with Shared Memory. - // let js_bytes = web_sys::js_sys::Uint8Array::from(wasm_bytes); - // let promise = web_sys::js_sys::WebAssembly::compile(&js_bytes.into()); - // let js_module_value = wasm_bindgen_futures::JsFuture::from(promise).await.unwrap(); - // let js_module = js_module_value - // .dyn_into::() - // .unwrap(); + use wasm_bindgen::JsCast; + use wasm_bindgen_futures::JsFuture; + use web_sys::js_sys::{Uint8Array, WebAssembly}; + + let js_bytes = Uint8Array::from(wasm_bytes); + let promise = WebAssembly::compile(&js_bytes.into()); + let js_module_value = JsFuture::from(promise).await.unwrap(); + let js_module = js_module_value.dyn_into::().unwrap(); Ok(Compiled { name: name.to_string(), - // js_module, + js_module, wasm_bytes: Arc::from(wasm_bytes), }) } diff --git a/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs b/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs index 622fd60..b3c2ce4 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs @@ -1,13 +1,30 @@ use futures::{FutureExt, future::LocalBoxFuture}; -use tracing::error; +use tracing::{error, info}; use wasm_bindgen::{JsValue, prelude::wasm_bindgen}; use web_sys::js_sys; #[wasm_bindgen] pub fn execute_worker_task(ptr: u32) -> js_sys::Promise { - let boxed: Box LocalBoxFuture<'static, ()> + Send>> = - unsafe { Box::from_raw(ptr as *mut _) }; - let fut = boxed(); + type Task = Box LocalBoxFuture<'static, ()> + Send>; + let task: Box = unsafe { Box::from_raw(ptr as *mut Task) }; + let fut = task(); + + wasm_bindgen_futures::future_to_promise(async move { + if let Err(e) = std::panic::AssertUnwindSafe(fut).catch_unwind().await { + error!("Panic in thread worker: {:?}", e); + } + Ok(JsValue::UNDEFINED) + }) +} + +#[wasm_bindgen] +pub fn execute_worker_task_with(ptr: u32, extra: JsValue) -> js_sys::Promise { + web_sys::console::log_2(&"Rust received extra:".into(), &extra); + info!("Rust extra type: {:?}", extra.js_typeof()); + + type Task = Box LocalBoxFuture<'static, ()> + Send>; + let task: Box = unsafe { Box::from_raw(ptr as *mut Task) }; + let fut = task(extra); wasm_bindgen_futures::future_to_promise(async move { if let Err(e) = std::panic::AssertUnwindSafe(fut).catch_unwind().await { diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker.js b/crates/wasmi-plugin-hdk/src/runtime/worker.js index 6e2df25..10951f5 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker.js +++ b/crates/wasmi-plugin-hdk/src/runtime/worker.js @@ -33,6 +33,14 @@ globalThis.onmessage = async ev => { } finally { globalThis.postMessage({ type: "Idle" }); } + } else if (ev.data.type === "run_with") { + const { taskPtr, extra } = ev.data; + console.log("(JS) extra:", extra, typeof extra); + try { + await wasmPkg.execute_worker_task_with(taskPtr, extra); + } finally { + globalThis.postMessage({ type: "Idle" }); + } } else { console.error(`Unknown message type: ${type}`); } diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs index 77e0d63..cbaadf8 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs @@ -75,6 +75,27 @@ impl WorkerPool { Ok(handle) } + pub async fn run_with( + &self, + extra: &wasm_bindgen::JsValue, + f: F, + ) -> Result, SpawnError> + where + F: FnOnce(wasm_bindgen::JsValue) -> Fut + Send + 'static, + Fut: Future + 'static, + { + let handle = self.get_or_spawn_worker().await?; + + let wrapper = move |v: wasm_bindgen::JsValue| -> LocalBoxFuture<'static, ()> { + let v = v.clone(); + Box::pin(async move { + f(v).await; + }) + }; + handle.run_with(extra, wrapper)?; + Ok(handle) + } + pub fn terminate(&self, worker_id: WorkerId) { let mut state = self.state.lock().unwrap(); if let Some(pos) = state.workers.iter().position(|w| w.id == worker_id) { @@ -172,6 +193,36 @@ impl WorkerHandle { Ok(()) } + fn run_with(&self, extra: &wasm_bindgen::JsValue, f: F) -> Result<(), SpawnError> + where + F: FnOnce(wasm_bindgen::JsValue) -> Fut + Send + 'static, + Fut: Future + 'static, + { + info!("run_with: extra type = {:?}", extra.js_typeof()); + info!( + "run_with: is Module = {:?}", + extra.is_instance_of::() + ); + + // 1. Box the function and get a raw pointer + let box_f: Box< + Box LocalBoxFuture<'static, ()> + Send>, + > = Box::new(Box::new(move |v| Box::pin(f(v)))); + let ptr = Box::into_raw(box_f) as u32; + + // 2. Load my worker + let worker = &self.worker; + + // 3. Send the "run" message with raw ptr + let msg = js_sys::Object::new(); + Reflect::set(&msg, &"type".into(), &"run_with".into()).unwrap(); + Reflect::set(&msg, &"taskPtr".into(), &ptr.into()).unwrap(); + Reflect::set(&msg, &"extra".into(), extra).unwrap(); + worker.post_message(&msg).unwrap(); + + Ok(()) + } + pub fn terminate(&self) { info!("Terminating worker {}", self.id); self.worker.terminate(); diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index 45ed986..270053c 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -9,6 +9,7 @@ use futures::{AsyncRead, AsyncWrite}; use thiserror::Error; use tracing::{error, info}; use wasm_bindgen::JsCast; +use web_sys::js_sys::{self, WebAssembly}; use crate::compile::Compiled; use crate::runtime::Runtime; @@ -64,10 +65,18 @@ impl Runtime for WorkerRuntime { let stderr_writer: MessageWriter = MessageWriter::new("".into()); // let (stderr_reader, stderr_writer) = non_blocking_pipe(); + let module = compiled.js_module; let pool = WorkerPool::global(); let handle = pool - .run(async move || { - let res = run_instance(compiled, stdin_reader, stdout_writer, stderr_writer).await; + .run_with(&module, move |extra| async move { + let res = run_instance( + extra, + &compiled.wasm_bytes, + stdin_reader, + stdout_writer, + stderr_writer, + ) + .await; if let Err(e) = res { error!("Error running Wasm instance: {}", e); } @@ -89,23 +98,26 @@ impl Runtime for WorkerRuntime { } async fn run_instance( - compiled: Compiled, + js_module: wasm_bindgen::JsValue, + wasm_bytes: &[u8], stdin: impl WasiReader + Send + Sync + 'static, stdout: impl Write + Send + Sync + 'static, stderr: impl Write + Send + Sync + 'static, ) -> Result<(), RunError> { + info!("Starting Wasm instance in WorkerRuntime"); let mut store = wasmer::Store::default(); - let wasm_bytes = compiled.wasm_bytes.as_ref(); - - let js_bytes = web_sys::js_sys::Uint8Array::from(wasm_bytes); - let promise = web_sys::js_sys::WebAssembly::compile(&js_bytes.into()); - let js_module_value = wasm_bindgen_futures::JsFuture::from(promise).await.unwrap(); - let js_module = js_module_value - .dyn_into::() - .unwrap(); - let module = (js_module, wasm_bytes.as_ref()).into(); - + info!("js_module type: {:?}", js_module.js_typeof()); + info!( + "is WebAssembly.Module: {:?}", + js_module.is_instance_of::() + ); + info!("Casting Wasm module"); + let js_module: WebAssembly::Module = js_module.dyn_into().unwrap(); + info!("Creating Wasm module"); + let module = (js_module, wasm_bytes).into(); + + info!("Creating WASI context"); let wasi_ctx = wasi_ctx::WasiCtx::new() .set_stdin(stdin) .set_stdout(stdout) From 71c2858b453841686770c69b0ca36f7ba20324b6 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sat, 27 Dec 2025 12:29:43 -0500 Subject: [PATCH 34/70] fix: bench-native --- crates/wasmi-plugin-hdk/benches/benchmark.rs | 65 +++++++++++++------ crates/wasmi-plugin-hdk/benches/wasm_bench.rs | 60 ++++++++--------- 2 files changed, 75 insertions(+), 50 deletions(-) diff --git a/crates/wasmi-plugin-hdk/benches/benchmark.rs b/crates/wasmi-plugin-hdk/benches/benchmark.rs index 8bd6b83..3a50c91 100644 --- a/crates/wasmi-plugin-hdk/benches/benchmark.rs +++ b/crates/wasmi-plugin-hdk/benches/benchmark.rs @@ -2,9 +2,11 @@ mod benchmarks { use criterion::{Criterion, criterion_group}; use serde_json::Value; - use std::sync::{Arc, Once}; + use std::{ + sync::{Arc, Once}, + time::Duration, + }; use tokio::runtime::Builder; - use tracing_subscriber::EnvFilter; use wasmi_plugin_hdk::{ plugin::{Plugin, PluginId}, server::HostServer, @@ -19,6 +21,17 @@ mod benchmarks { PLUGIN_WASM.to_vec() } + async fn load_plugin() -> Plugin, ())>> { + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) + .await + .unwrap() + .with_timeout(Duration::from_secs(10)); + + plugin + } + fn get_host_server() -> HostServer<(Option, ())> { HostServer::default() .with_method( @@ -44,10 +57,7 @@ mod benchmarks { setup_logs(); let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler.clone()).unwrap(); + let plugin = rt.block_on(load_plugin()); c.bench_function("ping", |b| { b.iter(|| { @@ -72,9 +82,10 @@ mod benchmarks { c.bench_function("lifecycle", |b| { b.iter(|| { - let plugin = - Plugin::new("test_plugin", wasm_bytes.clone(), handler.clone()).unwrap(); let fut = async { + let plugin = Plugin::new("test_plugin", &wasm_bytes, handler.clone()) + .await + .unwrap(); plugin.call("ping", Value::Null).await.unwrap(); }; @@ -89,10 +100,7 @@ mod benchmarks { setup_logs(); let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); + let plugin = rt.block_on(load_plugin()); c.bench_function("prime_sieve_small", |b| { b.iter(|| { @@ -113,10 +121,7 @@ mod benchmarks { setup_logs(); let rt = Builder::new_current_thread().enable_all().build().unwrap(); - - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); + let plugin = rt.block_on(load_plugin()); c.bench_function("prime_sieve_large", |b| { b.iter(|| { @@ -134,11 +139,10 @@ mod benchmarks { /// Benchmark sending many echo requests to the host, and receiving responses. pub fn bench_call_many(c: &mut Criterion) { - let rt = Builder::new_current_thread().enable_all().build().unwrap(); + setup_logs(); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler).unwrap(); + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + let plugin = rt.block_on(load_plugin()); c.bench_function("call_many", |b| { b.iter(|| { @@ -154,6 +158,27 @@ mod benchmarks { }); } + /// Benchmark sending many echo requests to the host, and receiving responses. + pub fn bench_call_many_async(c: &mut Criterion) { + setup_logs(); + + let rt = Builder::new_current_thread().enable_all().build().unwrap(); + let plugin = rt.block_on(load_plugin()); + + c.bench_function("call_many_async", |b| { + b.iter(|| { + let fut = async { + plugin + .call("call_many_async", Value::Number(200.into())) + .await + .unwrap(); + }; + + rt.block_on(fut); + }) + }); + } + criterion_group!( benches, bench_ping, diff --git a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs index 7e682c5..390a52d 100644 --- a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs +++ b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs @@ -43,12 +43,12 @@ fn get_host_server() -> HostServer<(Option, ())> { fn setup_logs() { INIT.call_once(|| { - tracing_wasm::set_as_global_default_with_config( - tracing_wasm::WASMLayerConfigBuilder::new() - .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) - .set_max_level(tracing::Level::INFO) - .build(), - ); + // tracing_wasm::set_as_global_default_with_config( + // tracing_wasm::WASMLayerConfigBuilder::new() + // .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) + // .set_max_level(tracing::Level::INFO) + // .build(), + // ); }); } @@ -70,30 +70,30 @@ async fn bench_ping_wasm(c: &mut Criterion) { .await; } -// /// Benchmark the full lifecycle of creating a plugin instance and calling a ping request. -// /// Tests the overhead of plugin instantiation. -// #[wasm_bindgen_bench] -// async fn bench_lifecycle(c: &mut Criterion) { -// setup_logs(); -// let wasm_bytes = Arc::new(load_plugin_wasm()); -// let handler = Arc::new(get_host_server()); - -// c.bench_async_function("lifecycle", |b| { -// let wasm_bytes = wasm_bytes.clone(); -// let handler = handler.clone(); -// Box::pin(b.iter_future(move || { -// let wasm_bytes = wasm_bytes.clone(); -// let handler = handler.clone(); -// async move { -// let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) -// .await -// .unwrap(); -// plugin.call("ping", serde_json::Value::Null).await.unwrap(); -// } -// })) -// }) -// .await; -// } +/// Benchmark the full lifecycle of creating a plugin instance and calling a ping request. +/// Tests the overhead of plugin instantiation. +#[wasm_bindgen_bench] +async fn bench_lifecycle(c: &mut Criterion) { + setup_logs(); + let wasm_bytes = Arc::new(load_plugin_wasm()); + let handler = Arc::new(get_host_server()); + + c.bench_async_function("lifecycle", |b| { + let wasm_bytes = wasm_bytes.clone(); + let handler = handler.clone(); + Box::pin(b.iter_future(move || { + let wasm_bytes = wasm_bytes.clone(); + let handler = handler.clone(); + async move { + let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) + .await + .unwrap(); + plugin.call("ping", serde_json::Value::Null).await.unwrap(); + } + })) + }) + .await; +} /// Benchmark the prime sieve function with a small input. Primarily tests the overhead /// of calling into the wasm module. From 98d1658934793ccaed3f149fd831b6c024cdaf6b Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sat, 27 Dec 2025 12:32:06 -0500 Subject: [PATCH 35/70] style: clippy --- .github/workflows/ci.yml | 3 ++- .vscode/settings.json | 6 ------ Cargo.lock | 14 -------------- Cargo.toml | 11 ++++++----- crates/test-plugin/Cargo.toml | 2 +- crates/wasmi-plugin-hdk/Cargo.toml | 15 ++++++--------- crates/wasmi-plugin-hdk/benches/benchmark.rs | 3 ++- .../src/runtime/worker_message.rs | 3 --- .../wasmi-plugin-hdk/src/runtime/worker_pool.rs | 3 --- .../src/runtime/worker_runtime.rs | 12 ++++++------ crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs | 2 +- crates/wasmi-plugin-pdk/Cargo.toml | 2 -- makefile | 10 +++++++++- shell.nix | 1 + 14 files changed, 34 insertions(+), 53 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 712706e..75ee8bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,4 +55,5 @@ jobs: touch crates/worker/pkg/worker.js touch crates/worker/pkg/worker_bg.wasm - - run: nix-shell --run "make clippy" \ No newline at end of file + - run: nix-shell --run "make clippy-native" + - run: nix-shell --run "make clippy-wasm" \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 52e9704..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rust-analyzer.cargo.target": "wasm32-unknown-unknown", - "rust-analyzer.check.targets": "wasm32-unknown-unknown", - "rust-analyzer.cargo.features": "all", - "rust-analyzer.procMacro.enable": true -} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index bb4e3ce..69fa4d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1666,16 +1666,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "serde_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - [[package]] name = "serde_core" version = "1.0.228" @@ -2364,14 +2354,12 @@ dependencies = [ name = "wasmi-plugin-hdk" version = "0.1.0" dependencies = [ - "async-trait", "criterion", "futures", "getrandom 0.3.4", "gloo-timers", "serde", "serde-wasm-bindgen", - "serde_bytes", "serde_json", "thiserror 2.0.17", "tokio", @@ -2394,13 +2382,11 @@ dependencies = [ name = "wasmi-plugin-pdk" version = "0.1.0" dependencies = [ - "async-trait", "futures", "serde", "serde_json", "thiserror 2.0.17", "tracing", - "tracing-subscriber", "wasmi-plugin-rt", ] diff --git a/Cargo.toml b/Cargo.toml index 2ad46af..dd85806 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,27 +21,28 @@ categories = ["wasm", "virtualization"] exclude = ["tests", "benches"] [workspace.dependencies] -async-trait = "0.1" chrono = { version = "0.4", features = ["wasmbind"] } +criterion = "0.8" futures = "0.3" getrandom = { version = "0.3.4", features = ["wasm_js"] } gloo-timers = { version = "0.3", features = ["futures"] } rand = "0.9" serde = { version = "1", features = ["derive"] } -serde_bytes = "0.11" -serde_json = "1" serde-wasm-bindgen = "0.6" +serde_json = "1" thiserror = "2" tokio = { version = "1", features = ["rt", "time", "sync", "macros"] } tracing = "0.1" tracing-subscriber = "0.3" +tracing-test = { version = "0.2" } tracing-wasm = "0.2" uuid = { version = "1", features = ["v4", "serde", "js"] } wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" +wasm-bindgen-test = "0.3" wasmer = { version = "6", default-features = false } -web-sys = "0.3" wasmi-plugin-hdk = { version = "0.1.0", path = "./crates/wasmi-plugin-hdk" } wasmi-plugin-pdk = { version = "0.1.0", path = "./crates/wasmi-plugin-pdk" } wasmi-plugin-rt = { version = "0.1.0", path = "./crates/wasmi-plugin-rt" } -web-time = "1" \ No newline at end of file +web-sys = "0.3" +web-time = "1" diff --git a/crates/test-plugin/Cargo.toml b/crates/test-plugin/Cargo.toml index 118b565..a366158 100644 --- a/crates/test-plugin/Cargo.toml +++ b/crates/test-plugin/Cargo.toml @@ -25,4 +25,4 @@ wasmi-plugin-pdk = { workspace = true } opt-level = 3 lto = "fat" codegen-units = 1 -strip = true \ No newline at end of file +strip = true diff --git a/crates/wasmi-plugin-hdk/Cargo.toml b/crates/wasmi-plugin-hdk/Cargo.toml index d37a4f6..7ffac8a 100644 --- a/crates/wasmi-plugin-hdk/Cargo.toml +++ b/crates/wasmi-plugin-hdk/Cargo.toml @@ -13,11 +13,9 @@ categories.workspace = true exclude.workspace = true [dependencies] -async-trait = { workspace = true } futures = { workspace = true } getrandom = { workspace = true } serde = { workspace = true } -serde_bytes = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } @@ -28,27 +26,26 @@ web-time = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { workspace = true } -wasmer = { workspace = true, default-features = true} +wasmer = { workspace = true, default-features = true } [target.'cfg(target_arch = "wasm32")'.dependencies] gloo-timers = { workspace = true } serde-wasm-bindgen = { workspace = true } -wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } -web-sys = { workspace = true, features=["Worker", "WorkerOptions", "DedicatedWorkerGlobalScope", "WorkerType", "MessageEvent", "ErrorEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } +wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } +web-sys = { workspace = true, features = ["Worker", "WorkerOptions", "DedicatedWorkerGlobalScope", "WorkerType", "MessageEvent", "ErrorEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } [dev-dependencies] tracing-subscriber = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -tracing-test = { version = "0.2.5", features = ["no-env-filter"] } -criterion = "0.8.0" +criterion = { workspace = true } +tracing-test = { workspace = true, features = ["no-env-filter"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] tracing-wasm = { workspace = true } -wasm-bindgen-test = "0.3" -web-sys = { version = "0.3", features = ["Performance", "Window"] } +wasm-bindgen-test = { workspace = true } [[bench]] name = "benchmark" diff --git a/crates/wasmi-plugin-hdk/benches/benchmark.rs b/crates/wasmi-plugin-hdk/benches/benchmark.rs index 3a50c91..b435b6f 100644 --- a/crates/wasmi-plugin-hdk/benches/benchmark.rs +++ b/crates/wasmi-plugin-hdk/benches/benchmark.rs @@ -185,7 +185,8 @@ mod benchmarks { bench_lifecycle, bench_prime_sieve_small, bench_prime_sieve_large, - bench_call_many + bench_call_many, + bench_call_many_async ); } diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_message.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_message.rs index e525540..aea3aa6 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_message.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_message.rs @@ -8,9 +8,6 @@ pub enum WorkerMessage { level: String, ts: f64, }, - PluginLog { - message: String, - }, Ready, Idle, } diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs index cbaadf8..7e567c2 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs @@ -280,9 +280,6 @@ fn on_message_handler( WorkerMessage::Log { message, level, ts } => { log_worker_message(&name, message, level, ts); } - WorkerMessage::PluginLog { message } => { - info!(target: "WORKER", "[{}] {}", name, message); - } WorkerMessage::Ready => { info!(target: "WORKER", "[{}] Ready", name); if let Some(tx) = init_tx.take() { diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index 270053c..2d2d94a 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -9,7 +9,7 @@ use futures::{AsyncRead, AsyncWrite}; use thiserror::Error; use tracing::{error, info}; use wasm_bindgen::JsCast; -use web_sys::js_sys::{self, WebAssembly}; +use web_sys::js_sys::WebAssembly; use crate::compile::Compiled; use crate::runtime::Runtime; @@ -25,11 +25,11 @@ pub struct WorkerRuntime { #[derive(Debug, Error)] enum RunError { #[error("WASI Error: {0}")] - WasiError(#[from] wasi_ctx::WasiError), + Wasi(#[from] wasi_ctx::WasiError), #[error("Runtime Error: {0}")] - RuntimeError(#[from] wasmer::RuntimeError), + Runtime(#[from] wasmer::RuntimeError), #[error("Compile Error: {0}")] - CompileError(#[from] wasmer::CompileError), + Compile(#[from] wasmer::CompileError), } impl WorkerRuntime { @@ -86,7 +86,7 @@ impl Runtime for WorkerRuntime { let id = handle.id; self.active_sessions.lock().await.insert(handle.id, handle); - return Ok((id, stdin_writer, stdout_reader)); + Ok((id, stdin_writer, stdout_reader)) } async fn terminate(self, instance_id: u64) { @@ -100,7 +100,7 @@ impl Runtime for WorkerRuntime { async fn run_instance( js_module: wasm_bindgen::JsValue, wasm_bytes: &[u8], - stdin: impl WasiReader + Send + Sync + 'static, + stdin: impl WasiReader + 'static, stdout: impl Write + Send + Sync + 'static, stderr: impl Write + Send + Sync + 'static, ) -> Result<(), RunError> { diff --git a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs index 2aa4be4..19745ea 100644 --- a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs +++ b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs @@ -104,7 +104,7 @@ impl WasiCtx { mut store: &mut wasmer::Store, module: &wasmer::Module, ) -> Result { - let (ctx, imports) = self.get_imports(&mut store); + let (ctx, imports) = self.get_imports(store); let instance = wasmer::Instance::new(&mut store, module, &imports)?; let memory = instance.exports.get_memory("memory")?; diff --git a/crates/wasmi-plugin-pdk/Cargo.toml b/crates/wasmi-plugin-pdk/Cargo.toml index 5ea9709..a69a104 100644 --- a/crates/wasmi-plugin-pdk/Cargo.toml +++ b/crates/wasmi-plugin-pdk/Cargo.toml @@ -13,11 +13,9 @@ categories.workspace = true exclude.workspace = true [dependencies] -async-trait = { workspace = true } futures = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { workspace = true } wasmi-plugin-rt = { workspace = true } diff --git a/makefile b/makefile index 744392e..8ade62c 100644 --- a/makefile +++ b/makefile @@ -9,10 +9,18 @@ build-plugin: build-all: build-plugin -clippy: build-all +clippy-native: build-all @echo "--- Running Clippy Lints ---" cargo clippy -- -D warnings +clippy-wasm: build-all + @echo "--- Running Clippy Lints for Wasm ---" + cargo clippy \ + -Z build-std=std,panic_abort \ + -p wasmi-plugin-hdk \ + --target wasm32-unknown-unknown \ + -- -D warnings + test-native: build-plugin @echo "--- Running Plugin Tests ---" cargo test diff --git a/shell.nix b/shell.nix index f908cf1..1905a90 100644 --- a/shell.nix +++ b/shell.nix @@ -15,6 +15,7 @@ in pkgs.mkShell { packages = with pkgs; [ rustToolchain +rust-analyzer cargo-sort cargo-machete samply From 5c893cf9ebfc5b953c848819dec325b819893e78 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sat, 27 Dec 2025 16:36:14 -0500 Subject: [PATCH 36/70] refactor: remove ready channel --- crates/wasmi-plugin-hdk/src/lib.rs | 6 +- crates/wasmi-plugin-hdk/src/runtime/worker.js | 60 ++++++++++--------- .../src/runtime/worker_message.rs | 1 - .../src/runtime/worker_pool.rs | 44 ++++---------- .../src/runtime/worker_runtime.rs | 28 ++++----- crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs | 11 ---- crates/wasmi-plugin-hdk/tests/worker_test.rs | 4 +- 7 files changed, 62 insertions(+), 92 deletions(-) diff --git a/crates/wasmi-plugin-hdk/src/lib.rs b/crates/wasmi-plugin-hdk/src/lib.rs index 660c22f..c03a590 100644 --- a/crates/wasmi-plugin-hdk/src/lib.rs +++ b/crates/wasmi-plugin-hdk/src/lib.rs @@ -1,8 +1,8 @@ -pub mod compile; +mod compile; pub mod host_handler; pub mod plugin; pub mod runtime; pub mod server; mod time; -mod transport; -pub mod wasi; +pub mod transport; +mod wasi; diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker.js b/crates/wasmi-plugin-hdk/src/runtime/worker.js index 10951f5..965de6b 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker.js +++ b/crates/wasmi-plugin-hdk/src/runtime/worker.js @@ -11,40 +11,42 @@ console.log("Worker started"); let wasmPkg = null; +let processingQueue = Promise.resolve(); globalThis.onmessage = async ev => { - try { - if (ev.data.type === "init") { - const { memory, module, sdkUrl } = ev.data; + processingQueue = processingQueue.then(async () => { + try { + if (ev.data.type === "init") { + const { memory, module, sdkUrl } = ev.data; - wasmPkg = await import(sdkUrl); - const init = wasmPkg.default || wasmPkg.init; + wasmPkg = await import(sdkUrl); + const init = wasmPkg.default || wasmPkg.init; - if (typeof init !== 'function') { - throw new Error("Wasm init is not a function"); - } + if (typeof init !== 'function') { + throw new Error("Wasm init is not a function"); + } - await init(module, memory); - globalThis.postMessage({ type: "Ready" }); - } else if (ev.data.type === "run") { - const { taskPtr } = ev.data; - try { - await wasmPkg.execute_worker_task(taskPtr); - } finally { - globalThis.postMessage({ type: "Idle" }); - } - } else if (ev.data.type === "run_with") { - const { taskPtr, extra } = ev.data; - console.log("(JS) extra:", extra, typeof extra); - try { - await wasmPkg.execute_worker_task_with(taskPtr, extra); - } finally { - globalThis.postMessage({ type: "Idle" }); + await init(module, memory); + } else if (ev.data.type === "run") { + const { taskPtr } = ev.data; + try { + await wasmPkg.execute_worker_task(taskPtr); + } finally { + globalThis.postMessage({ type: "Idle" }); + } + } else if (ev.data.type === "run_with") { + const { taskPtr, extra } = ev.data; + console.log("(JS) extra:", extra, typeof extra); + try { + await wasmPkg.execute_worker_task_with(taskPtr, extra); + } finally { + globalThis.postMessage({ type: "Idle" }); + } + } else { + console.error(`Unknown message type: ${type}`); } - } else { - console.error(`Unknown message type: ${type}`); + } catch (e) { + console.error(`Error in worker: ${e.message}`); } - } catch (e) { - console.error(`Error in worker: ${e.message}`); - } + }); }; \ No newline at end of file diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_message.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_message.rs index aea3aa6..d6108be 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_message.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_message.rs @@ -8,6 +8,5 @@ pub enum WorkerMessage { level: String, ts: f64, }, - Ready, Idle, } diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs index 7e567c2..bd96bf9 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs @@ -1,6 +1,6 @@ use std::sync::{Arc, Mutex, OnceLock}; -use futures::{channel::oneshot, future::LocalBoxFuture}; +use futures::future::LocalBoxFuture; use thiserror::Error; use tracing::{error, info, warn}; use wasm_bindgen::{ @@ -59,12 +59,13 @@ impl WorkerPool { }) } - pub async fn run(&self, f: F) -> Result, SpawnError> + #[allow(dead_code)] + pub fn run(&self, f: F) -> Result, SpawnError> where F: FnOnce() -> Fut + Send + 'static, Fut: Future + 'static, { - let handle = self.get_or_spawn_worker().await?; + let handle = self.get_or_spawn_worker()?; let wrapper = move || -> LocalBoxFuture<'static, ()> { Box::pin(async move { @@ -75,7 +76,7 @@ impl WorkerPool { Ok(handle) } - pub async fn run_with( + pub fn run_with( &self, extra: &wasm_bindgen::JsValue, f: F, @@ -84,7 +85,7 @@ impl WorkerPool { F: FnOnce(wasm_bindgen::JsValue) -> Fut + Send + 'static, Fut: Future + 'static, { - let handle = self.get_or_spawn_worker().await?; + let handle = self.get_or_spawn_worker()?; let wrapper = move |v: wasm_bindgen::JsValue| -> LocalBoxFuture<'static, ()> { let v = v.clone(); @@ -96,15 +97,7 @@ impl WorkerPool { Ok(handle) } - pub fn terminate(&self, worker_id: WorkerId) { - let mut state = self.state.lock().unwrap(); - if let Some(pos) = state.workers.iter().position(|w| w.id == worker_id) { - let handle = state.workers.remove(pos); - handle.terminate(); - } - } - - async fn get_or_spawn_worker(&self) -> Result, SpawnError> { + fn get_or_spawn_worker(&self) -> Result, SpawnError> { // Remove any that are terminated self.state.lock().unwrap().workers.retain(|w| { let worker_state = w.state.lock().unwrap(); @@ -122,7 +115,7 @@ impl WorkerPool { // Otherwise, spawn a new one let id = self.state.lock().unwrap().workers.len() as u64; - let handle = WorkerHandle::spawn(id).await?; + let handle = WorkerHandle::spawn(id)?; let handle = Arc::new(handle); self.state.lock().unwrap().workers.push(handle.clone()); Ok(handle) @@ -130,19 +123,19 @@ impl WorkerPool { } impl WorkerHandle { - pub async fn spawn(id: u64) -> Result { + pub fn spawn(id: u64) -> Result { let name = format!("worker-{}", id); // 2. Create the worker let options = WorkerOptions::new(); options.set_name(&name); options.set_type(WorkerType::Module); - let worker = Worker::new_with_options(&worker_url(), &options).unwrap(); + let worker = + Worker::new_with_options(&worker_url(), &options).map_err(SpawnError::JsError)?; // 3. Set up onmessage handler - let (init_tx, init_rx) = oneshot::channel(); let state = Arc::new(Mutex::new(WorkerState::Busy)); - let on_message = on_message_handler(init_tx, state.clone(), name.clone()); + let on_message = on_message_handler(state.clone(), name.clone()); let on_message = Closure::wrap(on_message); worker.set_onmessage(Some(on_message.as_ref().unchecked_ref())); @@ -159,9 +152,6 @@ impl WorkerHandle { Reflect::set(&msg, &"sdkUrl".into(), &sdk_url().into()).unwrap(); worker.post_message(&msg).unwrap(); - // 5. Wait for initialization - init_rx.await.unwrap(); - Ok(WorkerHandle { id, state, @@ -171,6 +161,7 @@ impl WorkerHandle { }) } + #[allow(dead_code)] fn run(&self, f: F) -> Result<(), SpawnError> where F: FnOnce() -> Fut + Send + 'static, @@ -261,12 +252,9 @@ fn current_module() -> JsValue { } fn on_message_handler( - init_tx: oneshot::Sender<()>, state: Arc>, name: String, ) -> Box { - let mut init_tx = Some(init_tx); - Box::new(move |e: web_sys::MessageEvent| { let Ok(msg) = serde_wasm_bindgen::from_value::(e.data()) else { error!( @@ -280,12 +268,6 @@ fn on_message_handler( WorkerMessage::Log { message, level, ts } => { log_worker_message(&name, message, level, ts); } - WorkerMessage::Ready => { - info!(target: "WORKER", "[{}] Ready", name); - if let Some(tx) = init_tx.take() { - let _ = tx.send(()); - } - } WorkerMessage::Idle => { info!(target: "WORKER", "[{}] Idle", name); let mut worker_state = state.lock().unwrap(); diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index 2d2d94a..fdfd7a5 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -67,21 +67,19 @@ impl Runtime for WorkerRuntime { let module = compiled.js_module; let pool = WorkerPool::global(); - let handle = pool - .run_with(&module, move |extra| async move { - let res = run_instance( - extra, - &compiled.wasm_bytes, - stdin_reader, - stdout_writer, - stderr_writer, - ) - .await; - if let Err(e) = res { - error!("Error running Wasm instance: {}", e); - } - }) - .await?; + let handle = pool.run_with(&module, move |extra| async move { + let res = run_instance( + extra, + &compiled.wasm_bytes, + stdin_reader, + stdout_writer, + stderr_writer, + ) + .await; + if let Err(e) = res { + error!("Error running Wasm instance: {}", e); + } + })?; let id = handle.id; self.active_sessions.lock().await.insert(handle.id, handle); diff --git a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs index 19745ea..7a3b7ed 100644 --- a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs +++ b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs @@ -30,15 +30,6 @@ pub struct WasiCtx { stdout_writer: Option>, stderr_writer: Option>, - // Time when the host should resume the guest from an out-of-fuel yield. - // If None, the guest should be started immediately. If Some, the guest - // should be resumed after this time is reached. - pub sleep_until: Option, - - /// Whether the Wasi instance is current paused waiting for stdin. If true, - /// the host may choose to wait for stdin to be ready before resuming execution. - pub awaiting_stdin: bool, - memory: Option, } @@ -64,8 +55,6 @@ impl WasiCtx { stdin_reader: None, stdout_writer: None, stderr_writer: None, - sleep_until: None, - awaiting_stdin: false, memory: None, } } diff --git a/crates/wasmi-plugin-hdk/tests/worker_test.rs b/crates/wasmi-plugin-hdk/tests/worker_test.rs index 80d455f..1e60f22 100644 --- a/crates/wasmi-plugin-hdk/tests/worker_test.rs +++ b/crates/wasmi-plugin-hdk/tests/worker_test.rs @@ -37,7 +37,7 @@ async fn test_worker() { info!("Spawning worker..."); let pool = WorkerPool::global(); - pool.run(task).await.expect("Failed to spawn worker"); + pool.run(task).expect("Failed to spawn worker"); info!("Waiting for worker response..."); let result = rx @@ -81,7 +81,7 @@ async fn test_worker_with_pipes() { info!("Spawning worker..."); let pool = WorkerPool::global(); - pool.run(task).await.expect("Failed to spawn worker"); + pool.run(task).expect("Failed to spawn worker"); // Sleep for a sec to ensure the worker is ready sleep(Duration::from_millis(10)).await; From 9ef2a572a212b702366204f21899b5bb62ad571c Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sat, 27 Dec 2025 17:16:38 -0500 Subject: [PATCH 37/70] refactor: make worker_pool mod --- crates/test-plugin/src/main.rs | 2 -- crates/wasmi-plugin-hdk/src/lib.rs | 1 + crates/wasmi-plugin-hdk/src/runtime/mod.rs | 6 ---- .../src/runtime/worker_runtime.rs | 35 ++++++++++++++++--- crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs | 14 +------- .../wasmi-plugin-hdk/src/worker_pool/mod.rs | 3 ++ .../src/{runtime => worker_pool}/worker.js | 2 ++ .../worker.rs} | 0 .../worker_message.rs | 0 .../{runtime => worker_pool}/worker_pool.rs | 2 +- crates/wasmi-plugin-hdk/tests/integration.rs | 2 +- crates/wasmi-plugin-hdk/tests/worker_test.rs | 4 ++- 12 files changed, 42 insertions(+), 29 deletions(-) create mode 100644 crates/wasmi-plugin-hdk/src/worker_pool/mod.rs rename crates/wasmi-plugin-hdk/src/{runtime => worker_pool}/worker.js (94%) rename crates/wasmi-plugin-hdk/src/{runtime/thread_worker.rs => worker_pool/worker.rs} (100%) rename crates/wasmi-plugin-hdk/src/{runtime => worker_pool}/worker_message.rs (100%) rename crates/wasmi-plugin-hdk/src/{runtime => worker_pool}/worker_pool.rs (99%) diff --git a/crates/test-plugin/src/main.rs b/crates/test-plugin/src/main.rs index 2a5d830..d9fa20d 100644 --- a/crates/test-plugin/src/main.rs +++ b/crates/test-plugin/src/main.rs @@ -43,7 +43,6 @@ async fn call_many(transport: Transport, limit: u64) -> Result<(), RpcError> { let transport_clone = transport.clone(); let task = async move { let resp = transport_clone.call("ping", Value::Null)?; - info!("Call {} got response: {:?}", i, resp.result); Ok::<(), RpcError>(()) }; tasks.push(task); @@ -70,7 +69,6 @@ async fn call_many_async(transport: Transport, limit: u64) -> Result<(), RpcErro let transport_clone = transport.clone(); let task = async move { let resp = transport_clone.call_async("ping", Value::Null).await?; - info!("Call {} got response: {:?}", i, resp.result); Ok::<(), RpcError>(()) }; tasks.push(task); diff --git a/crates/wasmi-plugin-hdk/src/lib.rs b/crates/wasmi-plugin-hdk/src/lib.rs index c03a590..815b511 100644 --- a/crates/wasmi-plugin-hdk/src/lib.rs +++ b/crates/wasmi-plugin-hdk/src/lib.rs @@ -6,3 +6,4 @@ pub mod server; mod time; pub mod transport; mod wasi; +pub mod worker_pool; diff --git a/crates/wasmi-plugin-hdk/src/runtime/mod.rs b/crates/wasmi-plugin-hdk/src/runtime/mod.rs index 62dea9d..5fa385a 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/mod.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/mod.rs @@ -10,12 +10,6 @@ pub use worker_runtime::WorkerRuntime; #[cfg(target_arch = "wasm32")] mod message_writer; #[cfg(target_arch = "wasm32")] -mod thread_worker; -#[cfg(target_arch = "wasm32")] -mod worker_message; -#[cfg(target_arch = "wasm32")] -pub mod worker_pool; -#[cfg(target_arch = "wasm32")] mod worker_runtime; use crate::compile::Compiled; diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index fdfd7a5..e78425a 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -4,19 +4,20 @@ use std::collections::HashMap; use std::io::Write; use std::sync::Arc; +use futures::io::BufReader; use futures::lock::Mutex; -use futures::{AsyncRead, AsyncWrite}; +use futures::{AsyncBufReadExt, AsyncRead, AsyncWrite}; use thiserror::Error; use tracing::{error, info}; use wasm_bindgen::JsCast; +use wasm_bindgen_futures::spawn_local; use web_sys::js_sys::WebAssembly; use crate::compile::Compiled; use crate::runtime::Runtime; -use crate::runtime::message_writer::MessageWriter; use crate::runtime::non_blocking_pipe::non_blocking_pipe; -use crate::runtime::worker_pool::{SpawnError, WorkerHandle, WorkerId, WorkerPool}; use crate::wasi::wasi_ctx::{self, WasiReader}; +use crate::worker_pool::worker_pool::{SpawnError, WorkerHandle, WorkerId, WorkerPool}; pub struct WorkerRuntime { active_sessions: Arc>>>, @@ -62,8 +63,10 @@ impl Runtime for WorkerRuntime { > { let (stdin_reader, stdin_writer) = non_blocking_pipe(); let (stdout_reader, stdout_writer) = non_blocking_pipe(); - let stderr_writer: MessageWriter = MessageWriter::new("".into()); - // let (stderr_reader, stderr_writer) = non_blocking_pipe(); + let (stderr_reader, stderr_writer) = non_blocking_pipe(); + // let stderr_writer: MessageWriter = MessageWriter::new("".into()); + + handle_stderr(&compiled.name, stderr_reader); let module = compiled.js_module; let pool = WorkerPool::global(); @@ -126,3 +129,25 @@ async fn run_instance( Ok(()) } + +fn handle_stderr(name: &str, stderr: impl AsyncRead + Unpin + Send + Sync + 'static) { + let name = name.to_string(); + spawn_local(async move { + let mut reader = BufReader::new(stderr); + let mut line = String::new(); + let name = name; + loop { + match reader.read_line(&mut line).await { + Ok(0) => break, // EOF + Ok(_) => { + info!("[{}] {}", name, line.trim_end()); + line.clear(); + } + Err(e) => { + error!("Error reading Wasm stderr: {}", e); + break; + } + } + } + }); +} diff --git a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs index 7a3b7ed..fb95e95 100644 --- a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs +++ b/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs @@ -2,7 +2,7 @@ use std::io::{BufReader, Read, Write}; use thiserror::Error; use tracing::{error, info, trace, warn}; use wasmer::{FunctionEnvMut, RuntimeError}; -use web_time::{Duration, Instant, SystemTime}; +use web_time::{Duration, SystemTime}; use crate::time::blocking_sleep; @@ -369,19 +369,11 @@ pub fn fd_write( } // Print the data to be copied as a string - info!( - "fd_write: writing {} bytes to fd {}: {:?}", - to_copy, - fd, - String::from_utf8_lossy(&scratch[..to_copy]) - ); match writer.write(&scratch[..to_copy]) { Ok(0) => { - info!("fd_write: writer returned 0 bytes written, stopping"); break; } Ok(n) => { - info!("fd_write: wrote {} bytes to fd {}", n, fd); written += n; total_written += n as u32; if n < to_copy { @@ -389,12 +381,10 @@ pub fn fd_write( } } Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - info!("fd_write: writer would block"); // Spin until writable continue; } Err(_) => { - info!("fd_write: writer error"); return Errno::Io as i32; } } @@ -402,12 +392,10 @@ pub fn fd_write( //? If we couldn't write the full iovec, stop processing further iovecs if written < buf_len { - info!("fd_write: short write, stopping further iovecs"); break; } } - info!("fd_write: total_written={}", total_written); // Write total_written into nwrite_ptr if view .write(nwrite_ptr as u64, &total_written.to_le_bytes()) diff --git a/crates/wasmi-plugin-hdk/src/worker_pool/mod.rs b/crates/wasmi-plugin-hdk/src/worker_pool/mod.rs new file mode 100644 index 0000000..2930d49 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/worker_pool/mod.rs @@ -0,0 +1,3 @@ +mod worker; +mod worker_message; +pub mod worker_pool; diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker.js b/crates/wasmi-plugin-hdk/src/worker_pool/worker.js similarity index 94% rename from crates/wasmi-plugin-hdk/src/runtime/worker.js rename to crates/wasmi-plugin-hdk/src/worker_pool/worker.js index 965de6b..c5bdda3 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker.js +++ b/crates/wasmi-plugin-hdk/src/worker_pool/worker.js @@ -1,3 +1,5 @@ +// Forwards JS and WASM logs to the main thread +// Very slow, so disabled unless required for debugging // ["log", "info", "warn", "error"].forEach(level => { // globalThis.console[level] = (...args) => { // globalThis.postMessage({ diff --git a/crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs b/crates/wasmi-plugin-hdk/src/worker_pool/worker.rs similarity index 100% rename from crates/wasmi-plugin-hdk/src/runtime/thread_worker.rs rename to crates/wasmi-plugin-hdk/src/worker_pool/worker.rs diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_message.rs b/crates/wasmi-plugin-hdk/src/worker_pool/worker_message.rs similarity index 100% rename from crates/wasmi-plugin-hdk/src/runtime/worker_message.rs rename to crates/wasmi-plugin-hdk/src/worker_pool/worker_message.rs diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs b/crates/wasmi-plugin-hdk/src/worker_pool/worker_pool.rs similarity index 99% rename from crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs rename to crates/wasmi-plugin-hdk/src/worker_pool/worker_pool.rs index bd96bf9..6dc15db 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_pool.rs +++ b/crates/wasmi-plugin-hdk/src/worker_pool/worker_pool.rs @@ -12,7 +12,7 @@ use web_sys::{ js_sys::{self, Array, Reflect, Uint8Array}, }; -use crate::runtime::worker_message::WorkerMessage; +use crate::worker_pool::worker_message::WorkerMessage; pub type WorkerId = u64; diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index 69211aa..2629741 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -26,7 +26,7 @@ async fn load_plugin() -> Plugin> { let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) .await .unwrap() - .with_timeout(Duration::from_secs(2)); + .with_timeout(Duration::from_secs(5)); plugin } diff --git a/crates/wasmi-plugin-hdk/tests/worker_test.rs b/crates/wasmi-plugin-hdk/tests/worker_test.rs index 1e60f22..2457c26 100644 --- a/crates/wasmi-plugin-hdk/tests/worker_test.rs +++ b/crates/wasmi-plugin-hdk/tests/worker_test.rs @@ -8,7 +8,9 @@ use std::{ }; use tracing::info; use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; -use wasmi_plugin_hdk::runtime::{non_blocking_pipe::non_blocking_pipe, worker_pool::WorkerPool}; +use wasmi_plugin_hdk::{ + runtime::non_blocking_pipe::non_blocking_pipe, worker_pool::worker_pool::WorkerPool, +}; wasm_bindgen_test_configure!(run_in_browser); From 1bc32cb074b5fbb19c8eee6de3624ea206f9056f Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sat, 27 Dec 2025 17:18:14 -0500 Subject: [PATCH 38/70] fix: disable worker_pool on native --- crates/test-plugin/src/main.rs | 8 ++++---- crates/wasmi-plugin-hdk/src/lib.rs | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/test-plugin/src/main.rs b/crates/test-plugin/src/main.rs index d9fa20d..77bc209 100644 --- a/crates/test-plugin/src/main.rs +++ b/crates/test-plugin/src/main.rs @@ -39,10 +39,10 @@ async fn call(transport: Transport, _: ()) -> Result { async fn call_many(transport: Transport, limit: u64) -> Result<(), RpcError> { let mut tasks = vec![]; - for i in 0..limit { + for _ in 0..limit { let transport_clone = transport.clone(); let task = async move { - let resp = transport_clone.call("ping", Value::Null)?; + transport_clone.call("ping", Value::Null)?; Ok::<(), RpcError>(()) }; tasks.push(task); @@ -65,10 +65,10 @@ async fn call_async(transport: Transport, _: ()) -> Result { async fn call_many_async(transport: Transport, limit: u64) -> Result<(), RpcError> { let mut tasks = vec![]; - for i in 0..limit { + for _ in 0..limit { let transport_clone = transport.clone(); let task = async move { - let resp = transport_clone.call_async("ping", Value::Null).await?; + transport_clone.call_async("ping", Value::Null).await?; Ok::<(), RpcError>(()) }; tasks.push(task); diff --git a/crates/wasmi-plugin-hdk/src/lib.rs b/crates/wasmi-plugin-hdk/src/lib.rs index 815b511..a02d4db 100644 --- a/crates/wasmi-plugin-hdk/src/lib.rs +++ b/crates/wasmi-plugin-hdk/src/lib.rs @@ -6,4 +6,5 @@ pub mod server; mod time; pub mod transport; mod wasi; +#[cfg(target_arch = "wasm32")] pub mod worker_pool; From d342cdcac39952ac4b3c99ca2aefdbb4901a5515 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sat, 27 Dec 2025 17:19:23 -0500 Subject: [PATCH 39/70] clippy --- crates/wasmi-plugin-hdk/src/runtime/message_writer.rs | 1 + crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs | 2 +- crates/wasmi-plugin-hdk/src/worker_pool/mod.rs | 2 +- .../src/worker_pool/{worker_pool.rs => pool.rs} | 0 crates/wasmi-plugin-hdk/tests/worker_test.rs | 2 +- 5 files changed, 4 insertions(+), 3 deletions(-) rename crates/wasmi-plugin-hdk/src/worker_pool/{worker_pool.rs => pool.rs} (100%) diff --git a/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs b/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs index 45e60af..f51410d 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs @@ -10,6 +10,7 @@ unsafe impl Send for MessageWriter {} unsafe impl Sync for MessageWriter {} impl MessageWriter { + #[allow(dead_code)] pub fn new(name: String) -> Self { Self { name } } diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index e78425a..cb6d341 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -17,7 +17,7 @@ use crate::compile::Compiled; use crate::runtime::Runtime; use crate::runtime::non_blocking_pipe::non_blocking_pipe; use crate::wasi::wasi_ctx::{self, WasiReader}; -use crate::worker_pool::worker_pool::{SpawnError, WorkerHandle, WorkerId, WorkerPool}; +use crate::worker_pool::pool::{SpawnError, WorkerHandle, WorkerId, WorkerPool}; pub struct WorkerRuntime { active_sessions: Arc>>>, diff --git a/crates/wasmi-plugin-hdk/src/worker_pool/mod.rs b/crates/wasmi-plugin-hdk/src/worker_pool/mod.rs index 2930d49..178a4da 100644 --- a/crates/wasmi-plugin-hdk/src/worker_pool/mod.rs +++ b/crates/wasmi-plugin-hdk/src/worker_pool/mod.rs @@ -1,3 +1,3 @@ +pub mod pool; mod worker; mod worker_message; -pub mod worker_pool; diff --git a/crates/wasmi-plugin-hdk/src/worker_pool/worker_pool.rs b/crates/wasmi-plugin-hdk/src/worker_pool/pool.rs similarity index 100% rename from crates/wasmi-plugin-hdk/src/worker_pool/worker_pool.rs rename to crates/wasmi-plugin-hdk/src/worker_pool/pool.rs diff --git a/crates/wasmi-plugin-hdk/tests/worker_test.rs b/crates/wasmi-plugin-hdk/tests/worker_test.rs index 2457c26..d26d719 100644 --- a/crates/wasmi-plugin-hdk/tests/worker_test.rs +++ b/crates/wasmi-plugin-hdk/tests/worker_test.rs @@ -9,7 +9,7 @@ use std::{ use tracing::info; use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; use wasmi_plugin_hdk::{ - runtime::non_blocking_pipe::non_blocking_pipe, worker_pool::worker_pool::WorkerPool, + runtime::non_blocking_pipe::non_blocking_pipe, worker_pool::pool::WorkerPool, }; wasm_bindgen_test_configure!(run_in_browser); From d8a0faeb8faf9361fbed39b082475317ae851c35 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sat, 27 Dec 2025 17:36:47 -0500 Subject: [PATCH 40/70] feat add transport trait --- crates/test-plugin/src/main.rs | 6 +- crates/wasmi-plugin-hdk/src/plugin.rs | 4 +- crates/wasmi-plugin-hdk/src/transport.rs | 21 ++-- crates/wasmi-plugin-hdk/tests/worker_test.rs | 114 ------------------- crates/wasmi-plugin-pdk/src/transport.rs | 24 +++- 5 files changed, 35 insertions(+), 134 deletions(-) delete mode 100644 crates/wasmi-plugin-hdk/tests/worker_test.rs diff --git a/crates/test-plugin/src/main.rs b/crates/test-plugin/src/main.rs index 77bc209..b3e37e1 100644 --- a/crates/test-plugin/src/main.rs +++ b/crates/test-plugin/src/main.rs @@ -3,7 +3,11 @@ use serde_json::{self, Value}; use std::io::stderr; use tracing::{error, info, level_filters::LevelFilter}; use tracing_subscriber::fmt; -use wasmi_plugin_pdk::{rpc_message::RpcError, server::PluginServer, transport::Transport}; +use wasmi_plugin_pdk::{ + rpc_message::RpcError, + server::PluginServer, + transport::{AsyncTransport, SyncTransport, Transport}, +}; async fn ping(_: Transport, _: ()) -> Result { Ok(Value::String("pong".to_string())) diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index bb425f2..fea6108 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -136,9 +136,9 @@ impl Plugin { handler: self.handler.clone(), uuid: self.id, }; - let transport = Transport::new(stdout_reader, stdin_writer); + let transport = Transport::new(stdout_reader, stdin_writer, handler); - let transport_task = transport.call(method, params, Some(handler)).fuse(); + let transport_task = transport.call_async(method, params).fuse(); let timeout = sleep(self.timeout).fuse(); futures::pin_mut!(transport_task, timeout); diff --git a/crates/wasmi-plugin-hdk/src/transport.rs b/crates/wasmi-plugin-hdk/src/transport.rs index ef84d60..f6bfb42 100644 --- a/crates/wasmi-plugin-hdk/src/transport.rs +++ b/crates/wasmi-plugin-hdk/src/transport.rs @@ -8,9 +8,10 @@ use wasmi_plugin_pdk::{ }; use wasmi_plugin_rt::yield_now; -pub struct Transport { +pub struct Transport { reader: BufReader, writer: W, + handler: H, } #[derive(Debug, Error)] @@ -28,19 +29,19 @@ pub enum TransportError { ErrorResponse(RpcErrorResponse), } -impl Transport { - pub fn new(reader: R, writer: W) -> Self { +impl> Transport { + pub fn new(reader: R, writer: W, handler: H) -> Self { Transport { reader: BufReader::new(reader), writer, + handler, } } - pub async fn call( + pub async fn call_async( mut self, method: &str, params: Value, - handler: Option>, ) -> Result { let id = 1; let request = RpcMessage::request(id, method.to_string(), params); @@ -74,7 +75,7 @@ impl Transport { return Err(TransportError::ErrorResponse(err)); } RpcMessage::RpcRequest(req) => { - self.handle_guest_req(req.id, &req.method, req.params, &handler) + self.handle_guest_req(req.id, &req.method, req.params) .await?; } _ => {} @@ -99,14 +100,8 @@ impl Transport { id: u64, method: &str, params: Value, - handler: &Option>, ) -> Result<(), TransportError> { - let Some(handler) = handler else { - info!("No handler, request ignored"); - return Ok(()); - }; - - match handler.handle(method, params).await { + match self.handler.handle(method, params).await { Ok(result) => { self.write_message(RpcMessage::response(id, result)).await?; } diff --git a/crates/wasmi-plugin-hdk/tests/worker_test.rs b/crates/wasmi-plugin-hdk/tests/worker_test.rs deleted file mode 100644 index d26d719..0000000 --- a/crates/wasmi-plugin-hdk/tests/worker_test.rs +++ /dev/null @@ -1,114 +0,0 @@ -#![cfg(target_arch = "wasm32")] - -use gloo_timers::future::sleep; -use std::{ - io::{BufRead, BufReader, Write}, - sync::Once, - time::Duration, -}; -use tracing::info; -use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; -use wasmi_plugin_hdk::{ - runtime::non_blocking_pipe::non_blocking_pipe, worker_pool::pool::WorkerPool, -}; - -wasm_bindgen_test_configure!(run_in_browser); - -static INIT: Once = Once::new(); - -fn setup_logs() { - INIT.call_once(|| { - tracing_wasm::set_as_global_default_with_config( - tracing_wasm::WASMLayerConfigBuilder::new() - .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) - .build(), - ); - }); -} - -#[wasm_bindgen_test] -async fn test_worker() { - setup_logs(); - - let (tx, rx) = futures::channel::oneshot::channel::(); - - let task = async move || { - info!("Hello from inside the worker!"); - tx.send(true).unwrap(); - }; - - info!("Spawning worker..."); - let pool = WorkerPool::global(); - pool.run(task).expect("Failed to spawn worker"); - - info!("Waiting for worker response..."); - let result = rx - .await - .expect("Worker dropped the sender without responding"); - - assert!(result); -} - -#[wasm_bindgen_test] -async fn test_worker_with_pipes() { - setup_logs(); - - let (stdin_reader, mut stdin_writer) = non_blocking_pipe(); - let (mut stdout_reader, stdout_writer) = non_blocking_pipe(); - - let task = async move || { - // Wait to recieve a message, then echo it back. - let mut writer = stdout_writer; - let mut reader = BufReader::new(stdin_reader); - let mut line = String::new(); - loop { - match reader.read_line(&mut line) { - Ok(0) => break, - Ok(_) => { - // Echo back the line - writer.write_all(line.as_bytes()).unwrap(); - writer.flush().unwrap(); - break; - } - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - // Would block, try again - continue; - } - Err(e) => { - panic!("Error reading from stdin: {}", e); - } - } - } - }; - - info!("Spawning worker..."); - let pool = WorkerPool::global(); - pool.run(task).expect("Failed to spawn worker"); - - // Sleep for a sec to ensure the worker is ready - sleep(Duration::from_millis(10)).await; - - let test_message = "Hello, worker!\n"; - info!("Sending message to worker: {}", test_message.trim()); - stdin_writer.write_all(test_message.as_bytes()).unwrap(); - stdin_writer.flush().unwrap(); - - let mut reader = BufReader::new(&mut stdout_reader); - let mut line = String::new(); - info!("Waiting for worker response..."); - loop { - match reader.read_line(&mut line) { - Ok(0) => panic!("Worker closed the pipe unexpectedly"), - Ok(_) => break, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - // Would block, try again - continue; - } - Err(e) => { - panic!("Error reading from stdout: {}", e); - } - } - } - - assert_eq!(line, test_message); -} diff --git a/crates/wasmi-plugin-pdk/src/transport.rs b/crates/wasmi-plugin-pdk/src/transport.rs index 4182776..62130d3 100644 --- a/crates/wasmi-plugin-pdk/src/transport.rs +++ b/crates/wasmi-plugin-pdk/src/transport.rs @@ -7,6 +7,18 @@ use crate::{ transport_driver::{DriverError, TransportDriver}, }; +pub trait SyncTransport { + fn call(&self, method: &str, params: Value) -> Result; +} + +pub trait AsyncTransport { + fn call_async( + &self, + method: &str, + params: Value, + ) -> impl std::future::Future> + Send; +} + pub struct Transport { driver: TransportDriver, } @@ -29,16 +41,20 @@ impl Transport { (transport, driver) } +} - pub fn call(&self, method: &str, params: Value) -> Result { +impl SyncTransport for Transport { + fn call(&self, method: &str, params: Value) -> Result { self.driver.call(method, params) } +} - pub async fn call_async( +impl AsyncTransport for Transport { + fn call_async( &self, method: &str, params: Value, - ) -> Result { - self.driver.call_async(method, params).await + ) -> impl std::future::Future> + Send { + self.driver.call_async(method, params) } } From 23af9ecc0a39b9f709adb5469c62b4b966114263 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sat, 27 Dec 2025 17:40:04 -0500 Subject: [PATCH 41/70] fix: make transport traits generic --- crates/wasmi-plugin-pdk/src/transport.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/wasmi-plugin-pdk/src/transport.rs b/crates/wasmi-plugin-pdk/src/transport.rs index 62130d3..09670aa 100644 --- a/crates/wasmi-plugin-pdk/src/transport.rs +++ b/crates/wasmi-plugin-pdk/src/transport.rs @@ -3,20 +3,20 @@ use std::io::{Read, Write}; use serde_json::Value; use crate::{ - rpc_message::RpcResponse, + rpc_message::{RpcError, RpcResponse}, transport_driver::{DriverError, TransportDriver}, }; -pub trait SyncTransport { - fn call(&self, method: &str, params: Value) -> Result; +pub trait SyncTransport> { + fn call(&self, method: &str, params: Value) -> Result; } -pub trait AsyncTransport { +pub trait AsyncTransport> { fn call_async( &self, method: &str, params: Value, - ) -> impl std::future::Future> + Send; + ) -> impl std::future::Future> + Send; } pub struct Transport { @@ -43,13 +43,13 @@ impl Transport { } } -impl SyncTransport for Transport { +impl SyncTransport for Transport { fn call(&self, method: &str, params: Value) -> Result { self.driver.call(method, params) } } -impl AsyncTransport for Transport { +impl AsyncTransport for Transport { fn call_async( &self, method: &str, From c565063ccea89366b89a4b6677477740c775d611 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sat, 27 Dec 2025 20:11:14 -0500 Subject: [PATCH 42/70] refactor: update traits --- crates/wasmi-plugin-pdk/src/rpc_message.rs | 6 ++++++ crates/wasmi-plugin-pdk/src/transport.rs | 7 ++++--- crates/wasmi-plugin-pdk/src/transport_driver.rs | 15 ++++----------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/wasmi-plugin-pdk/src/rpc_message.rs b/crates/wasmi-plugin-pdk/src/rpc_message.rs index 9028d9b..a894128 100644 --- a/crates/wasmi-plugin-pdk/src/rpc_message.rs +++ b/crates/wasmi-plugin-pdk/src/rpc_message.rs @@ -96,6 +96,12 @@ impl RpcError { } } +impl From for RpcError { + fn from(value: RpcErrorResponse) -> Self { + value.error + } +} + pub fn to_rpc_err(e: E) -> RpcError { RpcError::Custom(e.to_string()) } diff --git a/crates/wasmi-plugin-pdk/src/transport.rs b/crates/wasmi-plugin-pdk/src/transport.rs index 09670aa..2be7f9b 100644 --- a/crates/wasmi-plugin-pdk/src/transport.rs +++ b/crates/wasmi-plugin-pdk/src/transport.rs @@ -3,15 +3,16 @@ use std::io::{Read, Write}; use serde_json::Value; use crate::{ - rpc_message::{RpcError, RpcResponse}, + api::ApiError, + rpc_message::RpcResponse, transport_driver::{DriverError, TransportDriver}, }; -pub trait SyncTransport> { +pub trait SyncTransport { fn call(&self, method: &str, params: Value) -> Result; } -pub trait AsyncTransport> { +pub trait AsyncTransport { fn call_async( &self, method: &str, diff --git a/crates/wasmi-plugin-pdk/src/transport_driver.rs b/crates/wasmi-plugin-pdk/src/transport_driver.rs index be24de5..caa71ce 100644 --- a/crates/wasmi-plugin-pdk/src/transport_driver.rs +++ b/crates/wasmi-plugin-pdk/src/transport_driver.rs @@ -51,14 +51,8 @@ pub enum DriverError { #[error("EOF")] Eof, - #[error("Unmatched response ID")] - UnmatchedResponseId, - - #[error("Error Response: {0:?}")] - ErrorResponse(RpcErrorResponse), - - #[error("Missing Response")] - MissingResponse, + #[error("RPC Error: {0:?}")] + RpcError(#[from] RpcError), } impl From for RpcError { @@ -101,7 +95,7 @@ impl TransportDriver { // Await the response let res = revc_rx.await?; - let res = res.map_err(DriverError::ErrorResponse)?; + let res = res.map_err(|e| DriverError::RpcError(e.error))?; Ok(res) } @@ -122,10 +116,9 @@ impl TransportDriver { loop { let would_block = self.step(&mut reader, &mut line)?; - // Check if we have received the response match res_rx.try_recv() { Ok(Some(res)) => { - let res = res.map_err(DriverError::ErrorResponse)?; + let res = res.map_err(|e| DriverError::RpcError(e.error))?; return Ok(res); } Ok(None) => { From 21d62e432753df141f47b6e02fe18e74751197db Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sat, 27 Dec 2025 23:46:52 -0500 Subject: [PATCH 43/70] refactor: transport --- crates/wasmi-plugin-hdk/benches/wasm_bench.rs | 2 +- crates/wasmi-plugin-hdk/src/plugin.rs | 17 +- crates/wasmi-plugin-hdk/tests/integration.rs | 2 +- crates/wasmi-plugin-pdk/src/lib.rs | 1 - crates/wasmi-plugin-pdk/src/server.rs | 4 +- crates/wasmi-plugin-pdk/src/transport.rs | 238 ++++++++++++++++-- .../wasmi-plugin-pdk/src/transport_driver.rs | 205 --------------- 7 files changed, 229 insertions(+), 240 deletions(-) delete mode 100644 crates/wasmi-plugin-pdk/src/transport_driver.rs diff --git a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs index 390a52d..d3919ef 100644 --- a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs +++ b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs @@ -21,7 +21,7 @@ fn load_plugin_wasm() -> Vec { PLUGIN_WASM.to_vec() } -async fn load_plugin() -> Plugin, ())>> { +async fn load_plugin() -> Plugin { let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index fea6108..01ee16f 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -10,6 +10,7 @@ use wasmi_plugin_pdk::{ api::RequestHandler, router::BoxFuture, rpc_message::{RpcError, RpcResponse}, + transport::AsyncTransport, }; use crate::compile::Compiled; @@ -54,10 +55,10 @@ impl Display for PluginId { type Logger = Box; /// Plugin is an async-capable instance of a plugin -pub struct Plugin { +pub struct Plugin { name: String, id: PluginId, - handler: Arc, + handler: Arc, logger: Logger, compiled: Compiled, timeout: Duration, @@ -76,11 +77,11 @@ pub enum PluginError { PluginTimeout, } -impl Plugin { +impl Plugin { pub async fn new( name: &str, wasm_bytes: &[u8], - handler: Arc, + handler: Arc, ) -> Result { Ok(Plugin { name: name.to_string(), @@ -124,7 +125,13 @@ impl Plugin { } } -impl Plugin { +impl AsyncTransport for Plugin { + async fn call_async(&self, method: &str, params: Value) -> Result { + self.call(method, params).await + } +} + +impl Plugin { pub async fn call(&self, method: &str, params: Value) -> Result { let runtime = self.create_runtime(); let (id, stdin_writer, stdout_reader) = runtime diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index 2629741..ff707c9 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -20,7 +20,7 @@ fn load_plugin_wasm() -> Vec { PLUGIN_WASM.to_vec() } -async fn load_plugin() -> Plugin> { +async fn load_plugin() -> Plugin { let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) diff --git a/crates/wasmi-plugin-pdk/src/lib.rs b/crates/wasmi-plugin-pdk/src/lib.rs index c828391..b138bb9 100644 --- a/crates/wasmi-plugin-pdk/src/lib.rs +++ b/crates/wasmi-plugin-pdk/src/lib.rs @@ -4,4 +4,3 @@ pub mod router; pub mod rpc_message; pub mod server; pub mod transport; -mod transport_driver; diff --git a/crates/wasmi-plugin-pdk/src/server.rs b/crates/wasmi-plugin-pdk/src/server.rs index fb5ad82..9be53d1 100644 --- a/crates/wasmi-plugin-pdk/src/server.rs +++ b/crates/wasmi-plugin-pdk/src/server.rs @@ -31,8 +31,8 @@ enum PluginServerError { #[error("RPC error: {0}")] Rpc(#[from] RpcError), - #[error("Transport driver error: {0}")] - TransportDriver(#[from] crate::transport_driver::DriverError), + #[error("Transport error: {0}")] + Transport(#[from] crate::transport::TransportError), } #[allow(clippy::new_without_default)] diff --git a/crates/wasmi-plugin-pdk/src/transport.rs b/crates/wasmi-plugin-pdk/src/transport.rs index 2be7f9b..c1e1cb2 100644 --- a/crates/wasmi-plugin-pdk/src/transport.rs +++ b/crates/wasmi-plugin-pdk/src/transport.rs @@ -1,61 +1,249 @@ -use std::io::{Read, Write}; +use std::{ + collections::HashMap, + io::{BufRead, BufReader, Read, Write}, + sync::{Arc, Mutex, atomic::AtomicU64}, +}; +use futures::channel::oneshot; use serde_json::Value; +use thiserror::Error; +use tracing::{info, warn}; +use wasmi_plugin_rt::yield_now; use crate::{ - api::ApiError, - rpc_message::RpcResponse, - transport_driver::{DriverError, TransportDriver}, + poll_oneoff::wait_for_stdin, + router::MaybeSend, + rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcResponse}, }; -pub trait SyncTransport { +pub trait SyncTransport: Send + Sync + 'static { fn call(&self, method: &str, params: Value) -> Result; } -pub trait AsyncTransport { +pub trait AsyncTransport: Send + Sync + 'static { fn call_async( &self, method: &str, params: Value, - ) -> impl std::future::Future> + Send; + ) -> impl std::future::Future> + MaybeSend; } pub struct Transport { - driver: TransportDriver, + inner: Arc>, +} + +pub struct TransportDriver { + inner: Arc>, +} + +struct TransportInner { + reader: Mutex>, + writer: Mutex, + #[allow(clippy::type_complexity)] + pending: Mutex>>>, + next_id: AtomicU64, +} + +#[derive(Debug, Error)] +pub enum TransportError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("serde_json error: {0}")] + Serde(#[from] serde_json::Error), + + #[error("oneshot send error")] + OneshotSend, + + #[error("oneshot Canceled")] + OneshotCanceled(#[from] oneshot::Canceled), + + #[error("EOF")] + Eof, + + #[error("RPC Error: {0:?}")] + RpcError(#[from] RpcError), +} + +impl From for RpcError { + fn from(err: TransportError) -> Self { + RpcError::Custom(err.to_string()) + } } -impl Clone for Transport { +impl Clone for Transport { fn clone(&self) -> Self { - Self { - driver: self.driver.clone(), + Transport { + inner: self.inner.clone(), } } } impl Transport { pub fn new(reader: R, writer: W) -> (Self, TransportDriver) { - let driver = TransportDriver::new(reader, writer); + let inner = Arc::new(TransportInner { + reader: Mutex::new(BufReader::new(reader)), + writer: Mutex::new(writer), + pending: Mutex::new(HashMap::new()), + next_id: AtomicU64::new(0), + }); - let transport = Self { - driver: driver.clone(), - }; + ( + Transport { + inner: inner.clone(), + }, + TransportDriver { inner }, + ) + } +} - (transport, driver) +impl SyncTransport for Transport { + fn call(&self, method: &str, params: Value) -> Result { + self.inner.call(method, params) } } -impl SyncTransport for Transport { - fn call(&self, method: &str, params: Value) -> Result { - self.driver.call(method, params) +impl AsyncTransport for Transport { + async fn call_async(&self, method: &str, params: Value) -> Result { + self.inner.call_async(method, params).await } } -impl AsyncTransport for Transport { - fn call_async( +impl TransportDriver { + pub async fn run(&self) -> Result<(), TransportError> { + self.inner.run().await + } + + pub fn write_message(&self, message: &RpcMessage) -> Result<(), TransportError> { + self.inner.write_message(message) + } +} + +impl TransportInner { + async fn run(&self) -> Result<(), TransportError> { + let mut line = String::new(); + + loop { + self.step(&mut self.reader.lock().unwrap(), &mut line)?; + yield_now().await; + } + } + + fn call(&self, method: &str, params: Value) -> Result { + let id = self.next_id(); + let request = RpcMessage::request(id, method.to_string(), params); + + let (res_tx, mut res_rx) = oneshot::channel(); + self.pending.lock().unwrap().insert(id, res_tx); + self.write_message(&request)?; + + let mut reader = self.reader.lock().unwrap(); + let mut line = String::new(); + + // Drive IO until we get the response + loop { + let would_block = self.step(&mut reader, &mut line)?; + + match res_rx.try_recv() { + Ok(Some(res)) => { + let res = res.map_err(|e| TransportError::RpcError(e.error))?; + return Ok(res); + } + Ok(None) => { + if would_block { + wait_for_stdin(); + } + } + Err(e) => { + return Err(TransportError::OneshotCanceled(e)); + } + } + } + } + + async fn call_async(&self, method: &str, params: Value) -> Result { + let id = self.next_id(); + let request = RpcMessage::request(id, method.to_string(), params); + + let (recv_tx, revc_rx) = oneshot::channel(); + self.pending.lock().unwrap().insert(id, recv_tx); + self.write_message(&request)?; + + // Await the response + let res = revc_rx.await?; + let res = res.map_err(|e| TransportError::RpcError(e.error))?; + Ok(res) + } + + /// Perform a single read step, processing one incoming message if available. + /// Returns Ok(true) if no message was available (WouldBlock) or Ok(false) + /// if a message was processed. + fn step( &self, - method: &str, - params: Value, - ) -> impl std::future::Future> + Send { - self.driver.call_async(method, params) + reader: &mut std::sync::MutexGuard<'_, BufReader>, + line: &mut String, + ) -> Result { + match reader.read_line(line) { + Ok(0) => return Err(TransportError::Eof), + Ok(_) => {} + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(true), + Err(e) => return Err(TransportError::Io(e)), + } + + let msg: RpcMessage = serde_json::from_str(line)?; + line.clear(); + + self.handle_message(msg)?; + Ok(false) + } + + // TODO: Catch WouldBlock and retry later + pub fn write_message(&self, message: &RpcMessage) -> Result<(), TransportError> { + let message_str = serde_json::to_string(message)?; + let msg = format!("{}\n", message_str); + { + let mut writer = self.writer.lock().unwrap(); + writer.write_all(msg.as_bytes())?; + writer.flush()?; + } + + Ok(()) + } + + fn handle_message(&self, message: RpcMessage) -> Result<(), TransportError> { + match message { + RpcMessage::RpcResponse(res) => { + let sender = self.pending.lock().unwrap().remove(&res.id); + let Some(sender) = sender else { + warn!("Ignoring unmatched response ID: {}", res.id); + return Ok(()); + }; + + sender + .send(Ok(res)) + .map_err(|_| TransportError::OneshotSend)?; + } + RpcMessage::RpcErrorResponse(res) => { + let sender = self.pending.lock().unwrap().remove(&res.id); + let Some(sender) = sender else { + warn!("Ignoring unmatched response ID: {}", res.id); + return Ok(()); + }; + + sender + .send(Err(res)) + .map_err(|_| TransportError::OneshotSend)?; + } + RpcMessage::RpcRequest(_) => { + info!("Ignoring request message"); + } + } + + Ok(()) + } + + fn next_id(&self) -> u64 { + self.next_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) } } diff --git a/crates/wasmi-plugin-pdk/src/transport_driver.rs b/crates/wasmi-plugin-pdk/src/transport_driver.rs deleted file mode 100644 index caa71ce..0000000 --- a/crates/wasmi-plugin-pdk/src/transport_driver.rs +++ /dev/null @@ -1,205 +0,0 @@ -use std::{ - collections::HashMap, - io::{BufRead, BufReader, Read, Write}, - sync::{Arc, Mutex, atomic::AtomicU64}, -}; - -use futures::channel::oneshot; -use serde_json::Value; -use thiserror::Error; -use tracing::info; -use wasmi_plugin_rt::yield_now; - -use crate::{ - poll_oneoff::wait_for_stdin, - rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcResponse}, -}; - -pub struct TransportDriver { - reader: Arc>>, - writer: Arc>, - #[allow(clippy::type_complexity)] - pending: Arc>>>>, - next_id: Arc, -} - -impl Clone for TransportDriver { - fn clone(&self) -> Self { - Self { - reader: self.reader.clone(), - writer: self.writer.clone(), - pending: self.pending.clone(), - next_id: self.next_id.clone(), - } - } -} - -#[derive(Debug, Error)] -pub enum DriverError { - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - - #[error("serde_json error: {0}")] - Serde(#[from] serde_json::Error), - - #[error("oneshot send error")] - OneshotSend, - - #[error("oneshot Canceled")] - OneshotCanceled(#[from] oneshot::Canceled), - - #[error("EOF")] - Eof, - - #[error("RPC Error: {0:?}")] - RpcError(#[from] RpcError), -} - -impl From for RpcError { - fn from(value: DriverError) -> Self { - RpcError::custom(value.to_string()) - } -} - -impl TransportDriver { - pub fn new(reader: R, writer: W) -> Self { - Self { - reader: Arc::new(Mutex::new(BufReader::new(reader))), - writer: Arc::new(Mutex::new(writer)), - pending: Arc::new(Mutex::new(HashMap::new())), - next_id: Arc::new(AtomicU64::new(0)), - } - } - - pub async fn run(&mut self) -> Result<(), DriverError> { - let mut line = String::new(); - - loop { - self.step(&mut self.reader.lock().unwrap(), &mut line)?; - yield_now().await; - } - } - - /// Asynchronous call that sends a request and waits for the response. - pub async fn call_async( - &self, - method: &str, - params: Value, - ) -> Result { - let id = self.next_id(); - let request = RpcMessage::request(id, method.to_string(), params); - - let (recv_tx, revc_rx) = oneshot::channel(); - self.pending.lock().unwrap().insert(id, recv_tx); - self.write_message(&request)?; - - // Await the response - let res = revc_rx.await?; - let res = res.map_err(|e| DriverError::RpcError(e.error))?; - Ok(res) - } - - /// Synchronous call that sends a request and waits for the response. Handles any - /// unrelated incoming messages while waiting. - pub fn call(&self, method: &str, params: Value) -> Result { - let id = self.next_id(); - let request = RpcMessage::request(id, method.to_string(), params); - - let (res_tx, mut res_rx) = oneshot::channel(); - self.pending.lock().unwrap().insert(id, res_tx); - self.write_message(&request)?; - - let mut reader = self.reader.lock().unwrap(); - let mut line = String::new(); - - // Drive IO until we get the response - loop { - let would_block = self.step(&mut reader, &mut line)?; - - match res_rx.try_recv() { - Ok(Some(res)) => { - let res = res.map_err(|e| DriverError::RpcError(e.error))?; - return Ok(res); - } - Ok(None) => { - if would_block { - wait_for_stdin(); - } - } - Err(e) => { - return Err(DriverError::OneshotCanceled(e)); - } - } - } - } - - /// Perform a single read step, processing one incoming message if available. - /// Returns Ok(true) if no message was available (WouldBlock) or Ok(false) - /// if a message was processed. - fn step( - &self, - reader: &mut std::sync::MutexGuard<'_, BufReader>, - line: &mut String, - ) -> Result { - match reader.read_line(line) { - Ok(0) => return Err(DriverError::Eof), - Ok(_) => {} - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(true), - Err(e) => return Err(DriverError::Io(e)), - } - - let msg: RpcMessage = serde_json::from_str(line)?; - line.clear(); - - self.insert_response(msg)?; - Ok(false) - } - - // TODO: Catch WouldBlock and retry later - pub fn write_message(&self, message: &RpcMessage) -> Result<(), DriverError> { - let message_str = serde_json::to_string(message)?; - let msg = format!("{}\n", message_str); - { - let mut writer = self.writer.lock().unwrap(); - writer.write_all(msg.as_bytes())?; - writer.flush()?; - } - - Ok(()) - } - - fn insert_response(&self, message: RpcMessage) -> Result<(), DriverError> { - match message { - RpcMessage::RpcResponse(res) => { - let sender = self.pending.lock().unwrap().remove(&res.id); - let Some(sender) = sender else { - info!("Ignoring unmatched response ID: {}", res.id); - return Ok(()); - }; - - sender.send(Ok(res)).map_err(|_| DriverError::OneshotSend)?; - } - RpcMessage::RpcErrorResponse(res) => { - let sender = self.pending.lock().unwrap().remove(&res.id); - let Some(sender) = sender else { - info!("Ignoring unmatched response ID: {}", res.id); - return Ok(()); - }; - - sender - .send(Err(res)) - .map_err(|_| DriverError::OneshotSend)?; - } - RpcMessage::RpcRequest(_) => { - info!("Ignoring request message"); - } - } - - Ok(()) - } - - fn next_id(&self) -> u64 { - self.next_id - .fetch_add(1, std::sync::atomic::Ordering::Relaxed) - } -} From a019b73471634686e55ada952831b7b1321a7a01 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sun, 28 Dec 2025 15:49:57 -0500 Subject: [PATCH 44/70] refactor --- crates/test-plugin/src/main.rs | 13 +- crates/wasmi-plugin-hdk/benches/benchmark.rs | 36 +-- crates/wasmi-plugin-hdk/benches/wasm_bench.rs | 36 +-- crates/wasmi-plugin-hdk/src/host_handler.rs | 4 +- crates/wasmi-plugin-hdk/src/lib.rs | 4 +- crates/wasmi-plugin-hdk/src/plugin.rs | 209 ++++++++++-------- crates/wasmi-plugin-hdk/src/plugin_id.rs | 36 +++ crates/wasmi-plugin-hdk/src/runtime/mod.rs | 12 +- .../src/runtime/native_runtime.rs | 41 +--- .../src/runtime/worker_runtime.rs | 92 +++----- crates/wasmi-plugin-hdk/src/server.rs | 2 +- .../src/{transport.rs => session.rs} | 50 ++--- crates/wasmi-plugin-hdk/tests/integration.rs | 43 ++-- crates/wasmi-plugin-pdk/src/api.rs | 5 +- crates/wasmi-plugin-pdk/src/lib.rs | 2 +- crates/wasmi-plugin-pdk/src/router.rs | 8 +- crates/wasmi-plugin-pdk/src/rpc_message.rs | 89 +++----- .../src/{server.rs => runner.rs} | 17 +- crates/wasmi-plugin-pdk/src/transport.rs | 27 ++- 19 files changed, 376 insertions(+), 350 deletions(-) create mode 100644 crates/wasmi-plugin-hdk/src/plugin_id.rs rename crates/wasmi-plugin-hdk/src/{transport.rs => session.rs} (69%) rename crates/wasmi-plugin-pdk/src/{server.rs => runner.rs} (84%) diff --git a/crates/test-plugin/src/main.rs b/crates/test-plugin/src/main.rs index b3e37e1..1903cc8 100644 --- a/crates/test-plugin/src/main.rs +++ b/crates/test-plugin/src/main.rs @@ -1,11 +1,11 @@ use rand::Rng; use serde_json::{self, Value}; use std::io::stderr; -use tracing::{error, info, level_filters::LevelFilter}; +use tracing::{info, level_filters::LevelFilter}; use tracing_subscriber::fmt; use wasmi_plugin_pdk::{ - rpc_message::RpcError, - server::PluginServer, + rpc_message::{RpcError, RpcErrorContext}, + runner::PluginRunner, transport::{AsyncTransport, SyncTransport, Transport}, }; @@ -22,10 +22,7 @@ async fn get_random_number(_: Transport, _: ()) -> Result { async fn get_time(_: Transport, _: ()) -> Result { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .map_err(|e| { - error!("System time before UNIX EPOCH: {}", e); - RpcError::InternalError - })?; + .context("Time before UNIX_EPOCH")?; Ok(Value::Number(now.as_secs().into())) } @@ -129,7 +126,7 @@ fn main() { info!("Starting plugin..."); info!("Test"); - PluginServer::new() + PluginRunner::new() .with_method("ping", ping) .with_method("get_random_number", get_random_number) .with_method("get_time", get_time) diff --git a/crates/wasmi-plugin-hdk/benches/benchmark.rs b/crates/wasmi-plugin-hdk/benches/benchmark.rs index b435b6f..f6e01ca 100644 --- a/crates/wasmi-plugin-hdk/benches/benchmark.rs +++ b/crates/wasmi-plugin-hdk/benches/benchmark.rs @@ -7,10 +7,8 @@ mod benchmarks { time::Duration, }; use tokio::runtime::Builder; - use wasmi_plugin_hdk::{ - plugin::{Plugin, PluginId}, - server::HostServer, - }; + use wasmi_plugin_hdk::{plugin::Plugin, plugin_id::PluginId, server::HostServer}; + use wasmi_plugin_pdk::transport::AsyncTransport; const PLUGIN_WASM: &[u8] = include_bytes!("../../../target/wasm32-wasip1/release/test-plugin.wasm"); @@ -21,13 +19,14 @@ mod benchmarks { PLUGIN_WASM.to_vec() } - async fn load_plugin() -> Plugin, ())>> { + async fn load_plugin() -> Plugin { let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) + let plugin = Plugin::builder("test_plugin", wasm_bytes.clone(), handler) + .with_timeout(Duration::from_secs(10)) + .build() .await - .unwrap() - .with_timeout(Duration::from_secs(10)); + .unwrap(); plugin } @@ -62,7 +61,7 @@ mod benchmarks { c.bench_function("ping", |b| { b.iter(|| { let fut = async { - plugin.call("ping", Value::Null).await.unwrap(); + plugin.call_async("ping", Value::Null).await.unwrap(); }; rt.block_on(fut); @@ -83,10 +82,13 @@ mod benchmarks { c.bench_function("lifecycle", |b| { b.iter(|| { let fut = async { - let plugin = Plugin::new("test_plugin", &wasm_bytes, handler.clone()) - .await - .unwrap(); - plugin.call("ping", Value::Null).await.unwrap(); + let plugin = + Plugin::builder("test_plugin", wasm_bytes.clone(), handler.clone()) + .with_timeout(Duration::from_secs(10)) + .build() + .await + .unwrap(); + plugin.call_async("ping", Value::Null).await.unwrap(); }; rt.block_on(fut); @@ -106,7 +108,7 @@ mod benchmarks { b.iter(|| { let fut = async { plugin - .call("prime_sieve", Value::Number(1.into())) + .call_async("prime_sieve", Value::Number(1.into())) .await .unwrap(); }; @@ -127,7 +129,7 @@ mod benchmarks { b.iter(|| { let fut = async { plugin - .call("prime_sieve", Value::Number(1_000_000.into())) + .call_async("prime_sieve", Value::Number(1_000_000.into())) .await .unwrap(); }; @@ -148,7 +150,7 @@ mod benchmarks { b.iter(|| { let fut = async { plugin - .call("call_many", Value::Number(200.into())) + .call_async("call_many", Value::Number(200.into())) .await .unwrap(); }; @@ -169,7 +171,7 @@ mod benchmarks { b.iter(|| { let fut = async { plugin - .call("call_many_async", Value::Number(200.into())) + .call_async("call_many_async", Value::Number(200.into())) .await .unwrap(); }; diff --git a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs index d3919ef..e67401e 100644 --- a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs +++ b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs @@ -6,10 +6,8 @@ use std::{ }; use tracing::info; use wasm_bindgen_test::{Criterion, wasm_bindgen_bench}; -use wasmi_plugin_hdk::{ - plugin::{Plugin, PluginId}, - server::HostServer, -}; +use wasmi_plugin_hdk::{plugin::Plugin, plugin_id::PluginId, server::HostServer}; +use wasmi_plugin_pdk::transport::AsyncTransport; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -24,10 +22,11 @@ fn load_plugin_wasm() -> Vec { async fn load_plugin() -> Plugin { let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) + let plugin = Plugin::new("test_plugin", wasm_bytes, handler) + .with_timeout(Duration::from_secs(10)) + .build() .await - .unwrap() - .with_timeout(Duration::from_secs(10)); + .unwrap(); plugin } @@ -62,7 +61,10 @@ async fn bench_ping_wasm(c: &mut Criterion) { Box::pin(b.iter_future(move || { let plugin = plugin.clone(); async move { - plugin.call("ping", serde_json::Value::Null).await.unwrap(); + plugin + .call_async("ping", serde_json::Value::Null) + .await + .unwrap(); info!("ping call completed"); } })) @@ -75,7 +77,7 @@ async fn bench_ping_wasm(c: &mut Criterion) { #[wasm_bindgen_bench] async fn bench_lifecycle(c: &mut Criterion) { setup_logs(); - let wasm_bytes = Arc::new(load_plugin_wasm()); + let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); c.bench_async_function("lifecycle", |b| { @@ -85,10 +87,14 @@ async fn bench_lifecycle(c: &mut Criterion) { let wasm_bytes = wasm_bytes.clone(); let handler = handler.clone(); async move { - let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) + let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler) + .build() + .await + .unwrap(); + plugin + .call_async("ping", serde_json::Value::Null) .await .unwrap(); - plugin.call("ping", serde_json::Value::Null).await.unwrap(); } })) }) @@ -108,7 +114,7 @@ async fn bench_prime_sieve_small(c: &mut Criterion) { let plugin = plugin.clone(); async move { plugin - .call("prime_sieve", Value::Number(1.into())) + .call_async("prime_sieve", Value::Number(1.into())) .await .unwrap(); } @@ -129,7 +135,7 @@ async fn bench_prime_sieve_large(c: &mut Criterion) { let plugin = plugin.clone(); async move { plugin - .call("prime_sieve", Value::Number(1_000_000.into())) + .call_async("prime_sieve", Value::Number(1_000_000.into())) .await .unwrap(); } @@ -151,7 +157,7 @@ async fn bench_call_many(c: &mut Criterion) { async move { info!("Starting call_many benchmark iteration..."); plugin - .call("call_many", Value::Number(200.into())) + .call_async("call_many", Value::Number(200.into())) .await .unwrap(); } @@ -172,7 +178,7 @@ async fn bench_call_many_async(c: &mut Criterion) { async move { info!("Starting call_many_async benchmark iteration..."); plugin - .call("call_many_async", Value::Number(200.into())) + .call_async("call_many_async", Value::Number(200.into())) .await .unwrap(); } diff --git a/crates/wasmi-plugin-hdk/src/host_handler.rs b/crates/wasmi-plugin-hdk/src/host_handler.rs index de6160b..9f50e68 100644 --- a/crates/wasmi-plugin-hdk/src/host_handler.rs +++ b/crates/wasmi-plugin-hdk/src/host_handler.rs @@ -1,9 +1,9 @@ use serde_json::Value; use wasmi_plugin_pdk::{router::BoxFuture, rpc_message::RpcError}; -use crate::plugin::PluginId; +use crate::plugin_id::PluginId; -/// HostHandler is a wrapper around the `wasmi-pdk::RequestHandler` trait that adds +/// HostHandler is an extension of `wasmi-pdk::RequestHandler` that adds /// the plugin ID as the first arg in the `handle` method. It should be used /// by hosts that manage multiple plugins to differentiate request sources. pub trait HostHandler: Send + Sync { diff --git a/crates/wasmi-plugin-hdk/src/lib.rs b/crates/wasmi-plugin-hdk/src/lib.rs index a02d4db..c78157a 100644 --- a/crates/wasmi-plugin-hdk/src/lib.rs +++ b/crates/wasmi-plugin-hdk/src/lib.rs @@ -1,10 +1,12 @@ mod compile; pub mod host_handler; pub mod plugin; +pub mod plugin_id; pub mod runtime; pub mod server; +pub mod session; mod time; -pub mod transport; mod wasi; + #[cfg(target_arch = "wasm32")] pub mod worker_pool; diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index 01ee16f..b074585 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -1,11 +1,9 @@ -use std::{fmt::Display, sync::Arc, time::Duration}; +use std::{sync::Arc, time::Duration}; -use futures::FutureExt; -use serde::{Deserialize, Serialize}; +use futures::{AsyncBufReadExt, AsyncRead, FutureExt, future::FusedFuture, io::BufReader}; use serde_json::Value; use thiserror::Error; use tracing::info; -use uuid::Uuid; use wasmi_plugin_pdk::{ api::RequestHandler, router::BoxFuture, @@ -13,153 +11,171 @@ use wasmi_plugin_pdk::{ transport::AsyncTransport, }; -use crate::compile::Compiled; use crate::host_handler::HostHandler; use crate::runtime; use crate::runtime::Runtime; +use crate::session::{PluginSession, PluginSessionError}; use crate::time::sleep; -use crate::transport::{Transport, TransportError}; +use crate::{compile::Compiled, plugin_id::PluginId}; -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct PluginId(Uuid); - -impl Default for PluginId { - fn default() -> Self { - PluginId(Uuid::new_v4()) - } -} - -impl PluginId { - pub fn new() -> Self { - Self::default() - } -} - -impl From for PluginId { - fn from(value: u128) -> Self { - PluginId(Uuid::from_u128(value)) - } +/// Plugin is an async-capable instance of a plugin +#[derive(Clone)] +pub struct Plugin { + inner: Arc, } -impl Display for PluginId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if f.alternate() { - write!(f, "{}", self.0) // full: {:#} - } else { - let uuid_str = self.0.as_simple().to_string(); - write!(f, "{}", &uuid_str[uuid_str.len() - 6..]) // short {} - } - } +pub struct PluginBuilder { + id: PluginId, + name: String, + handler: Arc, + logger: Arc, + wasm_bytes: Vec, + timeout: Duration, } -type Logger = Box; - -/// Plugin is an async-capable instance of a plugin -pub struct Plugin { - name: String, +struct InnerPlugin { id: PluginId, handler: Arc, - logger: Logger, + logger: Arc, compiled: Compiled, timeout: Duration, - #[allow(dead_code)] - // TODO: Use fuel to terminate native plugins - max_fuel: Option, } +pub trait PluginLogger: Send + Sync + 'static { + fn log(&self, name: &str, message: &str) { + info!(target: "plugin", "[plugin] [{}] {}", name, message); + } +} + +struct DefaultPluginLogger; +impl PluginLogger for DefaultPluginLogger {} + #[derive(Debug, Error)] pub enum PluginError { #[error("Transport error")] - TransportError(#[from] TransportError), + TransportError(#[from] PluginSessionError), #[error("Bridge Error")] BridgeError(#[from] Box), #[error("Plugin timeout")] PluginTimeout, + #[error("Plugin compilation error")] + CompileError(#[from] wasmer::CompileError), } impl Plugin { - pub async fn new( - name: &str, - wasm_bytes: &[u8], + pub fn builder( + name: impl Into, + wasm_bytes: Vec, handler: Arc, - ) -> Result { - Ok(Plugin { - name: name.to_string(), + ) -> PluginBuilder { + PluginBuilder { id: PluginId::new(), + name: name.into(), handler, - logger: Box::new(default_plugin_logger), - max_fuel: None, + logger: Arc::new(DefaultPluginLogger), + wasm_bytes, timeout: Duration::from_secs(10), - compiled: Compiled::new(name, wasm_bytes).await?, + } + } +} + +impl PluginBuilder { + pub async fn build(self) -> Result { + let compiled = Compiled::new(&self.name, &self.wasm_bytes).await?; + + let inner = InnerPlugin { + id: self.id, + handler: self.handler, + logger: self.logger, + compiled, + timeout: self.timeout, + }; + + Ok(Plugin { + inner: Arc::new(inner), }) } - /// Sets the plugin ID for this instance. If no ID is provided a random - /// UUID is generated. pub fn with_id(mut self, id: PluginId) -> Self { self.id = id; self } - /// Sets a custom logger for the plugin instance. Plugins log messages to - /// stderr, which are captured and passed to this logger function. If - /// no logger is provided a default logger is used. - pub fn with_logger(mut self, logger: Logger) -> Self { - self.logger = Box::new(logger); + pub fn with_logger(mut self, logger: impl PluginLogger) -> Self { + self.logger = Arc::new(logger); self } - /// Sets a timeout duration for plugin calls. If a call takes longer than - /// this duration, it will be terminated and an error will be returned. pub fn with_timeout(mut self, timeout: Duration) -> Self { self.timeout = timeout; self } +} +impl Plugin { pub fn name(&self) -> &str { - &self.name + &self.inner.compiled.name } pub fn id(&self) -> PluginId { - self.id + self.inner.id } } impl AsyncTransport for Plugin { async fn call_async(&self, method: &str, params: Value) -> Result { - self.call(method, params).await - } -} - -impl Plugin { - pub async fn call(&self, method: &str, params: Value) -> Result { let runtime = self.create_runtime(); - let (id, stdin_writer, stdout_reader) = runtime - .spawn(self.compiled.clone()) + let (id, stdin_writer, stdout_reader, stderr_reader) = runtime + .spawn(self.inner.compiled.clone()) .await .map_err(|e| PluginError::BridgeError(e.into()))?; + //? stderr logging + let name = self.inner.compiled.name.clone(); + let logger = self.inner.logger.clone(); + let stderr_task = log_stderr(&name, stderr_reader, logger).fuse(); + + //? transport setup let handler = PluginCallback { - handler: self.handler.clone(), - uuid: self.id, + handler: self.inner.handler.clone(), + uuid: self.inner.id, }; - let transport = Transport::new(stdout_reader, stdin_writer, handler); - - let transport_task = transport.call_async(method, params).fuse(); - let timeout = sleep(self.timeout).fuse(); - futures::pin_mut!(transport_task, timeout); - - futures::select! { - res = transport_task => { - Ok(res?) - }, - _ = timeout => { - runtime.terminate(id).await; - Err(PluginError::PluginTimeout) + let session = PluginSession::new(stdout_reader, stdin_writer, handler); + let session_task = session.call_async(method, params).fuse(); + + //? timeout + let timeout = sleep(self.inner.timeout).fuse(); + + //? Pump futures until (session_task AND stderr_task) OR timeout + futures::pin_mut!(session_task, timeout, stderr_task); + + let mut rpc_result = None; + loop { + futures::select! { + res = session_task => { + rpc_result = Some(res); + if stderr_task.is_terminated() { + break; + } + } + _ = timeout => { + //? Assume the instance has hung, so terminate its runtime + runtime.terminate(id).await; + return Err(PluginError::PluginTimeout); + } + _ = stderr_task => { + if rpc_result.is_some() { + break; + } + } } } + + Ok(rpc_result.expect("Loop exited without result or timeout")?) } +} +impl Plugin { #[allow(clippy::type_complexity)] #[cfg(not(target_arch = "wasm32"))] fn create_runtime(&self) -> impl Runtime + Send + Sync + 'static { @@ -173,6 +189,8 @@ impl Plugin { } } +/// Helper struct that implements RequestHandler by forwarding it to a HostHandler +/// with the added `uuid` context. struct PluginCallback { handler: Arc, uuid: PluginId, @@ -185,6 +203,19 @@ impl RequestHandler for PluginCallback { } } -fn default_plugin_logger(name: &str, msg: &str) { - info!(target: "plugin", "[plugin] [{}] {}", name, msg); +async fn log_stderr( + name: &str, + reader: impl AsyncRead + Unpin + Send + Sync + 'static, + logger: Arc, +) { + let name = name.to_string(); + let mut reader = BufReader::new(reader); + let mut line = String::new(); + while let Ok(n) = reader.read_line(&mut line).await { + if n == 0 { + break; + } + logger.log(&name, line.trim_end()); + line.clear(); + } } diff --git a/crates/wasmi-plugin-hdk/src/plugin_id.rs b/crates/wasmi-plugin-hdk/src/plugin_id.rs new file mode 100644 index 0000000..45bd85a --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/plugin_id.rs @@ -0,0 +1,36 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PluginId(Uuid); + +impl Default for PluginId { + fn default() -> Self { + PluginId(Uuid::new_v4()) + } +} + +impl PluginId { + pub fn new() -> Self { + Self::default() + } +} + +impl From for PluginId { + fn from(value: u128) -> Self { + PluginId(Uuid::from_u128(value)) + } +} + +impl Display for PluginId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if f.alternate() { + write!(f, "{}", self.0) // full: {:#} + } else { + let uuid_str = self.0.as_simple().to_string(); + write!(f, "{}", &uuid_str[uuid_str.len() - 6..]) // short {} + } + } +} diff --git a/crates/wasmi-plugin-hdk/src/runtime/mod.rs b/crates/wasmi-plugin-hdk/src/runtime/mod.rs index 5fa385a..15690ca 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/mod.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/mod.rs @@ -8,16 +8,17 @@ mod native_runtime; #[cfg(target_arch = "wasm32")] pub use worker_runtime::WorkerRuntime; #[cfg(target_arch = "wasm32")] -mod message_writer; -#[cfg(target_arch = "wasm32")] mod worker_runtime; use crate::compile::Compiled; use futures::{AsyncRead, AsyncWrite}; +use wasmi_plugin_pdk::router::MaybeSend; pub trait Runtime { type Error: Into>; + /// Spawn a new plugin instance from the given compiled module. Returns the + /// instance ID and host-side stdio pipes. fn spawn( &self, compiled: Compiled, @@ -27,9 +28,12 @@ pub trait Runtime { u64, impl AsyncWrite + Unpin + Send + Sync + 'static, impl AsyncRead + Unpin + Send + Sync + 'static, + impl AsyncRead + Unpin + Send + Sync + 'static, ), Self::Error, >, - >; - fn terminate(self, instance_id: u64) -> impl std::future::Future; + > + MaybeSend; + + /// Forcefully terminate a hanging or otherwise failedplugin instance. + fn terminate(&self, instance_id: u64) -> impl std::future::Future + MaybeSend; } diff --git a/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs index 717f4ca..bb8dd7e 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs @@ -4,13 +4,11 @@ use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; -use futures::io::BufReader; use futures::lock::Mutex; -use futures::{AsyncBufReadExt, AsyncRead, AsyncWrite}; +use futures::{AsyncRead, AsyncWrite}; use thiserror::Error; -use tokio::spawn; use tokio::task::{JoinHandle, spawn_blocking}; -use tracing::{error, info}; +use tracing::error; use wasmer::Store; use crate::compile::Compiled; @@ -63,25 +61,24 @@ impl Runtime for NativeRuntime { u64, impl AsyncWrite + Unpin + Send + Sync + 'static, impl AsyncRead + Unpin + Send + Sync + 'static, + impl AsyncRead + Unpin + Send + Sync + 'static, ), Self::Error, > { let id = self.next_id.fetch_add(1, Ordering::Relaxed); - let name = compiled.name.clone(); let (stdin_reader, stdin_writer) = non_blocking_pipe(); let (stdout_reader, stdout_writer) = non_blocking_pipe(); let (stderr_reader, stderr_writer) = non_blocking_pipe(); let wasm_handle = wasm_handle(compiled, stdin_reader, stdout_writer, stderr_writer); - spawn_stderr(name, stderr_reader); let session = NativeSession { wasm_handle }; self.sessions.lock().await.insert(id, session); - Ok((id, stdin_writer, stdout_reader)) + Ok((id, stdin_writer, stdout_reader, stderr_reader)) } - async fn terminate(self, instance_id: u64) -> () { + async fn terminate(&self, instance_id: u64) -> () { if let Some(session) = self.sessions.lock().await.remove(&instance_id) { // TODO: Use gas metering to force terminate the wasm runtime session.wasm_handle.abort(); @@ -98,37 +95,11 @@ fn wasm_handle( spawn_blocking(move || { let result = run_instance(compiled, stdin_reader, stdout_writer, stderr_writer); if let Err(e) = result { - eprintln!("Plugin execution error: {:?}", e); + error!("Plugin execution error: {:?}", e); } }) } -fn spawn_stderr(name: String, mut stderr_reader: NonBlockingPipeReader) { - spawn(async move { - let mut stderr = BufReader::new(&mut stderr_reader); - let mut buffer = String::new(); - loop { - buffer.clear(); - match stderr.read_line(&mut buffer).await { - Ok(0) => { - break; - } - Ok(_) => { - info!("[plugin] [{}] {}", name, buffer.trim_end()); - } - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - tokio::task::yield_now().await; - continue; - } - Err(e) => { - error!("[WorkerBridge] Error reading from stderr: {}", e); - break; - } - } - } - }); -} - fn run_instance( compiled: Compiled, stdin: NonBlockingPipeReader, diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index cb6d341..c8e476c 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -4,18 +4,18 @@ use std::collections::HashMap; use std::io::Write; use std::sync::Arc; -use futures::io::BufReader; use futures::lock::Mutex; -use futures::{AsyncBufReadExt, AsyncRead, AsyncWrite}; +use futures::{AsyncRead, AsyncWrite}; use thiserror::Error; -use tracing::{error, info}; +use tracing::error; use wasm_bindgen::JsCast; -use wasm_bindgen_futures::spawn_local; use web_sys::js_sys::WebAssembly; use crate::compile::Compiled; use crate::runtime::Runtime; -use crate::runtime::non_blocking_pipe::non_blocking_pipe; +use crate::runtime::non_blocking_pipe::{ + NonBlockingPipeReader, NonBlockingPipeWriter, non_blocking_pipe, +}; use crate::wasi::wasi_ctx::{self, WasiReader}; use crate::worker_pool::pool::{SpawnError, WorkerHandle, WorkerId, WorkerPool}; @@ -58,39 +58,23 @@ impl Runtime for WorkerRuntime { u64, impl AsyncWrite + Unpin + Send + Sync + 'static, impl AsyncRead + Unpin + Send + Sync + 'static, + impl AsyncRead + Unpin + Send + Sync + 'static, ), Self::Error, > { let (stdin_reader, stdin_writer) = non_blocking_pipe(); let (stdout_reader, stdout_writer) = non_blocking_pipe(); let (stderr_reader, stderr_writer) = non_blocking_pipe(); - // let stderr_writer: MessageWriter = MessageWriter::new("".into()); - - handle_stderr(&compiled.name, stderr_reader); - - let module = compiled.js_module; - let pool = WorkerPool::global(); - let handle = pool.run_with(&module, move |extra| async move { - let res = run_instance( - extra, - &compiled.wasm_bytes, - stdin_reader, - stdout_writer, - stderr_writer, - ) - .await; - if let Err(e) = res { - error!("Error running Wasm instance: {}", e); - } - })?; + + let handle = wasm_handle(compiled, stdin_reader, stdout_writer, stderr_writer)?; let id = handle.id; - self.active_sessions.lock().await.insert(handle.id, handle); + self.active_sessions.lock().await.insert(id, handle); - Ok((id, stdin_writer, stdout_reader)) + Ok((id, stdin_writer, stdout_reader, stderr_reader)) } - async fn terminate(self, instance_id: u64) { + async fn terminate(&self, instance_id: u64) { let instance_id = instance_id as WorkerId; if let Some(session) = self.active_sessions.lock().await.remove(&instance_id) { session.terminate(); @@ -98,6 +82,29 @@ impl Runtime for WorkerRuntime { } } +fn wasm_handle( + compiled: Compiled, + stdin_reader: NonBlockingPipeReader, + stdout_writer: NonBlockingPipeWriter, + stderr_writer: NonBlockingPipeWriter, +) -> Result, SpawnError> { + let pool = WorkerPool::global(); + let module = compiled.js_module; + pool.run_with(&module, move |extra| async move { + let res = run_instance( + extra, + &compiled.wasm_bytes, + stdin_reader, + stdout_writer, + stderr_writer, + ) + .await; + if let Err(e) = res { + error!("Error running Wasm instance: {}", e); + } + }) +} + async fn run_instance( js_module: wasm_bindgen::JsValue, wasm_bytes: &[u8], @@ -105,20 +112,11 @@ async fn run_instance( stdout: impl Write + Send + Sync + 'static, stderr: impl Write + Send + Sync + 'static, ) -> Result<(), RunError> { - info!("Starting Wasm instance in WorkerRuntime"); let mut store = wasmer::Store::default(); - info!("js_module type: {:?}", js_module.js_typeof()); - info!( - "is WebAssembly.Module: {:?}", - js_module.is_instance_of::() - ); - info!("Casting Wasm module"); let js_module: WebAssembly::Module = js_module.dyn_into().unwrap(); - info!("Creating Wasm module"); let module = (js_module, wasm_bytes).into(); - info!("Creating WASI context"); let wasi_ctx = wasi_ctx::WasiCtx::new() .set_stdin(stdin) .set_stdout(stdout) @@ -129,25 +127,3 @@ async fn run_instance( Ok(()) } - -fn handle_stderr(name: &str, stderr: impl AsyncRead + Unpin + Send + Sync + 'static) { - let name = name.to_string(); - spawn_local(async move { - let mut reader = BufReader::new(stderr); - let mut line = String::new(); - let name = name; - loop { - match reader.read_line(&mut line).await { - Ok(0) => break, // EOF - Ok(_) => { - info!("[{}] {}", name, line.trim_end()); - line.clear(); - } - Err(e) => { - error!("Error reading Wasm stderr: {}", e); - break; - } - } - } - }); -} diff --git a/crates/wasmi-plugin-hdk/src/server.rs b/crates/wasmi-plugin-hdk/src/server.rs index d4a85fb..e68fae1 100644 --- a/crates/wasmi-plugin-hdk/src/server.rs +++ b/crates/wasmi-plugin-hdk/src/server.rs @@ -5,7 +5,7 @@ use wasmi_plugin_pdk::{ rpc_message::RpcError, }; -use crate::{host_handler::HostHandler, plugin::PluginId}; +use crate::{host_handler::HostHandler, plugin_id::PluginId}; pub struct HostServer { state: S, diff --git a/crates/wasmi-plugin-hdk/src/transport.rs b/crates/wasmi-plugin-hdk/src/session.rs similarity index 69% rename from crates/wasmi-plugin-hdk/src/transport.rs rename to crates/wasmi-plugin-hdk/src/session.rs index f6bfb42..a9a5390 100644 --- a/crates/wasmi-plugin-hdk/src/transport.rs +++ b/crates/wasmi-plugin-hdk/src/session.rs @@ -1,92 +1,92 @@ use futures::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, io::BufReader}; use serde_json::Value; use thiserror::Error; -use tracing::{error, info, warn}; +use tracing::{error, warn}; use wasmi_plugin_pdk::{ api::RequestHandler, - rpc_message::{RpcError, RpcErrorResponse, RpcMessage, RpcResponse}, + rpc_message::{RpcError, RpcMessage, RpcResponse}, }; use wasmi_plugin_rt::yield_now; -pub struct Transport { +/// A session for communicating with oneshot plugin over an async transport. +pub struct PluginSession { reader: BufReader, writer: W, handler: H, } #[derive(Debug, Error)] -pub enum TransportError { +pub enum PluginSessionError { #[error("I/O error: {0}")] Io(#[from] std::io::Error), - #[error("Serde error: {0}, data: {1}")] Serde(serde_json::Error, String), - #[error("EOF")] Eof, - - #[error("Error Response: {0:?}")] - ErrorResponse(RpcErrorResponse), + #[error(transparent)] + Rpc(RpcError), } -impl> Transport { +impl> + PluginSession +{ + /// Create a new PluginSession with the given reader, writer, and request handler. pub fn new(reader: R, writer: W, handler: H) -> Self { - Transport { + PluginSession { reader: BufReader::new(reader), writer, handler, } } + /// Calls a method asynchronously on the plugin and waits for the response. pub async fn call_async( mut self, method: &str, params: Value, - ) -> Result { + ) -> Result { let id = 1; let request = RpcMessage::request(id, method.to_string(), params); - info!("Sending request: {:?}", request); self.write_message(request).await?; let mut line = String::new(); loop { + line.clear(); match self.reader.read_line(&mut line).await { Ok(0) => { warn!("EOF reached while reading from transport"); - return Err(TransportError::Eof); + return Err(PluginSessionError::Eof); } Ok(_) => {} Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { yield_now().await; continue; } - Err(e) => return Err(TransportError::Io(e)), + Err(e) => return Err(PluginSessionError::Io(e)), } // Process complete lines - let msg: RpcMessage = - serde_json::from_str(&line).map_err(|e| TransportError::Serde(e, line.clone()))?; - line.clear(); + let msg: RpcMessage = serde_json::from_str(&line) + .map_err(|e| PluginSessionError::Serde(e, line.clone()))?; match msg { RpcMessage::RpcResponse(resp) if resp.id == id => return Ok(resp), RpcMessage::RpcErrorResponse(err) if err.id == id => { - return Err(TransportError::ErrorResponse(err)); + return Err(PluginSessionError::Rpc(err.error)); } RpcMessage::RpcRequest(req) => { - self.handle_guest_req(req.id, &req.method, req.params) - .await?; + self.handle_request(req.id, &req.method, req.params).await?; } _ => {} } } } - async fn write_message(&mut self, msg: RpcMessage) -> Result<(), TransportError> { + async fn write_message(&mut self, msg: RpcMessage) -> Result<(), PluginSessionError> { let msg: String = serde_json::to_string(&msg).map_err(|e| { error!("Serde error while serializing message: {}", e); - TransportError::Serde(e, "".to_string()) + PluginSessionError::Serde(e, "".to_string()) })?; let msg = format!("{}\n", msg); self.writer.write_all(msg.as_bytes()).await?; @@ -95,12 +95,12 @@ impl> T Ok(()) } - async fn handle_guest_req( + async fn handle_request( &mut self, id: u64, method: &str, params: Value, - ) -> Result<(), TransportError> { + ) -> Result<(), PluginSessionError> { match self.handler.handle(method, params).await { Ok(result) => { self.write_message(RpcMessage::response(id, result)).await?; diff --git a/crates/wasmi-plugin-hdk/tests/integration.rs b/crates/wasmi-plugin-hdk/tests/integration.rs index ff707c9..dcb8883 100644 --- a/crates/wasmi-plugin-hdk/tests/integration.rs +++ b/crates/wasmi-plugin-hdk/tests/integration.rs @@ -5,6 +5,7 @@ use std::{ }; use tracing::info; use wasmi_plugin_hdk::{plugin::Plugin, server::HostServer}; +use wasmi_plugin_pdk::transport::AsyncTransport; #[cfg(target_family = "wasm")] use wasm_bindgen_test::*; @@ -23,10 +24,11 @@ fn load_plugin_wasm() -> Vec { async fn load_plugin() -> Plugin { let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) + let plugin = Plugin::builder("test_plugin", wasm_bytes, handler) + .with_timeout(Duration::from_secs(5)) + .build() .await - .unwrap() - .with_timeout(Duration::from_secs(5)); + .unwrap(); plugin } @@ -74,11 +76,12 @@ async fn test_plugin() { let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) + let plugin = Plugin::builder("test_plugin", wasm_bytes.clone(), handler) + .with_timeout(Duration::from_secs(2)) + .build() .await - .unwrap() - .with_timeout(Duration::from_secs(2)); - let resp = plugin.call("ping", Value::Null).await.unwrap(); + .unwrap(); + let resp = plugin.call_async("ping", Value::Null).await.unwrap(); assert_eq!(resp.result.as_str().unwrap(), "pong"); } @@ -90,7 +93,10 @@ async fn test_get_random_number() { info!("Starting get_random_number test..."); let plugin = load_plugin().await; - let response = plugin.call("get_random_number", Value::Null).await.unwrap(); + let response = plugin + .call_async("get_random_number", Value::Null) + .await + .unwrap(); info!("Random number response: {:?}", response); response.result.as_u64().unwrap(); @@ -103,7 +109,7 @@ async fn test_get_time() { info!("Starting get_time test..."); let plugin = load_plugin().await; - let response = plugin.call("get_time", Value::Null).await.unwrap(); + let response = plugin.call_async("get_time", Value::Null).await.unwrap(); info!("Get time response: {:?}", response); @@ -124,17 +130,12 @@ async fn test_sleep() { setup_logs(); info!("Starting sleep test..."); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - - let plugin = Plugin::new("test_plugin", &wasm_bytes, handler) - .await - .unwrap(); + let plugin = load_plugin().await; let sleep_duration = 1500; // milliseconds let start = web_time::Instant::now(); plugin - .call("sleep", Value::Number(sleep_duration.into())) + .call_async("sleep", Value::Number(sleep_duration.into())) .await .unwrap(); let elapsed = start.elapsed().as_millis(); @@ -153,7 +154,7 @@ async fn test_call() { info!("Starting call test..."); let plugin = load_plugin().await; - let resp = plugin.call("call", Value::Null).await.unwrap(); + let resp = plugin.call_async("call", Value::Null).await.unwrap(); assert_eq!(resp.result.as_str().unwrap(), "pong"); } @@ -166,7 +167,7 @@ async fn test_call_many() { let plugin = load_plugin().await; plugin - .call("call_many", Value::Number(500.into())) + .call_async("call_many", Value::Number(500.into())) .await .unwrap(); } @@ -178,7 +179,7 @@ async fn test_call_async() { info!("Starting call_async test..."); let plugin = load_plugin().await; - let resp = plugin.call("call_async", Value::Null).await.unwrap(); + let resp = plugin.call_async("call_async", Value::Null).await.unwrap(); assert_eq!(resp.result.as_str().unwrap(), "pong"); } @@ -190,7 +191,7 @@ async fn test_call_many_async() { let plugin = load_plugin().await; plugin - .call("call_many_async", Value::Number(500.into())) + .call_async("call_many_async", Value::Number(500.into())) .await .unwrap(); } @@ -203,7 +204,7 @@ async fn test_prime_sieve() { let plugin = load_plugin().await; let response = plugin - .call("prime_sieve", Value::Number(1000.into())) + .call_async("prime_sieve", Value::Number(1000.into())) .await .unwrap(); diff --git a/crates/wasmi-plugin-pdk/src/api.rs b/crates/wasmi-plugin-pdk/src/api.rs index ebc5144..f13b530 100644 --- a/crates/wasmi-plugin-pdk/src/api.rs +++ b/crates/wasmi-plugin-pdk/src/api.rs @@ -1,9 +1,6 @@ use serde_json::Value; -use crate::{router::BoxFuture, rpc_message::RpcError}; - -pub trait ApiError: From + Send + Sync {} -impl ApiError for T where T: From + Send + Sync {} +use crate::router::BoxFuture; /// JSON-RPC request handler. /// diff --git a/crates/wasmi-plugin-pdk/src/lib.rs b/crates/wasmi-plugin-pdk/src/lib.rs index b138bb9..16a8eef 100644 --- a/crates/wasmi-plugin-pdk/src/lib.rs +++ b/crates/wasmi-plugin-pdk/src/lib.rs @@ -2,5 +2,5 @@ pub mod api; mod poll_oneoff; pub mod router; pub mod rpc_message; -pub mod server; +pub mod runner; pub mod transport; diff --git a/crates/wasmi-plugin-pdk/src/router.rs b/crates/wasmi-plugin-pdk/src/router.rs index 6a66048..805f0f6 100644 --- a/crates/wasmi-plugin-pdk/src/router.rs +++ b/crates/wasmi-plugin-pdk/src/router.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use serde::{Serialize, de::DeserializeOwned}; use serde_json::Value; -use tracing::warn; use crate::rpc_message::RpcError; @@ -24,6 +23,7 @@ impl MaybeSend for T {} type HandlerFn = dyn Send + Sync + Fn(S, Value) -> BoxFuture<'static, Result>; +/// A router for JSON-RPC methods, dispatching calls to registered handler functions pub struct Router { handlers: HashMap>>, } @@ -42,7 +42,7 @@ impl Router { } /// Register a new RPC method with the router. The method is identified by the - /// given name, and the handler function should accept the shared state and + /// given name, and the handler function should accept the shared state `S` and /// deserialized params. /// /// Handlers should implement: `async fn handler(state: S, params: P) -> Result` @@ -74,6 +74,9 @@ impl Router { self } + /// Handle a request by dispatching to the appropriate registered handler. + /// + /// If no handler is found for the method, returns `Err(MethodNotFound)`. pub async fn handle_with_state( &self, state: S, @@ -81,7 +84,6 @@ impl Router { params: Value, ) -> Result { let Some(handler) = self.handlers.get(method) else { - warn!("Method not found: {}", method); return Err(RpcError::MethodNotFound); }; handler(state, params).await diff --git a/crates/wasmi-plugin-pdk/src/rpc_message.rs b/crates/wasmi-plugin-pdk/src/rpc_message.rs index a894128..4ac877a 100644 --- a/crates/wasmi-plugin-pdk/src/rpc_message.rs +++ b/crates/wasmi-plugin-pdk/src/rpc_message.rs @@ -32,22 +32,29 @@ pub struct RpcErrorResponse { pub error: RpcError, } -#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +#[derive(Error, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub enum RpcError { #[error("Parse error")] ParseError, - #[error("Invalid request")] - InvalidRequest, #[error("Method not found")] MethodNotFound, #[error("Invalid params")] InvalidParams, - #[error("Internal error")] - InternalError, + /// Custom error with message. Messages are unstable and inconsistent across + /// different plugins and versions. They should NEVER be relied upon for + /// program logic. + /// + /// TODO: Consider poisoning equality checks on this variant to prevent + /// accidental reliance on error message contents. #[error("{0}")] Custom(String), } +pub trait RpcErrorContext { + fn context(self, msg: &str) -> Result; +} + impl RpcMessage { pub fn request(id: u64, method: String, params: Value) -> Self { RpcMessage::RpcRequest(RpcRequest { @@ -76,22 +83,7 @@ impl RpcMessage { } impl RpcError { - pub fn code(&self) -> i64 { - match self { - RpcError::ParseError => -32700, - RpcError::InvalidRequest => -32600, - RpcError::MethodNotFound => -32601, - RpcError::InvalidParams => -32602, - RpcError::InternalError => -32603, - RpcError::Custom(_) => -32000, - } - } - - pub fn message(&self) -> String { - self.to_string() - } - - pub fn custom(msg: impl Into) -> Self { + pub fn custom>(msg: S) -> Self { RpcError::Custom(msg.into()) } } @@ -102,43 +94,32 @@ impl From for RpcError { } } -pub fn to_rpc_err(e: E) -> RpcError { - RpcError::Custom(e.to_string()) +impl From for RpcError { + fn from(s: String) -> Self { + RpcError::Custom(s) + } +} + +impl From<&str> for RpcError { + fn from(s: &str) -> Self { + RpcError::Custom(s.to_string()) + } } -impl Serialize for RpcError { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let mut state = serializer.serialize_struct("RpcError", 2)?; - state.serialize_field("code", &self.code())?; - state.serialize_field("message", &self.message())?; - state.end() +impl From> for RpcError { + fn from(e: Box) -> Self { + RpcError::Custom(e.to_string()) } } -impl<'de> Deserialize<'de> for RpcError { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - struct RpcErrorData { - code: i64, - message: String, - } - - let data = RpcErrorData::deserialize(deserializer)?; - - Ok(match data.code { - -32700 => RpcError::ParseError, - -32600 => RpcError::InvalidRequest, - -32601 => RpcError::MethodNotFound, - -32602 => RpcError::InvalidParams, - -32603 => RpcError::InternalError, - _ => RpcError::Custom(data.message), - }) +impl RpcErrorContext for Result { + fn context(self, msg: &str) -> Result { + self.map_err(|e| RpcError::Custom(format!("{}: {}", msg, e))) + } +} + +impl RpcErrorContext for Option { + fn context(self, msg: &str) -> Result { + self.ok_or_else(|| RpcError::Custom(msg.to_string())) } } diff --git a/crates/wasmi-plugin-pdk/src/server.rs b/crates/wasmi-plugin-pdk/src/runner.rs similarity index 84% rename from crates/wasmi-plugin-pdk/src/server.rs rename to crates/wasmi-plugin-pdk/src/runner.rs index 9be53d1..03fe602 100644 --- a/crates/wasmi-plugin-pdk/src/server.rs +++ b/crates/wasmi-plugin-pdk/src/runner.rs @@ -11,9 +11,10 @@ use crate::{ transport::Transport, }; -/// Server is a RPC server that can handle requests by dispatching them to registered -/// handler functions. It stores a shared state `S` that is passed into each handler. -pub struct PluginServer { +/// A one-shot executor for plugins. Reads the host request from stdin, +/// dispatches it to the appropriate handler, and writes the response +/// to stdout. +pub struct PluginRunner { router: Router, } @@ -28,7 +29,7 @@ enum PluginServerError { #[error("IO error: {0}")] Io(#[from] std::io::Error), - #[error("RPC error: {0}")] + #[error(transparent)] Rpc(#[from] RpcError), #[error("Transport error: {0}")] @@ -36,12 +37,14 @@ enum PluginServerError { } #[allow(clippy::new_without_default)] -impl PluginServer { +impl PluginRunner { pub fn new() -> Self { let router = Router::new(); Self { router } } + /// Register a RPC method handler with the runner. The handler function + /// will be invoked if the request matches the given method name. pub fn with_method(mut self, name: &str, func: F) -> Self where P: DeserializeOwned + 'static, @@ -53,6 +56,8 @@ impl PluginServer { self } + /// Run the plugin, blocking the current thread until the incoming + /// request has been handled. pub fn run(self) { match self.try_run() { Ok(()) => {} @@ -68,7 +73,7 @@ impl PluginServer { let request = self.read_request()?; info!("PluginServer: Received request: {:?}", request); - let (transport, mut driver) = Transport::new(std::io::stdin(), std::io::stdout()); + let (transport, driver) = Transport::new(std::io::stdin(), std::io::stdout()); let resp = futures::executor::block_on(async { select! { res = self.router.handle_with_state(transport, &request.method, request.params).fuse() => { diff --git a/crates/wasmi-plugin-pdk/src/transport.rs b/crates/wasmi-plugin-pdk/src/transport.rs index c1e1cb2..533d3ab 100644 --- a/crates/wasmi-plugin-pdk/src/transport.rs +++ b/crates/wasmi-plugin-pdk/src/transport.rs @@ -61,7 +61,7 @@ pub enum TransportError { #[error("EOF")] Eof, - #[error("RPC Error: {0:?}")] + #[error(transparent)] RpcError(#[from] RpcError), } @@ -129,6 +129,11 @@ impl TransportInner { } } + /// Synchronously call a method, blocking until the response is received. + /// + /// This method takes over the current thread to drive IO until the response + /// is received. If responses to other requests (those made asynchronously) arrive + /// in the meantime, they will be processed and queued for delivery. fn call(&self, method: &str, params: Value) -> Result { let id = self.next_id(); let request = RpcMessage::request(id, method.to_string(), params); @@ -137,10 +142,11 @@ impl TransportInner { self.pending.lock().unwrap().insert(id, res_tx); self.write_message(&request)?; + //? Lock reader since we'll be driving IO blockingly let mut reader = self.reader.lock().unwrap(); let mut line = String::new(); - // Drive IO until we get the response + // Drive IO until we get our response loop { let would_block = self.step(&mut reader, &mut line)?; @@ -161,6 +167,11 @@ impl TransportInner { } } + /// Asynchronously call a method, returning a future that resolves + /// to the response. + /// + /// Multiple concurrent async calls can be made, and responses will be + /// matched to requests by ID. async fn call_async(&self, method: &str, params: Value) -> Result { let id = self.next_id(); let request = RpcMessage::request(id, method.to_string(), params); @@ -175,8 +186,8 @@ impl TransportInner { Ok(res) } - /// Perform a single read step, processing one incoming message if available. - /// Returns Ok(true) if no message was available (WouldBlock) or Ok(false) + /// Perform a single read, processing one incoming message if available. + /// Returns `Ok(true)` if no message was available (WouldBlock) or `Ok(false)` /// if a message was processed. fn step( &self, @@ -197,7 +208,7 @@ impl TransportInner { Ok(false) } - // TODO: Catch WouldBlock and retry later + // TODO: Catch WouldBlock pub fn write_message(&self, message: &RpcMessage) -> Result<(), TransportError> { let message_str = serde_json::to_string(message)?; let msg = format!("{}\n", message_str); @@ -234,8 +245,12 @@ impl TransportInner { .send(Err(res)) .map_err(|_| TransportError::OneshotSend)?; } - RpcMessage::RpcRequest(_) => { + RpcMessage::RpcRequest(req) => { info!("Ignoring request message"); + self.write_message(&RpcMessage::error_response( + req.id, + RpcError::custom("Cannot handle requests"), + ))?; } } From b0f845c55fc9515bcbc8a3365a7744a596736f8e Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sun, 28 Dec 2025 19:38:24 -0500 Subject: [PATCH 45/70] tidy --- crates/wasmi-plugin-hdk/src/plugin.rs | 16 +++++--- crates/wasmi-plugin-pdk/src/rpc_message.rs | 43 +++++++++++++++++++--- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index b074585..1d96a54 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -52,16 +52,22 @@ impl PluginLogger for DefaultPluginLogger {} #[derive(Debug, Error)] pub enum PluginError { - #[error("Transport error")] - TransportError(#[from] PluginSessionError), - #[error("Bridge Error")] - BridgeError(#[from] Box), + #[error("Session error")] + SessionError(#[from] PluginSessionError), + #[error("Runtime Error")] + RuntimeError(#[from] Box), #[error("Plugin timeout")] PluginTimeout, #[error("Plugin compilation error")] CompileError(#[from] wasmer::CompileError), } +impl From for RpcError { + fn from(value: PluginError) -> Self { + RpcError::custom(value.to_string()) + } +} + impl Plugin { pub fn builder( name: impl Into, @@ -128,7 +134,7 @@ impl AsyncTransport for Plugin { let (id, stdin_writer, stdout_reader, stderr_reader) = runtime .spawn(self.inner.compiled.clone()) .await - .map_err(|e| PluginError::BridgeError(e.into()))?; + .map_err(|e| PluginError::RuntimeError(e.into()))?; //? stderr logging let name = self.inner.compiled.name.clone(); diff --git a/crates/wasmi-plugin-pdk/src/rpc_message.rs b/crates/wasmi-plugin-pdk/src/rpc_message.rs index 4ac877a..ed961af 100644 --- a/crates/wasmi-plugin-pdk/src/rpc_message.rs +++ b/crates/wasmi-plugin-pdk/src/rpc_message.rs @@ -52,7 +52,15 @@ pub enum RpcError { } pub trait RpcErrorContext { - fn context(self, msg: &str) -> Result; + fn context(self, msg: impl Into) -> Result; + fn with_context(self, f: F) -> Result + where + F: FnOnce() -> S, + S: Into; +} + +pub trait ToRpcResult { + fn rpc_err(self) -> Result; } impl RpcMessage { @@ -113,13 +121,38 @@ impl From> for RpcError { } impl RpcErrorContext for Result { - fn context(self, msg: &str) -> Result { - self.map_err(|e| RpcError::Custom(format!("{}: {}", msg, e))) + fn context(self, msg: impl Into) -> Result { + self.map_err(|e| RpcError::Custom(format!("{}: {}", msg.into(), e))) + } + + fn with_context(self, f: F) -> Result + where + F: FnOnce() -> S, + S: Into, + { + self.map_err(|e| RpcError::Custom(format!("{}: {}", f().into(), e))) } } impl RpcErrorContext for Option { - fn context(self, msg: &str) -> Result { - self.ok_or_else(|| RpcError::Custom(msg.to_string())) + fn context(self, msg: impl Into) -> Result { + self.ok_or_else(|| RpcError::Custom(msg.into())) + } + + fn with_context(self, f: F) -> Result + where + F: FnOnce() -> S, + S: Into, + { + self.ok_or_else(|| RpcError::Custom(f().into())) + } +} + +impl ToRpcResult for Result +where + E: std::error::Error + Send + Sync + 'static, +{ + fn rpc_err(self) -> Result { + self.map_err(|e| RpcError::from(Box::new(e) as Box)) } } From e80739f23a36cb5ed39cb0eba737c705a6e3152d Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sun, 28 Dec 2025 22:23:48 -0500 Subject: [PATCH 46/70] style: shell.nix --- shell.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell.nix b/shell.nix index 1905a90..877edc6 100644 --- a/shell.nix +++ b/shell.nix @@ -15,7 +15,7 @@ in pkgs.mkShell { packages = with pkgs; [ rustToolchain -rust-analyzer + rust-analyzer cargo-sort cargo-machete samply From 153797998b34e49413499e88aaead0a7da61039e Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Mon, 29 Dec 2025 13:36:03 -0500 Subject: [PATCH 47/70] wip --- Cargo.lock | 8 ++++++++ Cargo.toml | 1 + crates/wasmi-plugin-coordinator/Cargo.toml | 21 ++++++++++++++++++++ crates/wasmi-plugin-coordinator/README.md | 23 ++++++++++++++++++++++ crates/wasmi-plugin-coordinator/src/lib.rs | 0 5 files changed, 53 insertions(+) create mode 100644 crates/wasmi-plugin-coordinator/Cargo.toml create mode 100644 crates/wasmi-plugin-coordinator/README.md create mode 100644 crates/wasmi-plugin-coordinator/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 69fa4d4..73b999b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2350,6 +2350,14 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "wasmi-plugin-coordinator" +version = "0.1.0" +dependencies = [ + "gloo-timers", + "tokio", +] + [[package]] name = "wasmi-plugin-hdk" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index dd85806..3e99324 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/wasmi-plugin-hdk", "crates/wasmi-plugin-pdk", "crates/wasmi-plugin-rt", + "crates/wasmi-plugin-coordinator" ] resolver = "2" diff --git a/crates/wasmi-plugin-coordinator/Cargo.toml b/crates/wasmi-plugin-coordinator/Cargo.toml new file mode 100644 index 0000000..8a66269 --- /dev/null +++ b/crates/wasmi-plugin-coordinator/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "wasmi-plugin-coordinator" +version.workspace = true +rust-version.workspace = true +description = "Atomics-enabled coordinator for compute web workers" +authors.workspace = true +repository.workspace = true +edition.workspace = true +readme.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true +exclude.workspace = true + +[dependencies] + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +gloo-timers = { workspace = true } + +[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] +tokio = { workspace = true } diff --git a/crates/wasmi-plugin-coordinator/README.md b/crates/wasmi-plugin-coordinator/README.md new file mode 100644 index 0000000..cb34be6 --- /dev/null +++ b/crates/wasmi-plugin-coordinator/README.md @@ -0,0 +1,23 @@ + + +## Definitions +- Main Thread: The main JavaScript thread that runs the UI. +- Coordinator: Compute Worker coordinator that manages multiple compute workers, dispatching tasks and acting as a bridge between the main thread and compute workers. +- Compute Worker: Web workers that can run blocking computations without freezing the UI. + +## Notes for the future Robert: +1. Use `worker.post_message` and `set_onmessage` to communicate between the main thread and compute workers directly: + +Can't do this because `wasmer`'s `function::call` function is sync and blocking. That's fine for nearly everything except `fd_read`. For `fd_read` we need to recieve a message from the thread running the `host` so we can either fetch data or call another plugin or whatnot. However, since receiving messages is handled via the `set_onmessage` callback, we would need to yield to the JS event loop for it to tick. Since we can't yield from a sync function, we can't receive messages while in a blocking call, and we can't implement `fd_read`. + +1.1. Use JSPI to fix this + +Good try, but no luck. While [JSPI](https://www.dgendill.com/posts/technology/2025-10-01-web-assembly-javascript-promise-integration.html) would allow us to forcefully yield to the JS event loop from within a sync function, it isn't supported by `wasm-bindgen` yet. You may think we could use raw linking to call JSPI directly, but we can't: + - `wasmer` requires certain `wasm-bindgen` imports, meaning we need to include those when spawning our worker. + - `wasm-bindgen` does not support JSPI imports yet. + - `wasm-bindgen` also doesn't support adding custom raw imports *at all*. + - And finally, you can't wrap a `WebAssembly.Suspending` function around or within a `wasm-bindgen` function because + - `wasm-bindgen` can't handle `WebAssembly.Suspending` functions if you try to import them directly. + - `WebAssembly.Suspending` functions can't be called from nested functions (IE if you wrapped it in a `wasm-bindgen` function). The function imported into the wasm MUST BE the `WebAssembly.Suspending` instance itself, not a wrapper that constructs or creates one. + +1. Use SharedArrayBuffers for shared memory between the main thread and compute workers, without needing full atomics or a coordinator diff --git a/crates/wasmi-plugin-coordinator/src/lib.rs b/crates/wasmi-plugin-coordinator/src/lib.rs new file mode 100644 index 0000000..e69de29 From e10ae8b9f851838c6d3694712b8d1fa37f2e4d3e Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Mon, 29 Dec 2025 17:27:01 -0500 Subject: [PATCH 48/70] docs --- crates/wasmi-plugin-coordinator/README.md | 57 ++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/crates/wasmi-plugin-coordinator/README.md b/crates/wasmi-plugin-coordinator/README.md index cb34be6..f38659c 100644 --- a/crates/wasmi-plugin-coordinator/README.md +++ b/crates/wasmi-plugin-coordinator/README.md @@ -12,6 +12,9 @@ Can't do this because `wasmer`'s `function::call` function is sync and blocking. 1.1. Use JSPI to fix this +NOTE: Based on the asyncify tests, this may not be feisible anyways due to performance. No clue whether JSPI would be faster or slower than asyncify, but because +it's doing the same stuff it'd probably be order-of-magnitude equal. + Good try, but no luck. While [JSPI](https://www.dgendill.com/posts/technology/2025-10-01-web-assembly-javascript-promise-integration.html) would allow us to forcefully yield to the JS event loop from within a sync function, it isn't supported by `wasm-bindgen` yet. You may think we could use raw linking to call JSPI directly, but we can't: - `wasmer` requires certain `wasm-bindgen` imports, meaning we need to include those when spawning our worker. - `wasm-bindgen` does not support JSPI imports yet. @@ -19,5 +22,57 @@ Good try, but no luck. While [JSPI](https://www.dgendill.com/posts/technology/2 - And finally, you can't wrap a `WebAssembly.Suspending` function around or within a `wasm-bindgen` function because - `wasm-bindgen` can't handle `WebAssembly.Suspending` functions if you try to import them directly. - `WebAssembly.Suspending` functions can't be called from nested functions (IE if you wrapped it in a `wasm-bindgen` function). The function imported into the wasm MUST BE the `WebAssembly.Suspending` instance itself, not a wrapper that constructs or creates one. + - You could *possibly* manually inject the suspending function into wasm-bindgen's imports manually using internal functions, but that would be very very fragile. + +Tracking issue: https://github.com/wasm-bindgen/wasm-bindgen/issues/3633 + +2. Use SharedArrayBuffers for shared memory between the main thread and compute workers, without needing full atomics or a coordinator + +I tried this for literally 20 hours and could never fully get rid of race conditions. I fully believe it's possibly, I just also fully believe that I'm not a clever enough programmer / familiar enough with web-based concurrency to pull it off. + +The main issue was that I could never get the read / write IDXs for the ring buffers to be perfectly in sync between the main and compute worker thread. Inevitably something would always get out of sync and both threads would deadlock waiting for a message from the other. + +3. Use atomics + shared memory in the main thread directly to communicate with compute workers. + +This nearly works if it weren't for the UI. The main thread is perfectly capable of using shared memory to talk with web workers and, by passing raw function ptrs between threads, you can get an interface that basically feels like std::thread::spawn. However, the main thread is also responsible for running UI and no matter how much I've tried I can't get dioxus compiling with atomics + shared memory enabled. + +Tracking issue: https://github.com/DioxusLabs/dioxus/issues/5079 + +4. Asyncify the wasi to alloy yielding to the caller when required, who can then yield to the event loop as needed. + +https://kripken.github.io/blog/wasm/2019/07/16/asyncify.html + +Asyncify SHOULD work, it's just too slow. For anything that does a lot of host calls, the overhead of unwinding / rewinding the stack is too high. Below are some benchmark results from a test I performed where I: + - Added asyncify. + - For all `fd_read` calls, if they would block, unwind the stack and return to the host. + +Otherwise it was running in the same shared memory environment as before. stdio used the NonBlockingPipe, not worker messages. This was purely measuring the overhead of asyncify + unwinding / rewinding + yielding to JS on `fd_read` calls. + +```bash +# Runs 200 `call` or `call_async` calls +Collecting 100 samples in estimated 5.2375 s (2000 iterations) +call_many_async time: [2.5450 ms 2.6233 ms 2.7039 ms] + +Collecting 100 samples in estimated 82.924 s (100 iterations) +call_many time: [819.96 ms 824.37 ms 828.77 ms] +``` + +It's pretty abysmal. Enough said. In the real world `call_many_async` may stay lower (will still be at least an order of magnitude slower). But `call_many` will only get worse since it needs to wait for the messages. Best-case it would be ~800ms, more likely ~2s. + +Other downsides I documented before testing things: + - Post-processing the wasm with the asyncify pass + - Causes a slowdown due to extra stack management & state machinery + - Requires wasm32-unknown-unknown-specific code in wasi_ctx to setup and call asyncify. + - Likely significant slowdown. + - The asyncify stack unwinding will be slow, even if we optimize it to only unwind on `fd_read` when required. + - Because unwinding / rewinding is slow, we probably won't be able to do it very often. So the idea of constantly running a background `read` task in the transport driver that enables concurrent async IO probably won't be feasible since it would require rewinding on every `fd_read` call. + +5. Use a seperate coordinator worker to manage compute workers and communicate with the main thread. + +Essentially a workarround for 3's issues. By having a coordinator worker that communicates with the compute workers using shared memory and with the main thread using postMessage, we can avoid the need for atomics in the main thread. The coordinator can manage all the compute instances and dispatch tasks to them via function pointers, and then relay messages back to the main thread. + +The primary issue here is software architecture. Deciding how to split responsibilities between the main thread and coordinator. + - If `host` is entirely in the coordinator, then the main thread must make a postMessage call for every host interaction, even those that don't involve compute workers. This probably won't be too slow, but will be annoying to setup (wasm-specific proxy that'll pass calls back and forth). + - If `host` is in the main thread, and only sends requests to the coordinator when compute workers are involved, then the coordinator will need to query the host whenever any computer worker needs to interact with the host. However, this will introduce similar problems to those outlined in the asyncify section and so is probably unacceptable. -1. Use SharedArrayBuffers for shared memory between the main thread and compute workers, without needing full atomics or a coordinator +Looks like we have our winner - Seperate coordinator with the host entirely in the coordinator! ~Yay to middleware~ From 92e5fe06194d04f9919783e403bf49c607956129 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Tue, 30 Dec 2025 11:36:32 -0500 Subject: [PATCH 49/70] feat: working coordinator architecture --- .cargo/config.toml | 11 - .vscode/settings.json | 6 + Cargo.lock | 15 +- .../.cargo/config.toml | 16 + crates/wasmi-plugin-coordinator/Cargo.toml | 20 +- crates/wasmi-plugin-coordinator/README.md | 5 +- crates/wasmi-plugin-coordinator/src/lib.rs | 4 + crates/wasmi-plugin-coordinator/src/worker.js | 20 + crates/wasmi-plugin-coordinator/src/worker.rs | 276 ++++++++++++++ .../src/worker_pool/mod.rs | 0 .../src/worker_pool/pool.rs | 27 +- .../src/worker_pool/worker.js | 2 +- .../src/worker_pool/worker.rs | 1 + .../src/worker_pool/worker_message.rs | 6 +- crates/wasmi-plugin-hdk/benches/wasm_bench.rs | 4 +- crates/wasmi-plugin-hdk/src/lib.rs | 5 +- crates/wasmi-plugin-hdk/src/plugin.rs | 25 +- .../src/runtime/coordinator_message.rs | 60 +++ .../src/runtime/message_writer.rs | 32 -- crates/wasmi-plugin-hdk/src/runtime/mod.rs | 2 + .../src/runtime/worker_runtime.rs | 357 ++++++++++++++---- crates/wasmi-plugin-hdk/src/session.rs | 8 +- crates/wasmi-plugin-rt/Cargo.toml | 2 + crates/wasmi-plugin-rt/src/lib.rs | 28 ++ makefile | 14 +- shell.nix | 4 +- 26 files changed, 769 insertions(+), 181 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 crates/wasmi-plugin-coordinator/.cargo/config.toml create mode 100644 crates/wasmi-plugin-coordinator/src/worker.js create mode 100644 crates/wasmi-plugin-coordinator/src/worker.rs rename crates/{wasmi-plugin-hdk => wasmi-plugin-coordinator}/src/worker_pool/mod.rs (100%) rename crates/{wasmi-plugin-hdk => wasmi-plugin-coordinator}/src/worker_pool/pool.rs (93%) rename crates/{wasmi-plugin-hdk => wasmi-plugin-coordinator}/src/worker_pool/worker.js (99%) rename crates/{wasmi-plugin-hdk => wasmi-plugin-coordinator}/src/worker_pool/worker.rs (95%) rename crates/{wasmi-plugin-hdk => wasmi-plugin-coordinator}/src/worker_pool/worker_message.rs (61%) create mode 100644 crates/wasmi-plugin-hdk/src/runtime/coordinator_message.rs delete mode 100644 crates/wasmi-plugin-hdk/src/runtime/message_writer.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 850378a..087a744 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,16 +1,5 @@ [target.wasm32-unknown-unknown] rustflags = [ "--cfg", "getrandom_backend=\"wasm_js\"", - "-C", "target-feature=+atomics,+bulk-memory", - "-C", "link-arg=--shared-memory", - "-C", "link-arg=--max-memory=1073741824", - "-C", "link-arg=--import-memory", - "-C", "link-arg=--export=__wasm_init_tls", - "-C", "link-arg=--export=__tls_size", - "-C", "link-arg=--export=__tls_align", - "-C", "link-arg=--export=__tls_base" ] runner = "wasm-bindgen-test-runner" - -[unstable] -build-std = ["std", "panic_abort"] \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..52e9704 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "rust-analyzer.cargo.target": "wasm32-unknown-unknown", + "rust-analyzer.check.targets": "wasm32-unknown-unknown", + "rust-analyzer.cargo.features": "all", + "rust-analyzer.procMacro.enable": true +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 73b999b..882ca11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2354,8 +2354,17 @@ dependencies = [ name = "wasmi-plugin-coordinator" version = "0.1.0" dependencies = [ - "gloo-timers", - "tokio", + "futures", + "serde", + "serde-wasm-bindgen", + "thiserror 2.0.17", + "tracing", + "tracing-wasm", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasmer", + "wasmi-plugin-hdk", + "web-sys", ] [[package]] @@ -2404,6 +2413,8 @@ version = "0.1.0" dependencies = [ "gloo-timers", "tokio", + "web-sys", + "web-time", ] [[package]] diff --git a/crates/wasmi-plugin-coordinator/.cargo/config.toml b/crates/wasmi-plugin-coordinator/.cargo/config.toml new file mode 100644 index 0000000..850378a --- /dev/null +++ b/crates/wasmi-plugin-coordinator/.cargo/config.toml @@ -0,0 +1,16 @@ +[target.wasm32-unknown-unknown] +rustflags = [ + "--cfg", "getrandom_backend=\"wasm_js\"", + "-C", "target-feature=+atomics,+bulk-memory", + "-C", "link-arg=--shared-memory", + "-C", "link-arg=--max-memory=1073741824", + "-C", "link-arg=--import-memory", + "-C", "link-arg=--export=__wasm_init_tls", + "-C", "link-arg=--export=__tls_size", + "-C", "link-arg=--export=__tls_align", + "-C", "link-arg=--export=__tls_base" +] +runner = "wasm-bindgen-test-runner" + +[unstable] +build-std = ["std", "panic_abort"] \ No newline at end of file diff --git a/crates/wasmi-plugin-coordinator/Cargo.toml b/crates/wasmi-plugin-coordinator/Cargo.toml index 8a66269..ebbbc70 100644 --- a/crates/wasmi-plugin-coordinator/Cargo.toml +++ b/crates/wasmi-plugin-coordinator/Cargo.toml @@ -12,10 +12,18 @@ keywords.workspace = true categories.workspace = true exclude.workspace = true -[dependencies] +[lib] +crate-type = ["cdylib"] -[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] -gloo-timers = { workspace = true } - -[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] -tokio = { workspace = true } +[target.'cfg(target_arch = "wasm32")'.dependencies] +futures = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +wasm-bindgen = { workspace = true } +wasm-bindgen-futures = { workspace = true } +web-sys = { workspace = true, features = ["Worker", "WorkerOptions", "DedicatedWorkerGlobalScope", "WorkerType", "MessageEvent", "ErrorEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } +serde = { workspace = true } +serde-wasm-bindgen = { workspace = true } +wasmi-plugin-hdk = { workspace = true } +wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } +tracing-wasm = { workspace = true } \ No newline at end of file diff --git a/crates/wasmi-plugin-coordinator/README.md b/crates/wasmi-plugin-coordinator/README.md index f38659c..04d86a5 100644 --- a/crates/wasmi-plugin-coordinator/README.md +++ b/crates/wasmi-plugin-coordinator/README.md @@ -73,6 +73,7 @@ Essentially a workarround for 3's issues. By having a coordinator worker that co The primary issue here is software architecture. Deciding how to split responsibilities between the main thread and coordinator. - If `host` is entirely in the coordinator, then the main thread must make a postMessage call for every host interaction, even those that don't involve compute workers. This probably won't be too slow, but will be annoying to setup (wasm-specific proxy that'll pass calls back and forth). - - If `host` is in the main thread, and only sends requests to the coordinator when compute workers are involved, then the coordinator will need to query the host whenever any computer worker needs to interact with the host. However, this will introduce similar problems to those outlined in the asyncify section and so is probably unacceptable. + - If `host` is in the main thread, and only sends requests to the coordinator when compute workers are involved, then the coordinator will need to query the host whenever any computer worker needs to interact with the host. -Looks like we have our winner - Seperate coordinator with the host entirely in the coordinator! ~Yay to middleware~ +I'm going to go with the latter approach since it seems cleaner. There is some significant added latency in messages vs shared memory for inter-plugin communication, but it also results in a much much much simpler architecture and UX for consumers of the library. + - Namely - with the former approach, it would require placing consumer code in the coordinator worker. That means consumers of the library would need to write and compile code for the coordinator, which means setting up rust nightly, atomics + shared memory, and having a seperate crate. With the latter I can just bundle the compiled generic `.wasm` coordinator with the library for use as-is. \ No newline at end of file diff --git a/crates/wasmi-plugin-coordinator/src/lib.rs b/crates/wasmi-plugin-coordinator/src/lib.rs index e69de29..bbc74bf 100644 --- a/crates/wasmi-plugin-coordinator/src/lib.rs +++ b/crates/wasmi-plugin-coordinator/src/lib.rs @@ -0,0 +1,4 @@ +#![cfg(target_arch = "wasm32")] + +mod worker; +mod worker_pool; diff --git a/crates/wasmi-plugin-coordinator/src/worker.js b/crates/wasmi-plugin-coordinator/src/worker.js new file mode 100644 index 0000000..d509f07 --- /dev/null +++ b/crates/wasmi-plugin-coordinator/src/worker.js @@ -0,0 +1,20 @@ +// console.log = function (...args) { +// self.postMessage({ type: 'Log', message: `${args.join(' ')}` }); +// }; + +// console.error = function (...args) { +// self.postMessage({ type: 'Log', message: `${args.join(' ')}` }); +// }; + +console.log("Worker JS initialized"); + +import init, { start_coordinator } from "{sdk_url}"; + +console.log("JS Glue loaded, initializing wasm..."); + +init('{wasm_url}').then(() => { + start_coordinator(); + console.log("Wasm module loaded, starting worker..."); +}).catch((err) => { + console.error("Worker Init Failed:", err); +}); diff --git a/crates/wasmi-plugin-coordinator/src/worker.rs b/crates/wasmi-plugin-coordinator/src/worker.rs new file mode 100644 index 0000000..7644c48 --- /dev/null +++ b/crates/wasmi-plugin-coordinator/src/worker.rs @@ -0,0 +1,276 @@ +use std::{cell::RefCell, collections::HashMap, io::Write, rc::Rc, sync::Arc}; + +use futures::AsyncRead; +use futures::AsyncReadExt; +use thiserror::Error; +use tracing::{error, info, warn}; +use wasm_bindgen::{ + JsCast, + prelude::{Closure, wasm_bindgen}, +}; +use wasmi_plugin_hdk::{ + runtime::coordinator_message::{CoordinatorMessage, InstanceId}, + runtime::non_blocking_pipe::{NonBlockingPipeWriter, non_blocking_pipe}, + wasi::wasi_ctx::{self, WasiReader}, +}; +use web_sys::{ + DedicatedWorkerGlobalScope, MessageEvent, + js_sys::{self, Reflect, Uint8Array, WebAssembly}, +}; + +use crate::worker_pool::pool::{SpawnError, WorkerHandle, WorkerPool}; + +struct CoordinatorState { + instance_channels: HashMap, + instance_handles: HashMap>, +} + +#[derive(Debug, Error)] +enum RunError { + #[error("WASI Error: {0}")] + Wasi(#[from] wasi_ctx::WasiError), + #[error("Runtime Error: {0}")] + Runtime(#[from] wasmer::RuntimeError), + #[error("Compile Error: {0}")] + Compile(#[from] wasmer::CompileError), +} + +#[wasm_bindgen] +pub async fn start_coordinator() { + tracing_wasm::set_as_global_default_with_config( + tracing_wasm::WASMLayerConfigBuilder::new() + .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) + .set_max_level(tracing::Level::INFO) + .build(), + ); + + let global = js_sys::global().unchecked_into::(); + + let state = CoordinatorState { + instance_channels: HashMap::new(), + instance_handles: HashMap::new(), + }; + let state = Rc::new(RefCell::new(state)); + + let onmessage = Closure::wrap(Box::new(move |e: MessageEvent| { + let mut state = state.borrow_mut(); + on_message(&mut state, e); + }) as Box); + + global.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + onmessage.forget(); + + // Send ready message to main thread + let ready_msg = CoordinatorMessage::Ready; + let ready_value = serde_wasm_bindgen::to_value(&ready_msg).unwrap(); + global.post_message(&ready_value).unwrap(); +} + +fn on_message(state: &mut CoordinatorState, e: web_sys::MessageEvent) { + let global = js_sys::global().unchecked_into::(); + let data = e.data(); + + let msg = match serde_wasm_bindgen::from_value::(data.clone()) { + Ok(msg) => msg, + Err(err) => { + error!("Failed to deserialize message: {}", err); + return; + } + }; + + match msg { + CoordinatorMessage::Stdin { id } => { + let Ok(data) = Reflect::get(&data, &"data".into()) else { + error!("Failed to get stdin data from message"); + return; + }; + + stdin(state, id, data.unchecked_into()); + } + CoordinatorMessage::Terminate { id } => { + terminate(state, id); + } + CoordinatorMessage::RunPlugin { id } => { + let Ok(wasm_bytes) = Reflect::get(&data, &"wasm_bytes".into()) else { + error!("Failed to get wasm_bytes from message"); + return; + }; + + let Ok(wasm_module) = Reflect::get(&data, &"wasm_module".into()) else { + error!("Failed to get wasm_module from message"); + return; + }; + + let status = match run_plugin( + state, + id, + wasm_bytes.unchecked_into(), + wasm_module.unchecked_into(), + ) { + Ok(_) => Ok(()), + Err(e) => Err(e.to_string()), + }; + + let response_msg = CoordinatorMessage::RunPluginResp { id, status }; + let response_value = serde_wasm_bindgen::to_value(&response_msg).unwrap(); + global.post_message(&response_value).unwrap(); + } + + _ => { + warn!("Unknown message received"); + } + } +} + +fn stdin(state: &mut CoordinatorState, id: InstanceId, data: Uint8Array) { + info!("Received stdin message for id {}", id); + let data: Vec = data.to_vec(); + + if let Some(writer) = state.instance_channels.get_mut(&id) { + if let Err(e) = writer.write_all(&data) { + error!("Failed to write to stdin pipe: {}", e); + } + } else { + warn!("No stdin channel found for instance {}", id); + } +} + +fn terminate(state: &mut CoordinatorState, id: InstanceId) { + if let Some(handle) = state.instance_handles.remove(&id) { + handle.terminate(); + } else { + warn!("No instance handle found for instance {}", id); + } + + state.instance_channels.remove(&id); +} + +fn run_plugin( + state: &mut CoordinatorState, + id: InstanceId, + wasm_bytes: Uint8Array, + wasm_module: WebAssembly::Module, +) -> Result, SpawnError> { + let wasm_bytes: Vec = wasm_bytes.to_vec(); + + let (stdin_reader, stdin_writer) = non_blocking_pipe(); + let (stdout_reader, stdout_writer) = non_blocking_pipe(); + let (stderr_reader, stderr_writer) = non_blocking_pipe(); + + // Start proxies + stdout_proxy(stdout_reader, id.clone()); + stderr_proxy(stderr_reader, id.clone()); + + state.instance_channels.insert(id.clone(), stdin_writer); + + let pool = WorkerPool::global(); + pool.run_with(&wasm_module, move |extra| async move { + info!("Running Wasm instance for id {}", id); + let res = run_instance( + extra, + wasm_bytes, + stdin_reader, + stdout_writer, + stderr_writer, + ) + .await; + if let Err(e) = res { + error!("Error running Wasm instance: {}", e); + } + }) +} + +async fn run_instance( + js_module: wasm_bindgen::JsValue, + wasm_bytes: Vec, + stdin: impl WasiReader + 'static, + stdout: impl Write + Send + Sync + 'static, + stderr: impl Write + Send + Sync + 'static, +) -> Result<(), RunError> { + let mut store = wasmer::Store::default(); + + let js_module: WebAssembly::Module = js_module.dyn_into().unwrap(); + let module = (js_module, wasm_bytes).into(); + + let wasi_ctx = wasi_ctx::WasiCtx::new() + .set_stdin(stdin) + .set_stdout(stdout) + .set_stderr(stderr); + + info!("Starting WASI instance..."); + let start = wasi_ctx.into_fn(&mut store, &module)?; + start.call(&mut store, &[])?; + + Ok(()) +} + +fn stdout_proxy(reader: impl AsyncRead + Unpin + 'static, id: InstanceId) { + wasm_bindgen_futures::spawn_local(async move { + let mut reader = reader; + let mut buffer = [0u8; 1024]; + + loop { + match reader.read(&mut buffer).await { + Ok(0) => break, // EOF + Ok(n) => { + let data = js_sys::Uint8Array::from(&buffer[..n]); + let stdout_msg = CoordinatorMessage::Stdout { id: id.clone() }; + let msg_value = serde_wasm_bindgen::to_value(&stdout_msg).unwrap(); + Reflect::set(&msg_value, &"data".into(), &data).unwrap(); + let global = js_sys::global().unchecked_into::(); + global.post_message(&msg_value).unwrap(); + } + Err(e) => { + error!("Error reading stdout for instance {}: {}", id, e); + break; + } + } + } + }); +} + +fn stderr_proxy(reader: impl AsyncRead + Unpin + 'static, id: InstanceId) { + wasm_bindgen_futures::spawn_local(async move { + let mut reader = reader; + let mut chunk_buffer = [0u8; 1024]; + let mut accumulated_data = Vec::with_capacity(1024); + + let send_buffer = |data: &[u8]| { + if data.is_empty() { + return; + } + + let js_data = js_sys::Uint8Array::from(data); + let stderr_msg = CoordinatorMessage::Stderr { id: id.clone() }; + let msg_value = serde_wasm_bindgen::to_value(&stderr_msg).unwrap(); + + js_sys::Reflect::set(&msg_value, &"data".into(), &js_data).unwrap(); + let global = js_sys::global().unchecked_into::(); + global.post_message(&msg_value).unwrap(); + }; + + loop { + match reader.read(&mut chunk_buffer).await { + Ok(0) => { + // EOF: Send any remaining data in the buffer + send_buffer(&accumulated_data); + break; + } + Ok(n) => { + accumulated_data.extend_from_slice(&chunk_buffer[..n]); + + // If we've hit or exceeded 1 KiB, send it and clear + if accumulated_data.len() >= 1024 { + send_buffer(&accumulated_data); + accumulated_data.clear(); + } + } + Err(e) => { + error!("Error reading stderr for instance {}: {}", id, e); + send_buffer(&accumulated_data); + break; + } + } + } + }); +} diff --git a/crates/wasmi-plugin-hdk/src/worker_pool/mod.rs b/crates/wasmi-plugin-coordinator/src/worker_pool/mod.rs similarity index 100% rename from crates/wasmi-plugin-hdk/src/worker_pool/mod.rs rename to crates/wasmi-plugin-coordinator/src/worker_pool/mod.rs diff --git a/crates/wasmi-plugin-hdk/src/worker_pool/pool.rs b/crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs similarity index 93% rename from crates/wasmi-plugin-hdk/src/worker_pool/pool.rs rename to crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs index 6dc15db..60a2037 100644 --- a/crates/wasmi-plugin-hdk/src/worker_pool/pool.rs +++ b/crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs @@ -2,7 +2,7 @@ use std::sync::{Arc, Mutex, OnceLock}; use futures::future::LocalBoxFuture; use thiserror::Error; -use tracing::{error, info, warn}; +use tracing::{error, info}; use wasm_bindgen::{ JsCast, JsValue, prelude::{Closure, wasm_bindgen}, @@ -152,6 +152,7 @@ impl WorkerHandle { Reflect::set(&msg, &"sdkUrl".into(), &sdk_url().into()).unwrap(); worker.post_message(&msg).unwrap(); + info!("Spawned worker {}", id); Ok(WorkerHandle { id, state, @@ -205,6 +206,7 @@ impl WorkerHandle { let worker = &self.worker; // 3. Send the "run" message with raw ptr + info!("Sending run_with message to worker {}", self.id); let msg = js_sys::Object::new(); Reflect::set(&msg, &"type".into(), &"run_with".into()).unwrap(); Reflect::set(&msg, &"taskPtr".into(), &ptr.into()).unwrap(); @@ -244,6 +246,10 @@ fn sdk_url() -> String { static IMPORT_META_URL: String; } + info!( + "SDK URL: {}", + IMPORT_META_URL.with(|url| url.clone().to_ascii_lowercase()) + ); IMPORT_META_URL.with(|url| url.clone()) } @@ -257,16 +263,13 @@ fn on_message_handler( ) -> Box { Box::new(move |e: web_sys::MessageEvent| { let Ok(msg) = serde_wasm_bindgen::from_value::(e.data()) else { - error!( - "WorkerBridge: Received unknown message from worker: {:?}", - e.data() - ); + error!("Received unknown message from worker: {:?}", e.data()); return; }; match msg { - WorkerMessage::Log { message, level, ts } => { - log_worker_message(&name, message, level, ts); + WorkerMessage::Log { message } => { + log_worker_message(&name, message); } WorkerMessage::Idle => { info!(target: "WORKER", "[{}] Idle", name); @@ -277,14 +280,8 @@ fn on_message_handler( }) } -fn log_worker_message(name: &str, msg: String, level: String, ts: f64) { - let _ = ts; - - match level.as_str() { - "warn" => warn!(target: "WORKER", "[{}] {}", name, msg), - "error" => error!(target: "WORKER", "[{}] {}", name, msg), - _ => info!(target: "WORKER", "[{}] {}", name, msg), - } +fn log_worker_message(name: &str, msg: String) { + info!(target: "WORKER", "[{}] {}", name, msg); } fn on_error_handler(name: String) -> Box { diff --git a/crates/wasmi-plugin-hdk/src/worker_pool/worker.js b/crates/wasmi-plugin-coordinator/src/worker_pool/worker.js similarity index 99% rename from crates/wasmi-plugin-hdk/src/worker_pool/worker.js rename to crates/wasmi-plugin-coordinator/src/worker_pool/worker.js index c5bdda3..1a8c26e 100644 --- a/crates/wasmi-plugin-hdk/src/worker_pool/worker.js +++ b/crates/wasmi-plugin-coordinator/src/worker_pool/worker.js @@ -51,4 +51,4 @@ globalThis.onmessage = async ev => { console.error(`Error in worker: ${e.message}`); } }); -}; \ No newline at end of file +}; diff --git a/crates/wasmi-plugin-hdk/src/worker_pool/worker.rs b/crates/wasmi-plugin-coordinator/src/worker_pool/worker.rs similarity index 95% rename from crates/wasmi-plugin-hdk/src/worker_pool/worker.rs rename to crates/wasmi-plugin-coordinator/src/worker_pool/worker.rs index b3c2ce4..c8fff1d 100644 --- a/crates/wasmi-plugin-hdk/src/worker_pool/worker.rs +++ b/crates/wasmi-plugin-coordinator/src/worker_pool/worker.rs @@ -27,6 +27,7 @@ pub fn execute_worker_task_with(ptr: u32, extra: JsValue) -> js_sys::Promise { let fut = task(extra); wasm_bindgen_futures::future_to_promise(async move { + info!("Executing worker task with extra..."); if let Err(e) = std::panic::AssertUnwindSafe(fut).catch_unwind().await { error!("Panic in thread worker: {:?}", e); } diff --git a/crates/wasmi-plugin-hdk/src/worker_pool/worker_message.rs b/crates/wasmi-plugin-coordinator/src/worker_pool/worker_message.rs similarity index 61% rename from crates/wasmi-plugin-hdk/src/worker_pool/worker_message.rs rename to crates/wasmi-plugin-coordinator/src/worker_pool/worker_message.rs index d6108be..d9116d2 100644 --- a/crates/wasmi-plugin-hdk/src/worker_pool/worker_message.rs +++ b/crates/wasmi-plugin-coordinator/src/worker_pool/worker_message.rs @@ -3,10 +3,6 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] #[serde(tag = "type")] pub enum WorkerMessage { - Log { - message: String, - level: String, - ts: f64, - }, + Log { message: String }, Idle, } diff --git a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs index e67401e..634a9c5 100644 --- a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs +++ b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs @@ -22,7 +22,7 @@ fn load_plugin_wasm() -> Vec { async fn load_plugin() -> Plugin { let wasm_bytes = load_plugin_wasm(); let handler = Arc::new(get_host_server()); - let plugin = Plugin::new("test_plugin", wasm_bytes, handler) + let plugin = Plugin::builder("test_plugin", wasm_bytes, handler) .with_timeout(Duration::from_secs(10)) .build() .await @@ -87,7 +87,7 @@ async fn bench_lifecycle(c: &mut Criterion) { let wasm_bytes = wasm_bytes.clone(); let handler = handler.clone(); async move { - let plugin = Plugin::new("test_plugin", wasm_bytes.clone(), handler) + let plugin = Plugin::builder("test_plugin", wasm_bytes.clone(), handler) .build() .await .unwrap(); diff --git a/crates/wasmi-plugin-hdk/src/lib.rs b/crates/wasmi-plugin-hdk/src/lib.rs index c78157a..9cee951 100644 --- a/crates/wasmi-plugin-hdk/src/lib.rs +++ b/crates/wasmi-plugin-hdk/src/lib.rs @@ -6,7 +6,4 @@ pub mod runtime; pub mod server; pub mod session; mod time; -mod wasi; - -#[cfg(target_arch = "wasm32")] -pub mod worker_pool; +pub mod wasi; diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index 1d96a54..e164bc7 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -1,9 +1,9 @@ use std::{sync::Arc, time::Duration}; -use futures::{AsyncBufReadExt, AsyncRead, FutureExt, future::FusedFuture, io::BufReader}; +use futures::{AsyncBufReadExt, AsyncRead, FutureExt, io::BufReader}; use serde_json::Value; use thiserror::Error; -use tracing::info; +use tracing::{error, info}; use wasmi_plugin_pdk::{ api::RequestHandler, router::BoxFuture, @@ -130,7 +130,7 @@ impl Plugin { impl AsyncTransport for Plugin { async fn call_async(&self, method: &str, params: Value) -> Result { - let runtime = self.create_runtime(); + let runtime = self.runtime(); let (id, stdin_writer, stdout_reader, stderr_reader) = runtime .spawn(self.inner.compiled.clone()) .await @@ -155,43 +155,36 @@ impl AsyncTransport for Plugin { //? Pump futures until (session_task AND stderr_task) OR timeout futures::pin_mut!(session_task, timeout, stderr_task); - let mut rpc_result = None; loop { futures::select! { res = session_task => { - rpc_result = Some(res); - if stderr_task.is_terminated() { - break; - } + return Ok(res?); } _ = timeout => { + error!("Plugin {} timed out after {:?}", self.name(), self.inner.timeout); //? Assume the instance has hung, so terminate its runtime runtime.terminate(id).await; return Err(PluginError::PluginTimeout); } _ = stderr_task => { - if rpc_result.is_some() { - break; - } + //? Stderr logging finished } } } - - Ok(rpc_result.expect("Loop exited without result or timeout")?) } } impl Plugin { #[allow(clippy::type_complexity)] #[cfg(not(target_arch = "wasm32"))] - fn create_runtime(&self) -> impl Runtime + Send + Sync + 'static { + fn runtime(&self) -> impl Runtime + Send + Sync + 'static { runtime::NativeRuntime::new() } #[allow(clippy::type_complexity)] #[cfg(target_arch = "wasm32")] - fn create_runtime(&self) -> impl Runtime + Send + Sync + 'static { - runtime::WorkerRuntime::new() + fn runtime(&self) -> impl Runtime + Send + Sync + 'static { + runtime::WorkerRuntime::global() } } diff --git a/crates/wasmi-plugin-hdk/src/runtime/coordinator_message.rs b/crates/wasmi-plugin-hdk/src/runtime/coordinator_message.rs new file mode 100644 index 0000000..ac0cbe5 --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/runtime/coordinator_message.rs @@ -0,0 +1,60 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub struct InstanceId(u32); + +impl InstanceId { + pub fn new() -> Self { + Self(getrandom::u32().expect("Could not get random for InstanceId")) + } +} + +impl From for InstanceId { + fn from(value: u64) -> Self { + Self(value as u32) + } +} + +impl From for u64 { + fn from(id: InstanceId) -> Self { + id.0 as u64 + } +} + +impl Display for InstanceId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum CoordinatorMessage { + /// WASM module attached as external bytes. + RunPlugin { + id: InstanceId, + }, + RunPluginResp { + id: InstanceId, + status: Result<(), String>, + }, + Stdin { + id: InstanceId, + }, + Stdout { + id: InstanceId, + }, + Stderr { + id: InstanceId, + }, + Log { + message: String, + }, + Terminate { + id: InstanceId, + }, + + Ready, +} diff --git a/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs b/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs deleted file mode 100644 index f51410d..0000000 --- a/crates/wasmi-plugin-hdk/src/runtime/message_writer.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::io::Write; - -use web_sys::{console::log_1, js_sys}; - -pub struct MessageWriter { - name: String, -} - -unsafe impl Send for MessageWriter {} -unsafe impl Sync for MessageWriter {} - -impl MessageWriter { - #[allow(dead_code)] - pub fn new(name: String) -> Self { - Self { name } - } -} - -impl Write for MessageWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - let mut prefix = format!("[{}] ", self.name).into_bytes(); - prefix.extend_from_slice(buf); - let prefix = prefix.as_slice(); - - log_1(js_sys::Uint8Array::from(prefix).as_ref()); - Ok(buf.len()) - } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } -} diff --git a/crates/wasmi-plugin-hdk/src/runtime/mod.rs b/crates/wasmi-plugin-hdk/src/runtime/mod.rs index 15690ca..cbff676 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/mod.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/mod.rs @@ -8,6 +8,8 @@ mod native_runtime; #[cfg(target_arch = "wasm32")] pub use worker_runtime::WorkerRuntime; #[cfg(target_arch = "wasm32")] +pub mod coordinator_message; +#[cfg(target_arch = "wasm32")] mod worker_runtime; use crate::compile::Compiled; diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index c8e476c..46ef2f8 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -1,53 +1,114 @@ -//! Web Worker runtime bridge, running the Wasm module in a Web Worker. - use std::collections::HashMap; use std::io::Write; -use std::sync::Arc; +use std::sync::{Arc, Mutex, OnceLock}; -use futures::lock::Mutex; -use futures::{AsyncRead, AsyncWrite}; +use futures::AsyncReadExt; +use futures::channel::oneshot::{self, Sender}; +use futures::future::{BoxFuture, Shared}; +use futures::{AsyncRead, AsyncWrite, FutureExt}; use thiserror::Error; -use tracing::error; -use wasm_bindgen::JsCast; -use web_sys::js_sys::WebAssembly; +use tracing::{error, info, warn}; +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::js_sys::Reflect; +use web_sys::{MessageEvent, Worker, WorkerOptions, WorkerType, js_sys}; use crate::compile::Compiled; use crate::runtime::Runtime; -use crate::runtime::non_blocking_pipe::{ - NonBlockingPipeReader, NonBlockingPipeWriter, non_blocking_pipe, -}; -use crate::wasi::wasi_ctx::{self, WasiReader}; -use crate::worker_pool::pool::{SpawnError, WorkerHandle, WorkerId, WorkerPool}; +use crate::runtime::coordinator_message::{CoordinatorMessage, InstanceId}; +use crate::runtime::non_blocking_pipe::non_blocking_pipe; pub struct WorkerRuntime { - active_sessions: Arc>>>, + inner: Arc>, +} + +pub struct InnerWorkerRuntime { + coordinator: Worker, + instances: HashMap, + ready_tx: Option>, + shared_ready: Shared>, + _onmessage_closure: Option>, +} + +struct PluginInstance { + stdout_writer: Box, + stderr_writer: Box, + run_plugin_resp_tx: Option>>, } #[derive(Debug, Error)] -enum RunError { - #[error("WASI Error: {0}")] - Wasi(#[from] wasi_ctx::WasiError), - #[error("Runtime Error: {0}")] - Runtime(#[from] wasmer::RuntimeError), - #[error("Compile Error: {0}")] - Compile(#[from] wasmer::CompileError), +pub enum SpawnError { + #[error("Worker Error: {0}")] + WorkerError(String), + #[error("Oneshot cancelled")] + OneshotCancelled, + #[error("Serde Error: {0}")] + Serde(#[from] serde_wasm_bindgen::Error), + #[error("Post Message Error: {0:?}")] + PostMessage(JsValue), + #[error("Reflect Error: {0:?}")] + Reflect(JsValue), } +static RUNTIME: OnceLock = OnceLock::new(); + +const WORKER_LOADER_JS: &str = include_str!("../../../wasmi-plugin-coordinator/src/worker.js"); +const WORKER_JS_GLUE: &str = + include_str!("../../../wasmi-plugin-coordinator/pkg/wasmi_plugin_coordinator.js"); +const WORKER_WASM_BYTES: &[u8] = + include_bytes!("../../../wasmi-plugin-coordinator/pkg/wasmi_plugin_coordinator_bg.wasm"); + +unsafe impl Send for InnerWorkerRuntime {} +unsafe impl Sync for InnerWorkerRuntime {} + impl WorkerRuntime { - pub fn new() -> Self { - WorkerRuntime { - active_sessions: Arc::new(Mutex::new(HashMap::new())), - } + pub fn global() -> &'static WorkerRuntime { + RUNTIME.get_or_init(|| Self::init()) } -} -impl Default for WorkerRuntime { - fn default() -> Self { - Self::new() + fn init() -> Self { + info!("Initializing WorkerRuntime"); + let options = WorkerOptions::new(); + options.set_name("coordinator"); + options.set_type(WorkerType::Module); + let worker = Worker::new_with_options(&worker_url().unwrap(), &options).unwrap(); + + let (ready_tx, ready_rx) = futures::channel::oneshot::channel(); + let shared_ready = ready_rx.map(|_| ()).boxed().shared(); + + let inner = Arc::new(Mutex::new(InnerWorkerRuntime { + coordinator: worker, + instances: HashMap::new(), + ready_tx: Some(ready_tx), + shared_ready, + _onmessage_closure: None, + })); + + let inner_clone = inner.clone(); + let onmessage = Closure::wrap(Box::new(move |e: MessageEvent| { + on_message(inner_clone.clone(), e); + }) as Box); + + inner + .lock() + .unwrap() + .coordinator + .set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + inner.lock().unwrap()._onmessage_closure = Some(onmessage); + + Self { inner } + } + + async fn wait_ready(&self) { + let shared_ready = { + let inner = self.inner.lock().unwrap(); + inner.shared_ready.clone() + }; + shared_ready.await; } } -impl Runtime for WorkerRuntime { +impl Runtime for &WorkerRuntime { type Error = SpawnError; async fn spawn( @@ -62,68 +123,212 @@ impl Runtime for WorkerRuntime { ), Self::Error, > { + self.wait_ready().await; + + let instance_id = InstanceId::new(); let (stdin_reader, stdin_writer) = non_blocking_pipe(); let (stdout_reader, stdout_writer) = non_blocking_pipe(); let (stderr_reader, stderr_writer) = non_blocking_pipe(); + let (run_plugin_resp_tx, run_plugin_resp_rx) = futures::channel::oneshot::channel(); + + let plugin_instance = PluginInstance { + stdout_writer: Box::new(stdout_writer), + stderr_writer: Box::new(stderr_writer), + run_plugin_resp_tx: Some(run_plugin_resp_tx), + }; + + self.inner + .lock() + .unwrap() + .instances + .insert(instance_id, plugin_instance); + + // Start stdin proxy + stdin_proxy(stdin_reader, instance_id, self.inner.clone()); + + // Notify the coordinator + let wasm_bytes_js = js_sys::Uint8Array::from(&compiled.wasm_bytes[..]); + let wasm_bytes_buffer = wasm_bytes_js.buffer(); + let wasm_module_js = compiled.js_module.clone(); + let spawn_msg = CoordinatorMessage::RunPlugin { id: instance_id }; + let msg_value = serde_wasm_bindgen::to_value(&spawn_msg)?; + Reflect::set(&msg_value, &"wasm_bytes".into(), &wasm_bytes_js) + .map_err(SpawnError::Reflect)?; + Reflect::set(&msg_value, &"wasm_module".into(), &wasm_module_js) + .map_err(SpawnError::Reflect)?; + let transfer = js_sys::Array::new(); + transfer.push(&wasm_bytes_buffer); - let handle = wasm_handle(compiled, stdin_reader, stdout_writer, stderr_writer)?; + self.inner + .lock() + .unwrap() + .coordinator + .post_message_with_transfer(&msg_value, &transfer) + .map_err(SpawnError::PostMessage)?; - let id = handle.id; - self.active_sessions.lock().await.insert(id, handle); + // Await the response + match run_plugin_resp_rx.await { + Ok(Ok(())) => {} + Ok(Err(err_msg)) => return Err(SpawnError::WorkerError(err_msg)), + Err(_) => return Err(SpawnError::OneshotCancelled), + } - Ok((id, stdin_writer, stdout_reader, stderr_reader)) + Ok(( + instance_id.into(), + stdin_writer, + stdout_reader, + stderr_reader, + )) } async fn terminate(&self, instance_id: u64) { - let instance_id = instance_id as WorkerId; - if let Some(session) = self.active_sessions.lock().await.remove(&instance_id) { - session.terminate(); - } + self.wait_ready().await; + + let instance_id: InstanceId = instance_id.into(); + self.inner.lock().unwrap().instances.remove(&instance_id); + + // Also notify the coordinator + let terminate_msg = CoordinatorMessage::Terminate { id: instance_id }; + let msg_value = serde_wasm_bindgen::to_value(&terminate_msg).unwrap(); + + self.inner + .lock() + .unwrap() + .coordinator + .post_message(&msg_value) + .unwrap(); } } -fn wasm_handle( - compiled: Compiled, - stdin_reader: NonBlockingPipeReader, - stdout_writer: NonBlockingPipeWriter, - stderr_writer: NonBlockingPipeWriter, -) -> Result, SpawnError> { - let pool = WorkerPool::global(); - let module = compiled.js_module; - pool.run_with(&module, move |extra| async move { - let res = run_instance( - extra, - &compiled.wasm_bytes, - stdin_reader, - stdout_writer, - stderr_writer, - ) - .await; - if let Err(e) = res { - error!("Error running Wasm instance: {}", e); - } - }) +fn worker_url() -> Result { + let options = web_sys::BlobPropertyBag::new(); + options.set_type("application/wasm"); + let wasm_blob = web_sys::Blob::new_with_u8_array_sequence_and_options( + &js_sys::Array::of1(&js_sys::Uint8Array::from(WORKER_WASM_BYTES)), + &options, + )?; + let wasm_url = web_sys::Url::create_object_url_with_blob(&wasm_blob)?; + + let options = web_sys::BlobPropertyBag::new(); + options.set_type("text/javascript"); + let sdk_blob = web_sys::Blob::new_with_u8_array_sequence_and_options( + &js_sys::Array::of1(&js_sys::JsString::from(WORKER_JS_GLUE)), + &options, + )?; + let sdk_url = web_sys::Url::create_object_url_with_blob(&sdk_blob)?; + + let loader_script = WORKER_LOADER_JS + .replace("{sdk_url}", &sdk_url) + .replace("{wasm_url}", &wasm_url); + + let options = web_sys::BlobPropertyBag::new(); + options.set_type("text/javascript"); + let blob = web_sys::Blob::new_with_u8_array_sequence_and_options( + &js_sys::Array::of1(&js_sys::JsString::from(loader_script)), + &options, + )?; + + web_sys::Url::create_object_url_with_blob(&blob) } -async fn run_instance( - js_module: wasm_bindgen::JsValue, - wasm_bytes: &[u8], - stdin: impl WasiReader + 'static, - stdout: impl Write + Send + Sync + 'static, - stderr: impl Write + Send + Sync + 'static, -) -> Result<(), RunError> { - let mut store = wasmer::Store::default(); +fn on_message(inner: Arc>, e: web_sys::MessageEvent) { + let data = e.data(); + + let Ok(msg) = serde_wasm_bindgen::from_value::(data.clone()) else { + error!("Worker: Failed to deserialize message"); + return; + }; - let js_module: WebAssembly::Module = js_module.dyn_into().unwrap(); - let module = (js_module, wasm_bytes).into(); + match msg { + CoordinatorMessage::Ready => { + info!("Coordinator is ready"); + let mut inner = inner.lock().unwrap(); + if let Some(tx) = inner.ready_tx.take() { + let _ = tx.send(()); + } + } + CoordinatorMessage::Stdout { id } => { + let Ok(data) = Reflect::get(&data, &"data".into()) else { + error!("Failed to get stdout data from message"); + return; + }; + let data: Vec = data.unchecked_into::().to_vec(); - let wasi_ctx = wasi_ctx::WasiCtx::new() - .set_stdin(stdin) - .set_stdout(stdout) - .set_stderr(stderr); + let mut inner = inner.lock().unwrap(); + if let Some(instance) = inner.instances.get_mut(&id) { + if let Err(e) = instance.stdout_writer.write_all(&data) { + error!("Failed to write to stdout pipe: {}", e); + } + } else { + warn!("Received stdout for unknown instance ID {:?}", id); + } + } + CoordinatorMessage::Stderr { id } => { + let Ok(data) = Reflect::get(&data, &"data".into()) else { + error!("Failed to get stderr data from message"); + return; + }; + let data: Vec = data.unchecked_into::().to_vec(); - let start = wasi_ctx.into_fn(&mut store, &module)?; - start.call(&mut store, &[])?; + let mut inner = inner.lock().unwrap(); + if let Some(instance) = inner.instances.get_mut(&id) { + if let Err(e) = instance.stderr_writer.write_all(&data) { + error!("Failed to write to stderr pipe: {}", e); + } + } else { + warn!("Received stderr for unknown instance ID {:?}", id); + } + } + CoordinatorMessage::RunPluginResp { id, status } => { + let mut inner = inner.lock().unwrap(); + if let Some(instance) = inner.instances.get_mut(&id) { + if let Some(tx) = instance.run_plugin_resp_tx.take() { + let _ = tx.send(status); + } else { + warn!("No response channel found for instance {:?}", id); + } + } else { + warn!("Received RunPluginResp for unknown instance ID {:?}", id); + } + } + CoordinatorMessage::Log { message } => { + info!("[Coordinator] {}", message); + } + _ => { + warn!("Unexpected message received from coordinator"); + } + } +} + +fn stdin_proxy( + reader: impl AsyncRead + Unpin + Send + 'static, + id: InstanceId, + inner: Arc>, +) { + wasm_bindgen_futures::spawn_local(async move { + let mut reader = reader; + let mut buffer = [0u8; 1024]; - Ok(()) + loop { + match reader.read(&mut buffer).await { + Ok(0) => break, // EOF + Ok(n) => { + let data = js_sys::Uint8Array::from(&buffer[..n]); + let stdin_msg = CoordinatorMessage::Stdin { id }; + let msg_value = serde_wasm_bindgen::to_value(&stdin_msg).unwrap(); + Reflect::set(&msg_value, &"data".into(), &data).unwrap(); + + let inner = inner.lock().unwrap(); + if let Err(e) = inner.coordinator.post_message(&msg_value) { + error!("Failed to post stdin message: {:?}", e); + break; + } + } + Err(e) => { + error!("Error reading from stdin: {}", e); + break; + } + } + } + }); } diff --git a/crates/wasmi-plugin-hdk/src/session.rs b/crates/wasmi-plugin-hdk/src/session.rs index a9a5390..93ebc62 100644 --- a/crates/wasmi-plugin-hdk/src/session.rs +++ b/crates/wasmi-plugin-hdk/src/session.rs @@ -1,7 +1,7 @@ use futures::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, io::BufReader}; use serde_json::Value; use thiserror::Error; -use tracing::{error, warn}; +use tracing::{error, info, warn}; use wasmi_plugin_pdk::{ api::RequestHandler, rpc_message::{RpcError, RpcMessage, RpcResponse}, @@ -65,6 +65,7 @@ impl> } Err(e) => return Err(PluginSessionError::Io(e)), } + info!("PluginSession: Received line: {}", line.trim()); // Process complete lines let msg: RpcMessage = serde_json::from_str(&line) @@ -78,7 +79,10 @@ impl> RpcMessage::RpcRequest(req) => { self.handle_request(req.id, &req.method, req.params).await?; } - _ => {} + _ => { + warn!("Unexpected message received: {:?}", msg); + continue; + } } } } diff --git a/crates/wasmi-plugin-rt/Cargo.toml b/crates/wasmi-plugin-rt/Cargo.toml index da7175c..0d51dea 100644 --- a/crates/wasmi-plugin-rt/Cargo.toml +++ b/crates/wasmi-plugin-rt/Cargo.toml @@ -13,9 +13,11 @@ categories.workspace = true exclude.workspace = true [dependencies] +web-time = { workspace = true } [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] gloo-timers = { workspace = true } +web-sys = { workspace = true } [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] tokio = { workspace = true } diff --git a/crates/wasmi-plugin-rt/src/lib.rs b/crates/wasmi-plugin-rt/src/lib.rs index 259ea2b..ce4907f 100644 --- a/crates/wasmi-plugin-rt/src/lib.rs +++ b/crates/wasmi-plugin-rt/src/lib.rs @@ -9,3 +9,31 @@ pub fn yield_now() -> impl std::future::Future { tokio::task::yield_now() } } + +pub async fn sleep(dur: web_time::Duration) { + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + { + gloo_timers::future::TimeoutFuture::new(dur.as_millis() as u32).await; + } + + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + { + tokio::time::sleep(dur).await; + } +} + +pub fn blocking_sleep(dur: web_time::Duration) { + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + { + let stn = web_sys::js_sys::SharedArrayBuffer::new(4); + let typed_array = web_sys::js_sys::Int32Array::new(&stn); + + let millis = dur.as_millis() as f64; + let _ = web_sys::js_sys::Atomics::wait_with_timeout(&typed_array, 0, 0, millis); + } + + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + { + std::thread::sleep(dur); + } +} diff --git a/makefile b/makefile index 8ade62c..57a23e2 100644 --- a/makefile +++ b/makefile @@ -1,13 +1,20 @@ # === Configuration === BROWSER := --firefox -.PHONY: help build-plugin build-worker build-all bench-wasm +.PHONY: help build-plugin build-coordinator build-all bench-wasm build-plugin: @echo "--- Building Plugin WASM ---" cargo build --target wasm32-wasip1 -p test-plugin --release -build-all: build-plugin +build-coordinator: + @echo "--- Building Coordinator WASM ---" + cd crates/wasmi-plugin-coordinator && wasm-pack build \ + --target web \ + --out-dir pkg \ + --release \ + +build-all: build-plugin build-coordinator clippy-native: build-all @echo "--- Running Clippy Lints ---" @@ -16,7 +23,6 @@ clippy-native: build-all clippy-wasm: build-all @echo "--- Running Clippy Lints for Wasm ---" cargo clippy \ - -Z build-std=std,panic_abort \ -p wasmi-plugin-hdk \ --target wasm32-unknown-unknown \ -- -D warnings @@ -32,13 +38,11 @@ bench-native: build-plugin test-wasm: build-all @echo "--- Running Wasm Tests ---" cargo test \ - -Z build-std=std,panic_abort \ -p wasmi-plugin-hdk \ --target wasm32-unknown-unknown bench-wasm: build-all @echo "--- Running Wasm Benchmarks ---" cargo bench \ - -Z build-std=std,panic_abort \ -p wasmi-plugin-hdk \ --target wasm32-unknown-unknown \ No newline at end of file diff --git a/shell.nix b/shell.nix index 877edc6..0ad7766 100644 --- a/shell.nix +++ b/shell.nix @@ -8,6 +8,7 @@ let extensions = [ "rust-src" ]; targets = [ "wasm32-wasip1" + "wasm32-unknown-unknown" ]; }; wasm-bindgen-cli_0_2_106 = pkgs.callPackage ./flakes/wasm-bindgen-cli.nix { }; @@ -29,8 +30,7 @@ pkgs.mkShell { shellHook = '' export WASM_BINDGEN_THREADS_HEADERS=1 - export CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner - export WASM_BINDGEN_TEST_TIMEOUT=300 + export WASM_BINDGEN_TEST_TIMEOUT=100 export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library" ''; } From 732fa5da9f4021bb390d2dd0fb3e946b787ababb Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Tue, 30 Dec 2025 11:57:27 -0500 Subject: [PATCH 50/70] fix: add transfer lists --- crates/wasmi-plugin-coordinator/src/worker.rs | 14 ++++++++++---- .../wasmi-plugin-hdk/src/runtime/worker_runtime.rs | 12 +++++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/crates/wasmi-plugin-coordinator/src/worker.rs b/crates/wasmi-plugin-coordinator/src/worker.rs index 7644c48..29d6725 100644 --- a/crates/wasmi-plugin-coordinator/src/worker.rs +++ b/crates/wasmi-plugin-coordinator/src/worker.rs @@ -218,7 +218,10 @@ fn stdout_proxy(reader: impl AsyncRead + Unpin + 'static, id: InstanceId) { let msg_value = serde_wasm_bindgen::to_value(&stdout_msg).unwrap(); Reflect::set(&msg_value, &"data".into(), &data).unwrap(); let global = js_sys::global().unchecked_into::(); - global.post_message(&msg_value).unwrap(); + let transfer_list = js_sys::Array::of1(&data.buffer()); + global + .post_message_with_transfer(&msg_value, &transfer_list) + .unwrap(); } Err(e) => { error!("Error reading stdout for instance {}: {}", id, e); @@ -240,13 +243,16 @@ fn stderr_proxy(reader: impl AsyncRead + Unpin + 'static, id: InstanceId) { return; } - let js_data = js_sys::Uint8Array::from(data); + let data = js_sys::Uint8Array::from(data); let stderr_msg = CoordinatorMessage::Stderr { id: id.clone() }; let msg_value = serde_wasm_bindgen::to_value(&stderr_msg).unwrap(); + js_sys::Reflect::set(&msg_value, &"data".into(), &data).unwrap(); - js_sys::Reflect::set(&msg_value, &"data".into(), &js_data).unwrap(); + let transfer_list = js_sys::Array::of1(&data.buffer()); let global = js_sys::global().unchecked_into::(); - global.post_message(&msg_value).unwrap(); + global + .post_message_with_transfer(&msg_value, &transfer_list) + .unwrap(); }; loop { diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index 46ef2f8..136bced 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -148,7 +148,6 @@ impl Runtime for &WorkerRuntime { // Notify the coordinator let wasm_bytes_js = js_sys::Uint8Array::from(&compiled.wasm_bytes[..]); - let wasm_bytes_buffer = wasm_bytes_js.buffer(); let wasm_module_js = compiled.js_module.clone(); let spawn_msg = CoordinatorMessage::RunPlugin { id: instance_id }; let msg_value = serde_wasm_bindgen::to_value(&spawn_msg)?; @@ -156,14 +155,13 @@ impl Runtime for &WorkerRuntime { .map_err(SpawnError::Reflect)?; Reflect::set(&msg_value, &"wasm_module".into(), &wasm_module_js) .map_err(SpawnError::Reflect)?; - let transfer = js_sys::Array::new(); - transfer.push(&wasm_bytes_buffer); + let transfer_list = js_sys::Array::of1(&wasm_bytes_js.buffer()); self.inner .lock() .unwrap() .coordinator - .post_message_with_transfer(&msg_value, &transfer) + .post_message_with_transfer(&msg_value, &transfer_list) .map_err(SpawnError::PostMessage)?; // Await the response @@ -317,9 +315,13 @@ fn stdin_proxy( let stdin_msg = CoordinatorMessage::Stdin { id }; let msg_value = serde_wasm_bindgen::to_value(&stdin_msg).unwrap(); Reflect::set(&msg_value, &"data".into(), &data).unwrap(); + let transfer_list = js_sys::Array::of1(&data.buffer()); let inner = inner.lock().unwrap(); - if let Err(e) = inner.coordinator.post_message(&msg_value) { + if let Err(e) = inner + .coordinator + .post_message_with_transfer(&msg_value, &transfer_list) + { error!("Failed to post stdin message: {:?}", e); break; } From 04c6e3c57898ca4c52094dbd3ee1a8aba5091f88 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Tue, 30 Dec 2025 11:58:35 -0500 Subject: [PATCH 51/70] style: clippy --- crates/wasmi-plugin-hdk/src/runtime/coordinator_message.rs | 6 ++++++ crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/wasmi-plugin-hdk/src/runtime/coordinator_message.rs b/crates/wasmi-plugin-hdk/src/runtime/coordinator_message.rs index ac0cbe5..83b7f3e 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/coordinator_message.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/coordinator_message.rs @@ -11,6 +11,12 @@ impl InstanceId { } } +impl Default for InstanceId { + fn default() -> Self { + Self::new() + } +} + impl From for InstanceId { fn from(value: u64) -> Self { Self(value as u32) diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index 136bced..128d1fd 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -63,7 +63,7 @@ unsafe impl Sync for InnerWorkerRuntime {} impl WorkerRuntime { pub fn global() -> &'static WorkerRuntime { - RUNTIME.get_or_init(|| Self::init()) + RUNTIME.get_or_init(Self::init) } fn init() -> Self { From 8151f03c1c0e10c74cbb647350cd2a1e4d23c6e8 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Tue, 30 Dec 2025 12:11:25 -0500 Subject: [PATCH 52/70] refactor: Simplify crates to reduce dep requirements --- Cargo.lock | 25 +++++++++++++++++ Cargo.toml | 6 ++++- .../Cargo.toml | 17 ++++++++++++ .../src/lib.rs} | 0 crates/wasmi-plugin-coordinator/Cargo.toml | 4 ++- crates/wasmi-plugin-coordinator/src/worker.rs | 11 ++++---- crates/wasmi-plugin-hdk/Cargo.toml | 2 ++ crates/wasmi-plugin-hdk/src/lib.rs | 2 -- crates/wasmi-plugin-hdk/src/plugin.rs | 2 +- crates/wasmi-plugin-hdk/src/runtime/mod.rs | 4 --- .../src/runtime/worker_runtime.rs | 4 +-- crates/wasmi-plugin-hdk/src/time.rs | 27 ------------------- crates/wasmi-plugin-hdk/src/wasi/mod.rs | 1 - crates/wasmi-plugin-wasi/Cargo.toml | 27 +++++++++++++++++++ crates/wasmi-plugin-wasi/src/lib.rs | 2 ++ .../src}/non_blocking_pipe.rs | 2 +- .../src}/wasi_ctx.rs | 3 +-- 17 files changed, 92 insertions(+), 47 deletions(-) create mode 100644 crates/wasmi-plugin-coordinator-protocol/Cargo.toml rename crates/{wasmi-plugin-hdk/src/runtime/coordinator_message.rs => wasmi-plugin-coordinator-protocol/src/lib.rs} (100%) delete mode 100644 crates/wasmi-plugin-hdk/src/time.rs delete mode 100644 crates/wasmi-plugin-hdk/src/wasi/mod.rs create mode 100644 crates/wasmi-plugin-wasi/Cargo.toml create mode 100644 crates/wasmi-plugin-wasi/src/lib.rs rename crates/{wasmi-plugin-hdk/src/runtime => wasmi-plugin-wasi/src}/non_blocking_pipe.rs (99%) rename crates/{wasmi-plugin-hdk/src/wasi => wasmi-plugin-wasi/src}/wasi_ctx.rs (99%) diff --git a/Cargo.lock b/Cargo.lock index 882ca11..ebbb4e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2363,10 +2363,20 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "wasmer", + "wasmi-plugin-coordinator-protocol", "wasmi-plugin-hdk", + "wasmi-plugin-wasi", "web-sys", ] +[[package]] +name = "wasmi-plugin-coordinator-protocol" +version = "0.1.0" +dependencies = [ + "getrandom 0.3.4", + "serde", +] + [[package]] name = "wasmi-plugin-hdk" version = "0.1.0" @@ -2389,8 +2399,10 @@ dependencies = [ "wasm-bindgen-futures", "wasm-bindgen-test", "wasmer", + "wasmi-plugin-coordinator-protocol", "wasmi-plugin-pdk", "wasmi-plugin-rt", + "wasmi-plugin-wasi", "web-sys", "web-time", ] @@ -2417,6 +2429,19 @@ dependencies = [ "web-time", ] +[[package]] +name = "wasmi-plugin-wasi" +version = "0.1.0" +dependencies = [ + "futures", + "getrandom 0.3.4", + "thiserror 2.0.17", + "tracing", + "wasmer", + "wasmi-plugin-rt", + "web-time", +] + [[package]] name = "wasmparser" version = "0.224.1" diff --git a/Cargo.toml b/Cargo.toml index 3e99324..b7892c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,12 @@ [workspace] members = [ "crates/test-plugin", + "crates/wasmi-plugin-coordinator", + "crates/wasmi-plugin-coordinator-protocol", "crates/wasmi-plugin-hdk", "crates/wasmi-plugin-pdk", "crates/wasmi-plugin-rt", - "crates/wasmi-plugin-coordinator" + "crates/wasmi-plugin-wasi" ] resolver = "2" @@ -42,8 +44,10 @@ wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" wasm-bindgen-test = "0.3" wasmer = { version = "6", default-features = false } +wasmi-plugin-coordinator-protocol = { version = "0.1.0", path = "./crates/wasmi-plugin-coordinator-protocol" } wasmi-plugin-hdk = { version = "0.1.0", path = "./crates/wasmi-plugin-hdk" } wasmi-plugin-pdk = { version = "0.1.0", path = "./crates/wasmi-plugin-pdk" } wasmi-plugin-rt = { version = "0.1.0", path = "./crates/wasmi-plugin-rt" } +wasmi-plugin-wasi = { version = "0.1.0", path = "./crates/wasmi-plugin-wasi" } web-sys = "0.3" web-time = "1" diff --git a/crates/wasmi-plugin-coordinator-protocol/Cargo.toml b/crates/wasmi-plugin-coordinator-protocol/Cargo.toml new file mode 100644 index 0000000..60a0227 --- /dev/null +++ b/crates/wasmi-plugin-coordinator-protocol/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "wasmi-plugin-coordinator-protocol" +version.workspace = true +rust-version.workspace = true +description = "Runtime utilities for wasmi-plugin-framework" +authors.workspace = true +repository.workspace = true +edition.workspace = true +readme.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true +exclude.workspace = true + +[dependencies] +serde = { workspace = true } +getrandom = { workspace = true } diff --git a/crates/wasmi-plugin-hdk/src/runtime/coordinator_message.rs b/crates/wasmi-plugin-coordinator-protocol/src/lib.rs similarity index 100% rename from crates/wasmi-plugin-hdk/src/runtime/coordinator_message.rs rename to crates/wasmi-plugin-coordinator-protocol/src/lib.rs diff --git a/crates/wasmi-plugin-coordinator/Cargo.toml b/crates/wasmi-plugin-coordinator/Cargo.toml index ebbbc70..bf8786a 100644 --- a/crates/wasmi-plugin-coordinator/Cargo.toml +++ b/crates/wasmi-plugin-coordinator/Cargo.toml @@ -25,5 +25,7 @@ web-sys = { workspace = true, features = ["Worker", "WorkerOptions", "DedicatedW serde = { workspace = true } serde-wasm-bindgen = { workspace = true } wasmi-plugin-hdk = { workspace = true } +wasmi-plugin-wasi = { workspace = true } wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } -tracing-wasm = { workspace = true } \ No newline at end of file +tracing-wasm = { workspace = true } +wasmi-plugin-coordinator-protocol = { workspace = true } \ No newline at end of file diff --git a/crates/wasmi-plugin-coordinator/src/worker.rs b/crates/wasmi-plugin-coordinator/src/worker.rs index 29d6725..c3d9429 100644 --- a/crates/wasmi-plugin-coordinator/src/worker.rs +++ b/crates/wasmi-plugin-coordinator/src/worker.rs @@ -8,11 +8,12 @@ use wasm_bindgen::{ JsCast, prelude::{Closure, wasm_bindgen}, }; -use wasmi_plugin_hdk::{ - runtime::coordinator_message::{CoordinatorMessage, InstanceId}, - runtime::non_blocking_pipe::{NonBlockingPipeWriter, non_blocking_pipe}, - wasi::wasi_ctx::{self, WasiReader}, -}; +use wasmi_plugin_coordinator_protocol::CoordinatorMessage; +use wasmi_plugin_coordinator_protocol::InstanceId; +use wasmi_plugin_wasi::non_blocking_pipe::NonBlockingPipeWriter; +use wasmi_plugin_wasi::non_blocking_pipe::non_blocking_pipe; +use wasmi_plugin_wasi::wasi_ctx; +use wasmi_plugin_wasi::wasi_ctx::WasiReader; use web_sys::{ DedicatedWorkerGlobalScope, MessageEvent, js_sys::{self, Reflect, Uint8Array, WebAssembly}, diff --git a/crates/wasmi-plugin-hdk/Cargo.toml b/crates/wasmi-plugin-hdk/Cargo.toml index 7ffac8a..18485c2 100644 --- a/crates/wasmi-plugin-hdk/Cargo.toml +++ b/crates/wasmi-plugin-hdk/Cargo.toml @@ -22,6 +22,7 @@ tracing = { workspace = true } uuid = { workspace = true } wasmi-plugin-pdk = { workspace = true } wasmi-plugin-rt = { workspace = true } +wasmi-plugin-wasi = { workspace = true } web-time = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] @@ -29,6 +30,7 @@ tokio = { workspace = true } wasmer = { workspace = true, default-features = true } [target.'cfg(target_arch = "wasm32")'.dependencies] +wasmi-plugin-coordinator-protocol = { workspace = true } gloo-timers = { workspace = true } serde-wasm-bindgen = { workspace = true } wasm-bindgen = { workspace = true } diff --git a/crates/wasmi-plugin-hdk/src/lib.rs b/crates/wasmi-plugin-hdk/src/lib.rs index 9cee951..6589ea4 100644 --- a/crates/wasmi-plugin-hdk/src/lib.rs +++ b/crates/wasmi-plugin-hdk/src/lib.rs @@ -5,5 +5,3 @@ pub mod plugin_id; pub mod runtime; pub mod server; pub mod session; -mod time; -pub mod wasi; diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index e164bc7..be3116f 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -10,12 +10,12 @@ use wasmi_plugin_pdk::{ rpc_message::{RpcError, RpcResponse}, transport::AsyncTransport, }; +use wasmi_plugin_rt::sleep; use crate::host_handler::HostHandler; use crate::runtime; use crate::runtime::Runtime; use crate::session::{PluginSession, PluginSessionError}; -use crate::time::sleep; use crate::{compile::Compiled, plugin_id::PluginId}; /// Plugin is an async-capable instance of a plugin diff --git a/crates/wasmi-plugin-hdk/src/runtime/mod.rs b/crates/wasmi-plugin-hdk/src/runtime/mod.rs index cbff676..a7d6c99 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/mod.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/mod.rs @@ -1,5 +1,3 @@ -pub mod non_blocking_pipe; - #[cfg(not(target_arch = "wasm32"))] pub use native_runtime::NativeRuntime; #[cfg(not(target_arch = "wasm32"))] @@ -8,8 +6,6 @@ mod native_runtime; #[cfg(target_arch = "wasm32")] pub use worker_runtime::WorkerRuntime; #[cfg(target_arch = "wasm32")] -pub mod coordinator_message; -#[cfg(target_arch = "wasm32")] mod worker_runtime; use crate::compile::Compiled; diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index 128d1fd..c371f54 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -10,13 +10,13 @@ use thiserror::Error; use tracing::{error, info, warn}; use wasm_bindgen::prelude::Closure; use wasm_bindgen::{JsCast, JsValue}; +use wasmi_plugin_coordinator_protocol::{CoordinatorMessage, InstanceId}; +use wasmi_plugin_wasi::non_blocking_pipe::non_blocking_pipe; use web_sys::js_sys::Reflect; use web_sys::{MessageEvent, Worker, WorkerOptions, WorkerType, js_sys}; use crate::compile::Compiled; use crate::runtime::Runtime; -use crate::runtime::coordinator_message::{CoordinatorMessage, InstanceId}; -use crate::runtime::non_blocking_pipe::non_blocking_pipe; pub struct WorkerRuntime { inner: Arc>, diff --git a/crates/wasmi-plugin-hdk/src/time.rs b/crates/wasmi-plugin-hdk/src/time.rs deleted file mode 100644 index 4c62ac4..0000000 --- a/crates/wasmi-plugin-hdk/src/time.rs +++ /dev/null @@ -1,27 +0,0 @@ -pub async fn sleep(dur: web_time::Duration) { - #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] - { - gloo_timers::future::TimeoutFuture::new(dur.as_millis() as u32).await; - } - - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] - { - tokio::time::sleep(dur).await; - } -} - -pub fn blocking_sleep(dur: web_time::Duration) { - #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] - { - let stn = web_sys::js_sys::SharedArrayBuffer::new(4); - let typed_array = web_sys::js_sys::Int32Array::new(&stn); - - let millis = dur.as_millis() as f64; - let _ = web_sys::js_sys::Atomics::wait_with_timeout(&typed_array, 0, 0, millis); - } - - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] - { - std::thread::sleep(dur); - } -} diff --git a/crates/wasmi-plugin-hdk/src/wasi/mod.rs b/crates/wasmi-plugin-hdk/src/wasi/mod.rs deleted file mode 100644 index d2ad2d6..0000000 --- a/crates/wasmi-plugin-hdk/src/wasi/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod wasi_ctx; diff --git a/crates/wasmi-plugin-wasi/Cargo.toml b/crates/wasmi-plugin-wasi/Cargo.toml new file mode 100644 index 0000000..bdfb988 --- /dev/null +++ b/crates/wasmi-plugin-wasi/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "wasmi-plugin-wasi" +version.workspace = true +rust-version.workspace = true +description = "Runtime utilities for wasmi-plugin-framework" +authors.workspace = true +repository.workspace = true +edition.workspace = true +readme.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true +exclude.workspace = true + +[dependencies] +web-time = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +wasmi-plugin-rt = { workspace = true } +getrandom = { workspace = true } +futures = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +wasmer = { workspace = true, default-features = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } \ No newline at end of file diff --git a/crates/wasmi-plugin-wasi/src/lib.rs b/crates/wasmi-plugin-wasi/src/lib.rs new file mode 100644 index 0000000..aefd07f --- /dev/null +++ b/crates/wasmi-plugin-wasi/src/lib.rs @@ -0,0 +1,2 @@ +pub mod non_blocking_pipe; +pub mod wasi_ctx; diff --git a/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs b/crates/wasmi-plugin-wasi/src/non_blocking_pipe.rs similarity index 99% rename from crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs rename to crates/wasmi-plugin-wasi/src/non_blocking_pipe.rs index b34bb22..5210c1c 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/non_blocking_pipe.rs +++ b/crates/wasmi-plugin-wasi/src/non_blocking_pipe.rs @@ -12,7 +12,7 @@ use std::task::{Context, Poll, Waker}; use futures::{AsyncRead, AsyncWrite}; -use crate::wasi::wasi_ctx::WasiReader; +use crate::wasi_ctx::WasiReader; struct Inner { buf: VecDeque, diff --git a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs b/crates/wasmi-plugin-wasi/src/wasi_ctx.rs similarity index 99% rename from crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs rename to crates/wasmi-plugin-wasi/src/wasi_ctx.rs index fb95e95..e67790a 100644 --- a/crates/wasmi-plugin-hdk/src/wasi/wasi_ctx.rs +++ b/crates/wasmi-plugin-wasi/src/wasi_ctx.rs @@ -2,10 +2,9 @@ use std::io::{BufReader, Read, Write}; use thiserror::Error; use tracing::{error, info, trace, warn}; use wasmer::{FunctionEnvMut, RuntimeError}; +use wasmi_plugin_rt::blocking_sleep; use web_time::{Duration, SystemTime}; -use crate::time::blocking_sleep; - pub trait WasiReader: Read + Send + Sync { /// Blocks the current thread until the reader is ready to read, or the timeout is reached. fn wait_ready(&self, timeout: Option); From fa6912001b821da809e7403cb88038b53891b0cb Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Tue, 30 Dec 2025 12:17:12 -0500 Subject: [PATCH 53/70] style: tidy --- Cargo.lock | 2 -- Cargo.toml | 2 +- crates/wasmi-plugin-coordinator-protocol/Cargo.toml | 2 +- crates/wasmi-plugin-coordinator/Cargo.toml | 13 ++++++------- crates/wasmi-plugin-hdk/Cargo.toml | 3 +-- .../wasmi-plugin-hdk/src/runtime/native_runtime.rs | 4 ++-- crates/wasmi-plugin-wasi/Cargo.toml | 8 ++++---- shell.nix | 1 + 8 files changed, 16 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ebbb4e4..d2a08ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2364,7 +2364,6 @@ dependencies = [ "wasm-bindgen-futures", "wasmer", "wasmi-plugin-coordinator-protocol", - "wasmi-plugin-hdk", "wasmi-plugin-wasi", "web-sys", ] @@ -2383,7 +2382,6 @@ version = "0.1.0" dependencies = [ "criterion", "futures", - "getrandom 0.3.4", "gloo-timers", "serde", "serde-wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index b7892c0..6f3bb49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ "crates/wasmi-plugin-hdk", "crates/wasmi-plugin-pdk", "crates/wasmi-plugin-rt", - "crates/wasmi-plugin-wasi" + "crates/wasmi-plugin-wasi", ] resolver = "2" diff --git a/crates/wasmi-plugin-coordinator-protocol/Cargo.toml b/crates/wasmi-plugin-coordinator-protocol/Cargo.toml index 60a0227..53788df 100644 --- a/crates/wasmi-plugin-coordinator-protocol/Cargo.toml +++ b/crates/wasmi-plugin-coordinator-protocol/Cargo.toml @@ -13,5 +13,5 @@ categories.workspace = true exclude.workspace = true [dependencies] -serde = { workspace = true } getrandom = { workspace = true } +serde = { workspace = true } diff --git a/crates/wasmi-plugin-coordinator/Cargo.toml b/crates/wasmi-plugin-coordinator/Cargo.toml index bf8786a..b54f778 100644 --- a/crates/wasmi-plugin-coordinator/Cargo.toml +++ b/crates/wasmi-plugin-coordinator/Cargo.toml @@ -17,15 +17,14 @@ crate-type = ["cdylib"] [target.'cfg(target_arch = "wasm32")'.dependencies] futures = { workspace = true } +serde = { workspace = true } +serde-wasm-bindgen = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } +tracing-wasm = { workspace = true } wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } -web-sys = { workspace = true, features = ["Worker", "WorkerOptions", "DedicatedWorkerGlobalScope", "WorkerType", "MessageEvent", "ErrorEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } -serde = { workspace = true } -serde-wasm-bindgen = { workspace = true } -wasmi-plugin-hdk = { workspace = true } -wasmi-plugin-wasi = { workspace = true } wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } -tracing-wasm = { workspace = true } -wasmi-plugin-coordinator-protocol = { workspace = true } \ No newline at end of file +wasmi-plugin-coordinator-protocol = { workspace = true } +wasmi-plugin-wasi = { workspace = true } +web-sys = { workspace = true, features = ["Worker", "WorkerOptions", "DedicatedWorkerGlobalScope", "WorkerType", "MessageEvent", "ErrorEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } diff --git a/crates/wasmi-plugin-hdk/Cargo.toml b/crates/wasmi-plugin-hdk/Cargo.toml index 18485c2..338b8b5 100644 --- a/crates/wasmi-plugin-hdk/Cargo.toml +++ b/crates/wasmi-plugin-hdk/Cargo.toml @@ -14,7 +14,6 @@ exclude.workspace = true [dependencies] futures = { workspace = true } -getrandom = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } @@ -30,12 +29,12 @@ tokio = { workspace = true } wasmer = { workspace = true, default-features = true } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasmi-plugin-coordinator-protocol = { workspace = true } gloo-timers = { workspace = true } serde-wasm-bindgen = { workspace = true } wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } +wasmi-plugin-coordinator-protocol = { workspace = true } web-sys = { workspace = true, features = ["Worker", "WorkerOptions", "DedicatedWorkerGlobalScope", "WorkerType", "MessageEvent", "ErrorEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } [dev-dependencies] diff --git a/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs index bb8dd7e..387000b 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs @@ -13,10 +13,10 @@ use wasmer::Store; use crate::compile::Compiled; use crate::runtime::Runtime; -use crate::runtime::non_blocking_pipe::{ +use wasmi_plugin_wasi::non_blocking_pipe::{ NonBlockingPipeReader, NonBlockingPipeWriter, non_blocking_pipe, }; -use crate::wasi::wasi_ctx::WasiCtx; +use wasmi_plugin_wasi::wasi_ctx::WasiCtx; pub struct NativeRuntime { sessions: Arc>>, diff --git a/crates/wasmi-plugin-wasi/Cargo.toml b/crates/wasmi-plugin-wasi/Cargo.toml index bdfb988..bf8d767 100644 --- a/crates/wasmi-plugin-wasi/Cargo.toml +++ b/crates/wasmi-plugin-wasi/Cargo.toml @@ -13,15 +13,15 @@ categories.workspace = true exclude.workspace = true [dependencies] -web-time = { workspace = true } +futures = { workspace = true } +getrandom = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } wasmi-plugin-rt = { workspace = true } -getrandom = { workspace = true } -futures = { workspace = true } +web-time = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] wasmer = { workspace = true, default-features = true } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } \ No newline at end of file +wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } diff --git a/shell.nix b/shell.nix index 0ad7766..556681a 100644 --- a/shell.nix +++ b/shell.nix @@ -19,6 +19,7 @@ pkgs.mkShell { rust-analyzer cargo-sort cargo-machete + cargo-udeps samply binaryen wasm-pack From 995fb5f61f0547e0c4bfb66d1c4aa3df660752a0 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Tue, 30 Dec 2025 12:18:27 -0500 Subject: [PATCH 54/70] style: remove depricated CI step --- .github/workflows/ci.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75ee8bb..6697902 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,12 +31,6 @@ jobs: - name: Magic Nix Cache uses: DeterminateSystems/magic-nix-cache-action@v8 - - name: Create dummy assets to satisfy compiler - run: | - mkdir -p crates/worker/pkg - touch crates/worker/pkg/worker.js - touch crates/worker/pkg/worker_bg.wasm - - run: nix-shell --run "make test-wasm" - run: nix-shell --run "make bench-wasm" @@ -49,11 +43,5 @@ jobs: - name: Magic Nix Cache uses: DeterminateSystems/magic-nix-cache-action@v8 - - name: Create dummy assets to satisfy compiler - run: | - mkdir -p crates/worker/pkg - touch crates/worker/pkg/worker.js - touch crates/worker/pkg/worker_bg.wasm - - run: nix-shell --run "make clippy-native" - run: nix-shell --run "make clippy-wasm" \ No newline at end of file From 7efa2329af7dacda082b4e6871ecf75985889256 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Tue, 30 Dec 2025 12:23:25 -0500 Subject: [PATCH 55/70] ci: add rust cache --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6697902..b7c2684 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ jobs: uses: DeterminateSystems/nix-installer-action@v14 - name: Magic Nix Cache uses: DeterminateSystems/magic-nix-cache-action@v8 + - name: Rust Cache + uses: Swatinem/rust-cache@v2 - run: nix-shell --run "make test-native" - run: nix-shell --run "make bench-native" @@ -30,6 +32,8 @@ jobs: uses: DeterminateSystems/nix-installer-action@v14 - name: Magic Nix Cache uses: DeterminateSystems/magic-nix-cache-action@v8 + - name: Rust Cache + uses: Swatinem/rust-cache@v2 - run: nix-shell --run "make test-wasm" - run: nix-shell --run "make bench-wasm" @@ -42,6 +46,8 @@ jobs: uses: DeterminateSystems/nix-installer-action@v14 - name: Magic Nix Cache uses: DeterminateSystems/magic-nix-cache-action@v8 + - name: Rust Cache + uses: Swatinem/rust-cache@v2 - run: nix-shell --run "make clippy-native" - run: nix-shell --run "make clippy-wasm" \ No newline at end of file From 0b9bea476d5146fdd5c75f0e261ef29e5e20ad07 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Tue, 30 Dec 2025 13:57:20 -0500 Subject: [PATCH 56/70] style: logs --- crates/wasmi-plugin-coordinator/src/worker.js | 5 ---- crates/wasmi-plugin-coordinator/src/worker.rs | 4 ++- .../src/worker_pool/pool.rs | 25 ++++++++++++------- .../src/worker_pool/worker.js | 2 +- .../src/worker_pool/worker.rs | 17 +++++++++++-- crates/wasmi-plugin-hdk/src/plugin.rs | 2 +- 6 files changed, 36 insertions(+), 19 deletions(-) diff --git a/crates/wasmi-plugin-coordinator/src/worker.js b/crates/wasmi-plugin-coordinator/src/worker.js index d509f07..8e98771 100644 --- a/crates/wasmi-plugin-coordinator/src/worker.js +++ b/crates/wasmi-plugin-coordinator/src/worker.js @@ -6,15 +6,10 @@ // self.postMessage({ type: 'Log', message: `${args.join(' ')}` }); // }; -console.log("Worker JS initialized"); - import init, { start_coordinator } from "{sdk_url}"; -console.log("JS Glue loaded, initializing wasm..."); - init('{wasm_url}').then(() => { start_coordinator(); - console.log("Wasm module loaded, starting worker..."); }).catch((err) => { console.error("Worker Init Failed:", err); }); diff --git a/crates/wasmi-plugin-coordinator/src/worker.rs b/crates/wasmi-plugin-coordinator/src/worker.rs index c3d9429..ff063a3 100644 --- a/crates/wasmi-plugin-coordinator/src/worker.rs +++ b/crates/wasmi-plugin-coordinator/src/worker.rs @@ -30,7 +30,7 @@ struct CoordinatorState { enum RunError { #[error("WASI Error: {0}")] Wasi(#[from] wasi_ctx::WasiError), - #[error("Runtime Error: {0}")] + #[error("Wasmer Runtime Error: {0}")] Runtime(#[from] wasmer::RuntimeError), #[error("Compile Error: {0}")] Compile(#[from] wasmer::CompileError), @@ -45,6 +45,7 @@ pub async fn start_coordinator() { .build(), ); + info!("Starting Wasmi Plugin Coordinator..."); let global = js_sys::global().unchecked_into::(); let state = CoordinatorState { @@ -62,6 +63,7 @@ pub async fn start_coordinator() { onmessage.forget(); // Send ready message to main thread + info!("Coordinator ready"); let ready_msg = CoordinatorMessage::Ready; let ready_value = serde_wasm_bindgen::to_value(&ready_msg).unwrap(); global.post_message(&ready_value).unwrap(); diff --git a/crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs b/crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs index 60a2037..bf3c999 100644 --- a/crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs +++ b/crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs @@ -45,6 +45,10 @@ pub struct WorkerHandle { pub enum SpawnError { #[error("JavaScript error: {0:?}")] JsError(JsValue), + #[error("PostMessage error: {0:?}")] + PostMessageError(JsValue), + #[error("Reflect error: {0:?}")] + ReflectError(JsValue), } static GLOBAL_POOL: OnceLock = OnceLock::new(); @@ -125,6 +129,7 @@ impl WorkerPool { impl WorkerHandle { pub fn spawn(id: u64) -> Result { let name = format!("worker-{}", id); + info!("Spawning worker {}", id); // 2. Create the worker let options = WorkerOptions::new(); @@ -145,12 +150,18 @@ impl WorkerHandle { worker.set_onerror(Some(on_error.as_ref().unchecked_ref())); // 4. Send the "init" message with memory and module + info!("Initializing worker {}...", id); let msg = js_sys::Object::new(); - Reflect::set(&msg, &"type".into(), &"init".into()).unwrap(); - Reflect::set(&msg, &"memory".into(), &wasm_bindgen::memory()).unwrap(); - Reflect::set(&msg, &"module".into(), ¤t_module()).unwrap(); - Reflect::set(&msg, &"sdkUrl".into(), &sdk_url().into()).unwrap(); - worker.post_message(&msg).unwrap(); + Reflect::set(&msg, &"type".into(), &"init".into()).map_err(SpawnError::ReflectError)?; + Reflect::set(&msg, &"memory".into(), &wasm_bindgen::memory()) + .map_err(SpawnError::ReflectError)?; + Reflect::set(&msg, &"module".into(), ¤t_module()) + .map_err(SpawnError::ReflectError)?; + Reflect::set(&msg, &"sdkUrl".into(), &sdk_url().into()) + .map_err(SpawnError::ReflectError)?; + worker + .post_message(&msg) + .map_err(SpawnError::PostMessageError)?; info!("Spawned worker {}", id); Ok(WorkerHandle { @@ -246,10 +257,6 @@ fn sdk_url() -> String { static IMPORT_META_URL: String; } - info!( - "SDK URL: {}", - IMPORT_META_URL.with(|url| url.clone().to_ascii_lowercase()) - ); IMPORT_META_URL.with(|url| url.clone()) } diff --git a/crates/wasmi-plugin-coordinator/src/worker_pool/worker.js b/crates/wasmi-plugin-coordinator/src/worker_pool/worker.js index 1a8c26e..53f54bd 100644 --- a/crates/wasmi-plugin-coordinator/src/worker_pool/worker.js +++ b/crates/wasmi-plugin-coordinator/src/worker_pool/worker.js @@ -11,7 +11,6 @@ // }; // }); -console.log("Worker started"); let wasmPkg = null; let processingQueue = Promise.resolve(); @@ -29,6 +28,7 @@ globalThis.onmessage = async ev => { } await init(module, memory); + wasmPkg.init_compute_worker(); } else if (ev.data.type === "run") { const { taskPtr } = ev.data; try { diff --git a/crates/wasmi-plugin-coordinator/src/worker_pool/worker.rs b/crates/wasmi-plugin-coordinator/src/worker_pool/worker.rs index c8fff1d..ae2ba32 100644 --- a/crates/wasmi-plugin-coordinator/src/worker_pool/worker.rs +++ b/crates/wasmi-plugin-coordinator/src/worker_pool/worker.rs @@ -3,8 +3,22 @@ use tracing::{error, info}; use wasm_bindgen::{JsValue, prelude::wasm_bindgen}; use web_sys::js_sys; +#[wasm_bindgen] +pub fn init_compute_worker() { + tracing_wasm::set_as_global_default_with_config( + tracing_wasm::WASMLayerConfigBuilder::new() + .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) + .set_max_level(tracing::Level::INFO) + .build(), + ); + + info!("Compute worker initialized"); +} + #[wasm_bindgen] pub fn execute_worker_task(ptr: u32) -> js_sys::Promise { + info!("Executing worker task..."); + type Task = Box LocalBoxFuture<'static, ()> + Send>; let task: Box = unsafe { Box::from_raw(ptr as *mut Task) }; let fut = task(); @@ -19,8 +33,7 @@ pub fn execute_worker_task(ptr: u32) -> js_sys::Promise { #[wasm_bindgen] pub fn execute_worker_task_with(ptr: u32, extra: JsValue) -> js_sys::Promise { - web_sys::console::log_2(&"Rust received extra:".into(), &extra); - info!("Rust extra type: {:?}", extra.js_typeof()); + info!("Executing worker task with extra..."); type Task = Box LocalBoxFuture<'static, ()> + Send>; let task: Box = unsafe { Box::from_raw(ptr as *mut Task) }; diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index be3116f..e481986 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -54,7 +54,7 @@ impl PluginLogger for DefaultPluginLogger {} pub enum PluginError { #[error("Session error")] SessionError(#[from] PluginSessionError), - #[error("Runtime Error")] + #[error("Runtime Error: {0}")] RuntimeError(#[from] Box), #[error("Plugin timeout")] PluginTimeout, From edda933f59495d14b6d9e40649ac9c918193ba4a Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Tue, 30 Dec 2025 17:34:22 -0500 Subject: [PATCH 57/70] refactor: improve --- Cargo.lock | 12 +++ crates/wasmi-plugin-coordinator/Cargo.toml | 2 + crates/wasmi-plugin-coordinator/src/worker.js | 2 + crates/wasmi-plugin-coordinator/src/worker.rs | 97 +++++++++---------- .../src/worker_pool/pool.rs | 17 +--- .../src/worker_pool/worker.js | 1 - .../src/worker_pool/worker.rs | 23 ++--- .../src/runtime/native_runtime.rs | 2 +- .../src/runtime/worker_runtime.rs | 9 +- crates/wasmi-plugin-hdk/src/server.rs | 4 + crates/wasmi-plugin-hdk/src/session.rs | 3 +- crates/wasmi-plugin-pdk/src/runner.rs | 5 +- crates/wasmi-plugin-pdk/src/transport.rs | 7 +- crates/wasmi-plugin-wasi/src/wasi_ctx.rs | 3 - 14 files changed, 96 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2a08ab..461bba6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,6 +304,16 @@ dependencies = [ "cc", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "convert_case" version = "0.10.0" @@ -2354,11 +2364,13 @@ dependencies = [ name = "wasmi-plugin-coordinator" version = "0.1.0" dependencies = [ + "console_error_panic_hook", "futures", "serde", "serde-wasm-bindgen", "thiserror 2.0.17", "tracing", + "tracing-subscriber", "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/crates/wasmi-plugin-coordinator/Cargo.toml b/crates/wasmi-plugin-coordinator/Cargo.toml index b54f778..16d874c 100644 --- a/crates/wasmi-plugin-coordinator/Cargo.toml +++ b/crates/wasmi-plugin-coordinator/Cargo.toml @@ -16,11 +16,13 @@ exclude.workspace = true crate-type = ["cdylib"] [target.'cfg(target_arch = "wasm32")'.dependencies] +console_error_panic_hook = "0.1.7" futures = { workspace = true } serde = { workspace = true } serde-wasm-bindgen = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } +tracing-subscriber = { workspace = true } tracing-wasm = { workspace = true } wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } diff --git a/crates/wasmi-plugin-coordinator/src/worker.js b/crates/wasmi-plugin-coordinator/src/worker.js index 8e98771..88cd453 100644 --- a/crates/wasmi-plugin-coordinator/src/worker.js +++ b/crates/wasmi-plugin-coordinator/src/worker.js @@ -6,6 +6,8 @@ // self.postMessage({ type: 'Log', message: `${args.join(' ')}` }); // }; +self.name = "coordinator"; + import init, { start_coordinator } from "{sdk_url}"; init('{wasm_url}').then(() => { diff --git a/crates/wasmi-plugin-coordinator/src/worker.rs b/crates/wasmi-plugin-coordinator/src/worker.rs index ff063a3..07ed5d7 100644 --- a/crates/wasmi-plugin-coordinator/src/worker.rs +++ b/crates/wasmi-plugin-coordinator/src/worker.rs @@ -1,7 +1,8 @@ use std::{cell::RefCell, collections::HashMap, io::Write, rc::Rc, sync::Arc}; +use futures::AsyncBufReadExt; use futures::AsyncRead; -use futures::AsyncReadExt; +use futures::io::BufReader; use thiserror::Error; use tracing::{error, info, warn}; use wasm_bindgen::{ @@ -30,10 +31,12 @@ struct CoordinatorState { enum RunError { #[error("WASI Error: {0}")] Wasi(#[from] wasi_ctx::WasiError), - #[error("Wasmer Runtime Error: {0}")] - Runtime(#[from] wasmer::RuntimeError), #[error("Compile Error: {0}")] Compile(#[from] wasmer::CompileError), + #[error("Wasmer Runtime Error: {0}")] + Runtime(#[from] wasmer::RuntimeError), + #[error("Wasmer Trap Error: {0}")] + Trap(String), } #[wasm_bindgen] @@ -126,7 +129,6 @@ fn on_message(state: &mut CoordinatorState, e: web_sys::MessageEvent) { } fn stdin(state: &mut CoordinatorState, id: InstanceId, data: Uint8Array) { - info!("Received stdin message for id {}", id); let data: Vec = data.to_vec(); if let Some(writer) = state.instance_channels.get_mut(&id) { @@ -168,22 +170,20 @@ fn run_plugin( let pool = WorkerPool::global(); pool.run_with(&wasm_module, move |extra| async move { - info!("Running Wasm instance for id {}", id); let res = run_instance( extra, wasm_bytes, stdin_reader, stdout_writer, stderr_writer, - ) - .await; + ); if let Err(e) = res { error!("Error running Wasm instance: {}", e); } }) } -async fn run_instance( +fn run_instance( js_module: wasm_bindgen::JsValue, wasm_bytes: Vec, stdin: impl WasiReader + 'static, @@ -200,27 +200,36 @@ async fn run_instance( .set_stdout(stdout) .set_stderr(stderr); - info!("Starting WASI instance..."); let start = wasi_ctx.into_fn(&mut store, &module)?; - start.call(&mut store, &[])?; - - Ok(()) + match start.call(&mut store, &[]) { + Ok(_) => Ok(()), + Err(e) => { + if let Some(trap) = e.clone().to_trap() { + Err(RunError::Trap(trap.message().to_string())) + } else { + Err(RunError::Runtime(e)) + } + } + } } fn stdout_proxy(reader: impl AsyncRead + Unpin + 'static, id: InstanceId) { wasm_bindgen_futures::spawn_local(async move { - let mut reader = reader; - let mut buffer = [0u8; 1024]; + let mut reader = BufReader::new(reader); + let mut line = String::new(); loop { - match reader.read(&mut buffer).await { + line.clear(); + match reader.read_line(&mut line).await { Ok(0) => break, // EOF - Ok(n) => { - let data = js_sys::Uint8Array::from(&buffer[..n]); + Ok(_) => { + let global = js_sys::global().unchecked_into::(); + + let data = js_sys::Uint8Array::from(line.as_bytes()); let stdout_msg = CoordinatorMessage::Stdout { id: id.clone() }; let msg_value = serde_wasm_bindgen::to_value(&stdout_msg).unwrap(); Reflect::set(&msg_value, &"data".into(), &data).unwrap(); - let global = js_sys::global().unchecked_into::(); + let transfer_list = js_sys::Array::of1(&data.buffer()); global .post_message_with_transfer(&msg_value, &transfer_list) @@ -237,46 +246,28 @@ fn stdout_proxy(reader: impl AsyncRead + Unpin + 'static, id: InstanceId) { fn stderr_proxy(reader: impl AsyncRead + Unpin + 'static, id: InstanceId) { wasm_bindgen_futures::spawn_local(async move { - let mut reader = reader; - let mut chunk_buffer = [0u8; 1024]; - let mut accumulated_data = Vec::with_capacity(1024); - - let send_buffer = |data: &[u8]| { - if data.is_empty() { - return; - } + let mut reader = BufReader::new(reader); + let mut line = String::new(); - let data = js_sys::Uint8Array::from(data); - let stderr_msg = CoordinatorMessage::Stderr { id: id.clone() }; - let msg_value = serde_wasm_bindgen::to_value(&stderr_msg).unwrap(); - js_sys::Reflect::set(&msg_value, &"data".into(), &data).unwrap(); + loop { + line.clear(); + match reader.read_line(&mut line).await { + Ok(0) => break, // EOF + Ok(_) => { + let global = js_sys::global().unchecked_into::(); - let transfer_list = js_sys::Array::of1(&data.buffer()); - let global = js_sys::global().unchecked_into::(); - global - .post_message_with_transfer(&msg_value, &transfer_list) - .unwrap(); - }; + let data = js_sys::Uint8Array::from(line.as_bytes()); + let stdout_msg = CoordinatorMessage::Stderr { id: id.clone() }; + let msg_value = serde_wasm_bindgen::to_value(&stdout_msg).unwrap(); + Reflect::set(&msg_value, &"data".into(), &data).unwrap(); - loop { - match reader.read(&mut chunk_buffer).await { - Ok(0) => { - // EOF: Send any remaining data in the buffer - send_buffer(&accumulated_data); - break; - } - Ok(n) => { - accumulated_data.extend_from_slice(&chunk_buffer[..n]); - - // If we've hit or exceeded 1 KiB, send it and clear - if accumulated_data.len() >= 1024 { - send_buffer(&accumulated_data); - accumulated_data.clear(); - } + let transfer_list = js_sys::Array::of1(&data.buffer()); + global + .post_message_with_transfer(&msg_value, &transfer_list) + .unwrap(); } Err(e) => { - error!("Error reading stderr for instance {}: {}", id, e); - send_buffer(&accumulated_data); + error!("Error reading stdout for instance {}: {}", id, e); break; } } diff --git a/crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs b/crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs index bf3c999..66f3395 100644 --- a/crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs +++ b/crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs @@ -129,14 +129,13 @@ impl WorkerPool { impl WorkerHandle { pub fn spawn(id: u64) -> Result { let name = format!("worker-{}", id); - info!("Spawning worker {}", id); // 2. Create the worker let options = WorkerOptions::new(); options.set_name(&name); options.set_type(WorkerType::Module); let worker = - Worker::new_with_options(&worker_url(), &options).map_err(SpawnError::JsError)?; + Worker::new_with_options(&worker_url(id), &options).map_err(SpawnError::JsError)?; // 3. Set up onmessage handler let state = Arc::new(Mutex::new(WorkerState::Busy)); @@ -150,7 +149,6 @@ impl WorkerHandle { worker.set_onerror(Some(on_error.as_ref().unchecked_ref())); // 4. Send the "init" message with memory and module - info!("Initializing worker {}...", id); let msg = js_sys::Object::new(); Reflect::set(&msg, &"type".into(), &"init".into()).map_err(SpawnError::ReflectError)?; Reflect::set(&msg, &"memory".into(), &wasm_bindgen::memory()) @@ -201,12 +199,6 @@ impl WorkerHandle { F: FnOnce(wasm_bindgen::JsValue) -> Fut + Send + 'static, Fut: Future + 'static, { - info!("run_with: extra type = {:?}", extra.js_typeof()); - info!( - "run_with: is Module = {:?}", - extra.is_instance_of::() - ); - // 1. Box the function and get a raw pointer let box_f: Box< Box LocalBoxFuture<'static, ()> + Send>, @@ -217,7 +209,6 @@ impl WorkerHandle { let worker = &self.worker; // 3. Send the "run" message with raw ptr - info!("Sending run_with message to worker {}", self.id); let msg = js_sys::Object::new(); Reflect::set(&msg, &"type".into(), &"run_with".into()).unwrap(); Reflect::set(&msg, &"taskPtr".into(), &ptr.into()).unwrap(); @@ -228,15 +219,15 @@ impl WorkerHandle { } pub fn terminate(&self) { - info!("Terminating worker {}", self.id); self.worker.terminate(); let mut state = self.state.lock().unwrap(); *state = WorkerState::Terminated; } } -fn worker_url() -> String { - let script = include_str!("./worker.js"); +fn worker_url(id: u64) -> String { + let mut script = include_str!("./worker.js").to_string(); + script.push_str(format!("\n//# sourceURL=worker-{}.js", id).as_str()); let options = web_sys::BlobPropertyBag::new(); options.set_type("application/javascript"); diff --git a/crates/wasmi-plugin-coordinator/src/worker_pool/worker.js b/crates/wasmi-plugin-coordinator/src/worker_pool/worker.js index 53f54bd..e6aa74c 100644 --- a/crates/wasmi-plugin-coordinator/src/worker_pool/worker.js +++ b/crates/wasmi-plugin-coordinator/src/worker_pool/worker.js @@ -38,7 +38,6 @@ globalThis.onmessage = async ev => { } } else if (ev.data.type === "run_with") { const { taskPtr, extra } = ev.data; - console.log("(JS) extra:", extra, typeof extra); try { await wasmPkg.execute_worker_task_with(taskPtr, extra); } finally { diff --git a/crates/wasmi-plugin-coordinator/src/worker_pool/worker.rs b/crates/wasmi-plugin-coordinator/src/worker_pool/worker.rs index ae2ba32..229e379 100644 --- a/crates/wasmi-plugin-coordinator/src/worker_pool/worker.rs +++ b/crates/wasmi-plugin-coordinator/src/worker_pool/worker.rs @@ -1,24 +1,24 @@ use futures::{FutureExt, future::LocalBoxFuture}; -use tracing::{error, info}; +use tracing::error; +use tracing_subscriber::layer::SubscriberExt; use wasm_bindgen::{JsValue, prelude::wasm_bindgen}; use web_sys::js_sys; #[wasm_bindgen] pub fn init_compute_worker() { - tracing_wasm::set_as_global_default_with_config( - tracing_wasm::WASMLayerConfigBuilder::new() - .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) - .set_max_level(tracing::Level::INFO) - .build(), + console_error_panic_hook::set_once(); + + let config = tracing_wasm::WASMLayerConfigBuilder::new() + .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) + .set_max_level(tracing::Level::INFO) + .build(); + let _ = tracing::subscriber::set_global_default( + tracing_subscriber::Registry::default().with(tracing_wasm::WASMLayer::new(config)), ); - - info!("Compute worker initialized"); } #[wasm_bindgen] pub fn execute_worker_task(ptr: u32) -> js_sys::Promise { - info!("Executing worker task..."); - type Task = Box LocalBoxFuture<'static, ()> + Send>; let task: Box = unsafe { Box::from_raw(ptr as *mut Task) }; let fut = task(); @@ -33,14 +33,11 @@ pub fn execute_worker_task(ptr: u32) -> js_sys::Promise { #[wasm_bindgen] pub fn execute_worker_task_with(ptr: u32, extra: JsValue) -> js_sys::Promise { - info!("Executing worker task with extra..."); - type Task = Box LocalBoxFuture<'static, ()> + Send>; let task: Box = unsafe { Box::from_raw(ptr as *mut Task) }; let fut = task(extra); wasm_bindgen_futures::future_to_promise(async move { - info!("Executing worker task with extra..."); if let Err(e) = std::panic::AssertUnwindSafe(fut).catch_unwind().await { error!("Panic in thread worker: {:?}", e); } diff --git a/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs index 387000b..7fd91a9 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/native_runtime.rs @@ -29,7 +29,7 @@ struct NativeSession { #[derive(Debug, Error)] pub enum NativeRuntimeError { - #[error("Wasmer runtime error: {0}")] + #[error("Wasmer Runtime Error: {0}")] WasmerError(#[from] wasmer::RuntimeError), #[error("IO error: {0}")] IOError(#[from] std::io::Error), diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index c371f54..76f3ecc 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -209,8 +209,9 @@ fn worker_url() -> Result { let options = web_sys::BlobPropertyBag::new(); options.set_type("text/javascript"); + let sdk_script = format!("{}\n//# sourceURL=coordinator.js", WORKER_JS_GLUE); let sdk_blob = web_sys::Blob::new_with_u8_array_sequence_and_options( - &js_sys::Array::of1(&js_sys::JsString::from(WORKER_JS_GLUE)), + &js_sys::Array::of1(&js_sys::JsString::from(sdk_script)), &options, )?; let sdk_url = web_sys::Url::create_object_url_with_blob(&sdk_blob)?; @@ -274,7 +275,11 @@ fn on_message(inner: Arc>, e: web_sys::MessageEvent) { error!("Failed to write to stderr pipe: {}", e); } } else { - warn!("Received stderr for unknown instance ID {:?}", id); + warn!( + "Received stderr for unknown instance ID {:?}: {}", + id, + String::from_utf8_lossy(&data) + ); } } CoordinatorMessage::RunPluginResp { id, status } => { diff --git a/crates/wasmi-plugin-hdk/src/server.rs b/crates/wasmi-plugin-hdk/src/server.rs index e68fae1..5c08058 100644 --- a/crates/wasmi-plugin-hdk/src/server.rs +++ b/crates/wasmi-plugin-hdk/src/server.rs @@ -23,6 +23,10 @@ impl Default for HostServer { impl HostServer { pub fn new(state: S) -> Self { + std::panic::set_hook(Box::new(|panic_info| { + eprintln!("GUEST PANIC: {}", panic_info); + })); + Self { router: Router::new(), state, diff --git a/crates/wasmi-plugin-hdk/src/session.rs b/crates/wasmi-plugin-hdk/src/session.rs index 93ebc62..4fc811e 100644 --- a/crates/wasmi-plugin-hdk/src/session.rs +++ b/crates/wasmi-plugin-hdk/src/session.rs @@ -1,7 +1,7 @@ use futures::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, io::BufReader}; use serde_json::Value; use thiserror::Error; -use tracing::{error, info, warn}; +use tracing::{error, warn}; use wasmi_plugin_pdk::{ api::RequestHandler, rpc_message::{RpcError, RpcMessage, RpcResponse}, @@ -65,7 +65,6 @@ impl> } Err(e) => return Err(PluginSessionError::Io(e)), } - info!("PluginSession: Received line: {}", line.trim()); // Process complete lines let msg: RpcMessage = serde_json::from_str(&line) diff --git a/crates/wasmi-plugin-pdk/src/runner.rs b/crates/wasmi-plugin-pdk/src/runner.rs index 03fe602..b28aa80 100644 --- a/crates/wasmi-plugin-pdk/src/runner.rs +++ b/crates/wasmi-plugin-pdk/src/runner.rs @@ -80,7 +80,10 @@ impl PluginRunner { res }, drive_err = driver.run().fuse() => { - panic!("Transport driver exited unexpectedly: {:?}", drive_err); + match drive_err { + Ok(_) => Err(RpcError::custom("Transport driver exited unexpectedly")), + Err(e) => Err(e.into()), + } } } }); diff --git a/crates/wasmi-plugin-pdk/src/transport.rs b/crates/wasmi-plugin-pdk/src/transport.rs index 533d3ab..daf14bf 100644 --- a/crates/wasmi-plugin-pdk/src/transport.rs +++ b/crates/wasmi-plugin-pdk/src/transport.rs @@ -28,14 +28,17 @@ pub trait AsyncTransport: Send + Sync + 'static { ) -> impl std::future::Future> + MaybeSend; } +#[derive(Debug)] pub struct Transport { inner: Arc>, } +#[derive(Debug)] pub struct TransportDriver { inner: Arc>, } +#[derive(Debug)] struct TransportInner { reader: Mutex>, writer: Mutex, @@ -176,12 +179,12 @@ impl TransportInner { let id = self.next_id(); let request = RpcMessage::request(id, method.to_string(), params); - let (recv_tx, revc_rx) = oneshot::channel(); + let (recv_tx, recv_rx) = oneshot::channel(); self.pending.lock().unwrap().insert(id, recv_tx); self.write_message(&request)?; // Await the response - let res = revc_rx.await?; + let res = recv_rx.await?; let res = res.map_err(|e| TransportError::RpcError(e.error))?; Ok(res) } diff --git a/crates/wasmi-plugin-wasi/src/wasi_ctx.rs b/crates/wasmi-plugin-wasi/src/wasi_ctx.rs index e67790a..d1ffafc 100644 --- a/crates/wasmi-plugin-wasi/src/wasi_ctx.rs +++ b/crates/wasmi-plugin-wasi/src/wasi_ctx.rs @@ -281,9 +281,6 @@ pub fn fd_read( Some(r) => match r.read(&mut host_buf) { Ok(n) => n, Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - // trace!("fd_read: WouldBlock, yielding to host"); - // ctx.awaiting_stdin = true; - // let _ = sched_yield(caller); return Errno::Again as i32; } Err(_) => return Errno::Fault as i32, From 91641893474387c1f69b0944cbad5b95ba9eeffa Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Tue, 30 Dec 2025 18:32:39 -0500 Subject: [PATCH 58/70] feat: call_many --- crates/test-plugin/src/main.rs | 18 ++----- crates/wasmi-plugin-pdk/src/transport.rs | 65 ++++++++++++++++++------ 2 files changed, 54 insertions(+), 29 deletions(-) diff --git a/crates/test-plugin/src/main.rs b/crates/test-plugin/src/main.rs index 1903cc8..f134376 100644 --- a/crates/test-plugin/src/main.rs +++ b/crates/test-plugin/src/main.rs @@ -6,7 +6,7 @@ use tracing_subscriber::fmt; use wasmi_plugin_pdk::{ rpc_message::{RpcError, RpcErrorContext}, runner::PluginRunner, - transport::{AsyncTransport, SyncTransport, Transport}, + transport::{AsyncTransport, SyncManyTransport, SyncTransport, Transport}, }; async fn ping(_: Transport, _: ()) -> Result { @@ -39,22 +39,12 @@ async fn call(transport: Transport, _: ()) -> Result { } async fn call_many(transport: Transport, limit: u64) -> Result<(), RpcError> { - let mut tasks = vec![]; + let mut calls = Vec::new(); for _ in 0..limit { - let transport_clone = transport.clone(); - let task = async move { - transport_clone.call("ping", Value::Null)?; - Ok::<(), RpcError>(()) - }; - tasks.push(task); + calls.push(("ping", Value::Null)); } - let resps: Vec> = - futures::future::join_all(tasks).await.into_iter().collect(); - - for resp in resps { - resp?; - } + transport.call_many(calls)?; Ok(()) } diff --git a/crates/wasmi-plugin-pdk/src/transport.rs b/crates/wasmi-plugin-pdk/src/transport.rs index daf14bf..4a3aa1a 100644 --- a/crates/wasmi-plugin-pdk/src/transport.rs +++ b/crates/wasmi-plugin-pdk/src/transport.rs @@ -20,6 +20,10 @@ pub trait SyncTransport: Send + Sync + 'static { fn call(&self, method: &str, params: Value) -> Result; } +pub trait SyncManyTransport: Send + Sync + 'static { + fn call_many(&self, calls: Vec<(&str, Value)>) -> Result, E>; +} + pub trait AsyncTransport: Send + Sync + 'static { fn call_async( &self, @@ -106,6 +110,12 @@ impl SyncTransport for Transport { } } +impl SyncManyTransport for Transport { + fn call_many(&self, calls: Vec<(&str, Value)>) -> Result, TransportError> { + self.inner.call_many(calls) + } +} + impl AsyncTransport for Transport { async fn call_async(&self, method: &str, params: Value) -> Result { self.inner.call_async(method, params).await @@ -138,36 +148,61 @@ impl TransportInner { /// is received. If responses to other requests (those made asynchronously) arrive /// in the meantime, they will be processed and queued for delivery. fn call(&self, method: &str, params: Value) -> Result { - let id = self.next_id(); - let request = RpcMessage::request(id, method.to_string(), params); + let calls = vec![(method, params)]; + let mut results = self.call_many(calls)?; + Ok(results.remove(0)) + } - let (res_tx, mut res_rx) = oneshot::channel(); - self.pending.lock().unwrap().insert(id, res_tx); - self.write_message(&request)?; + fn call_many(&self, calls: Vec<(&str, Value)>) -> Result, TransportError> { + let mut res_rx_list: Vec> = calls + .into_iter() + .map(|(method, params)| { + let id = self.next_id(); + + let (res_tx, res_rx) = oneshot::channel(); + self.pending.lock().unwrap().insert(id, res_tx); + self.write_message(&RpcMessage::request(id, method.to_string(), params))?; + Ok(res_rx) + }) + .collect::>()?; //? Lock reader since we'll be driving IO blockingly + let mut results = Vec::new(); let mut reader = self.reader.lock().unwrap(); let mut line = String::new(); // Drive IO until we get our response - loop { + while results.len() < res_rx_list.len() + results.len() { let would_block = self.step(&mut reader, &mut line)?; - match res_rx.try_recv() { + // Poll and remove finished receivers + let mut err = None; + res_rx_list.retain_mut(|rx| match rx.try_recv() { Ok(Some(res)) => { - let res = res.map_err(|e| TransportError::RpcError(e.error))?; - return Ok(res); - } - Ok(None) => { - if would_block { - wait_for_stdin(); - } + results.push(res.map_err(|e| TransportError::RpcError(e.error))); + false // Remove from list } + Ok(None) => true, // Keep in list Err(e) => { - return Err(TransportError::OneshotCanceled(e)); + err = Some(TransportError::OneshotCanceled(e)); + false // Remove from list } + }); + + if let Some(e) = err { + return Err(e); + } + if would_block && !res_rx_list.is_empty() { + wait_for_stdin(); + } + if res_rx_list.is_empty() { + break; } } + + let mut results: Vec<_> = results.into_iter().collect::>()?; + results.sort_by_key(|res| res.id); + Ok(results) } /// Asynchronously call a method, returning a future that resolves From b2d1f1b0d1b828ac740d733c520c75b7ccd2ef8b Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Tue, 30 Dec 2025 20:54:42 -0500 Subject: [PATCH 59/70] fix: make host session channel-based This way it can actually handle multiple plugin requests concurrently --- crates/wasmi-plugin-hdk/src/session.rs | 162 +++++++++++++++++-------- 1 file changed, 109 insertions(+), 53 deletions(-) diff --git a/crates/wasmi-plugin-hdk/src/session.rs b/crates/wasmi-plugin-hdk/src/session.rs index 4fc811e..c62f2d2 100644 --- a/crates/wasmi-plugin-hdk/src/session.rs +++ b/crates/wasmi-plugin-hdk/src/session.rs @@ -1,18 +1,24 @@ -use futures::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, io::BufReader}; +use std::sync::Arc; + +use futures::{ + AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt, StreamExt, + io::BufReader, stream::FuturesUnordered, +}; use serde_json::Value; use thiserror::Error; use tracing::{error, warn}; use wasmi_plugin_pdk::{ api::RequestHandler, + router::BoxFuture, rpc_message::{RpcError, RpcMessage, RpcResponse}, }; use wasmi_plugin_rt::yield_now; /// A session for communicating with oneshot plugin over an async transport. pub struct PluginSession { - reader: BufReader, + reader: R, writer: W, - handler: H, + handler: Arc, } #[derive(Debug, Error)] @@ -24,7 +30,9 @@ pub enum PluginSessionError { #[error("EOF")] Eof, #[error(transparent)] - Rpc(RpcError), + Rpc(#[from] RpcError), + #[error("Channel closed")] + ChannelClosed, } impl> @@ -33,87 +41,135 @@ impl> /// Create a new PluginSession with the given reader, writer, and request handler. pub fn new(reader: R, writer: W, handler: H) -> Self { PluginSession { - reader: BufReader::new(reader), + reader: reader, writer, - handler, + handler: Arc::new(handler), } } /// Calls a method asynchronously on the plugin and waits for the response. pub async fn call_async( - mut self, + self, method: &str, params: Value, ) -> Result { + let (inbound_tx, inbound_rx) = futures::channel::mpsc::unbounded(); + let (outbound_tx, outbound_rx) = futures::channel::mpsc::unbounded(); + + //? Send initial request let id = 1; let request = RpcMessage::request(id, method.to_string(), params); - self.write_message(request).await?; + outbound_tx.unbounded_send(request).unwrap(); - let mut line = String::new(); + let reader_fut = reader_task(self.reader, inbound_tx).fuse(); + let writer_fut = writer_task(self.writer, outbound_rx).fuse(); + let dispatcher_fut = + dispatcher_task(self.handler.clone(), inbound_rx, outbound_tx, id).fuse(); + + futures::pin_mut!(reader_fut, writer_fut, dispatcher_fut); loop { - line.clear(); - match self.reader.read_line(&mut line).await { - Ok(0) => { - warn!("EOF reached while reading from transport"); + futures::select_biased! { + result = dispatcher_fut => return result, + reader_result = reader_fut => { + reader_result?; return Err(PluginSessionError::Eof); } - Ok(_) => {} - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - yield_now().await; - continue; + writer_result = writer_fut => { + writer_result?; + return Err(PluginSessionError::ChannelClosed); } - Err(e) => return Err(PluginSessionError::Io(e)), } + } + } +} - // Process complete lines - let msg: RpcMessage = serde_json::from_str(&line) - .map_err(|e| PluginSessionError::Serde(e, line.clone()))?; +async fn reader_task( + reader: R, + tx: futures::channel::mpsc::UnboundedSender, +) -> Result<(), PluginSessionError> { + let mut reader = BufReader::new(reader); + let mut line = String::new(); - match msg { - RpcMessage::RpcResponse(resp) if resp.id == id => return Ok(resp), - RpcMessage::RpcErrorResponse(err) if err.id == id => { - return Err(PluginSessionError::Rpc(err.error)); - } - RpcMessage::RpcRequest(req) => { - self.handle_request(req.id, &req.method, req.params).await?; - } - _ => { - warn!("Unexpected message received: {:?}", msg); - continue; - } + loop { + line.clear(); + match reader.read_line(&mut line).await { + Ok(0) => return Err(PluginSessionError::Eof), + Ok(_) => {} + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + yield_now().await; + continue; } + Err(e) => return Err(PluginSessionError::Io(e)), } + + let msg: RpcMessage = + serde_json::from_str(&line).map_err(|e| PluginSessionError::Serde(e, line.clone()))?; + tx.unbounded_send(msg) + .map_err(|_| PluginSessionError::ChannelClosed)?; } +} - async fn write_message(&mut self, msg: RpcMessage) -> Result<(), PluginSessionError> { +async fn writer_task( + mut writer: W, + mut rx: futures::channel::mpsc::UnboundedReceiver, +) -> Result<(), PluginSessionError> { + while let Some(msg) = rx.next().await { let msg: String = serde_json::to_string(&msg).map_err(|e| { error!("Serde error while serializing message: {}", e); PluginSessionError::Serde(e, "".to_string()) })?; - let msg = format!("{}\n", msg); - self.writer.write_all(msg.as_bytes()).await?; - self.writer.flush().await?; - Ok(()) + let msg = format!("{}\n", msg); + writer.write_all(msg.as_bytes()).await?; + writer.flush().await?; } - async fn handle_request( - &mut self, - id: u64, - method: &str, - params: Value, - ) -> Result<(), PluginSessionError> { - match self.handler.handle(method, params).await { - Ok(result) => { - self.write_message(RpcMessage::response(id, result)).await?; - } - Err(error) => { - self.write_message(RpcMessage::error_response(id, error)) - .await?; + Ok(()) +} + +async fn dispatcher_task>( + handler: Arc, + mut inbound: futures::channel::mpsc::UnboundedReceiver, + mut outbound: futures::channel::mpsc::UnboundedSender, + awaiting_id: u64, +) -> Result { + let mut pending: FuturesUnordered)>> = + FuturesUnordered::new(); + + loop { + futures::select_biased! { + // Complete handler + (id, result) = pending.select_next_some() => { + let msg = match result { + Ok(value) => RpcMessage::response(id, value), + Err(error) => RpcMessage::error_response(id, error), + }; + outbound.send(msg).await.map_err(|_| PluginSessionError::ChannelClosed)?; + }, + + // Inbound message + msg = inbound.next() => { + let Some(msg) = msg else { + return Err(PluginSessionError::ChannelClosed); + }; + + match msg { + RpcMessage::RpcResponse(resp) if resp.id == awaiting_id => return Ok(resp), + RpcMessage::RpcErrorResponse(err) if err.id == awaiting_id => return Err(err.error.into()), + RpcMessage::RpcRequest(req) => { + let handler = handler.clone(); + let fut = async move { + let result = handler.handle(&req.method, req.params).await; + (req.id, result) + }; + pending.push(Box::pin(fut)); + }, + _ => { + warn!("Unexpected message: {:?}", msg); + } + } } } - - Ok(()) } } From f5de7c916fbc1df7b24f9f8676bfecef361e32a5 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Tue, 30 Dec 2025 22:55:57 -0500 Subject: [PATCH 60/70] refactor: improved non_blocking_pipe reads --- .../src/non_blocking_pipe.rs | 63 ++++++++----------- 1 file changed, 25 insertions(+), 38 deletions(-) diff --git a/crates/wasmi-plugin-wasi/src/non_blocking_pipe.rs b/crates/wasmi-plugin-wasi/src/non_blocking_pipe.rs index 5210c1c..50240ec 100644 --- a/crates/wasmi-plugin-wasi/src/non_blocking_pipe.rs +++ b/crates/wasmi-plugin-wasi/src/non_blocking_pipe.rs @@ -47,22 +47,25 @@ pub fn non_blocking_pipe() -> (NonBlockingPipeReader, NonBlockingPipeWriter) { impl Read for NonBlockingPipeReader { fn read(&mut self, buf: &mut [u8]) -> io::Result { let mut inner = self.inner.lock().unwrap(); - let mut n = 0; - while n < buf.len() { - if let Some(b) = inner.buf.pop_front() { - buf[n] = b; - n += 1; - } else { - break; + + let (front, back) = inner.buf.as_slices(); + let to_read = buf.len().min(front.len() + back.len()); + + if to_read > 0 { + let from_front = to_read.min(front.len()); + buf[..from_front].copy_from_slice(&front[..from_front]); + + if from_front < to_read { + let from_back = to_read - from_front; + buf[from_front..to_read].copy_from_slice(&back[..from_back]); } - } - if n == 0 && inner.closed { + + inner.buf.drain(..to_read); + Ok(to_read) + } else if inner.closed { Ok(0) - } else if n == 0 { - // no data, behave like a nonblocking pipe - Err(io::ErrorKind::WouldBlock.into()) } else { - Ok(n) + Err(io::ErrorKind::WouldBlock.into()) } } } @@ -81,35 +84,19 @@ impl WasiReader for NonBlockingPipeReader { impl AsyncRead for NonBlockingPipeReader { fn poll_read( - self: Pin<&mut Self>, + mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8], ) -> Poll> { - let mut inner = self.inner.lock().unwrap(); - - let mut bytes_read = 0; - - // Read available data - while bytes_read < buf.len() { - if let Some(b) = inner.buf.pop_front() { - buf[bytes_read] = b; - bytes_read += 1; - } else { - break; + let read = self.as_mut().get_mut().read(buf); + match read { + Ok(n) => Poll::Ready(Ok(n)), + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + let mut inner = self.inner.lock().unwrap(); + inner.waker = Some(cx.waker().clone()); + Poll::Pending } - } - - if bytes_read > 0 { - inner.waker = None; // Clear stale waker - Poll::Ready(Ok(bytes_read)) - } else if inner.closed { - // EOF - inner.waker = None; - Poll::Ready(Ok(0)) - } else { - // No data available, register waker and return pending - inner.waker = Some(cx.waker().clone()); - Poll::Pending + Err(e) => Poll::Ready(Err(e)), } } } From 9dc2eb0876deabad900c91cfb48b5d649aa26d22 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Tue, 30 Dec 2025 22:57:09 -0500 Subject: [PATCH 61/70] perf: Add wasm-opt and enable release optimizations --- crates/test-plugin/Cargo.toml | 11 +++++++++-- crates/wasmi-plugin-coordinator/Cargo.toml | 13 +++++++++++++ makefile | 8 +++++++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/crates/test-plugin/Cargo.toml b/crates/test-plugin/Cargo.toml index a366158..ff05dec 100644 --- a/crates/test-plugin/Cargo.toml +++ b/crates/test-plugin/Cargo.toml @@ -23,6 +23,13 @@ wasmi-plugin-pdk = { workspace = true } [profile.release] opt-level = 3 -lto = "fat" +lto = "thin" codegen-units = 1 -strip = true +panic = "abort" + +# Keep function names in release builds +strip = "none" +debug = "line-tables-only" + +[profile.release.package."*"] +opt-level = 3 \ No newline at end of file diff --git a/crates/wasmi-plugin-coordinator/Cargo.toml b/crates/wasmi-plugin-coordinator/Cargo.toml index 16d874c..24cdc30 100644 --- a/crates/wasmi-plugin-coordinator/Cargo.toml +++ b/crates/wasmi-plugin-coordinator/Cargo.toml @@ -30,3 +30,16 @@ wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] wasmi-plugin-coordinator-protocol = { workspace = true } wasmi-plugin-wasi = { workspace = true } web-sys = { workspace = true, features = ["Worker", "WorkerOptions", "DedicatedWorkerGlobalScope", "WorkerType", "MessageEvent", "ErrorEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } + +[profile.release] +opt-level = 3 +lto = "thin" +codegen-units = 1 +panic = "abort" + +# Keep function names in release builds +strip = "none" +debug = "line-tables-only" + +[profile.release.package."*"] +opt-level = 3 \ No newline at end of file diff --git a/makefile b/makefile index 57a23e2..965b4c6 100644 --- a/makefile +++ b/makefile @@ -6,13 +6,19 @@ BROWSER := --firefox build-plugin: @echo "--- Building Plugin WASM ---" cargo build --target wasm32-wasip1 -p test-plugin --release + wasm-opt -O3 --debuginfo \ + target/wasm32-wasip1/release/test-plugin.wasm \ + -o target/wasm32-wasip1/release/test-plugin.wasm build-coordinator: @echo "--- Building Coordinator WASM ---" cd crates/wasmi-plugin-coordinator && wasm-pack build \ --target web \ --out-dir pkg \ - --release \ + --release + wasm-opt -O3 --debuginfo \ + crates/wasmi-plugin-coordinator/pkg/wasmi_plugin_coordinator_bg.wasm \ + -o crates/wasmi-plugin-coordinator/pkg/wasmi_plugin_coordinator_bg.wasm build-all: build-plugin build-coordinator From a9a1eab230d5833ccc433dfa413d029e416a1aea Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Wed, 31 Dec 2025 00:25:53 -0500 Subject: [PATCH 62/70] refactor: don't transfer wasm_bytes to coordinator / worker --- Cargo.lock | 1 + .../Cargo.toml | 1 + .../src/lib.rs | 9 +++++- crates/wasmi-plugin-coordinator/src/worker.rs | 29 ++++--------------- .../src/runtime/worker_runtime.rs | 8 ++--- 5 files changed, 17 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 461bba6..1806da3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2386,6 +2386,7 @@ version = "0.1.0" dependencies = [ "getrandom 0.3.4", "serde", + "thiserror 2.0.17", ] [[package]] diff --git a/crates/wasmi-plugin-coordinator-protocol/Cargo.toml b/crates/wasmi-plugin-coordinator-protocol/Cargo.toml index 53788df..b295a9d 100644 --- a/crates/wasmi-plugin-coordinator-protocol/Cargo.toml +++ b/crates/wasmi-plugin-coordinator-protocol/Cargo.toml @@ -15,3 +15,4 @@ exclude.workspace = true [dependencies] getrandom = { workspace = true } serde = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/wasmi-plugin-coordinator-protocol/src/lib.rs b/crates/wasmi-plugin-coordinator-protocol/src/lib.rs index 83b7f3e..670ad49 100644 --- a/crates/wasmi-plugin-coordinator-protocol/src/lib.rs +++ b/crates/wasmi-plugin-coordinator-protocol/src/lib.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; +use thiserror::Error; #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, Debug)] pub struct InstanceId(u32); @@ -35,6 +36,12 @@ impl Display for InstanceId { } } +#[derive(Debug, Error, Serialize, Deserialize, Eq, PartialEq)] +pub enum RunPluginError { + #[error("Runtime error: {0}")] + RuntimeError(String), +} + #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "type")] pub enum CoordinatorMessage { @@ -44,7 +51,7 @@ pub enum CoordinatorMessage { }, RunPluginResp { id: InstanceId, - status: Result<(), String>, + status: Result<(), RunPluginError>, }, Stdin { id: InstanceId, diff --git a/crates/wasmi-plugin-coordinator/src/worker.rs b/crates/wasmi-plugin-coordinator/src/worker.rs index 07ed5d7..eba5c99 100644 --- a/crates/wasmi-plugin-coordinator/src/worker.rs +++ b/crates/wasmi-plugin-coordinator/src/worker.rs @@ -11,6 +11,7 @@ use wasm_bindgen::{ }; use wasmi_plugin_coordinator_protocol::CoordinatorMessage; use wasmi_plugin_coordinator_protocol::InstanceId; +use wasmi_plugin_coordinator_protocol::RunPluginError; use wasmi_plugin_wasi::non_blocking_pipe::NonBlockingPipeWriter; use wasmi_plugin_wasi::non_blocking_pipe::non_blocking_pipe; use wasmi_plugin_wasi::wasi_ctx; @@ -97,24 +98,14 @@ fn on_message(state: &mut CoordinatorState, e: web_sys::MessageEvent) { terminate(state, id); } CoordinatorMessage::RunPlugin { id } => { - let Ok(wasm_bytes) = Reflect::get(&data, &"wasm_bytes".into()) else { - error!("Failed to get wasm_bytes from message"); - return; - }; - let Ok(wasm_module) = Reflect::get(&data, &"wasm_module".into()) else { error!("Failed to get wasm_module from message"); return; }; - let status = match run_plugin( - state, - id, - wasm_bytes.unchecked_into(), - wasm_module.unchecked_into(), - ) { + let status = match run_plugin(state, id, wasm_module.unchecked_into()) { Ok(_) => Ok(()), - Err(e) => Err(e.to_string()), + Err(e) => Err(RunPluginError::RuntimeError(e.to_string())), }; let response_msg = CoordinatorMessage::RunPluginResp { id, status }; @@ -153,11 +144,8 @@ fn terminate(state: &mut CoordinatorState, id: InstanceId) { fn run_plugin( state: &mut CoordinatorState, id: InstanceId, - wasm_bytes: Uint8Array, wasm_module: WebAssembly::Module, ) -> Result, SpawnError> { - let wasm_bytes: Vec = wasm_bytes.to_vec(); - let (stdin_reader, stdin_writer) = non_blocking_pipe(); let (stdout_reader, stdout_writer) = non_blocking_pipe(); let (stderr_reader, stderr_writer) = non_blocking_pipe(); @@ -170,13 +158,7 @@ fn run_plugin( let pool = WorkerPool::global(); pool.run_with(&wasm_module, move |extra| async move { - let res = run_instance( - extra, - wasm_bytes, - stdin_reader, - stdout_writer, - stderr_writer, - ); + let res = run_instance(extra, stdin_reader, stdout_writer, stderr_writer); if let Err(e) = res { error!("Error running Wasm instance: {}", e); } @@ -185,7 +167,6 @@ fn run_plugin( fn run_instance( js_module: wasm_bindgen::JsValue, - wasm_bytes: Vec, stdin: impl WasiReader + 'static, stdout: impl Write + Send + Sync + 'static, stderr: impl Write + Send + Sync + 'static, @@ -193,7 +174,7 @@ fn run_instance( let mut store = wasmer::Store::default(); let js_module: WebAssembly::Module = js_module.dyn_into().unwrap(); - let module = (js_module, wasm_bytes).into(); + let module = js_module.into(); let wasi_ctx = wasi_ctx::WasiCtx::new() .set_stdin(stdin) diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index 76f3ecc..64f09b9 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -147,21 +147,17 @@ impl Runtime for &WorkerRuntime { stdin_proxy(stdin_reader, instance_id, self.inner.clone()); // Notify the coordinator - let wasm_bytes_js = js_sys::Uint8Array::from(&compiled.wasm_bytes[..]); let wasm_module_js = compiled.js_module.clone(); let spawn_msg = CoordinatorMessage::RunPlugin { id: instance_id }; let msg_value = serde_wasm_bindgen::to_value(&spawn_msg)?; - Reflect::set(&msg_value, &"wasm_bytes".into(), &wasm_bytes_js) - .map_err(SpawnError::Reflect)?; Reflect::set(&msg_value, &"wasm_module".into(), &wasm_module_js) .map_err(SpawnError::Reflect)?; - let transfer_list = js_sys::Array::of1(&wasm_bytes_js.buffer()); self.inner .lock() .unwrap() .coordinator - .post_message_with_transfer(&msg_value, &transfer_list) + .post_message(&msg_value) .map_err(SpawnError::PostMessage)?; // Await the response @@ -286,7 +282,7 @@ fn on_message(inner: Arc>, e: web_sys::MessageEvent) { let mut inner = inner.lock().unwrap(); if let Some(instance) = inner.instances.get_mut(&id) { if let Some(tx) = instance.run_plugin_resp_tx.take() { - let _ = tx.send(status); + let _ = tx.send(status.map_err(|e| e.to_string())); } else { warn!("No response channel found for instance {:?}", id); } From b3f3592f03a86d568b003a9664e604c1b88ce652 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Wed, 31 Dec 2025 23:05:08 -0500 Subject: [PATCH 63/70] tidy: logs --- crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs | 1 - crates/wasmi-plugin-hdk/src/plugin.rs | 4 ++-- crates/wasmi-plugin-pdk/src/runner.rs | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs b/crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs index 66f3395..fea17b4 100644 --- a/crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs +++ b/crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs @@ -270,7 +270,6 @@ fn on_message_handler( log_worker_message(&name, message); } WorkerMessage::Idle => { - info!(target: "WORKER", "[{}] Idle", name); let mut worker_state = state.lock().unwrap(); *worker_state = WorkerState::Idle; } diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index e481986..423487b 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -52,13 +52,13 @@ impl PluginLogger for DefaultPluginLogger {} #[derive(Debug, Error)] pub enum PluginError { - #[error("Session error")] + #[error("Session error: {0}")] SessionError(#[from] PluginSessionError), #[error("Runtime Error: {0}")] RuntimeError(#[from] Box), #[error("Plugin timeout")] PluginTimeout, - #[error("Plugin compilation error")] + #[error("Plugin compilation error: {0}")] CompileError(#[from] wasmer::CompileError), } diff --git a/crates/wasmi-plugin-pdk/src/runner.rs b/crates/wasmi-plugin-pdk/src/runner.rs index b28aa80..38f4b55 100644 --- a/crates/wasmi-plugin-pdk/src/runner.rs +++ b/crates/wasmi-plugin-pdk/src/runner.rs @@ -71,7 +71,6 @@ impl PluginRunner { fn try_run(&self) -> Result<(), PluginServerError> { //? Read request let request = self.read_request()?; - info!("PluginServer: Received request: {:?}", request); let (transport, driver) = Transport::new(std::io::stdin(), std::io::stdout()); let resp = futures::executor::block_on(async { From aae13f39ac5b3dd910e9ae8aef52283baacba953 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Wed, 31 Dec 2025 23:19:33 -0500 Subject: [PATCH 64/70] fix: hdk session reader race condition --- crates/wasmi-plugin-hdk/src/session.rs | 33 ++++++++++++++++---------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/crates/wasmi-plugin-hdk/src/session.rs b/crates/wasmi-plugin-hdk/src/session.rs index c62f2d2..ce47308 100644 --- a/crates/wasmi-plugin-hdk/src/session.rs +++ b/crates/wasmi-plugin-hdk/src/session.rs @@ -41,7 +41,7 @@ impl> /// Create a new PluginSession with the given reader, writer, and request handler. pub fn new(reader: R, writer: W, handler: H) -> Self { PluginSession { - reader: reader, + reader, writer, handler: Arc::new(handler), } @@ -68,17 +68,20 @@ impl> futures::pin_mut!(reader_fut, writer_fut, dispatcher_fut); - loop { - futures::select_biased! { - result = dispatcher_fut => return result, - reader_result = reader_fut => { - reader_result?; - return Err(PluginSessionError::Eof); - } - writer_result = writer_fut => { - writer_result?; - return Err(PluginSessionError::ChannelClosed); - } + futures::select_biased! { + result = dispatcher_fut => result, + reader_result = reader_fut => { + reader_result?; + //? If reader task ends, it means the pipe has closed. However, + //? the final response might still be in flight in the dispatcher + //? task, so we wait for it to complete. If the pipe did EOF without + //? sending a response, the dispatcher will eventually starve + //? and EOF itself. + dispatcher_fut.await + } + writer_result = writer_fut => { + writer_result?; + Err(PluginSessionError::ChannelClosed) } } } @@ -94,7 +97,11 @@ async fn reader_task( loop { line.clear(); match reader.read_line(&mut line).await { - Ok(0) => return Err(PluginSessionError::Eof), + Ok(0) => { + //? EOF reached. Returning Ok allows the dispatcher to finish + //? processing any pending messages. + return Ok(()); + } Ok(_) => {} Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { yield_now().await; From 30ce0bf4fdf00b9a3eae3ce236259292b0c5ceea Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Thu, 8 Jan 2026 16:12:13 -0500 Subject: [PATCH 65/70] fix: switch to tokio executor Alloy and presumably other libs assume tokio, so better for compatibility --- Cargo.lock | 1 + crates/wasmi-plugin-pdk/Cargo.toml | 1 + crates/wasmi-plugin-pdk/src/runner.rs | 31 ++++++++++++++++----------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1806da3..397d918 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2426,6 +2426,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.17", + "tokio", "tracing", "wasmi-plugin-rt", ] diff --git a/crates/wasmi-plugin-pdk/Cargo.toml b/crates/wasmi-plugin-pdk/Cargo.toml index a69a104..e8814c5 100644 --- a/crates/wasmi-plugin-pdk/Cargo.toml +++ b/crates/wasmi-plugin-pdk/Cargo.toml @@ -14,6 +14,7 @@ exclude.workspace = true [dependencies] futures = { workspace = true } +tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/crates/wasmi-plugin-pdk/src/runner.rs b/crates/wasmi-plugin-pdk/src/runner.rs index 38f4b55..11b3178 100644 --- a/crates/wasmi-plugin-pdk/src/runner.rs +++ b/crates/wasmi-plugin-pdk/src/runner.rs @@ -1,13 +1,13 @@ use std::io::{BufRead, BufReader}; -use futures::{FutureExt, select}; use serde::{Serialize, de::DeserializeOwned}; use thiserror::Error; +use tokio::select; use tracing::{error, info}; use crate::{ router::{MaybeSend, Router}, - rpc_message::{RpcError, RpcMessage, RpcRequest}, + rpc_message::{RpcError, RpcErrorContext, RpcMessage, RpcRequest}, transport::Transport, }; @@ -73,18 +73,25 @@ impl PluginRunner { let request = self.read_request()?; let (transport, driver) = Transport::new(std::io::stdin(), std::io::stdout()); - let resp = futures::executor::block_on(async { - select! { - res = self.router.handle_with_state(transport, &request.method, request.params).fuse() => { - res - }, - drive_err = driver.run().fuse() => { - match drive_err { - Ok(_) => Err(RpcError::custom("Transport driver exited unexpectedly")), - Err(e) => Err(e.into()), + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .context("Error creating tokio runtime")?; + + let resp = rt.block_on(async { + let local = tokio::task::LocalSet::new(); + local.run_until(async { + select! { + res = self.router.handle_with_state(transport, &request.method, request.params) => res, + drive_err = driver.run() => { + match drive_err { + Ok(_) => Err(RpcError::custom("Transport driver exited unexpectedly")), + Err(e) => Err(e.into()), + } } } - } + }).await }); //? Send response From 562b90b313b4ad9f4ca525bd5ee85cba4cb28676 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Sat, 10 Jan 2026 16:52:50 -0500 Subject: [PATCH 66/70] feat: replace `PluginId` with `InstanceId` in `HostHandler` --- crates/test-plugin/src/main.rs | 2 -- crates/wasmi-plugin-hdk/src/host_handler.rs | 6 ++-- crates/wasmi-plugin-hdk/src/instance_id.rs | 32 +++++++++++++++++++++ crates/wasmi-plugin-hdk/src/lib.rs | 1 + crates/wasmi-plugin-hdk/src/plugin.rs | 6 ++-- crates/wasmi-plugin-hdk/src/server.rs | 10 +++---- crates/wasmi-plugin-pdk/src/runner.rs | 3 +- 7 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 crates/wasmi-plugin-hdk/src/instance_id.rs diff --git a/crates/test-plugin/src/main.rs b/crates/test-plugin/src/main.rs index f134376..204f1e0 100644 --- a/crates/test-plugin/src/main.rs +++ b/crates/test-plugin/src/main.rs @@ -113,8 +113,6 @@ fn main() { .with_ansi(false) .without_time() .init(); - info!("Starting plugin..."); - info!("Test"); PluginRunner::new() .with_method("ping", ping) diff --git a/crates/wasmi-plugin-hdk/src/host_handler.rs b/crates/wasmi-plugin-hdk/src/host_handler.rs index 9f50e68..46434fd 100644 --- a/crates/wasmi-plugin-hdk/src/host_handler.rs +++ b/crates/wasmi-plugin-hdk/src/host_handler.rs @@ -1,15 +1,15 @@ use serde_json::Value; use wasmi_plugin_pdk::{router::BoxFuture, rpc_message::RpcError}; -use crate::plugin_id::PluginId; +use crate::instance_id::InstanceId; /// HostHandler is an extension of `wasmi-pdk::RequestHandler` that adds -/// the plugin ID as the first arg in the `handle` method. It should be used +/// the instance ID as the first arg in the `handle` method. It should be used /// by hosts that manage multiple plugins to differentiate request sources. pub trait HostHandler: Send + Sync { fn handle<'a>( &'a self, - plugin: PluginId, + instance: InstanceId, method: &'a str, params: Value, ) -> BoxFuture<'a, Result>; diff --git a/crates/wasmi-plugin-hdk/src/instance_id.rs b/crates/wasmi-plugin-hdk/src/instance_id.rs new file mode 100644 index 0000000..5872f1a --- /dev/null +++ b/crates/wasmi-plugin-hdk/src/instance_id.rs @@ -0,0 +1,32 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::plugin_id::PluginId; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct InstanceId { + pub plugin: PluginId, + pub id: Uuid, +} + +impl InstanceId { + pub fn new(plugin: PluginId) -> Self { + InstanceId { + plugin, + id: Uuid::new_v4(), + } + } +} + +impl Display for InstanceId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if f.alternate() { + write!(f, "{}:{}", self.plugin, self.id) // full: {:#} + } else { + let uuid_str = self.id.as_simple().to_string(); + write!(f, "{},{}", self.plugin, &uuid_str[uuid_str.len() - 6..]) // short {} + } + } +} diff --git a/crates/wasmi-plugin-hdk/src/lib.rs b/crates/wasmi-plugin-hdk/src/lib.rs index 6589ea4..1a2c801 100644 --- a/crates/wasmi-plugin-hdk/src/lib.rs +++ b/crates/wasmi-plugin-hdk/src/lib.rs @@ -1,5 +1,6 @@ mod compile; pub mod host_handler; +pub mod instance_id; pub mod plugin; pub mod plugin_id; pub mod runtime; diff --git a/crates/wasmi-plugin-hdk/src/plugin.rs b/crates/wasmi-plugin-hdk/src/plugin.rs index 423487b..958c59f 100644 --- a/crates/wasmi-plugin-hdk/src/plugin.rs +++ b/crates/wasmi-plugin-hdk/src/plugin.rs @@ -12,11 +12,11 @@ use wasmi_plugin_pdk::{ }; use wasmi_plugin_rt::sleep; -use crate::host_handler::HostHandler; use crate::runtime; use crate::runtime::Runtime; use crate::session::{PluginSession, PluginSessionError}; use crate::{compile::Compiled, plugin_id::PluginId}; +use crate::{host_handler::HostHandler, instance_id::InstanceId}; /// Plugin is an async-capable instance of a plugin #[derive(Clone)] @@ -144,7 +144,7 @@ impl AsyncTransport for Plugin { //? transport setup let handler = PluginCallback { handler: self.inner.handler.clone(), - uuid: self.inner.id, + uuid: InstanceId::new(self.inner.id), }; let session = PluginSession::new(stdout_reader, stdin_writer, handler); let session_task = session.call_async(method, params).fuse(); @@ -192,7 +192,7 @@ impl Plugin { /// with the added `uuid` context. struct PluginCallback { handler: Arc, - uuid: PluginId, + uuid: InstanceId, } impl RequestHandler for PluginCallback { diff --git a/crates/wasmi-plugin-hdk/src/server.rs b/crates/wasmi-plugin-hdk/src/server.rs index 5c08058..0f4490e 100644 --- a/crates/wasmi-plugin-hdk/src/server.rs +++ b/crates/wasmi-plugin-hdk/src/server.rs @@ -5,11 +5,11 @@ use wasmi_plugin_pdk::{ rpc_message::RpcError, }; -use crate::{host_handler::HostHandler, plugin_id::PluginId}; +use crate::{host_handler::HostHandler, instance_id::InstanceId}; pub struct HostServer { state: S, - router: Router<(PluginId, S)>, + router: Router<(InstanceId, S)>, } impl Default for HostServer { @@ -37,7 +37,7 @@ impl HostServer { where P: DeserializeOwned + 'static, R: Serialize + 'static, - F: Fn((PluginId, S), P) -> Fut + Send + Sync + 'static, + F: Fn((InstanceId, S), P) -> Fut + Send + Sync + 'static, Fut: Future> + MaybeSend + 'static, { self.router = self.router.with_method(name, func); @@ -48,12 +48,12 @@ impl HostServer { impl HostHandler for HostServer { fn handle<'a>( &'a self, - plugin: PluginId, + instance: InstanceId, method: &'a str, params: Value, ) -> BoxFuture<'a, Result> { Box::pin(async move { - let state = (plugin, self.state.clone()); + let state = (instance, self.state.clone()); self.router.handle_with_state(state, method, params).await }) } diff --git a/crates/wasmi-plugin-pdk/src/runner.rs b/crates/wasmi-plugin-pdk/src/runner.rs index 11b3178..db11d34 100644 --- a/crates/wasmi-plugin-pdk/src/runner.rs +++ b/crates/wasmi-plugin-pdk/src/runner.rs @@ -3,7 +3,7 @@ use std::io::{BufRead, BufReader}; use serde::{Serialize, de::DeserializeOwned}; use thiserror::Error; use tokio::select; -use tracing::{error, info}; +use tracing::error; use crate::{ router::{MaybeSend, Router}, @@ -99,7 +99,6 @@ impl PluginRunner { Ok(result) => RpcMessage::response(request.id, result), Err(error) => RpcMessage::error_response(request.id, error), }; - info!("PluginServer: Sending response: {:?}", resp); driver.write_message(&resp)?; Ok(()) From b445fec04906915eea0791ddc5a4ffb983f6210f Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Fri, 16 Jan 2026 14:44:01 -0500 Subject: [PATCH 67/70] feat: include assets in repo --- .../wasmi-plugin-coordinator/assets/worker.js | 1084 +++++++++++++++++ .../assets/worker.wasm | Bin 0 -> 368657 bytes .../src/runtime/worker_runtime.rs | 5 +- makefile | 5 + 4 files changed, 1091 insertions(+), 3 deletions(-) create mode 100644 crates/wasmi-plugin-coordinator/assets/worker.js create mode 100644 crates/wasmi-plugin-coordinator/assets/worker.wasm diff --git a/crates/wasmi-plugin-coordinator/assets/worker.js b/crates/wasmi-plugin-coordinator/assets/worker.js new file mode 100644 index 0000000..9a70c11 --- /dev/null +++ b/crates/wasmi-plugin-coordinator/assets/worker.js @@ -0,0 +1,1084 @@ +let wasm; + +function addToExternrefTable0(obj) { + const idx = wasm.__externref_table_alloc(); + wasm.__wbindgen_externrefs.set(idx, obj); + return idx; +} + +const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(state => state.dtor(state.a, state.b)); + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches && builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +function getArrayU8FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer !== wasm.memory.buffer) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.buffer !== wasm.memory.buffer) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + const idx = addToExternrefTable0(e); + wasm.__wbindgen_exn_store(idx); + } +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function makeMutClosure(arg0, arg1, dtor, f) { + const state = { a: arg0, b: arg1, cnt: 1, dtor }; + const real = (...args) => { + + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + state.a = a; + real._wbg_cb_unref(); + } + }; + real._wbg_cb_unref = () => { + if (--state.cnt === 0) { + state.dtor(state.a, state.b); + state.a = 0; + CLOSURE_DTORS.unregister(state); + } + }; + CLOSURE_DTORS.register(real, state, state); + return real; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +let cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : undefined); +if (cachedTextDecoder) cachedTextDecoder.decode(); + +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().slice(ptr, ptr + len)); +} + +const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder() : undefined); + +if (cachedTextEncoder) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + } +} + +let WASM_VECTOR_LEN = 0; + +function wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___wasm_bindgen_99d0e8a507791149___JsValue_____(arg0, arg1, arg2) { + wasm.wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___wasm_bindgen_99d0e8a507791149___JsValue_____(arg0, arg1, arg2); +} + +function wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___web_sys_d796e95248ce21f5___features__gen_ErrorEvent__ErrorEvent_____(arg0, arg1, arg2) { + wasm.wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___web_sys_d796e95248ce21f5___features__gen_ErrorEvent__ErrorEvent_____(arg0, arg1, arg2); +} + +function wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___wasm_bindgen_99d0e8a507791149___JsValue__wasm_bindgen_99d0e8a507791149___JsValue_____(arg0, arg1, arg2, arg3) { + wasm.wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___wasm_bindgen_99d0e8a507791149___JsValue__wasm_bindgen_99d0e8a507791149___JsValue_____(arg0, arg1, arg2, arg3); +} + +const __wbindgen_enum_WorkerType = ["classic", "module"]; + +const TrapFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_trap_free(ptr >>> 0, 1)); + +/** + * A struct representing a Trap + */ +export class Trap { + static __wrap(ptr) { + ptr = ptr >>> 0; + const obj = Object.create(Trap.prototype); + obj.__wbg_ptr = ptr; + TrapFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + TrapFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_trap_free(ptr, 0); + } + /** + * A marker method to indicate that an object is an instance of the `Trap` + * class. + */ + static __wbg_wasmer_trap() { + wasm.trap___wbg_wasmer_trap(); + } +} +if (Symbol.dispose) Trap.prototype[Symbol.dispose] = Trap.prototype.free; + +/** + * @param {number} ptr + * @returns {Promise} + */ +export function execute_worker_task(ptr) { + const ret = wasm.execute_worker_task(ptr); + return ret; +} + +/** + * @param {number} ptr + * @param {any} extra + * @returns {Promise} + */ +export function execute_worker_task_with(ptr, extra) { + const ret = wasm.execute_worker_task_with(ptr, extra); + return ret; +} + +export function init_compute_worker() { + wasm.init_compute_worker(); +} + +/** + * @returns {Promise} + */ +export function start_coordinator() { + const ret = wasm.start_coordinator(); + return ret; +} + +const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']); + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + } catch (e) { + const validResponse = module.ok && EXPECTED_RESPONSE_TYPES.has(module.type); + + if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + } else { + return instance; + } + } +} + +function __wbg_get_imports(memory) { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbg_BigInt_f9ddbcf8ed387722 = function(arg0) { + const ret = BigInt(arg0); + return ret; + }; + imports.wbg.__wbg_Error_52673b7de5a0ca89 = function(arg0, arg1) { + const ret = Error(getStringFromWasm0(arg0, arg1)); + return ret; + }; + imports.wbg.__wbg_Number_2d1dcfcf4ec51736 = function(arg0) { + const ret = Number(arg0); + return ret; + }; + imports.wbg.__wbg_String_8f0eb39a4a4c2f66 = function(arg0, arg1) { + const ret = String(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbg___wbindgen_bigint_get_as_i64_6e32f5e6aff02e1d = function(arg0, arg1) { + const v = arg1; + const ret = typeof(v) === 'bigint' ? v : undefined; + getDataViewMemory0().setBigInt64(arg0 + 8 * 1, isLikeNone(ret) ? BigInt(0) : ret, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); + }; + imports.wbg.__wbg___wbindgen_boolean_get_dea25b33882b895b = function(arg0) { + const v = arg0; + const ret = typeof(v) === 'boolean' ? v : undefined; + return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0; + }; + imports.wbg.__wbg___wbindgen_debug_string_adfb662ae34724b6 = function(arg0, arg1) { + const ret = debugString(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbg___wbindgen_function_table_932edf419e6641fe = function() { + const ret = wasm.__wbindgen_export; + return ret; + }; + imports.wbg.__wbg___wbindgen_in_0d3e1e8f0c669317 = function(arg0, arg1) { + const ret = arg0 in arg1; + return ret; + }; + imports.wbg.__wbg___wbindgen_is_bigint_0e1a2e3f55cfae27 = function(arg0) { + const ret = typeof(arg0) === 'bigint'; + return ret; + }; + imports.wbg.__wbg___wbindgen_is_function_8d400b8b1af978cd = function(arg0) { + const ret = typeof(arg0) === 'function'; + return ret; + }; + imports.wbg.__wbg___wbindgen_is_object_ce774f3490692386 = function(arg0) { + const val = arg0; + const ret = typeof(val) === 'object' && val !== null; + return ret; + }; + imports.wbg.__wbg___wbindgen_is_undefined_f6b95eab589e0269 = function(arg0) { + const ret = arg0 === undefined; + return ret; + }; + imports.wbg.__wbg___wbindgen_jsval_eq_b6101cc9cef1fe36 = function(arg0, arg1) { + const ret = arg0 === arg1; + return ret; + }; + imports.wbg.__wbg___wbindgen_jsval_loose_eq_766057600fdd1b0d = function(arg0, arg1) { + const ret = arg0 == arg1; + return ret; + }; + imports.wbg.__wbg___wbindgen_lt_b8a98c478e0bc0c2 = function(arg0, arg1) { + const ret = arg0 < arg1; + return ret; + }; + imports.wbg.__wbg___wbindgen_memory_a342e963fbcabd68 = function() { + const ret = wasm.memory; + return ret; + }; + imports.wbg.__wbg___wbindgen_module_967adef62ea6cbf8 = function() { + const ret = __wbg_init.__wbindgen_wasm_module; + return ret; + }; + imports.wbg.__wbg___wbindgen_neg_7dce5cdff91fb914 = function(arg0) { + const ret = -arg0; + return ret; + }; + imports.wbg.__wbg___wbindgen_number_get_9619185a74197f95 = function(arg0, arg1) { + const obj = arg1; + const ret = typeof(obj) === 'number' ? obj : undefined; + getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); + }; + imports.wbg.__wbg___wbindgen_rethrow_78714972834ecdf1 = function(arg0) { + throw arg0; + }; + imports.wbg.__wbg___wbindgen_shr_df5f0f11a6e22f2e = function(arg0, arg1) { + const ret = arg0 >> arg1; + return ret; + }; + imports.wbg.__wbg___wbindgen_string_get_a2a31e16edf96e42 = function(arg0, arg1) { + const obj = arg1; + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbg___wbindgen_throw_dd24417ed36fc46e = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + imports.wbg.__wbg__wbg_cb_unref_87dfb5aaa0cbcea7 = function(arg0) { + arg0._wbg_cb_unref(); + }; + imports.wbg.__wbg_apply_0be48b84f7a793c6 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = Reflect.apply(arg0, arg1, arg2); + return ret; + }, arguments) }; + imports.wbg.__wbg_async_bba5a2ac54b734df = function(arg0) { + const ret = arg0.async; + return ret; + }; + imports.wbg.__wbg_bind_65a4715c4a8c8727 = function(arg0, arg1, arg2, arg3) { + const ret = arg0.bind(arg1, arg2, arg3); + return ret; + }; + imports.wbg.__wbg_buffer_063cd102cc769a1c = function(arg0) { + const ret = arg0.buffer; + return ret; + }; + imports.wbg.__wbg_buffer_6cb2fecb1f253d71 = function(arg0) { + const ret = arg0.buffer; + return ret; + }; + imports.wbg.__wbg_byteLength_166ad9a51ecaa5f1 = function(arg0) { + const ret = arg0.byteLength; + return ret; + }; + imports.wbg.__wbg_call_3020136f7a2d6e44 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.call(arg1, arg2); + return ret; + }, arguments) }; + imports.wbg.__wbg_call_abb4ff46ce38be40 = function() { return handleError(function (arg0, arg1) { + const ret = arg0.call(arg1); + return ret; + }, arguments) }; + imports.wbg.__wbg_constructor_bd34b914d5a3a404 = function(arg0) { + const ret = arg0.constructor; + return ret; + }; + imports.wbg.__wbg_createObjectURL_7d9f7f8f41373850 = function() { return handleError(function (arg0, arg1) { + const ret = URL.createObjectURL(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, arguments) }; + imports.wbg.__wbg_data_8bf4ae669a78a688 = function(arg0) { + const ret = arg0.data; + return ret; + }; + imports.wbg.__wbg_done_62ea16af4ce34b24 = function(arg0) { + const ret = arg0.done; + return ret; + }; + imports.wbg.__wbg_entries_83c79938054e065f = function(arg0) { + const ret = Object.entries(arg0); + return ret; + }; + imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + } + }; + imports.wbg.__wbg_exports_58ef4156ca04d176 = function(arg0) { + const ret = arg0.exports; + return ret; + }; + imports.wbg.__wbg_exports_c060467610de1d8a = function(arg0) { + const ret = WebAssembly.Module.exports(arg0); + return ret; + }; + imports.wbg.__wbg_filename_e3800f22e3dfacab = function(arg0, arg1) { + const ret = arg1.filename; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbg_getPrototypeOf_847b9c7e1dd3c978 = function() { return handleError(function (arg0) { + const ret = Reflect.getPrototypeOf(arg0); + return ret; + }, arguments) }; + imports.wbg.__wbg_getRandomValues_ea728b1d79dae146 = function() { return handleError(function (arg0) { + globalThis.crypto.getRandomValues(arg0); + }, arguments) }; + imports.wbg.__wbg_get_16458e8ef25ea5fa = function() { return handleError(function (arg0, arg1) { + const ret = arg0.get(arg1 >>> 0); + return ret; + }, arguments) }; + imports.wbg.__wbg_get_6b7bd52aca3f9671 = function(arg0, arg1) { + const ret = arg0[arg1 >>> 0]; + return ret; + }; + imports.wbg.__wbg_get_af9dab7e9603ea93 = function() { return handleError(function (arg0, arg1) { + const ret = Reflect.get(arg0, arg1); + return ret; + }, arguments) }; + imports.wbg.__wbg_imports_d7c6f1e23e012391 = function(arg0) { + const ret = WebAssembly.Module.imports(arg0); + return ret; + }; + imports.wbg.__wbg_instanceof_ArrayBuffer_f3320d2419cd0355 = function(arg0) { + let result; + try { + result = arg0 instanceof ArrayBuffer; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_instanceof_Error_3443650560328fa9 = function(arg0) { + let result; + try { + result = arg0 instanceof Error; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_instanceof_Function_cb4bf9f70c667635 = function(arg0) { + let result; + try { + result = arg0 instanceof Function; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_instanceof_Global_fb07a06f4c6f25b9 = function(arg0) { + let result; + try { + result = arg0 instanceof WebAssembly.Global; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_instanceof_Map_084be8da74364158 = function(arg0) { + let result; + try { + result = arg0 instanceof Map; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_instanceof_Memory_a361de6f661b44e2 = function(arg0) { + let result; + try { + result = arg0 instanceof WebAssembly.Memory; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_instanceof_Module_d37a279bc9a18feb = function(arg0) { + let result; + try { + result = arg0 instanceof WebAssembly.Module; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_instanceof_Object_577e21051f7bcb79 = function(arg0) { + let result; + try { + result = arg0 instanceof Object; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_instanceof_Table_b6578721c6242aeb = function(arg0) { + let result; + try { + result = arg0 instanceof WebAssembly.Table; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_instanceof_Tag_18b644dbc43991d0 = function(arg0) { + let result; + try { + result = arg0 instanceof WebAssembly.Tag; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_instanceof_Uint8Array_da54ccc9d3e09434 = function(arg0) { + let result; + try { + result = arg0 instanceof Uint8Array; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_isArray_51fd9e6422c0a395 = function(arg0) { + const ret = Array.isArray(arg0); + return ret; + }; + imports.wbg.__wbg_isSafeInteger_ae7d3f054d55fa16 = function(arg0) { + const ret = Number.isSafeInteger(arg0); + return ret; + }; + imports.wbg.__wbg_iterator_27b7c8b35ab3e86b = function() { + const ret = Symbol.iterator; + return ret; + }; + imports.wbg.__wbg_length_22ac23eaec9d8053 = function(arg0) { + const ret = arg0.length; + return ret; + }; + imports.wbg.__wbg_length_d45040a40c570362 = function(arg0) { + const ret = arg0.length; + return ret; + }; + imports.wbg.__wbg_lineno_db90b4d01f04fae1 = function(arg0) { + const ret = arg0.lineno; + return ret; + }; + imports.wbg.__wbg_log_0cc1b7768397bcfe = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + console.log(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3), getStringFromWasm0(arg4, arg5), getStringFromWasm0(arg6, arg7)); + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + } + }; + imports.wbg.__wbg_log_cb9e190acc5753fb = function(arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + console.log(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + } + }; + imports.wbg.__wbg_mark_7438147ce31e9d4b = function(arg0, arg1) { + performance.mark(getStringFromWasm0(arg0, arg1)); + }; + imports.wbg.__wbg_measure_fb7825c11612c823 = function() { return handleError(function (arg0, arg1, arg2, arg3) { + let deferred0_0; + let deferred0_1; + let deferred1_0; + let deferred1_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + deferred1_0 = arg2; + deferred1_1 = arg3; + performance.measure(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3)); + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); + } + }, arguments) }; + imports.wbg.__wbg_message_0305fa7903f4b3d9 = function(arg0) { + const ret = arg0.message; + return ret; + }; + imports.wbg.__wbg_message_0ff7f09380783844 = function(arg0, arg1) { + const ret = arg1.message; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbg_new_1acc79167e62f5df = function() { return handleError(function (arg0, arg1) { + const ret = new WebAssembly.Instance(arg0, arg1); + return ret; + }, arguments) }; + imports.wbg.__wbg_new_1ba21ce319a06297 = function() { + const ret = new Object(); + return ret; + }; + imports.wbg.__wbg_new_25f239778d6112b9 = function() { + const ret = new Array(); + return ret; + }; + imports.wbg.__wbg_new_53cb1e86c1ef5d2a = function() { return handleError(function (arg0, arg1) { + const ret = new Worker(getStringFromWasm0(arg0, arg1)); + return ret; + }, arguments) }; + imports.wbg.__wbg_new_6421f6084cc5bc5a = function(arg0) { + const ret = new Uint8Array(arg0); + return ret; + }; + imports.wbg.__wbg_new_8a6f238a6ece86ea = function() { + const ret = new Error(); + return ret; + }; + imports.wbg.__wbg_new_ab1fe2437c86d873 = function(arg0) { + const ret = new SharedArrayBuffer(arg0 >>> 0); + return ret; + }; + imports.wbg.__wbg_new_de1e660b88fc921f = function(arg0) { + const ret = new Int32Array(arg0); + return ret; + }; + imports.wbg.__wbg_new_ff12d2b041fb48f1 = function(arg0, arg1) { + try { + var state0 = {a: arg0, b: arg1}; + var cb0 = (arg0, arg1) => { + const a = state0.a; + state0.a = 0; + try { + return wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___wasm_bindgen_99d0e8a507791149___JsValue__wasm_bindgen_99d0e8a507791149___JsValue_____(a, state0.b, arg0, arg1); + } finally { + state0.a = a; + } + }; + const ret = new Promise(cb0); + return ret; + } finally { + state0.a = state0.b = 0; + } + }; + imports.wbg.__wbg_new_from_slice_f9c22b9153b26992 = function(arg0, arg1) { + const ret = new Uint8Array(getArrayU8FromWasm0(arg0, arg1)); + return ret; + }; + imports.wbg.__wbg_new_no_args_cb138f77cf6151ee = function(arg0, arg1) { + const ret = new Function(getStringFromWasm0(arg0, arg1)); + return ret; + }; + imports.wbg.__wbg_new_with_length_12c6de4fac33117a = function(arg0) { + const ret = new Array(arg0 >>> 0); + return ret; + }; + imports.wbg.__wbg_new_with_length_aa5eaf41d35235e5 = function(arg0) { + const ret = new Uint8Array(arg0 >>> 0); + return ret; + }; + imports.wbg.__wbg_new_with_options_2978557c2c268ef3 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = new Worker(getStringFromWasm0(arg0, arg1), arg2); + return ret; + }, arguments) }; + imports.wbg.__wbg_new_with_u8_array_sequence_and_options_d4def9ec0588c7ec = function() { return handleError(function (arg0, arg1) { + const ret = new Blob(arg0, arg1); + return ret; + }, arguments) }; + imports.wbg.__wbg_next_138a17bbf04e926c = function(arg0) { + const ret = arg0.next; + return ret; + }; + imports.wbg.__wbg_next_3cfe5c0fe2a4cc53 = function() { return handleError(function (arg0) { + const ret = arg0.next(); + return ret; + }, arguments) }; + imports.wbg.__wbg_now_69d776cd24f5215b = function() { + const ret = Date.now(); + return ret; + }; + imports.wbg.__wbg_of_6505a0eb509da02e = function(arg0) { + const ret = Array.of(arg0); + return ret; + }; + imports.wbg.__wbg_of_7779827fa663eec8 = function(arg0, arg1, arg2) { + const ret = Array.of(arg0, arg1, arg2); + return ret; + }; + imports.wbg.__wbg_postMessage_07504dbe15265d5c = function() { return handleError(function (arg0, arg1) { + arg0.postMessage(arg1); + }, arguments) }; + imports.wbg.__wbg_postMessage_49af172ec8bdaa5a = function() { return handleError(function (arg0, arg1, arg2) { + arg0.postMessage(arg1, arg2); + }, arguments) }; + imports.wbg.__wbg_postMessage_7243f814e0cfb266 = function() { return handleError(function (arg0, arg1) { + arg0.postMessage(arg1); + }, arguments) }; + imports.wbg.__wbg_prototypesetcall_dfe9b766cdc1f1fd = function(arg0, arg1, arg2) { + Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); + }; + imports.wbg.__wbg_push_7d9be8f38fc13975 = function(arg0, arg1) { + const ret = arg0.push(arg1); + return ret; + }; + imports.wbg.__wbg_queueMicrotask_9b549dfce8865860 = function(arg0) { + const ret = arg0.queueMicrotask; + return ret; + }; + imports.wbg.__wbg_queueMicrotask_fca69f5bfad613a5 = function(arg0) { + queueMicrotask(arg0); + }; + imports.wbg.__wbg_resolve_fd5bfbaa4ce36e1e = function(arg0) { + const ret = Promise.resolve(arg0); + return ret; + }; + imports.wbg.__wbg_set_169e13b608078b7b = function(arg0, arg1, arg2) { + arg0.set(getArrayU8FromWasm0(arg1, arg2)); + }; + imports.wbg.__wbg_set_3f1d0b984ed272ed = function(arg0, arg1, arg2) { + arg0[arg1] = arg2; + }; + imports.wbg.__wbg_set_781438a03c0c3c81 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = Reflect.set(arg0, arg1, arg2); + return ret; + }, arguments) }; + imports.wbg.__wbg_set_7df433eea03a5c14 = function(arg0, arg1, arg2) { + arg0[arg1 >>> 0] = arg2; + }; + imports.wbg.__wbg_set_name_5399dd71553bb72d = function(arg0, arg1, arg2) { + arg0.name = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_set_onerror_d5671da43c08b208 = function(arg0, arg1) { + arg0.onerror = arg1; + }; + imports.wbg.__wbg_set_onmessage_5fe29d0fb54cb575 = function(arg0, arg1) { + arg0.onmessage = arg1; + }; + imports.wbg.__wbg_set_onmessage_deb94985de696ac7 = function(arg0, arg1) { + arg0.onmessage = arg1; + }; + imports.wbg.__wbg_set_type_7ce650670a34c68f = function(arg0, arg1, arg2) { + arg0.type = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_set_type_c2eb2929316959f4 = function(arg0, arg1) { + arg0.type = __wbindgen_enum_WorkerType[arg1]; + }; + imports.wbg.__wbg_stack_0ed75d68575b0f3c = function(arg0, arg1) { + const ret = arg1.stack; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbg_static_accessor_GLOBAL_769e6b65d6557335 = function() { + const ret = typeof global === 'undefined' ? null : global; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_static_accessor_GLOBAL_THIS_60cf02db4de8e1c1 = function() { + const ret = typeof globalThis === 'undefined' ? null : globalThis; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_static_accessor_IMPORT_META_URL_d241d5473d4f98c3 = function(arg0) { + const ret = import.meta.url; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbg_static_accessor_SELF_08f5a74c69739274 = function() { + const ret = typeof self === 'undefined' ? null : self; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_static_accessor_WINDOW_a8924b26aa92d024 = function() { + const ret = typeof window === 'undefined' ? null : window; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_subarray_845f2f5bce7d061a = function(arg0, arg1, arg2) { + const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0); + return ret; + }; + imports.wbg.__wbg_terminate_08a1236dd2e69da0 = function(arg0) { + arg0.terminate(); + }; + imports.wbg.__wbg_then_4f95312d68691235 = function(arg0, arg1) { + const ret = arg0.then(arg1); + return ret; + }; + imports.wbg.__wbg_toString_99815908ae93d7d1 = function(arg0, arg1, arg2) { + const ret = arg1.toString(arg2); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbg_toString_f07112df359c997f = function(arg0) { + const ret = arg0.toString(); + return ret; + }; + imports.wbg.__wbg_trap_new = function(arg0) { + const ret = Trap.__wrap(arg0); + return ret; + }; + imports.wbg.__wbg_value_4cd497eeadba94bd = function(arg0) { + const ret = arg0.value; + return ret; + }; + imports.wbg.__wbg_value_57b7b035e117f7ee = function(arg0) { + const ret = arg0.value; + return ret; + }; + imports.wbg.__wbg_waitAsync_8afec80ffd213eca = function(arg0, arg1, arg2) { + const ret = Atomics.waitAsync(arg0, arg1 >>> 0, arg2); + return ret; + }; + imports.wbg.__wbg_waitAsync_c186cb97ffacd552 = function() { + const ret = Atomics.waitAsync; + return ret; + }; + imports.wbg.__wbg_wait_ceb109d0011d6567 = function() { return handleError(function (arg0, arg1, arg2, arg3) { + const ret = Atomics.wait(arg0, arg1 >>> 0, arg2, arg3); + return ret; + }, arguments) }; + imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }; + imports.wbg.__wbindgen_cast_4625c577ab2ec9ee = function(arg0) { + // Cast intrinsic for `U64 -> Externref`. + const ret = BigInt.asUintN(64, arg0); + return ret; + }; + imports.wbg.__wbindgen_cast_5799a4c49e587223 = function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { dtor_idx: 204, function: Function { arguments: [NamedExternref("MessageEvent")], shim_idx: 205, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_99d0e8a507791149___closure__destroy___dyn_core_b0d591bc3b1f24da___ops__function__FnMut__wasm_bindgen_99d0e8a507791149___JsValue____Output_______, wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___wasm_bindgen_99d0e8a507791149___JsValue_____); + return ret; + }; + imports.wbg.__wbindgen_cast_9ae0607507abb057 = function(arg0) { + // Cast intrinsic for `I64 -> Externref`. + const ret = arg0; + return ret; + }; + imports.wbg.__wbindgen_cast_b52493afb474412a = function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { dtor_idx: 204, function: Function { arguments: [Externref], shim_idx: 205, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_99d0e8a507791149___closure__destroy___dyn_core_b0d591bc3b1f24da___ops__function__FnMut__wasm_bindgen_99d0e8a507791149___JsValue____Output_______, wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___wasm_bindgen_99d0e8a507791149___JsValue_____); + return ret; + }; + imports.wbg.__wbindgen_cast_c27f97883acb4e05 = function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { dtor_idx: 183, function: Function { arguments: [NamedExternref("ErrorEvent")], shim_idx: 184, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_99d0e8a507791149___closure__destroy___dyn_core_b0d591bc3b1f24da___ops__function__FnMut__web_sys_d796e95248ce21f5___features__gen_ErrorEvent__ErrorEvent____Output_______, wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___web_sys_d796e95248ce21f5___features__gen_ErrorEvent__ErrorEvent_____); + return ret; + }; + imports.wbg.__wbindgen_cast_d6cd19b81560fd6e = function(arg0) { + // Cast intrinsic for `F64 -> Externref`. + const ret = arg0; + return ret; + }; + imports.wbg.__wbindgen_cast_e7b45dd881f38ce3 = function(arg0, arg1) { + // Cast intrinsic for `U128 -> Externref`. + const ret = (BigInt.asUintN(64, arg0) | (BigInt.asUintN(64, arg1) << BigInt(64))); + return ret; + }; + imports.wbg.__wbindgen_init_externref_table = function() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + }; + imports.wbg.__wbindgen_link_203404ece0e9bab9 = function(arg0) { + const val = `onmessage = function (ev) { + let [ia, index, value] = ev.data; + ia = new Int32Array(ia.buffer); + let result = Atomics.wait(ia, index, value); + postMessage(result); + }; + `; + const ret = typeof URL.createObjectURL === 'undefined' ? "data:application/javascript," + encodeURIComponent(val) : URL.createObjectURL(new Blob([val], { type: "text/javascript" })); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.memory = memory || new WebAssembly.Memory({initial:18,maximum:16384,shared:true}); + + return imports; +} + +function __wbg_finalize_init(instance, module, thread_stack_size) { + wasm = instance.exports; + __wbg_init.__wbindgen_wasm_module = module; + cachedDataViewMemory0 = null; + cachedUint8ArrayMemory0 = null; + + if (typeof thread_stack_size !== 'undefined' && (typeof thread_stack_size !== 'number' || thread_stack_size === 0 || thread_stack_size % 65536 !== 0)) { throw 'invalid stack size' } + wasm.__wbindgen_start(thread_stack_size); + return wasm; +} + +function initSync(module, memory) { + if (wasm !== undefined) return wasm; + + let thread_stack_size + if (typeof module !== 'undefined') { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module, memory, thread_stack_size} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(memory); + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + const instance = new WebAssembly.Instance(module, imports); + return __wbg_finalize_init(instance, module, thread_stack_size); +} + +async function __wbg_init(module_or_path, memory) { + if (wasm !== undefined) return wasm; + + let thread_stack_size + if (typeof module_or_path !== 'undefined') { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path, memory, thread_stack_size} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (typeof module_or_path === 'undefined') { + module_or_path = new URL('wasmi_plugin_coordinator_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(memory); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module, thread_stack_size); +} + +export { initSync }; +export default __wbg_init; diff --git a/crates/wasmi-plugin-coordinator/assets/worker.wasm b/crates/wasmi-plugin-coordinator/assets/worker.wasm new file mode 100644 index 0000000000000000000000000000000000000000..dd2dbafd64d77ec4fa364f1667975920ea0f9227 GIT binary patch literal 368657 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%0Dpqi$U)+MsW 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 = OnceLock::new(); const WORKER_LOADER_JS: &str = include_str!("../../../wasmi-plugin-coordinator/src/worker.js"); -const WORKER_JS_GLUE: &str = - include_str!("../../../wasmi-plugin-coordinator/pkg/wasmi_plugin_coordinator.js"); +const WORKER_JS_GLUE: &str = include_str!("../../../wasmi-plugin-coordinator/assets/worker.js"); const WORKER_WASM_BYTES: &[u8] = - include_bytes!("../../../wasmi-plugin-coordinator/pkg/wasmi_plugin_coordinator_bg.wasm"); + include_bytes!("../../../wasmi-plugin-coordinator/assets/worker.wasm"); unsafe impl Send for InnerWorkerRuntime {} unsafe impl Sync for InnerWorkerRuntime {} diff --git a/makefile b/makefile index 965b4c6..91ec171 100644 --- a/makefile +++ b/makefile @@ -1,5 +1,6 @@ # === Configuration === BROWSER := --firefox +ASSETS_DIR := crates/wasmi-plugin-coordinator/assets .PHONY: help build-plugin build-coordinator build-all bench-wasm @@ -20,6 +21,10 @@ build-coordinator: crates/wasmi-plugin-coordinator/pkg/wasmi_plugin_coordinator_bg.wasm \ -o crates/wasmi-plugin-coordinator/pkg/wasmi_plugin_coordinator_bg.wasm + mkdir -p $(ASSETS_DIR) + @cp crates/wasmi-plugin-coordinator/pkg/wasmi_plugin_coordinator_bg.wasm $(ASSETS_DIR)/worker.wasm + @cp crates/wasmi-plugin-coordinator/pkg/wasmi_plugin_coordinator.js $(ASSETS_DIR)/worker.js + build-all: build-plugin build-coordinator clippy-native: build-all From 60ad1bb79b5beab5321159f2a7325de5eb51f530 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Fri, 16 Jan 2026 14:46:28 -0500 Subject: [PATCH 68/70] feat: artifact checker --- .github/workflows/ci.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7c2684..f0bc5e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,4 +50,21 @@ jobs: uses: Swatinem/rust-cache@v2 - run: nix-shell --run "make clippy-native" - - run: nix-shell --run "make clippy-wasm" \ No newline at end of file + - run: nix-shell --run "make clippy-wasm" + + check-artifacts: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v14 + - name: Magic Nix Cache + uses: DeterminateSystems/magic-nix-cache-action@v8 + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + + - name: Rebuild Wasm Artifacts + run: nix-shell --run "make build-all" + + - name: Ensure Artifacts Match Source + run: git diff --exit-code \ No newline at end of file From 758e50438a2e46b1f97d225501b8a453fc978126 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Thu, 21 May 2026 22:14:15 -0400 Subject: [PATCH 69/70] switch to nix flake --- flake.lock | 99 +++++++++++++++++++++++++++++++++++++ flake.nix | 70 ++++++++++++++++++++++++++ flakes/wasm-bindgen-cli.nix | 19 ------- shell.nix | 37 -------------- todo.md | 47 ------------------ 5 files changed, 169 insertions(+), 103 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix delete mode 100644 flakes/wasm-bindgen-cli.nix delete mode 100644 shell.nix delete mode 100644 todo.md diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..67e77eb --- /dev/null +++ b/flake.lock @@ -0,0 +1,99 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1779102034, + "narHash": "sha256-vZJZjLo513IeI8hjzHFc6TDezUd4uCE2Eq4SNO3DNNg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "687f05a9184cad4eaf905c48b63649e3a86f5433", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay", + "unstable": "unstable" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1779333539, + "narHash": "sha256-lpmN2lrBDZDPjov2cbD3bOOJsI0fkKolKXasYPCqSys=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "672fa5fc5608d5cd82286a6f69aaf84a40b4fe41", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "unstable": { + "locked": { + "lastModified": 1778869304, + "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..288d72f --- /dev/null +++ b/flake.nix @@ -0,0 +1,70 @@ +{ + description = "Tlock dev shell"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + unstable.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs, + unstable, + rust-overlay, + flake-utils, + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlays.default ]; + }; + + unstablePkgs = import unstable { + inherit system; + }; + + rustToolchain = pkgs.rust-bin.nightly.latest.default.override { + extensions = [ + "rust-src" + "llvm-tools" + "rust-analyzer" + ]; + targets = [ + "wasm32-wasip1" + "wasm32-unknown-unknown" + ]; + }; + + rustfmtNightly = pkgs.rust-bin.nightly.latest.rustfmt; + in + { + devShells = { + default = pkgs.mkShell { + packages = [ + rustToolchain + pkgs.cargo-sort + pkgs.cargo-machete + pkgs.cargo-udeps + pkgs.samply + pkgs.binaryen + pkgs.wasm-pack + pkgs.geckodriver + unstablePkgs.wasm-bindgen-cli_0_2_118 + pkgs.wasm-tools + pkgs.wabt + ]; + + env.WASM_BINDGEN = "${unstablePkgs.wasm-bindgen-cli_0_2_121}/bin/wasm-bindgen"; + }; + }; + } + ); +} diff --git a/flakes/wasm-bindgen-cli.nix b/flakes/wasm-bindgen-cli.nix deleted file mode 100644 index dac40d2..0000000 --- a/flakes/wasm-bindgen-cli.nix +++ /dev/null @@ -1,19 +0,0 @@ -{ - buildWasmBindgenCli, - fetchCrate, - rustPlatform, -}: - -buildWasmBindgenCli rec { - src = fetchCrate { - pname = "wasm-bindgen-cli"; - version = "0.2.106"; - hash = "sha256-M6WuGl7EruNopHZbqBpucu4RWz44/MSdv6f0zkYw+44="; - }; - - cargoDeps = rustPlatform.fetchCargoVendor { - inherit src; - inherit (src) pname version; - hash = "sha256-ElDatyOwdKwHg3bNH/1pcxKI7LXkhsotlDPQjiLHBwA="; - }; -} diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 556681a..0000000 --- a/shell.nix +++ /dev/null @@ -1,37 +0,0 @@ -let - pkgs = import { - overlays = [ - (import (builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz")) - ]; - }; - rustToolchain = pkgs.rust-bin.nightly.latest.default.override { - extensions = [ "rust-src" ]; - targets = [ - "wasm32-wasip1" - "wasm32-unknown-unknown" - ]; - }; - wasm-bindgen-cli_0_2_106 = pkgs.callPackage ./flakes/wasm-bindgen-cli.nix { }; -in -pkgs.mkShell { - packages = with pkgs; [ - rustToolchain - rust-analyzer - cargo-sort - cargo-machete - cargo-udeps - samply - binaryen - wasm-pack - geckodriver - wasm-bindgen-cli_0_2_106 - wasm-tools - wabt - ]; - - shellHook = '' - export WASM_BINDGEN_THREADS_HEADERS=1 - export WASM_BINDGEN_TEST_TIMEOUT=100 - export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library" - ''; -} diff --git a/todo.md b/todo.md deleted file mode 100644 index 21cd847..0000000 --- a/todo.md +++ /dev/null @@ -1,47 +0,0 @@ -[PLUGIN_0] [50.278] [test_plugin] INFO test_plugin: Starting plugin... -[WORKER_0] [50.279] INFO /runtime/shared_pipe.rs:152 SharedPipe_WORKER_STDIN::read called, buf len=8192 -[PLUGIN_0] [50.279] [test_plugin] INFO wasmi_plugin_pdk::server: PluginServer: Received request: RpcRequest { jsonrpc: "2.0", id: 1, method: "call_many", params: Number(200) } - -#? These are logs from a background read loop running in the plugin to enable async calls. But they're not relevant right now -[PLUGIN_0] [50.279] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver_Background: Awaiting incoming message... -[WORKER_0] [50.279] INFO /runtime/shared_pipe.rs:152 SharedPipe_WORKER_STDIN::read called, buf len=8192 -[WORKER_0] [50.279] INFO /runtime/shared_pipe.rs:168 SharedPipe_WORKER_STDIN::read WouldBlock -[PLUGIN_0] [50.279] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver_Background: Message processed, yielding... - -[PLUGIN_0] [50.279] [test_plugin] INFO test_plugin: Making call 0 -[PLUGIN_0] [50.280] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Sending request ID 0 -[WORKER_0] [50.280] INFO /runtime/shared_pipe.rs:187 SharedPipe_WORKER_STDOUT::write called, buf len=55 -[WORKER_0] [50.280] INFO /runtime/shared_pipe.rs:215 SharedPipe_WORKER_STDOUT::write wrote 55, new_w=55 -[PLUGIN_0] [50.280] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Locking reader... -[PLUGIN_0] [50.280] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Waiting for response id=0 -[WORKER_0] [50.280] INFO /runtime/shared_pipe.rs:152 SharedPipe_WORKER_STDIN::read called, buf len=8192 -[WORKER_0] [50.280] INFO /runtime/shared_pipe.rs:168 SharedPipe_WORKER_STDIN::read WouldBlock -[PLUGIN_0] [50.280] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: WouldBlock, yielding... id=0 -[WORKER_0] [50.280] INFO wasi/wasi_ctx.rs:569 wasi poll_oneoff(1046552, 1046600, 1, 1046636) -[WORKER_0] [50.280] INFO wasi/wasi_ctx.rs:670 poll_oneoff: waiting for stdin or clock timeout... -[WORKER_0] [50.280] INFO /runtime/shared_pipe.rs:241 SharedPipe_WORKER_STDIN::wait_ready called, timeout=inf -[WORKER_0] [50.280] INFO /runtime/shared_pipe.rs:250 SharedPipe_WORKER_STDIN::wait_ready checking r=59, current_w=59 -[WORKER_0] [50.281] INFO /runtime/shared_pipe.rs:277 SharedPipe_WORKER_STDIN::wait_ready resolved -[PLUGIN_0] [50.281] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Resuming... id=0 -[WORKER_0] [50.281] INFO /runtime/shared_pipe.rs:152 SharedPipe_WORKER_STDIN::read called, buf len=8192 -[PLUGIN_0] [50.281] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Received message: RpcResponse(RpcResponse { jsonrpc: "2.0", id: 0, result: String("pong") }) -[PLUGIN_0] [50.281] [test_plugin] INFO test_plugin: Call 0 got response: String("pong") - -[PLUGIN_0] [50.281] [test_plugin] INFO test_plugin: Making call 1 -[PLUGIN_0] [50.281] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Sending request ID 1 -[WORKER_0] [50.281] INFO /runtime/shared_pipe.rs:187 SharedPipe_WORKER_STDOUT::write called, buf len=55 -[WORKER_0] [50.281] INFO /runtime/shared_pipe.rs:215 SharedPipe_WORKER_STDOUT::write wrote 55, new_w=110 -[PLUGIN_0] [50.281] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Locking reader... -[PLUGIN_0] [50.281] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Waiting for response id=1 -[WORKER_0] [50.281] INFO /runtime/shared_pipe.rs:152 SharedPipe_WORKER_STDIN::read called, buf len=8192 -[WORKER_0] [50.281] INFO /runtime/shared_pipe.rs:168 SharedPipe_WORKER_STDIN::read WouldBlock -[PLUGIN_0] [50.281] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: WouldBlock, yielding... id=1 -[WORKER_0] [50.281] INFO wasi/wasi_ctx.rs:569 wasi poll_oneoff(1046552, 1046600, 1, 1046636) -[WORKER_0] [50.281] INFO wasi/wasi_ctx.rs:670 poll_oneoff: waiting for stdin or clock timeout... -[WORKER_0] [50.281] INFO /runtime/shared_pipe.rs:241 SharedPipe_WORKER_STDIN::wait_ready called, timeout=inf -[WORKER_0] [50.281] INFO /runtime/shared_pipe.rs:250 SharedPipe_WORKER_STDIN::wait_ready checking r=100, current_w=100 -[WORKER_0] [50.282] INFO /runtime/shared_pipe.rs:277 SharedPipe_WORKER_STDIN::wait_ready resolved -[PLUGIN_0] [50.282] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Resuming... id=1 -[WORKER_0] [50.282] INFO /runtime/shared_pipe.rs:152 SharedPipe_WORKER_STDIN::read called, buf len=8192 -[PLUGIN_0] [50.282] [test_plugin] INFO wasmi_plugin_pdk::transport_driver: TransportDriver: Received message: RpcResponse(RpcResponse { jsonrpc: "2.0", id: 1, result: String("pong") }) -[PLUGIN_0] [50.282] [test_plugin] INFO test_plugin: Call 1 got response: String("pong") \ No newline at end of file From d2af0fdcaf0dba244eeb3098baef4141e1ae8ff5 Mon Sep 17 00:00:00 2001 From: Robert MacWha Date: Thu, 21 May 2026 22:33:37 -0400 Subject: [PATCH 70/70] feat: switch to atomics-based impl for wasm --- .cargo/config.toml | 12 + Cargo.lock | 1338 +++++++++-------- Cargo.toml | 12 +- crates/test-plugin/Cargo.toml | 5 +- .../Cargo.toml | 18 - .../src/lib.rs | 73 - .../.cargo/config.toml | 16 - crates/wasmi-plugin-coordinator/Cargo.toml | 45 - crates/wasmi-plugin-coordinator/README.md | 79 - .../wasmi-plugin-coordinator/assets/worker.js | 1084 ------------- .../assets/worker.wasm | Bin 368657 -> 0 bytes crates/wasmi-plugin-coordinator/src/lib.rs | 4 - crates/wasmi-plugin-coordinator/src/worker.js | 17 - crates/wasmi-plugin-coordinator/src/worker.rs | 257 ---- crates/wasmi-plugin-hdk/Cargo.toml | 23 +- crates/wasmi-plugin-hdk/benches/benchmark.rs | 34 +- crates/wasmi-plugin-hdk/benches/wasm_bench.rs | 54 +- crates/wasmi-plugin-hdk/src/runtime/mod.rs | 2 + .../src/runtime/wasm_worker_pool}/mod.rs | 0 .../src/runtime/wasm_worker_pool}/pool.rs | 8 +- .../src/runtime/wasm_worker_pool}/worker.js | 0 .../src/runtime/wasm_worker_pool}/worker.rs | 9 - .../wasm_worker_pool}/worker_message.rs | 0 .../src/runtime/worker_runtime.rs | 538 ++++--- makefile | 19 +- 25 files changed, 1071 insertions(+), 2576 deletions(-) delete mode 100644 crates/wasmi-plugin-coordinator-protocol/Cargo.toml delete mode 100644 crates/wasmi-plugin-coordinator-protocol/src/lib.rs delete mode 100644 crates/wasmi-plugin-coordinator/.cargo/config.toml delete mode 100644 crates/wasmi-plugin-coordinator/Cargo.toml delete mode 100644 crates/wasmi-plugin-coordinator/README.md delete mode 100644 crates/wasmi-plugin-coordinator/assets/worker.js delete mode 100644 crates/wasmi-plugin-coordinator/assets/worker.wasm delete mode 100644 crates/wasmi-plugin-coordinator/src/lib.rs delete mode 100644 crates/wasmi-plugin-coordinator/src/worker.js delete mode 100644 crates/wasmi-plugin-coordinator/src/worker.rs rename crates/{wasmi-plugin-coordinator/src/worker_pool => wasmi-plugin-hdk/src/runtime/wasm_worker_pool}/mod.rs (100%) rename crates/{wasmi-plugin-coordinator/src/worker_pool => wasmi-plugin-hdk/src/runtime/wasm_worker_pool}/pool.rs (98%) rename crates/{wasmi-plugin-coordinator/src/worker_pool => wasmi-plugin-hdk/src/runtime/wasm_worker_pool}/worker.js (100%) rename crates/{wasmi-plugin-coordinator/src/worker_pool => wasmi-plugin-hdk/src/runtime/wasm_worker_pool}/worker.rs (75%) rename crates/{wasmi-plugin-coordinator/src/worker_pool => wasmi-plugin-hdk/src/runtime/wasm_worker_pool}/worker_message.rs (100%) diff --git a/.cargo/config.toml b/.cargo/config.toml index 087a744..83278d5 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,17 @@ [target.wasm32-unknown-unknown] rustflags = [ "--cfg", "getrandom_backend=\"wasm_js\"", + "-C", "target-feature=+atomics,+bulk-memory", + "-C", "link-arg=--shared-memory", + "-C", "link-arg=--max-memory=1073741824", + "-C", "link-arg=--import-memory", + "-C", "link-arg=--export=__heap_base", + "-C", "link-arg=--export=__wasm_init_tls", + "-C", "link-arg=--export=__tls_size", + "-C", "link-arg=--export=__tls_align", + "-C", "link-arg=--export=__tls_base" ] runner = "wasm-bindgen-test-runner" + +[unstable] +build-std = ["std", "panic_abort"] \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 397d918..2c694b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,18 +17,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -47,6 +35,12 @@ dependencies = [ "cc", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anes" version = "0.1.6" @@ -55,9 +49,15 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" @@ -73,7 +73,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -99,11 +99,11 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.70.1" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -114,7 +114,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.111", + "syn", ] [[package]] @@ -125,34 +125,26 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" -version = "0.10.4" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" dependencies = [ - "bytecheck_derive 0.6.12", - "ptr_meta 0.1.4", - "simdutf8", + "allocator-api2", ] [[package]] @@ -161,23 +153,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" dependencies = [ - "bytecheck_derive 0.8.2", - "ptr_meta 0.3.1", + "bytecheck_derive", + "ptr_meta", "rancor", "simdutf8", ] -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "bytecheck_derive" version = "0.8.2" @@ -186,20 +167,20 @@ checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] -name = "byteorder" -version = "1.5.0" +name = "bytes" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] -name = "bytes" -version = "1.11.0" +name = "bytesize" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" [[package]] name = "cast" @@ -209,9 +190,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.49" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "shlex", @@ -272,18 +253,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstyle", "clap_lex", @@ -291,15 +272,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] @@ -314,6 +295,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "convert_case" version = "0.10.0" @@ -325,9 +312,9 @@ dependencies = [ [[package]] name = "corosensei" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2b4c7e3e97730e6b0b8c5ff5ca82c663d1a645e4f630f4fa4c24e80626787e" +checksum = "2c54787b605c7df106ceccf798df23da4f2e09918defad66705d1cedf3bb914f" dependencies = [ "autocfg", "cfg-if", @@ -338,35 +325,57 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.17" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ "libc", ] +[[package]] +name = "cranelift-assembler-x64" +version = "0.129.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b242b4c3675139f52f0b55624fb92571551a344305c5998f55ad20fa527bc55" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.129.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "499715f19799219f32641b14f2a162f91e50bc1b61c2d2184c2be971716f5c56" +dependencies = [ + "cranelift-srcgen", +] + [[package]] name = "cranelift-bforest" -version = "0.110.2" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305d51c180ebdc46ef61bc60c54ae6512db3bc9a05842a1f1e762e45977019ab" +checksum = "9a92d78cc3f087d7e7073828f08d98c7074a3a062b6b29a1b7783ce74305685e" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-bitset" -version = "0.110.3" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "690d8ae6c73748e5ce3d8fe59034dceadb8823e6c8994ba324141c5eae909b0e" +checksum = "edcc73d756f2e0d7eda6144fe64a2bc69c624de893cb1be51f1442aed77881d2" +dependencies = [ + "wasmtime-internal-core", +] [[package]] name = "cranelift-codegen" -version = "0.110.2" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7ca95e831c18d1356da783765c344207cbdffea91e13e47fa9327dbb2e0719" +checksum = "683d94c2cd0d73b41369b88da1129589bc3a2d99cf49979af1d14751f35b7a1b" dependencies = [ "bumpalo", + "cranelift-assembler-x64", "cranelift-bforest", "cranelift-bitset", "cranelift-codegen-meta", @@ -374,53 +383,60 @@ dependencies = [ "cranelift-control", "cranelift-entity", "cranelift-isle", - "gimli 0.28.1", - "hashbrown 0.14.5", + "gimli 0.33.0", + "hashbrown 0.15.5", + "libm", "log", "regalloc2", "rustc-hash", + "serde", "smallvec", "target-lexicon", + "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen-meta" -version = "0.110.3" +version = "0.129.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0a2d2ab65e6cbf91f81781d8da65ec2005510f18300eff21a99526ed6785863" +checksum = "483b2c94a1b7f6fba0714387ba34ca56d114b2214a80be018acbb2ed40e09a1e" dependencies = [ + "cranelift-assembler-x64-meta", "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", ] [[package]] name = "cranelift-codegen-shared" -version = "0.110.3" +version = "0.129.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efcff860573cf3db9ae98fbd949240d78b319df686cc306872e7fab60e9c84d7" +checksum = "c4aae718c336a52d90d4ebe9a2d8c3cf0906a4bee78f0e6867e777eebbe554fe" [[package]] name = "cranelift-control" -version = "0.110.3" +version = "0.129.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d70e5b75c2d5541ef80a99966ccd97aaa54d2a6af19ea31759a28538e1685a" +checksum = "a18e94519070dc56cddb71906a08cea6a28a1d7c58ed501b88f273fa6b45fa07" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.110.2" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a48cb0a194c9ba82fec35a1e492055388d89b2e3c03dee9dcf2488892be8004d" +checksum = "59d8e72637246edd2cba337939850caa8b201f6315925ec4c156fdd089999699" dependencies = [ "cranelift-bitset", + "wasmtime-internal-core", ] [[package]] name = "cranelift-frontend" -version = "0.110.2" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8327afc6c1c05f4be62fefce5b439fa83521c65363a322e86ea32c85e7ceaf64" +checksum = "4c31db0085c3dfa131e739c3b26f9f9c84d69a9459627aac1ac4ef8355e3411b" dependencies = [ "cranelift-codegen", "log", @@ -430,9 +446,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.110.2" +version = "0.129.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56b08621c00321efcfa3eee6a3179adc009e21ea8d24ca7adc3c326184bc3f48" +checksum = "524d804c1ebd8c542e6f64e71aa36934cec17c5da4a9ae3799796220317f5d23" + +[[package]] +name = "cranelift-srcgen" +version = "0.129.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a001a9dc4557d9e2be324bc932621c0aa9bf33b74dfefa2338f0bf8913329" [[package]] name = "crc32fast" @@ -445,9 +467,9 @@ dependencies = [ [[package]] name = "criterion" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0dfe5e9e71bdcf4e4954f7d14da74d1cdb92a3a07686452d1509652684b1aab" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ "alloca", "anes", @@ -470,14 +492,23 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de36c2bee19fba779808f92bf5d9b0fa5a40095c277aba10c458a12b35d21d6" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", "itertools 0.13.0", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -520,12 +551,11 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "generic-array", - "typenum", + "hybrid-array", ] [[package]] @@ -548,7 +578,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -559,14 +589,14 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.111", + "syn", ] [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -578,93 +608,83 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "derive_more" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", - "syn 2.0.111", + "syn", "unicode-xid", ] [[package]] name = "digest" -version = "0.10.7" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer", + "const-oid", "crypto-common", ] [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "enum-iterator" -version = "0.7.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" dependencies = [ "enum-iterator-derive", ] [[package]] name = "enum-iterator-derive" -version = "0.7.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] name = "enumset" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d" dependencies = [ "enumset_derive", ] [[package]] name = "enumset_derive" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" +checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -684,34 +704,32 @@ dependencies = [ ] [[package]] -name = "fallible-iterator" -version = "0.3.0" +name = "fastrand" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -723,11 +741,23 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -740,9 +770,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -750,15 +780,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -767,38 +797,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -808,63 +838,55 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" -version = "0.2.16" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "r-efi 5.3.0", + "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", + "wasip3", "wasm-bindgen", ] [[package]] name = "gimli" -version = "0.28.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" -dependencies = [ - "fallible-iterator", - "indexmap", - "stable_deref_trait", -] +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "gimli" -version = "0.32.3" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] [[package]] name = "glob" @@ -897,33 +919,39 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.13.2" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "ahash", + "foldhash 0.1.5", ] [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "ahash", + "foldhash 0.2.0", ] [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] -name = "hashbrown" -version = "0.16.1" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hex" @@ -931,6 +959,21 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -939,44 +982,48 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.12.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itertools" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -989,9 +1036,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "leb128" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" [[package]] name = "leb128fmt" @@ -1001,9 +1048,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -1017,20 +1064,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "libredox" -version = "0.1.11" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" -dependencies = [ - "bitflags 2.10.0", - "libc", - "redox_syscall 0.6.0", -] +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libunwind" @@ -1040,9 +1076,9 @@ checksum = "0c6639b70a7ce854b79c70d7e83f16b5dc0137cc914f3d7d03803b513ecc67ac" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "lock_api" @@ -1068,13 +1104,19 @@ dependencies = [ "libc", ] +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + [[package]] name = "macho-unwind-info" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb4bdc8b0ce69932332cf76d24af69c3a155242af95c226b2ab6c2e371ed1149" dependencies = [ - "thiserror 2.0.17", + "thiserror", "zerocopy", "zerocopy-derive", ] @@ -1090,9 +1132,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" @@ -1103,6 +1145,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -1140,9 +1191,9 @@ dependencies = [ [[package]] name = "more-asserts" -version = "0.2.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7843ec2de400bcbc6a6328c958dc38e5359da6e93e72e37bc5246bf1ae776389" +checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" [[package]] name = "munge" @@ -1161,7 +1212,7 @@ checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -1195,32 +1246,32 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ - "crc32fast", - "flate2", - "hashbrown 0.14.5", - "indexmap", "memchr", - "ruzstd", ] [[package]] name = "object" -version = "0.37.3" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" dependencies = [ + "crc32fast", + "flate2", + "hashbrown 0.16.1", + "indexmap", "memchr", + "ruzstd", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "oorandom" @@ -1238,6 +1289,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + [[package]] name = "parking_lot_core" version = "0.9.12" @@ -1246,7 +1307,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] @@ -1259,15 +1320,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "plotters" @@ -1313,7 +1368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.111", + "syn", ] [[package]] @@ -1335,45 +1390,25 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive 0.1.4", -] - [[package]] name = "ptr_meta" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" dependencies = [ - "ptr_meta_derive 0.3.1", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "ptr_meta_derive", ] [[package]] @@ -1384,14 +1419,14 @@ checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1402,20 +1437,26 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rancor" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" dependencies = [ - "ptr_meta 0.3.1", + "ptr_meta", ] [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", "rand_core", @@ -1433,18 +1474,24 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -1466,36 +1513,28 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_syscall" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" -dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", ] [[package]] name = "regalloc2" -version = "0.9.3" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad156d539c879b7a24a363a2016d77961786e71f48f2e2fc8302a92abd2429a6" +checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa" dependencies = [ - "hashbrown 0.13.2", + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", "log", "rustc-hash", - "slice-group-by", "smallvec", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -1505,9 +1544,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1516,9 +1555,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "region" @@ -1528,7 +1567,7 @@ checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" dependencies = [ "bitflags 1.3.2", "libc", - "mach2", + "mach2 0.4.3", "windows-sys 0.52.0", ] @@ -1538,21 +1577,21 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" dependencies = [ - "bytecheck 0.8.2", + "bytecheck", ] [[package]] name = "rkyv" -version = "0.8.12" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a640b26f007713818e9a9b65d34da1cf58538207b052916a83d80e43f3ffa4" +checksum = "73389e0c99e664f919275ab5b5b0471391fe9a8de61e1dff9b1eaf56a90f16e3" dependencies = [ - "bytecheck 0.8.2", + "bytecheck", "bytes", - "hashbrown 0.15.5", + "hashbrown 0.17.1", "indexmap", "munge", - "ptr_meta 0.3.1", + "ptr_meta", "rancor", "rend", "rkyv_derive", @@ -1562,26 +1601,26 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.8.12" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd83f5f173ff41e00337d97f6572e416d022ef8a19f371817259ae960324c482" +checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -1594,11 +1633,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -1613,21 +1652,13 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ruzstd" -version = "0.5.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58c4eb8a81997cf040a091d1f7e1938aeab6749d3a0dfa73af43cdc32393483d" +checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8" dependencies = [ - "byteorder", - "derive_more 0.99.20", "twox-hash", ] -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "same-file" version = "1.0.6" @@ -1645,15 +1676,15 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "self_cell" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -1693,27 +1724,27 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] name = "sha2" -version = "0.10.9" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures", @@ -1736,7 +1767,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6c99835bad52957e7aa241d3975ed17c1e5f8c92026377d117a606f36b84b16" dependencies = [ "bytes", - "memmap2", + "memmap2 0.6.2", ] [[package]] @@ -1747,9 +1778,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simdutf8" @@ -1759,15 +1790,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "slice-group-by" -version = "0.3.1" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -1781,28 +1806,11 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1811,9 +1819,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.44" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -1822,9 +1830,22 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.16" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] [[package]] name = "test-plugin" @@ -1840,42 +1861,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.69" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -1899,9 +1900,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -1914,9 +1915,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "pin-project-lite", "tokio-macros", @@ -1924,20 +1925,20 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -1952,14 +1953,14 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -1978,9 +1979,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -1996,9 +1997,9 @@ dependencies = [ [[package]] name = "tracing-test" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" +checksum = "19a4c448db514d4f24c5ddb9f73f2ee71bfb24c526cf0c570ba142d1119e0051" dependencies = [ "tracing-core", "tracing-subscriber", @@ -2007,12 +2008,12 @@ dependencies = [ [[package]] name = "tracing-test-macro" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" +checksum = "ad06847b7afb65c7866a36664b75c40b895e318cea4f71299f013fb22965329d" dependencies = [ "quote", - "syn 2.0.111", + "syn", ] [[package]] @@ -2028,31 +2029,27 @@ dependencies = [ [[package]] name = "twox-hash" -version = "1.6.3" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" -dependencies = [ - "cfg-if", - "static_assertions", -] +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" @@ -2068,11 +2065,11 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "uuid" -version = "1.19.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -2084,12 +2081,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "walkdir" version = "2.5.0" @@ -2101,25 +2092,28 @@ dependencies = [ ] [[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "wasip2" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] [[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -2130,22 +2124,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2153,31 +2144,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.111", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.56" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e90e66d265d3a1efc0e72a54809ab90b9c0c515915c67cdf658689d2c22c6c" +checksum = "6bb55e2540ad1c56eec35fd63e2aea15f83b11ce487fd2de9ad11578dfc047ea" dependencies = [ "async-trait", "cast", @@ -2192,40 +2183,69 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", ] [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.56" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7150335716dce6028bead2b848e72f47b45e7b9422f64cccdc23bedca89affc1" +checksum = "caf0ca1bd612b988616bac1ab34c4e4290ef18f7148a1d8b7f31c150080e9295" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cda5ecc67248c48d3e705d3e03e00af905769b78b9d2a1678b663b8b9d4472" + [[package]] name = "wasm-encoder" -version = "0.243.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55db9c896d70bd9fa535ce83cd4e1f2ec3726b0edd2142079f594fc3be1cb35" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ "leb128fmt", - "wasmparser 0.243.0", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.250.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2271adb766023046af314460f1fae02cc34ea16d736d93404d3b65be44270923" +dependencies = [ + "leb128fmt", + "wasmparser 0.250.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", ] [[package]] name = "wasmer" -version = "6.1.0" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d85671948f8886a1cc946141c0b688a5617603c103699a5fceeebeb4e75b0b6" +checksum = "57bf3ce47ae9ef62e35c0b23e89b72250548d6d960d0832a5638b05a701769a8" dependencies = [ "bindgen", "bytes", "cfg-if", "cmake", - "derive_more 2.1.0", + "derive_more", "indexmap", "js-sys", "more-asserts", @@ -2236,7 +2256,7 @@ dependencies = [ "shared-buffer", "tar", "target-lexicon", - "thiserror 1.0.69", + "thiserror", "tracing", "wasm-bindgen", "wasmer-compiler", @@ -2244,53 +2264,60 @@ dependencies = [ "wasmer-derive", "wasmer-types", "wasmer-vm", - "wasmparser 0.224.1", + "wasmparser 0.245.1", "wat", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "wasmer-compiler" -version = "6.1.0" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4946475adc0af265af8f10aadf4d4a3c64845bcd3801c655bdd81ce5e3ee869b" +checksum = "1998787df9de3e84b66616766fb795e9aab954a8ca1e40eb92ef759e325bc782" dependencies = [ "backtrace", "bytes", "cfg-if", + "crossbeam-channel", "enum-iterator", "enumset", + "itertools 0.14.0", "leb128", "libc", "macho-unwind-info", - "memmap2", + "memmap2 0.9.10", "more-asserts", - "object 0.32.2", + "object 0.38.1", + "rangemap", + "rayon", "region", "rkyv", "self_cell", "shared-buffer", "smallvec", "target-lexicon", - "thiserror 1.0.69", + "tempfile", + "thiserror", "wasmer-types", "wasmer-vm", - "wasmparser 0.224.1", - "windows-sys 0.59.0", - "xxhash-rust", + "wasmparser 0.245.1", + "which", + "windows-sys 0.61.2", ] [[package]] name = "wasmer-compiler-cranelift" -version = "6.1.0" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f9c2050941b4e3f6bb82d32bd796a6c1750d2f97cbd892f07b384bb6af6f8" +checksum = "757fd205d4e2aac6662fc75f4859e6675ab51f4436b86086d47bfa83439c8931" dependencies = [ "cranelift-codegen", "cranelift-entity", "cranelift-frontend", - "gimli 0.28.1", - "itertools 0.12.1", + "gimli 0.33.0", + "indexmap", + "itertools 0.14.0", + "leb128", "more-asserts", "rayon", "smallvec", @@ -2302,43 +2329,43 @@ dependencies = [ [[package]] name = "wasmer-derive" -version = "6.1.0" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546f3380840cd63fdcc390f04cd19002f2dfa19b4691b77ecbd27642bd93452" +checksum = "80beffc36bead448bac84e1145d860523729c4e34e441332d56076a8a16b2d64" dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] name = "wasmer-types" -version = "6.1.0" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a4027ce165e8dc776dc5e2a3231a96983e6dc7330efd97b793cfc4e973ad0c" +checksum = "c805281d86063190ad76fbc12d6397aaf33ba60a5c9834e98ee0dcb8f889df7d" dependencies = [ - "bytecheck 0.6.12", + "bytecheck", "enum-iterator", "enumset", - "getrandom 0.2.16", + "getrandom 0.4.2", "hex", "indexmap", "more-asserts", "rkyv", "sha2", "target-lexicon", - "thiserror 1.0.69", - "xxhash-rust", + "thiserror", ] [[package]] name = "wasmer-vm" -version = "6.1.0" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c37d5be291eea00a00d077ce3a427bb3074709ee386ec358f18f0b7da33be01" +checksum = "2be14431ae698689440eadaa3d9563e266da4f889adda2749743f317680daa20" dependencies = [ "backtrace", + "bytesize", "cc", "cfg-if", "corosensei", @@ -2346,60 +2373,36 @@ dependencies = [ "dashmap", "enum-iterator", "fnv", + "gimli 0.33.0", "indexmap", + "itertools 0.14.0", "libc", "libunwind", - "mach2", + "mach2 0.6.0", "memoffset", "more-asserts", + "parking_lot", "region", "rustversion", "scopeguard", - "thiserror 1.0.69", + "thiserror", "wasmer-types", - "windows-sys 0.59.0", -] - -[[package]] -name = "wasmi-plugin-coordinator" -version = "0.1.0" -dependencies = [ - "console_error_panic_hook", - "futures", - "serde", - "serde-wasm-bindgen", - "thiserror 2.0.17", - "tracing", - "tracing-subscriber", - "tracing-wasm", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasmer", - "wasmi-plugin-coordinator-protocol", - "wasmi-plugin-wasi", - "web-sys", -] - -[[package]] -name = "wasmi-plugin-coordinator-protocol" -version = "0.1.0" -dependencies = [ - "getrandom 0.3.4", - "serde", - "thiserror 2.0.17", + "windows-sys 0.61.2", ] [[package]] name = "wasmi-plugin-hdk" version = "0.1.0" dependencies = [ + "console_error_panic_hook", "criterion", "futures", + "getrandom 0.3.4", "gloo-timers", "serde", "serde-wasm-bindgen", "serde_json", - "thiserror 2.0.17", + "thiserror", "tokio", "tracing", "tracing-subscriber", @@ -2410,7 +2413,6 @@ dependencies = [ "wasm-bindgen-futures", "wasm-bindgen-test", "wasmer", - "wasmi-plugin-coordinator-protocol", "wasmi-plugin-pdk", "wasmi-plugin-rt", "wasmi-plugin-wasi", @@ -2425,7 +2427,7 @@ dependencies = [ "futures", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror", "tokio", "tracing", "wasmi-plugin-rt", @@ -2447,7 +2449,7 @@ version = "0.1.0" dependencies = [ "futures", "getrandom 0.3.4", - "thiserror 2.0.17", + "thiserror", "tracing", "wasmer", "wasmi-plugin-rt", @@ -2456,51 +2458,72 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.224.1" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f17a5917c2ddd3819e84c661fae0d6ba29d7b9c1f0e96c708c65a9c4188e11" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", ] [[package]] name = "wasmparser" -version = "0.243.0" +version = "0.250.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d" +checksum = "071d99cdfb8111603ed05500506c3298a940b58d609dd0259d3981785dd33556" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "indexmap", "semver", ] +[[package]] +name = "wasmtime-internal-core" +version = "42.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a4a3f055a804a2f3d86e816a9df78a8fa57762212a8506164959224a40cd48" +dependencies = [ + "libm", +] + [[package]] name = "wast" -version = "243.0.0" +version = "250.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df21d01c2d91e46cb7a221d79e58a2d210ea02020d57c092e79255cc2999ca7f" +checksum = "69e9294a1f0204aeb5c47e95165517f43ef3cc895918c4f3e939380d4c290f4a" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width", - "wasm-encoder", + "wasm-encoder 0.250.0", ] [[package]] name = "wat" -version = "1.243.0" +version = "1.250.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226a9a91cd80a50449312fef0c75c23478fcecfcc4092bdebe1dc8e760ef521b" +checksum = "0a549ed329a70e444e0f7796391ab2a87d0aef30ddde9f60e16e429224fafd02" dependencies = [ "wast", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -2516,6 +2539,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" +dependencies = [ + "libc", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2559,7 +2591,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -2568,16 +2600,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -2595,31 +2618,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -2628,36 +2634,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" @@ -2665,64 +2653,122 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_i686_msvc" +name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_i686_msvc" -version = "0.53.1" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_x86_64_gnu" +name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "wit-bindgen-rust" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" +name = "wit-bindgen-rust-macro" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] [[package]] -name = "wit-bindgen" -version = "0.46.0" +name = "wit-component" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.244.0", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] [[package]] name = "xattr" @@ -2734,28 +2780,28 @@ dependencies = [ "rustix", ] -[[package]] -name = "xxhash-rust" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" - [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 6f3bb49..3c7af10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,6 @@ [workspace] members = [ "crates/test-plugin", - "crates/wasmi-plugin-coordinator", - "crates/wasmi-plugin-coordinator-protocol", "crates/wasmi-plugin-hdk", "crates/wasmi-plugin-pdk", "crates/wasmi-plugin-rt", @@ -39,12 +37,14 @@ tracing = "0.1" tracing-subscriber = "0.3" tracing-test = { version = "0.2" } tracing-wasm = "0.2" -uuid = { version = "1", features = ["v4", "serde", "js"] } -wasm-bindgen = "0.2" +uuid = { version = "1", features = ["v4", "serde", "js"] } +# `wasm-bindgen` 0.2.119 - 0.2.121 introduced a backward-incompatible change that +# conflicts with wasmer 7.1. Once wasmer 7.2+ is released, check and update +# wasm-bindgen to the latest. +wasm-bindgen = "=0.2.118" wasm-bindgen-futures = "0.4" wasm-bindgen-test = "0.3" -wasmer = { version = "6", default-features = false } -wasmi-plugin-coordinator-protocol = { version = "0.1.0", path = "./crates/wasmi-plugin-coordinator-protocol" } +wasmer = { version = "7", default-features = false } wasmi-plugin-hdk = { version = "0.1.0", path = "./crates/wasmi-plugin-hdk" } wasmi-plugin-pdk = { version = "0.1.0", path = "./crates/wasmi-plugin-pdk" } wasmi-plugin-rt = { version = "0.1.0", path = "./crates/wasmi-plugin-rt" } diff --git a/crates/test-plugin/Cargo.toml b/crates/test-plugin/Cargo.toml index ff05dec..921746d 100644 --- a/crates/test-plugin/Cargo.toml +++ b/crates/test-plugin/Cargo.toml @@ -22,7 +22,7 @@ tracing-subscriber = { workspace = true } wasmi-plugin-pdk = { workspace = true } [profile.release] -opt-level = 3 +opt-level = "z" lto = "thin" codegen-units = 1 panic = "abort" @@ -30,6 +30,3 @@ panic = "abort" # Keep function names in release builds strip = "none" debug = "line-tables-only" - -[profile.release.package."*"] -opt-level = 3 \ No newline at end of file diff --git a/crates/wasmi-plugin-coordinator-protocol/Cargo.toml b/crates/wasmi-plugin-coordinator-protocol/Cargo.toml deleted file mode 100644 index b295a9d..0000000 --- a/crates/wasmi-plugin-coordinator-protocol/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "wasmi-plugin-coordinator-protocol" -version.workspace = true -rust-version.workspace = true -description = "Runtime utilities for wasmi-plugin-framework" -authors.workspace = true -repository.workspace = true -edition.workspace = true -readme.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -exclude.workspace = true - -[dependencies] -getrandom = { workspace = true } -serde = { workspace = true } -thiserror = { workspace = true } diff --git a/crates/wasmi-plugin-coordinator-protocol/src/lib.rs b/crates/wasmi-plugin-coordinator-protocol/src/lib.rs deleted file mode 100644 index 670ad49..0000000 --- a/crates/wasmi-plugin-coordinator-protocol/src/lib.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::fmt::Display; - -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, Debug)] -pub struct InstanceId(u32); - -impl InstanceId { - pub fn new() -> Self { - Self(getrandom::u32().expect("Could not get random for InstanceId")) - } -} - -impl Default for InstanceId { - fn default() -> Self { - Self::new() - } -} - -impl From for InstanceId { - fn from(value: u64) -> Self { - Self(value as u32) - } -} - -impl From for u64 { - fn from(id: InstanceId) -> Self { - id.0 as u64 - } -} - -impl Display for InstanceId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive(Debug, Error, Serialize, Deserialize, Eq, PartialEq)] -pub enum RunPluginError { - #[error("Runtime error: {0}")] - RuntimeError(String), -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum CoordinatorMessage { - /// WASM module attached as external bytes. - RunPlugin { - id: InstanceId, - }, - RunPluginResp { - id: InstanceId, - status: Result<(), RunPluginError>, - }, - Stdin { - id: InstanceId, - }, - Stdout { - id: InstanceId, - }, - Stderr { - id: InstanceId, - }, - Log { - message: String, - }, - Terminate { - id: InstanceId, - }, - - Ready, -} diff --git a/crates/wasmi-plugin-coordinator/.cargo/config.toml b/crates/wasmi-plugin-coordinator/.cargo/config.toml deleted file mode 100644 index 850378a..0000000 --- a/crates/wasmi-plugin-coordinator/.cargo/config.toml +++ /dev/null @@ -1,16 +0,0 @@ -[target.wasm32-unknown-unknown] -rustflags = [ - "--cfg", "getrandom_backend=\"wasm_js\"", - "-C", "target-feature=+atomics,+bulk-memory", - "-C", "link-arg=--shared-memory", - "-C", "link-arg=--max-memory=1073741824", - "-C", "link-arg=--import-memory", - "-C", "link-arg=--export=__wasm_init_tls", - "-C", "link-arg=--export=__tls_size", - "-C", "link-arg=--export=__tls_align", - "-C", "link-arg=--export=__tls_base" -] -runner = "wasm-bindgen-test-runner" - -[unstable] -build-std = ["std", "panic_abort"] \ No newline at end of file diff --git a/crates/wasmi-plugin-coordinator/Cargo.toml b/crates/wasmi-plugin-coordinator/Cargo.toml deleted file mode 100644 index 24cdc30..0000000 --- a/crates/wasmi-plugin-coordinator/Cargo.toml +++ /dev/null @@ -1,45 +0,0 @@ -[package] -name = "wasmi-plugin-coordinator" -version.workspace = true -rust-version.workspace = true -description = "Atomics-enabled coordinator for compute web workers" -authors.workspace = true -repository.workspace = true -edition.workspace = true -readme.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -exclude.workspace = true - -[lib] -crate-type = ["cdylib"] - -[target.'cfg(target_arch = "wasm32")'.dependencies] -console_error_panic_hook = "0.1.7" -futures = { workspace = true } -serde = { workspace = true } -serde-wasm-bindgen = { workspace = true } -thiserror = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { workspace = true } -tracing-wasm = { workspace = true } -wasm-bindgen = { workspace = true } -wasm-bindgen-futures = { workspace = true } -wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } -wasmi-plugin-coordinator-protocol = { workspace = true } -wasmi-plugin-wasi = { workspace = true } -web-sys = { workspace = true, features = ["Worker", "WorkerOptions", "DedicatedWorkerGlobalScope", "WorkerType", "MessageEvent", "ErrorEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } - -[profile.release] -opt-level = 3 -lto = "thin" -codegen-units = 1 -panic = "abort" - -# Keep function names in release builds -strip = "none" -debug = "line-tables-only" - -[profile.release.package."*"] -opt-level = 3 \ No newline at end of file diff --git a/crates/wasmi-plugin-coordinator/README.md b/crates/wasmi-plugin-coordinator/README.md deleted file mode 100644 index 04d86a5..0000000 --- a/crates/wasmi-plugin-coordinator/README.md +++ /dev/null @@ -1,79 +0,0 @@ - - -## Definitions -- Main Thread: The main JavaScript thread that runs the UI. -- Coordinator: Compute Worker coordinator that manages multiple compute workers, dispatching tasks and acting as a bridge between the main thread and compute workers. -- Compute Worker: Web workers that can run blocking computations without freezing the UI. - -## Notes for the future Robert: -1. Use `worker.post_message` and `set_onmessage` to communicate between the main thread and compute workers directly: - -Can't do this because `wasmer`'s `function::call` function is sync and blocking. That's fine for nearly everything except `fd_read`. For `fd_read` we need to recieve a message from the thread running the `host` so we can either fetch data or call another plugin or whatnot. However, since receiving messages is handled via the `set_onmessage` callback, we would need to yield to the JS event loop for it to tick. Since we can't yield from a sync function, we can't receive messages while in a blocking call, and we can't implement `fd_read`. - -1.1. Use JSPI to fix this - -NOTE: Based on the asyncify tests, this may not be feisible anyways due to performance. No clue whether JSPI would be faster or slower than asyncify, but because -it's doing the same stuff it'd probably be order-of-magnitude equal. - -Good try, but no luck. While [JSPI](https://www.dgendill.com/posts/technology/2025-10-01-web-assembly-javascript-promise-integration.html) would allow us to forcefully yield to the JS event loop from within a sync function, it isn't supported by `wasm-bindgen` yet. You may think we could use raw linking to call JSPI directly, but we can't: - - `wasmer` requires certain `wasm-bindgen` imports, meaning we need to include those when spawning our worker. - - `wasm-bindgen` does not support JSPI imports yet. - - `wasm-bindgen` also doesn't support adding custom raw imports *at all*. - - And finally, you can't wrap a `WebAssembly.Suspending` function around or within a `wasm-bindgen` function because - - `wasm-bindgen` can't handle `WebAssembly.Suspending` functions if you try to import them directly. - - `WebAssembly.Suspending` functions can't be called from nested functions (IE if you wrapped it in a `wasm-bindgen` function). The function imported into the wasm MUST BE the `WebAssembly.Suspending` instance itself, not a wrapper that constructs or creates one. - - You could *possibly* manually inject the suspending function into wasm-bindgen's imports manually using internal functions, but that would be very very fragile. - -Tracking issue: https://github.com/wasm-bindgen/wasm-bindgen/issues/3633 - -2. Use SharedArrayBuffers for shared memory between the main thread and compute workers, without needing full atomics or a coordinator - -I tried this for literally 20 hours and could never fully get rid of race conditions. I fully believe it's possibly, I just also fully believe that I'm not a clever enough programmer / familiar enough with web-based concurrency to pull it off. - -The main issue was that I could never get the read / write IDXs for the ring buffers to be perfectly in sync between the main and compute worker thread. Inevitably something would always get out of sync and both threads would deadlock waiting for a message from the other. - -3. Use atomics + shared memory in the main thread directly to communicate with compute workers. - -This nearly works if it weren't for the UI. The main thread is perfectly capable of using shared memory to talk with web workers and, by passing raw function ptrs between threads, you can get an interface that basically feels like std::thread::spawn. However, the main thread is also responsible for running UI and no matter how much I've tried I can't get dioxus compiling with atomics + shared memory enabled. - -Tracking issue: https://github.com/DioxusLabs/dioxus/issues/5079 - -4. Asyncify the wasi to alloy yielding to the caller when required, who can then yield to the event loop as needed. - -https://kripken.github.io/blog/wasm/2019/07/16/asyncify.html - -Asyncify SHOULD work, it's just too slow. For anything that does a lot of host calls, the overhead of unwinding / rewinding the stack is too high. Below are some benchmark results from a test I performed where I: - - Added asyncify. - - For all `fd_read` calls, if they would block, unwind the stack and return to the host. - -Otherwise it was running in the same shared memory environment as before. stdio used the NonBlockingPipe, not worker messages. This was purely measuring the overhead of asyncify + unwinding / rewinding + yielding to JS on `fd_read` calls. - -```bash -# Runs 200 `call` or `call_async` calls -Collecting 100 samples in estimated 5.2375 s (2000 iterations) -call_many_async time: [2.5450 ms 2.6233 ms 2.7039 ms] - -Collecting 100 samples in estimated 82.924 s (100 iterations) -call_many time: [819.96 ms 824.37 ms 828.77 ms] -``` - -It's pretty abysmal. Enough said. In the real world `call_many_async` may stay lower (will still be at least an order of magnitude slower). But `call_many` will only get worse since it needs to wait for the messages. Best-case it would be ~800ms, more likely ~2s. - -Other downsides I documented before testing things: - - Post-processing the wasm with the asyncify pass - - Causes a slowdown due to extra stack management & state machinery - - Requires wasm32-unknown-unknown-specific code in wasi_ctx to setup and call asyncify. - - Likely significant slowdown. - - The asyncify stack unwinding will be slow, even if we optimize it to only unwind on `fd_read` when required. - - Because unwinding / rewinding is slow, we probably won't be able to do it very often. So the idea of constantly running a background `read` task in the transport driver that enables concurrent async IO probably won't be feasible since it would require rewinding on every `fd_read` call. - -5. Use a seperate coordinator worker to manage compute workers and communicate with the main thread. - -Essentially a workarround for 3's issues. By having a coordinator worker that communicates with the compute workers using shared memory and with the main thread using postMessage, we can avoid the need for atomics in the main thread. The coordinator can manage all the compute instances and dispatch tasks to them via function pointers, and then relay messages back to the main thread. - -The primary issue here is software architecture. Deciding how to split responsibilities between the main thread and coordinator. - - If `host` is entirely in the coordinator, then the main thread must make a postMessage call for every host interaction, even those that don't involve compute workers. This probably won't be too slow, but will be annoying to setup (wasm-specific proxy that'll pass calls back and forth). - - If `host` is in the main thread, and only sends requests to the coordinator when compute workers are involved, then the coordinator will need to query the host whenever any computer worker needs to interact with the host. - -I'm going to go with the latter approach since it seems cleaner. There is some significant added latency in messages vs shared memory for inter-plugin communication, but it also results in a much much much simpler architecture and UX for consumers of the library. - - Namely - with the former approach, it would require placing consumer code in the coordinator worker. That means consumers of the library would need to write and compile code for the coordinator, which means setting up rust nightly, atomics + shared memory, and having a seperate crate. With the latter I can just bundle the compiled generic `.wasm` coordinator with the library for use as-is. \ No newline at end of file diff --git a/crates/wasmi-plugin-coordinator/assets/worker.js b/crates/wasmi-plugin-coordinator/assets/worker.js deleted file mode 100644 index 9a70c11..0000000 --- a/crates/wasmi-plugin-coordinator/assets/worker.js +++ /dev/null @@ -1,1084 +0,0 @@ -let wasm; - -function addToExternrefTable0(obj) { - const idx = wasm.__externref_table_alloc(); - wasm.__wbindgen_externrefs.set(idx, obj); - return idx; -} - -const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') - ? { register: () => {}, unregister: () => {} } - : new FinalizationRegistry(state => state.dtor(state.a, state.b)); - -function debugString(val) { - // primitive types - const type = typeof val; - if (type == 'number' || type == 'boolean' || val == null) { - return `${val}`; - } - if (type == 'string') { - return `"${val}"`; - } - if (type == 'symbol') { - const description = val.description; - if (description == null) { - return 'Symbol'; - } else { - return `Symbol(${description})`; - } - } - if (type == 'function') { - const name = val.name; - if (typeof name == 'string' && name.length > 0) { - return `Function(${name})`; - } else { - return 'Function'; - } - } - // objects - if (Array.isArray(val)) { - const length = val.length; - let debug = '['; - if (length > 0) { - debug += debugString(val[0]); - } - for(let i = 1; i < length; i++) { - debug += ', ' + debugString(val[i]); - } - debug += ']'; - return debug; - } - // Test for built-in - const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); - let className; - if (builtInMatches && builtInMatches.length > 1) { - className = builtInMatches[1]; - } else { - // Failed to match the standard '[object ClassName]' - return toString.call(val); - } - if (className == 'Object') { - // we're a user defined class or Object - // JSON.stringify avoids problems with cycles, and is generally much - // easier than looping through ownProperties of `val`. - try { - return 'Object(' + JSON.stringify(val) + ')'; - } catch (_) { - return 'Object'; - } - } - // errors - if (val instanceof Error) { - return `${val.name}: ${val.message}\n${val.stack}`; - } - // TODO we could test for more things here, like `Set`s and `Map`s. - return className; -} - -function getArrayU8FromWasm0(ptr, len) { - ptr = ptr >>> 0; - return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); -} - -let cachedDataViewMemory0 = null; -function getDataViewMemory0() { - if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer !== wasm.memory.buffer) { - cachedDataViewMemory0 = new DataView(wasm.memory.buffer); - } - return cachedDataViewMemory0; -} - -function getStringFromWasm0(ptr, len) { - ptr = ptr >>> 0; - return decodeText(ptr, len); -} - -let cachedUint8ArrayMemory0 = null; -function getUint8ArrayMemory0() { - if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.buffer !== wasm.memory.buffer) { - cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); - } - return cachedUint8ArrayMemory0; -} - -function handleError(f, args) { - try { - return f.apply(this, args); - } catch (e) { - const idx = addToExternrefTable0(e); - wasm.__wbindgen_exn_store(idx); - } -} - -function isLikeNone(x) { - return x === undefined || x === null; -} - -function makeMutClosure(arg0, arg1, dtor, f) { - const state = { a: arg0, b: arg1, cnt: 1, dtor }; - const real = (...args) => { - - // First up with a closure we increment the internal reference - // count. This ensures that the Rust closure environment won't - // be deallocated while we're invoking it. - state.cnt++; - const a = state.a; - state.a = 0; - try { - return f(a, state.b, ...args); - } finally { - state.a = a; - real._wbg_cb_unref(); - } - }; - real._wbg_cb_unref = () => { - if (--state.cnt === 0) { - state.dtor(state.a, state.b); - state.a = 0; - CLOSURE_DTORS.unregister(state); - } - }; - CLOSURE_DTORS.register(real, state, state); - return real; -} - -function passStringToWasm0(arg, malloc, realloc) { - if (realloc === undefined) { - const buf = cachedTextEncoder.encode(arg); - const ptr = malloc(buf.length, 1) >>> 0; - getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); - WASM_VECTOR_LEN = buf.length; - return ptr; - } - - let len = arg.length; - let ptr = malloc(len, 1) >>> 0; - - const mem = getUint8ArrayMemory0(); - - let offset = 0; - - for (; offset < len; offset++) { - const code = arg.charCodeAt(offset); - if (code > 0x7F) break; - mem[ptr + offset] = code; - } - if (offset !== len) { - if (offset !== 0) { - arg = arg.slice(offset); - } - ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; - const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); - const ret = cachedTextEncoder.encodeInto(arg, view); - - offset += ret.written; - ptr = realloc(ptr, len, offset, 1) >>> 0; - } - - WASM_VECTOR_LEN = offset; - return ptr; -} - -let cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : undefined); -if (cachedTextDecoder) cachedTextDecoder.decode(); - -const MAX_SAFARI_DECODE_BYTES = 2146435072; -let numBytesDecoded = 0; -function decodeText(ptr, len) { - numBytesDecoded += len; - if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { - cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); - cachedTextDecoder.decode(); - numBytesDecoded = len; - } - return cachedTextDecoder.decode(getUint8ArrayMemory0().slice(ptr, ptr + len)); -} - -const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder() : undefined); - -if (cachedTextEncoder) { - cachedTextEncoder.encodeInto = function (arg, view) { - const buf = cachedTextEncoder.encode(arg); - view.set(buf); - return { - read: arg.length, - written: buf.length - }; - } -} - -let WASM_VECTOR_LEN = 0; - -function wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___wasm_bindgen_99d0e8a507791149___JsValue_____(arg0, arg1, arg2) { - wasm.wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___wasm_bindgen_99d0e8a507791149___JsValue_____(arg0, arg1, arg2); -} - -function wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___web_sys_d796e95248ce21f5___features__gen_ErrorEvent__ErrorEvent_____(arg0, arg1, arg2) { - wasm.wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___web_sys_d796e95248ce21f5___features__gen_ErrorEvent__ErrorEvent_____(arg0, arg1, arg2); -} - -function wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___wasm_bindgen_99d0e8a507791149___JsValue__wasm_bindgen_99d0e8a507791149___JsValue_____(arg0, arg1, arg2, arg3) { - wasm.wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___wasm_bindgen_99d0e8a507791149___JsValue__wasm_bindgen_99d0e8a507791149___JsValue_____(arg0, arg1, arg2, arg3); -} - -const __wbindgen_enum_WorkerType = ["classic", "module"]; - -const TrapFinalization = (typeof FinalizationRegistry === 'undefined') - ? { register: () => {}, unregister: () => {} } - : new FinalizationRegistry(ptr => wasm.__wbg_trap_free(ptr >>> 0, 1)); - -/** - * A struct representing a Trap - */ -export class Trap { - static __wrap(ptr) { - ptr = ptr >>> 0; - const obj = Object.create(Trap.prototype); - obj.__wbg_ptr = ptr; - TrapFinalization.register(obj, obj.__wbg_ptr, obj); - return obj; - } - __destroy_into_raw() { - const ptr = this.__wbg_ptr; - this.__wbg_ptr = 0; - TrapFinalization.unregister(this); - return ptr; - } - free() { - const ptr = this.__destroy_into_raw(); - wasm.__wbg_trap_free(ptr, 0); - } - /** - * A marker method to indicate that an object is an instance of the `Trap` - * class. - */ - static __wbg_wasmer_trap() { - wasm.trap___wbg_wasmer_trap(); - } -} -if (Symbol.dispose) Trap.prototype[Symbol.dispose] = Trap.prototype.free; - -/** - * @param {number} ptr - * @returns {Promise} - */ -export function execute_worker_task(ptr) { - const ret = wasm.execute_worker_task(ptr); - return ret; -} - -/** - * @param {number} ptr - * @param {any} extra - * @returns {Promise} - */ -export function execute_worker_task_with(ptr, extra) { - const ret = wasm.execute_worker_task_with(ptr, extra); - return ret; -} - -export function init_compute_worker() { - wasm.init_compute_worker(); -} - -/** - * @returns {Promise} - */ -export function start_coordinator() { - const ret = wasm.start_coordinator(); - return ret; -} - -const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']); - -async function __wbg_load(module, imports) { - if (typeof Response === 'function' && module instanceof Response) { - if (typeof WebAssembly.instantiateStreaming === 'function') { - try { - return await WebAssembly.instantiateStreaming(module, imports); - } catch (e) { - const validResponse = module.ok && EXPECTED_RESPONSE_TYPES.has(module.type); - - if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { - console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); - - } else { - throw e; - } - } - } - - const bytes = await module.arrayBuffer(); - return await WebAssembly.instantiate(bytes, imports); - } else { - const instance = await WebAssembly.instantiate(module, imports); - - if (instance instanceof WebAssembly.Instance) { - return { instance, module }; - } else { - return instance; - } - } -} - -function __wbg_get_imports(memory) { - const imports = {}; - imports.wbg = {}; - imports.wbg.__wbg_BigInt_f9ddbcf8ed387722 = function(arg0) { - const ret = BigInt(arg0); - return ret; - }; - imports.wbg.__wbg_Error_52673b7de5a0ca89 = function(arg0, arg1) { - const ret = Error(getStringFromWasm0(arg0, arg1)); - return ret; - }; - imports.wbg.__wbg_Number_2d1dcfcf4ec51736 = function(arg0) { - const ret = Number(arg0); - return ret; - }; - imports.wbg.__wbg_String_8f0eb39a4a4c2f66 = function(arg0, arg1) { - const ret = String(arg1); - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }; - imports.wbg.__wbg___wbindgen_bigint_get_as_i64_6e32f5e6aff02e1d = function(arg0, arg1) { - const v = arg1; - const ret = typeof(v) === 'bigint' ? v : undefined; - getDataViewMemory0().setBigInt64(arg0 + 8 * 1, isLikeNone(ret) ? BigInt(0) : ret, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); - }; - imports.wbg.__wbg___wbindgen_boolean_get_dea25b33882b895b = function(arg0) { - const v = arg0; - const ret = typeof(v) === 'boolean' ? v : undefined; - return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0; - }; - imports.wbg.__wbg___wbindgen_debug_string_adfb662ae34724b6 = function(arg0, arg1) { - const ret = debugString(arg1); - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }; - imports.wbg.__wbg___wbindgen_function_table_932edf419e6641fe = function() { - const ret = wasm.__wbindgen_export; - return ret; - }; - imports.wbg.__wbg___wbindgen_in_0d3e1e8f0c669317 = function(arg0, arg1) { - const ret = arg0 in arg1; - return ret; - }; - imports.wbg.__wbg___wbindgen_is_bigint_0e1a2e3f55cfae27 = function(arg0) { - const ret = typeof(arg0) === 'bigint'; - return ret; - }; - imports.wbg.__wbg___wbindgen_is_function_8d400b8b1af978cd = function(arg0) { - const ret = typeof(arg0) === 'function'; - return ret; - }; - imports.wbg.__wbg___wbindgen_is_object_ce774f3490692386 = function(arg0) { - const val = arg0; - const ret = typeof(val) === 'object' && val !== null; - return ret; - }; - imports.wbg.__wbg___wbindgen_is_undefined_f6b95eab589e0269 = function(arg0) { - const ret = arg0 === undefined; - return ret; - }; - imports.wbg.__wbg___wbindgen_jsval_eq_b6101cc9cef1fe36 = function(arg0, arg1) { - const ret = arg0 === arg1; - return ret; - }; - imports.wbg.__wbg___wbindgen_jsval_loose_eq_766057600fdd1b0d = function(arg0, arg1) { - const ret = arg0 == arg1; - return ret; - }; - imports.wbg.__wbg___wbindgen_lt_b8a98c478e0bc0c2 = function(arg0, arg1) { - const ret = arg0 < arg1; - return ret; - }; - imports.wbg.__wbg___wbindgen_memory_a342e963fbcabd68 = function() { - const ret = wasm.memory; - return ret; - }; - imports.wbg.__wbg___wbindgen_module_967adef62ea6cbf8 = function() { - const ret = __wbg_init.__wbindgen_wasm_module; - return ret; - }; - imports.wbg.__wbg___wbindgen_neg_7dce5cdff91fb914 = function(arg0) { - const ret = -arg0; - return ret; - }; - imports.wbg.__wbg___wbindgen_number_get_9619185a74197f95 = function(arg0, arg1) { - const obj = arg1; - const ret = typeof(obj) === 'number' ? obj : undefined; - getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); - }; - imports.wbg.__wbg___wbindgen_rethrow_78714972834ecdf1 = function(arg0) { - throw arg0; - }; - imports.wbg.__wbg___wbindgen_shr_df5f0f11a6e22f2e = function(arg0, arg1) { - const ret = arg0 >> arg1; - return ret; - }; - imports.wbg.__wbg___wbindgen_string_get_a2a31e16edf96e42 = function(arg0, arg1) { - const obj = arg1; - const ret = typeof(obj) === 'string' ? obj : undefined; - var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }; - imports.wbg.__wbg___wbindgen_throw_dd24417ed36fc46e = function(arg0, arg1) { - throw new Error(getStringFromWasm0(arg0, arg1)); - }; - imports.wbg.__wbg__wbg_cb_unref_87dfb5aaa0cbcea7 = function(arg0) { - arg0._wbg_cb_unref(); - }; - imports.wbg.__wbg_apply_0be48b84f7a793c6 = function() { return handleError(function (arg0, arg1, arg2) { - const ret = Reflect.apply(arg0, arg1, arg2); - return ret; - }, arguments) }; - imports.wbg.__wbg_async_bba5a2ac54b734df = function(arg0) { - const ret = arg0.async; - return ret; - }; - imports.wbg.__wbg_bind_65a4715c4a8c8727 = function(arg0, arg1, arg2, arg3) { - const ret = arg0.bind(arg1, arg2, arg3); - return ret; - }; - imports.wbg.__wbg_buffer_063cd102cc769a1c = function(arg0) { - const ret = arg0.buffer; - return ret; - }; - imports.wbg.__wbg_buffer_6cb2fecb1f253d71 = function(arg0) { - const ret = arg0.buffer; - return ret; - }; - imports.wbg.__wbg_byteLength_166ad9a51ecaa5f1 = function(arg0) { - const ret = arg0.byteLength; - return ret; - }; - imports.wbg.__wbg_call_3020136f7a2d6e44 = function() { return handleError(function (arg0, arg1, arg2) { - const ret = arg0.call(arg1, arg2); - return ret; - }, arguments) }; - imports.wbg.__wbg_call_abb4ff46ce38be40 = function() { return handleError(function (arg0, arg1) { - const ret = arg0.call(arg1); - return ret; - }, arguments) }; - imports.wbg.__wbg_constructor_bd34b914d5a3a404 = function(arg0) { - const ret = arg0.constructor; - return ret; - }; - imports.wbg.__wbg_createObjectURL_7d9f7f8f41373850 = function() { return handleError(function (arg0, arg1) { - const ret = URL.createObjectURL(arg1); - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }, arguments) }; - imports.wbg.__wbg_data_8bf4ae669a78a688 = function(arg0) { - const ret = arg0.data; - return ret; - }; - imports.wbg.__wbg_done_62ea16af4ce34b24 = function(arg0) { - const ret = arg0.done; - return ret; - }; - imports.wbg.__wbg_entries_83c79938054e065f = function(arg0) { - const ret = Object.entries(arg0); - return ret; - }; - imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) { - let deferred0_0; - let deferred0_1; - try { - deferred0_0 = arg0; - deferred0_1 = arg1; - console.error(getStringFromWasm0(arg0, arg1)); - } finally { - wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); - } - }; - imports.wbg.__wbg_exports_58ef4156ca04d176 = function(arg0) { - const ret = arg0.exports; - return ret; - }; - imports.wbg.__wbg_exports_c060467610de1d8a = function(arg0) { - const ret = WebAssembly.Module.exports(arg0); - return ret; - }; - imports.wbg.__wbg_filename_e3800f22e3dfacab = function(arg0, arg1) { - const ret = arg1.filename; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }; - imports.wbg.__wbg_getPrototypeOf_847b9c7e1dd3c978 = function() { return handleError(function (arg0) { - const ret = Reflect.getPrototypeOf(arg0); - return ret; - }, arguments) }; - imports.wbg.__wbg_getRandomValues_ea728b1d79dae146 = function() { return handleError(function (arg0) { - globalThis.crypto.getRandomValues(arg0); - }, arguments) }; - imports.wbg.__wbg_get_16458e8ef25ea5fa = function() { return handleError(function (arg0, arg1) { - const ret = arg0.get(arg1 >>> 0); - return ret; - }, arguments) }; - imports.wbg.__wbg_get_6b7bd52aca3f9671 = function(arg0, arg1) { - const ret = arg0[arg1 >>> 0]; - return ret; - }; - imports.wbg.__wbg_get_af9dab7e9603ea93 = function() { return handleError(function (arg0, arg1) { - const ret = Reflect.get(arg0, arg1); - return ret; - }, arguments) }; - imports.wbg.__wbg_imports_d7c6f1e23e012391 = function(arg0) { - const ret = WebAssembly.Module.imports(arg0); - return ret; - }; - imports.wbg.__wbg_instanceof_ArrayBuffer_f3320d2419cd0355 = function(arg0) { - let result; - try { - result = arg0 instanceof ArrayBuffer; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_instanceof_Error_3443650560328fa9 = function(arg0) { - let result; - try { - result = arg0 instanceof Error; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_instanceof_Function_cb4bf9f70c667635 = function(arg0) { - let result; - try { - result = arg0 instanceof Function; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_instanceof_Global_fb07a06f4c6f25b9 = function(arg0) { - let result; - try { - result = arg0 instanceof WebAssembly.Global; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_instanceof_Map_084be8da74364158 = function(arg0) { - let result; - try { - result = arg0 instanceof Map; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_instanceof_Memory_a361de6f661b44e2 = function(arg0) { - let result; - try { - result = arg0 instanceof WebAssembly.Memory; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_instanceof_Module_d37a279bc9a18feb = function(arg0) { - let result; - try { - result = arg0 instanceof WebAssembly.Module; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_instanceof_Object_577e21051f7bcb79 = function(arg0) { - let result; - try { - result = arg0 instanceof Object; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_instanceof_Table_b6578721c6242aeb = function(arg0) { - let result; - try { - result = arg0 instanceof WebAssembly.Table; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_instanceof_Tag_18b644dbc43991d0 = function(arg0) { - let result; - try { - result = arg0 instanceof WebAssembly.Tag; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_instanceof_Uint8Array_da54ccc9d3e09434 = function(arg0) { - let result; - try { - result = arg0 instanceof Uint8Array; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_isArray_51fd9e6422c0a395 = function(arg0) { - const ret = Array.isArray(arg0); - return ret; - }; - imports.wbg.__wbg_isSafeInteger_ae7d3f054d55fa16 = function(arg0) { - const ret = Number.isSafeInteger(arg0); - return ret; - }; - imports.wbg.__wbg_iterator_27b7c8b35ab3e86b = function() { - const ret = Symbol.iterator; - return ret; - }; - imports.wbg.__wbg_length_22ac23eaec9d8053 = function(arg0) { - const ret = arg0.length; - return ret; - }; - imports.wbg.__wbg_length_d45040a40c570362 = function(arg0) { - const ret = arg0.length; - return ret; - }; - imports.wbg.__wbg_lineno_db90b4d01f04fae1 = function(arg0) { - const ret = arg0.lineno; - return ret; - }; - imports.wbg.__wbg_log_0cc1b7768397bcfe = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) { - let deferred0_0; - let deferred0_1; - try { - deferred0_0 = arg0; - deferred0_1 = arg1; - console.log(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3), getStringFromWasm0(arg4, arg5), getStringFromWasm0(arg6, arg7)); - } finally { - wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); - } - }; - imports.wbg.__wbg_log_cb9e190acc5753fb = function(arg0, arg1) { - let deferred0_0; - let deferred0_1; - try { - deferred0_0 = arg0; - deferred0_1 = arg1; - console.log(getStringFromWasm0(arg0, arg1)); - } finally { - wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); - } - }; - imports.wbg.__wbg_mark_7438147ce31e9d4b = function(arg0, arg1) { - performance.mark(getStringFromWasm0(arg0, arg1)); - }; - imports.wbg.__wbg_measure_fb7825c11612c823 = function() { return handleError(function (arg0, arg1, arg2, arg3) { - let deferred0_0; - let deferred0_1; - let deferred1_0; - let deferred1_1; - try { - deferred0_0 = arg0; - deferred0_1 = arg1; - deferred1_0 = arg2; - deferred1_1 = arg3; - performance.measure(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3)); - } finally { - wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); - wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); - } - }, arguments) }; - imports.wbg.__wbg_message_0305fa7903f4b3d9 = function(arg0) { - const ret = arg0.message; - return ret; - }; - imports.wbg.__wbg_message_0ff7f09380783844 = function(arg0, arg1) { - const ret = arg1.message; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }; - imports.wbg.__wbg_new_1acc79167e62f5df = function() { return handleError(function (arg0, arg1) { - const ret = new WebAssembly.Instance(arg0, arg1); - return ret; - }, arguments) }; - imports.wbg.__wbg_new_1ba21ce319a06297 = function() { - const ret = new Object(); - return ret; - }; - imports.wbg.__wbg_new_25f239778d6112b9 = function() { - const ret = new Array(); - return ret; - }; - imports.wbg.__wbg_new_53cb1e86c1ef5d2a = function() { return handleError(function (arg0, arg1) { - const ret = new Worker(getStringFromWasm0(arg0, arg1)); - return ret; - }, arguments) }; - imports.wbg.__wbg_new_6421f6084cc5bc5a = function(arg0) { - const ret = new Uint8Array(arg0); - return ret; - }; - imports.wbg.__wbg_new_8a6f238a6ece86ea = function() { - const ret = new Error(); - return ret; - }; - imports.wbg.__wbg_new_ab1fe2437c86d873 = function(arg0) { - const ret = new SharedArrayBuffer(arg0 >>> 0); - return ret; - }; - imports.wbg.__wbg_new_de1e660b88fc921f = function(arg0) { - const ret = new Int32Array(arg0); - return ret; - }; - imports.wbg.__wbg_new_ff12d2b041fb48f1 = function(arg0, arg1) { - try { - var state0 = {a: arg0, b: arg1}; - var cb0 = (arg0, arg1) => { - const a = state0.a; - state0.a = 0; - try { - return wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___wasm_bindgen_99d0e8a507791149___JsValue__wasm_bindgen_99d0e8a507791149___JsValue_____(a, state0.b, arg0, arg1); - } finally { - state0.a = a; - } - }; - const ret = new Promise(cb0); - return ret; - } finally { - state0.a = state0.b = 0; - } - }; - imports.wbg.__wbg_new_from_slice_f9c22b9153b26992 = function(arg0, arg1) { - const ret = new Uint8Array(getArrayU8FromWasm0(arg0, arg1)); - return ret; - }; - imports.wbg.__wbg_new_no_args_cb138f77cf6151ee = function(arg0, arg1) { - const ret = new Function(getStringFromWasm0(arg0, arg1)); - return ret; - }; - imports.wbg.__wbg_new_with_length_12c6de4fac33117a = function(arg0) { - const ret = new Array(arg0 >>> 0); - return ret; - }; - imports.wbg.__wbg_new_with_length_aa5eaf41d35235e5 = function(arg0) { - const ret = new Uint8Array(arg0 >>> 0); - return ret; - }; - imports.wbg.__wbg_new_with_options_2978557c2c268ef3 = function() { return handleError(function (arg0, arg1, arg2) { - const ret = new Worker(getStringFromWasm0(arg0, arg1), arg2); - return ret; - }, arguments) }; - imports.wbg.__wbg_new_with_u8_array_sequence_and_options_d4def9ec0588c7ec = function() { return handleError(function (arg0, arg1) { - const ret = new Blob(arg0, arg1); - return ret; - }, arguments) }; - imports.wbg.__wbg_next_138a17bbf04e926c = function(arg0) { - const ret = arg0.next; - return ret; - }; - imports.wbg.__wbg_next_3cfe5c0fe2a4cc53 = function() { return handleError(function (arg0) { - const ret = arg0.next(); - return ret; - }, arguments) }; - imports.wbg.__wbg_now_69d776cd24f5215b = function() { - const ret = Date.now(); - return ret; - }; - imports.wbg.__wbg_of_6505a0eb509da02e = function(arg0) { - const ret = Array.of(arg0); - return ret; - }; - imports.wbg.__wbg_of_7779827fa663eec8 = function(arg0, arg1, arg2) { - const ret = Array.of(arg0, arg1, arg2); - return ret; - }; - imports.wbg.__wbg_postMessage_07504dbe15265d5c = function() { return handleError(function (arg0, arg1) { - arg0.postMessage(arg1); - }, arguments) }; - imports.wbg.__wbg_postMessage_49af172ec8bdaa5a = function() { return handleError(function (arg0, arg1, arg2) { - arg0.postMessage(arg1, arg2); - }, arguments) }; - imports.wbg.__wbg_postMessage_7243f814e0cfb266 = function() { return handleError(function (arg0, arg1) { - arg0.postMessage(arg1); - }, arguments) }; - imports.wbg.__wbg_prototypesetcall_dfe9b766cdc1f1fd = function(arg0, arg1, arg2) { - Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); - }; - imports.wbg.__wbg_push_7d9be8f38fc13975 = function(arg0, arg1) { - const ret = arg0.push(arg1); - return ret; - }; - imports.wbg.__wbg_queueMicrotask_9b549dfce8865860 = function(arg0) { - const ret = arg0.queueMicrotask; - return ret; - }; - imports.wbg.__wbg_queueMicrotask_fca69f5bfad613a5 = function(arg0) { - queueMicrotask(arg0); - }; - imports.wbg.__wbg_resolve_fd5bfbaa4ce36e1e = function(arg0) { - const ret = Promise.resolve(arg0); - return ret; - }; - imports.wbg.__wbg_set_169e13b608078b7b = function(arg0, arg1, arg2) { - arg0.set(getArrayU8FromWasm0(arg1, arg2)); - }; - imports.wbg.__wbg_set_3f1d0b984ed272ed = function(arg0, arg1, arg2) { - arg0[arg1] = arg2; - }; - imports.wbg.__wbg_set_781438a03c0c3c81 = function() { return handleError(function (arg0, arg1, arg2) { - const ret = Reflect.set(arg0, arg1, arg2); - return ret; - }, arguments) }; - imports.wbg.__wbg_set_7df433eea03a5c14 = function(arg0, arg1, arg2) { - arg0[arg1 >>> 0] = arg2; - }; - imports.wbg.__wbg_set_name_5399dd71553bb72d = function(arg0, arg1, arg2) { - arg0.name = getStringFromWasm0(arg1, arg2); - }; - imports.wbg.__wbg_set_onerror_d5671da43c08b208 = function(arg0, arg1) { - arg0.onerror = arg1; - }; - imports.wbg.__wbg_set_onmessage_5fe29d0fb54cb575 = function(arg0, arg1) { - arg0.onmessage = arg1; - }; - imports.wbg.__wbg_set_onmessage_deb94985de696ac7 = function(arg0, arg1) { - arg0.onmessage = arg1; - }; - imports.wbg.__wbg_set_type_7ce650670a34c68f = function(arg0, arg1, arg2) { - arg0.type = getStringFromWasm0(arg1, arg2); - }; - imports.wbg.__wbg_set_type_c2eb2929316959f4 = function(arg0, arg1) { - arg0.type = __wbindgen_enum_WorkerType[arg1]; - }; - imports.wbg.__wbg_stack_0ed75d68575b0f3c = function(arg0, arg1) { - const ret = arg1.stack; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }; - imports.wbg.__wbg_static_accessor_GLOBAL_769e6b65d6557335 = function() { - const ret = typeof global === 'undefined' ? null : global; - return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); - }; - imports.wbg.__wbg_static_accessor_GLOBAL_THIS_60cf02db4de8e1c1 = function() { - const ret = typeof globalThis === 'undefined' ? null : globalThis; - return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); - }; - imports.wbg.__wbg_static_accessor_IMPORT_META_URL_d241d5473d4f98c3 = function(arg0) { - const ret = import.meta.url; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }; - imports.wbg.__wbg_static_accessor_SELF_08f5a74c69739274 = function() { - const ret = typeof self === 'undefined' ? null : self; - return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); - }; - imports.wbg.__wbg_static_accessor_WINDOW_a8924b26aa92d024 = function() { - const ret = typeof window === 'undefined' ? null : window; - return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); - }; - imports.wbg.__wbg_subarray_845f2f5bce7d061a = function(arg0, arg1, arg2) { - const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0); - return ret; - }; - imports.wbg.__wbg_terminate_08a1236dd2e69da0 = function(arg0) { - arg0.terminate(); - }; - imports.wbg.__wbg_then_4f95312d68691235 = function(arg0, arg1) { - const ret = arg0.then(arg1); - return ret; - }; - imports.wbg.__wbg_toString_99815908ae93d7d1 = function(arg0, arg1, arg2) { - const ret = arg1.toString(arg2); - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }; - imports.wbg.__wbg_toString_f07112df359c997f = function(arg0) { - const ret = arg0.toString(); - return ret; - }; - imports.wbg.__wbg_trap_new = function(arg0) { - const ret = Trap.__wrap(arg0); - return ret; - }; - imports.wbg.__wbg_value_4cd497eeadba94bd = function(arg0) { - const ret = arg0.value; - return ret; - }; - imports.wbg.__wbg_value_57b7b035e117f7ee = function(arg0) { - const ret = arg0.value; - return ret; - }; - imports.wbg.__wbg_waitAsync_8afec80ffd213eca = function(arg0, arg1, arg2) { - const ret = Atomics.waitAsync(arg0, arg1 >>> 0, arg2); - return ret; - }; - imports.wbg.__wbg_waitAsync_c186cb97ffacd552 = function() { - const ret = Atomics.waitAsync; - return ret; - }; - imports.wbg.__wbg_wait_ceb109d0011d6567 = function() { return handleError(function (arg0, arg1, arg2, arg3) { - const ret = Atomics.wait(arg0, arg1 >>> 0, arg2, arg3); - return ret; - }, arguments) }; - imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) { - // Cast intrinsic for `Ref(String) -> Externref`. - const ret = getStringFromWasm0(arg0, arg1); - return ret; - }; - imports.wbg.__wbindgen_cast_4625c577ab2ec9ee = function(arg0) { - // Cast intrinsic for `U64 -> Externref`. - const ret = BigInt.asUintN(64, arg0); - return ret; - }; - imports.wbg.__wbindgen_cast_5799a4c49e587223 = function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { dtor_idx: 204, function: Function { arguments: [NamedExternref("MessageEvent")], shim_idx: 205, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. - const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_99d0e8a507791149___closure__destroy___dyn_core_b0d591bc3b1f24da___ops__function__FnMut__wasm_bindgen_99d0e8a507791149___JsValue____Output_______, wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___wasm_bindgen_99d0e8a507791149___JsValue_____); - return ret; - }; - imports.wbg.__wbindgen_cast_9ae0607507abb057 = function(arg0) { - // Cast intrinsic for `I64 -> Externref`. - const ret = arg0; - return ret; - }; - imports.wbg.__wbindgen_cast_b52493afb474412a = function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { dtor_idx: 204, function: Function { arguments: [Externref], shim_idx: 205, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. - const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_99d0e8a507791149___closure__destroy___dyn_core_b0d591bc3b1f24da___ops__function__FnMut__wasm_bindgen_99d0e8a507791149___JsValue____Output_______, wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___wasm_bindgen_99d0e8a507791149___JsValue_____); - return ret; - }; - imports.wbg.__wbindgen_cast_c27f97883acb4e05 = function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { dtor_idx: 183, function: Function { arguments: [NamedExternref("ErrorEvent")], shim_idx: 184, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. - const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_99d0e8a507791149___closure__destroy___dyn_core_b0d591bc3b1f24da___ops__function__FnMut__web_sys_d796e95248ce21f5___features__gen_ErrorEvent__ErrorEvent____Output_______, wasm_bindgen_99d0e8a507791149___convert__closures_____invoke___web_sys_d796e95248ce21f5___features__gen_ErrorEvent__ErrorEvent_____); - return ret; - }; - imports.wbg.__wbindgen_cast_d6cd19b81560fd6e = function(arg0) { - // Cast intrinsic for `F64 -> Externref`. - const ret = arg0; - return ret; - }; - imports.wbg.__wbindgen_cast_e7b45dd881f38ce3 = function(arg0, arg1) { - // Cast intrinsic for `U128 -> Externref`. - const ret = (BigInt.asUintN(64, arg0) | (BigInt.asUintN(64, arg1) << BigInt(64))); - return ret; - }; - imports.wbg.__wbindgen_init_externref_table = function() { - const table = wasm.__wbindgen_externrefs; - const offset = table.grow(4); - table.set(0, undefined); - table.set(offset + 0, undefined); - table.set(offset + 1, null); - table.set(offset + 2, true); - table.set(offset + 3, false); - }; - imports.wbg.__wbindgen_link_203404ece0e9bab9 = function(arg0) { - const val = `onmessage = function (ev) { - let [ia, index, value] = ev.data; - ia = new Int32Array(ia.buffer); - let result = Atomics.wait(ia, index, value); - postMessage(result); - }; - `; - const ret = typeof URL.createObjectURL === 'undefined' ? "data:application/javascript," + encodeURIComponent(val) : URL.createObjectURL(new Blob([val], { type: "text/javascript" })); - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }; - imports.wbg.memory = memory || new WebAssembly.Memory({initial:18,maximum:16384,shared:true}); - - return imports; -} - -function __wbg_finalize_init(instance, module, thread_stack_size) { - wasm = instance.exports; - __wbg_init.__wbindgen_wasm_module = module; - cachedDataViewMemory0 = null; - cachedUint8ArrayMemory0 = null; - - if (typeof thread_stack_size !== 'undefined' && (typeof thread_stack_size !== 'number' || thread_stack_size === 0 || thread_stack_size % 65536 !== 0)) { throw 'invalid stack size' } - wasm.__wbindgen_start(thread_stack_size); - return wasm; -} - -function initSync(module, memory) { - if (wasm !== undefined) return wasm; - - let thread_stack_size - if (typeof module !== 'undefined') { - if (Object.getPrototypeOf(module) === Object.prototype) { - ({module, memory, thread_stack_size} = module) - } else { - console.warn('using deprecated parameters for `initSync()`; pass a single object instead') - } - } - - const imports = __wbg_get_imports(memory); - if (!(module instanceof WebAssembly.Module)) { - module = new WebAssembly.Module(module); - } - const instance = new WebAssembly.Instance(module, imports); - return __wbg_finalize_init(instance, module, thread_stack_size); -} - -async function __wbg_init(module_or_path, memory) { - if (wasm !== undefined) return wasm; - - let thread_stack_size - if (typeof module_or_path !== 'undefined') { - if (Object.getPrototypeOf(module_or_path) === Object.prototype) { - ({module_or_path, memory, thread_stack_size} = module_or_path) - } else { - console.warn('using deprecated parameters for the initialization function; pass a single object instead') - } - } - - if (typeof module_or_path === 'undefined') { - module_or_path = new URL('wasmi_plugin_coordinator_bg.wasm', import.meta.url); - } - const imports = __wbg_get_imports(memory); - - if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { - module_or_path = fetch(module_or_path); - } - - const { instance, module } = await __wbg_load(await module_or_path, imports); - - return __wbg_finalize_init(instance, module, thread_stack_size); -} - -export { initSync }; -export default __wbg_init; diff --git a/crates/wasmi-plugin-coordinator/assets/worker.wasm b/crates/wasmi-plugin-coordinator/assets/worker.wasm deleted file mode 100644 index dd2dbafd64d77ec4fa364f1667975920ea0f9227..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 368657 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%0Dpqi$U)+MsW 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 { - start_coordinator(); -}).catch((err) => { - console.error("Worker Init Failed:", err); -}); diff --git a/crates/wasmi-plugin-coordinator/src/worker.rs b/crates/wasmi-plugin-coordinator/src/worker.rs deleted file mode 100644 index eba5c99..0000000 --- a/crates/wasmi-plugin-coordinator/src/worker.rs +++ /dev/null @@ -1,257 +0,0 @@ -use std::{cell::RefCell, collections::HashMap, io::Write, rc::Rc, sync::Arc}; - -use futures::AsyncBufReadExt; -use futures::AsyncRead; -use futures::io::BufReader; -use thiserror::Error; -use tracing::{error, info, warn}; -use wasm_bindgen::{ - JsCast, - prelude::{Closure, wasm_bindgen}, -}; -use wasmi_plugin_coordinator_protocol::CoordinatorMessage; -use wasmi_plugin_coordinator_protocol::InstanceId; -use wasmi_plugin_coordinator_protocol::RunPluginError; -use wasmi_plugin_wasi::non_blocking_pipe::NonBlockingPipeWriter; -use wasmi_plugin_wasi::non_blocking_pipe::non_blocking_pipe; -use wasmi_plugin_wasi::wasi_ctx; -use wasmi_plugin_wasi::wasi_ctx::WasiReader; -use web_sys::{ - DedicatedWorkerGlobalScope, MessageEvent, - js_sys::{self, Reflect, Uint8Array, WebAssembly}, -}; - -use crate::worker_pool::pool::{SpawnError, WorkerHandle, WorkerPool}; - -struct CoordinatorState { - instance_channels: HashMap, - instance_handles: HashMap>, -} - -#[derive(Debug, Error)] -enum RunError { - #[error("WASI Error: {0}")] - Wasi(#[from] wasi_ctx::WasiError), - #[error("Compile Error: {0}")] - Compile(#[from] wasmer::CompileError), - #[error("Wasmer Runtime Error: {0}")] - Runtime(#[from] wasmer::RuntimeError), - #[error("Wasmer Trap Error: {0}")] - Trap(String), -} - -#[wasm_bindgen] -pub async fn start_coordinator() { - tracing_wasm::set_as_global_default_with_config( - tracing_wasm::WASMLayerConfigBuilder::new() - .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) - .set_max_level(tracing::Level::INFO) - .build(), - ); - - info!("Starting Wasmi Plugin Coordinator..."); - let global = js_sys::global().unchecked_into::(); - - let state = CoordinatorState { - instance_channels: HashMap::new(), - instance_handles: HashMap::new(), - }; - let state = Rc::new(RefCell::new(state)); - - let onmessage = Closure::wrap(Box::new(move |e: MessageEvent| { - let mut state = state.borrow_mut(); - on_message(&mut state, e); - }) as Box); - - global.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); - onmessage.forget(); - - // Send ready message to main thread - info!("Coordinator ready"); - let ready_msg = CoordinatorMessage::Ready; - let ready_value = serde_wasm_bindgen::to_value(&ready_msg).unwrap(); - global.post_message(&ready_value).unwrap(); -} - -fn on_message(state: &mut CoordinatorState, e: web_sys::MessageEvent) { - let global = js_sys::global().unchecked_into::(); - let data = e.data(); - - let msg = match serde_wasm_bindgen::from_value::(data.clone()) { - Ok(msg) => msg, - Err(err) => { - error!("Failed to deserialize message: {}", err); - return; - } - }; - - match msg { - CoordinatorMessage::Stdin { id } => { - let Ok(data) = Reflect::get(&data, &"data".into()) else { - error!("Failed to get stdin data from message"); - return; - }; - - stdin(state, id, data.unchecked_into()); - } - CoordinatorMessage::Terminate { id } => { - terminate(state, id); - } - CoordinatorMessage::RunPlugin { id } => { - let Ok(wasm_module) = Reflect::get(&data, &"wasm_module".into()) else { - error!("Failed to get wasm_module from message"); - return; - }; - - let status = match run_plugin(state, id, wasm_module.unchecked_into()) { - Ok(_) => Ok(()), - Err(e) => Err(RunPluginError::RuntimeError(e.to_string())), - }; - - let response_msg = CoordinatorMessage::RunPluginResp { id, status }; - let response_value = serde_wasm_bindgen::to_value(&response_msg).unwrap(); - global.post_message(&response_value).unwrap(); - } - - _ => { - warn!("Unknown message received"); - } - } -} - -fn stdin(state: &mut CoordinatorState, id: InstanceId, data: Uint8Array) { - let data: Vec = data.to_vec(); - - if let Some(writer) = state.instance_channels.get_mut(&id) { - if let Err(e) = writer.write_all(&data) { - error!("Failed to write to stdin pipe: {}", e); - } - } else { - warn!("No stdin channel found for instance {}", id); - } -} - -fn terminate(state: &mut CoordinatorState, id: InstanceId) { - if let Some(handle) = state.instance_handles.remove(&id) { - handle.terminate(); - } else { - warn!("No instance handle found for instance {}", id); - } - - state.instance_channels.remove(&id); -} - -fn run_plugin( - state: &mut CoordinatorState, - id: InstanceId, - wasm_module: WebAssembly::Module, -) -> Result, SpawnError> { - let (stdin_reader, stdin_writer) = non_blocking_pipe(); - let (stdout_reader, stdout_writer) = non_blocking_pipe(); - let (stderr_reader, stderr_writer) = non_blocking_pipe(); - - // Start proxies - stdout_proxy(stdout_reader, id.clone()); - stderr_proxy(stderr_reader, id.clone()); - - state.instance_channels.insert(id.clone(), stdin_writer); - - let pool = WorkerPool::global(); - pool.run_with(&wasm_module, move |extra| async move { - let res = run_instance(extra, stdin_reader, stdout_writer, stderr_writer); - if let Err(e) = res { - error!("Error running Wasm instance: {}", e); - } - }) -} - -fn run_instance( - js_module: wasm_bindgen::JsValue, - stdin: impl WasiReader + 'static, - stdout: impl Write + Send + Sync + 'static, - stderr: impl Write + Send + Sync + 'static, -) -> Result<(), RunError> { - let mut store = wasmer::Store::default(); - - let js_module: WebAssembly::Module = js_module.dyn_into().unwrap(); - let module = js_module.into(); - - let wasi_ctx = wasi_ctx::WasiCtx::new() - .set_stdin(stdin) - .set_stdout(stdout) - .set_stderr(stderr); - - let start = wasi_ctx.into_fn(&mut store, &module)?; - match start.call(&mut store, &[]) { - Ok(_) => Ok(()), - Err(e) => { - if let Some(trap) = e.clone().to_trap() { - Err(RunError::Trap(trap.message().to_string())) - } else { - Err(RunError::Runtime(e)) - } - } - } -} - -fn stdout_proxy(reader: impl AsyncRead + Unpin + 'static, id: InstanceId) { - wasm_bindgen_futures::spawn_local(async move { - let mut reader = BufReader::new(reader); - let mut line = String::new(); - - loop { - line.clear(); - match reader.read_line(&mut line).await { - Ok(0) => break, // EOF - Ok(_) => { - let global = js_sys::global().unchecked_into::(); - - let data = js_sys::Uint8Array::from(line.as_bytes()); - let stdout_msg = CoordinatorMessage::Stdout { id: id.clone() }; - let msg_value = serde_wasm_bindgen::to_value(&stdout_msg).unwrap(); - Reflect::set(&msg_value, &"data".into(), &data).unwrap(); - - let transfer_list = js_sys::Array::of1(&data.buffer()); - global - .post_message_with_transfer(&msg_value, &transfer_list) - .unwrap(); - } - Err(e) => { - error!("Error reading stdout for instance {}: {}", id, e); - break; - } - } - } - }); -} - -fn stderr_proxy(reader: impl AsyncRead + Unpin + 'static, id: InstanceId) { - wasm_bindgen_futures::spawn_local(async move { - let mut reader = BufReader::new(reader); - let mut line = String::new(); - - loop { - line.clear(); - match reader.read_line(&mut line).await { - Ok(0) => break, // EOF - Ok(_) => { - let global = js_sys::global().unchecked_into::(); - - let data = js_sys::Uint8Array::from(line.as_bytes()); - let stdout_msg = CoordinatorMessage::Stderr { id: id.clone() }; - let msg_value = serde_wasm_bindgen::to_value(&stdout_msg).unwrap(); - Reflect::set(&msg_value, &"data".into(), &data).unwrap(); - - let transfer_list = js_sys::Array::of1(&data.buffer()); - global - .post_message_with_transfer(&msg_value, &transfer_list) - .unwrap(); - } - Err(e) => { - error!("Error reading stdout for instance {}: {}", id, e); - break; - } - } - } - }); -} diff --git a/crates/wasmi-plugin-hdk/Cargo.toml b/crates/wasmi-plugin-hdk/Cargo.toml index 338b8b5..2563165 100644 --- a/crates/wasmi-plugin-hdk/Cargo.toml +++ b/crates/wasmi-plugin-hdk/Cargo.toml @@ -33,9 +33,26 @@ gloo-timers = { workspace = true } serde-wasm-bindgen = { workspace = true } wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } -wasmer = { workspace = true, features = ["js-default", "js-serializable-module"] } -wasmi-plugin-coordinator-protocol = { workspace = true } -web-sys = { workspace = true, features = ["Worker", "WorkerOptions", "DedicatedWorkerGlobalScope", "WorkerType", "MessageEvent", "ErrorEvent", "Blob", "BlobPropertyBag", "Url", "console", "TextDecoder"] } +wasmer = { workspace = true, features = [ + "js-default", + "js-serializable-module", +] } +web-sys = { workspace = true, features = [ + "Worker", + "WorkerOptions", + "DedicatedWorkerGlobalScope", + "WorkerType", + "MessageEvent", + "ErrorEvent", + "Blob", + "BlobPropertyBag", + "Url", + "console", + "TextDecoder", +] } +console_error_panic_hook = "0.1.7" +tracing-wasm = { workspace = true } +getrandom = { workspace = true } [dev-dependencies] tracing-subscriber = { workspace = true } diff --git a/crates/wasmi-plugin-hdk/benches/benchmark.rs b/crates/wasmi-plugin-hdk/benches/benchmark.rs index f6e01ca..766a6e6 100644 --- a/crates/wasmi-plugin-hdk/benches/benchmark.rs +++ b/crates/wasmi-plugin-hdk/benches/benchmark.rs @@ -51,16 +51,25 @@ mod benchmarks { }); } - /// Benchmark a single ping request to the wasm module. - pub fn bench_ping(c: &mut Criterion) { + /// Benchmark the full lifecycle of creating a plugin instance and calling a ping request. + /// Tests the overhead of plugin instantiation. + pub fn bench_lifecycle(c: &mut Criterion) { setup_logs(); let rt = Builder::new_current_thread().enable_all().build().unwrap(); - let plugin = rt.block_on(load_plugin()); - c.bench_function("ping", |b| { + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); + + c.bench_function("lifecycle", |b| { b.iter(|| { let fut = async { + let plugin = + Plugin::builder("test_plugin", wasm_bytes.clone(), handler.clone()) + .with_timeout(Duration::from_secs(10)) + .build() + .await + .unwrap(); plugin.call_async("ping", Value::Null).await.unwrap(); }; @@ -69,25 +78,16 @@ mod benchmarks { }); } - /// Benchmark the full lifecycle of creating a plugin instance and calling a ping request. - /// Tests the overhead of plugin instantiation. - pub fn bench_lifecycle(c: &mut Criterion) { + /// Benchmark a single ping request to the wasm module. + pub fn bench_ping(c: &mut Criterion) { setup_logs(); let rt = Builder::new_current_thread().enable_all().build().unwrap(); + let plugin = rt.block_on(load_plugin()); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); - - c.bench_function("lifecycle", |b| { + c.bench_function("ping", |b| { b.iter(|| { let fut = async { - let plugin = - Plugin::builder("test_plugin", wasm_bytes.clone(), handler.clone()) - .with_timeout(Duration::from_secs(10)) - .build() - .await - .unwrap(); plugin.call_async("ping", Value::Null).await.unwrap(); }; diff --git a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs index 634a9c5..bb48718 100644 --- a/crates/wasmi-plugin-hdk/benches/wasm_bench.rs +++ b/crates/wasmi-plugin-hdk/benches/wasm_bench.rs @@ -5,7 +5,7 @@ use std::{ time::Duration, }; use tracing::info; -use wasm_bindgen_test::{Criterion, wasm_bindgen_bench}; +use wasm_bindgen_test::{__rt::criterion::Criterion, wasm_bindgen_bench}; use wasmi_plugin_hdk::{plugin::Plugin, plugin_id::PluginId, server::HostServer}; use wasmi_plugin_pdk::transport::AsyncTransport; @@ -42,59 +42,59 @@ fn get_host_server() -> HostServer<(Option, ())> { fn setup_logs() { INIT.call_once(|| { - // tracing_wasm::set_as_global_default_with_config( - // tracing_wasm::WASMLayerConfigBuilder::new() - // .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) - // .set_max_level(tracing::Level::INFO) - // .build(), - // ); + // tracing_wasm::set_as_global_default_with_config( + // tracing_wasm::WASMLayerConfigBuilder::new() + // .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) + // .set_max_level(tracing::Level::INFO) + // .build(), + // ); }); } +/// Benchmark the full lifecycle of creating a plugin instance and calling a ping request. +/// Tests the overhead of plugin instantiation. #[wasm_bindgen_bench] -async fn bench_ping_wasm(c: &mut Criterion) { +async fn bench_lifecycle(c: &mut Criterion) { setup_logs(); - let plugin = Arc::new(load_plugin().await); + let wasm_bytes = load_plugin_wasm(); + let handler = Arc::new(get_host_server()); - c.bench_async_function("ping", |b| { - let plugin = plugin.clone(); + c.bench_async_function("lifecycle", |b| { + let wasm_bytes = wasm_bytes.clone(); + let handler = handler.clone(); Box::pin(b.iter_future(move || { - let plugin = plugin.clone(); + let wasm_bytes = wasm_bytes.clone(); + let handler = handler.clone(); async move { + let plugin = Plugin::builder("test_plugin", wasm_bytes.clone(), handler) + .build() + .await + .unwrap(); plugin .call_async("ping", serde_json::Value::Null) .await .unwrap(); - info!("ping call completed"); } })) }) .await; } -/// Benchmark the full lifecycle of creating a plugin instance and calling a ping request. -/// Tests the overhead of plugin instantiation. #[wasm_bindgen_bench] -async fn bench_lifecycle(c: &mut Criterion) { +async fn bench_ping_wasm(c: &mut Criterion) { setup_logs(); - let wasm_bytes = load_plugin_wasm(); - let handler = Arc::new(get_host_server()); + let plugin = Arc::new(load_plugin().await); - c.bench_async_function("lifecycle", |b| { - let wasm_bytes = wasm_bytes.clone(); - let handler = handler.clone(); + c.bench_async_function("ping", |b| { + let plugin = plugin.clone(); Box::pin(b.iter_future(move || { - let wasm_bytes = wasm_bytes.clone(); - let handler = handler.clone(); + let plugin = plugin.clone(); async move { - let plugin = Plugin::builder("test_plugin", wasm_bytes.clone(), handler) - .build() - .await - .unwrap(); plugin .call_async("ping", serde_json::Value::Null) .await .unwrap(); + info!("ping call completed"); } })) }) diff --git a/crates/wasmi-plugin-hdk/src/runtime/mod.rs b/crates/wasmi-plugin-hdk/src/runtime/mod.rs index a7d6c99..a9abd6d 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/mod.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/mod.rs @@ -6,6 +6,8 @@ mod native_runtime; #[cfg(target_arch = "wasm32")] pub use worker_runtime::WorkerRuntime; #[cfg(target_arch = "wasm32")] +mod wasm_worker_pool; +#[cfg(target_arch = "wasm32")] mod worker_runtime; use crate::compile::Compiled; diff --git a/crates/wasmi-plugin-coordinator/src/worker_pool/mod.rs b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/mod.rs similarity index 100% rename from crates/wasmi-plugin-coordinator/src/worker_pool/mod.rs rename to crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/mod.rs diff --git a/crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/pool.rs similarity index 98% rename from crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs rename to crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/pool.rs index fea17b4..8bc41d1 100644 --- a/crates/wasmi-plugin-coordinator/src/worker_pool/pool.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/pool.rs @@ -12,9 +12,7 @@ use web_sys::{ js_sys::{self, Array, Reflect, Uint8Array}, }; -use crate::worker_pool::worker_message::WorkerMessage; - -pub type WorkerId = u64; +use crate::runtime::wasm_worker_pool::worker_message::WorkerMessage; /// Pool that manages a set of web workers for executing tasks /// asynchronously. Workers are reused for multiple tasks to minimize @@ -34,7 +32,6 @@ enum WorkerState { } pub struct WorkerHandle { - pub id: WorkerId, state: Arc>, worker: web_sys::Worker, _on_message: wasm_bindgen::prelude::Closure, @@ -161,9 +158,8 @@ impl WorkerHandle { .post_message(&msg) .map_err(SpawnError::PostMessageError)?; - info!("Spawned worker {}", id); + info!("Spawned worker {}", name); Ok(WorkerHandle { - id, state, worker, _on_message: on_message, diff --git a/crates/wasmi-plugin-coordinator/src/worker_pool/worker.js b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/worker.js similarity index 100% rename from crates/wasmi-plugin-coordinator/src/worker_pool/worker.js rename to crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/worker.js diff --git a/crates/wasmi-plugin-coordinator/src/worker_pool/worker.rs b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/worker.rs similarity index 75% rename from crates/wasmi-plugin-coordinator/src/worker_pool/worker.rs rename to crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/worker.rs index 229e379..75ca6ac 100644 --- a/crates/wasmi-plugin-coordinator/src/worker_pool/worker.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/worker.rs @@ -1,20 +1,11 @@ use futures::{FutureExt, future::LocalBoxFuture}; use tracing::error; -use tracing_subscriber::layer::SubscriberExt; use wasm_bindgen::{JsValue, prelude::wasm_bindgen}; use web_sys::js_sys; #[wasm_bindgen] pub fn init_compute_worker() { console_error_panic_hook::set_once(); - - let config = tracing_wasm::WASMLayerConfigBuilder::new() - .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) - .set_max_level(tracing::Level::INFO) - .build(); - let _ = tracing::subscriber::set_global_default( - tracing_subscriber::Registry::default().with(tracing_wasm::WASMLayer::new(config)), - ); } #[wasm_bindgen] diff --git a/crates/wasmi-plugin-coordinator/src/worker_pool/worker_message.rs b/crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/worker_message.rs similarity index 100% rename from crates/wasmi-plugin-coordinator/src/worker_pool/worker_message.rs rename to crates/wasmi-plugin-hdk/src/runtime/wasm_worker_pool/worker_message.rs diff --git a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs index a362198..ad2b619 100644 --- a/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs +++ b/crates/wasmi-plugin-hdk/src/runtime/worker_runtime.rs @@ -2,39 +2,35 @@ use std::collections::HashMap; use std::io::Write; use std::sync::{Arc, Mutex, OnceLock}; -use futures::AsyncReadExt; -use futures::channel::oneshot::{self, Sender}; -use futures::future::{BoxFuture, Shared}; -use futures::{AsyncRead, AsyncWrite, FutureExt}; +use futures::{AsyncRead, AsyncWrite}; use thiserror::Error; -use tracing::{error, info, warn}; -use wasm_bindgen::prelude::Closure; -use wasm_bindgen::{JsCast, JsValue}; -use wasmi_plugin_coordinator_protocol::{CoordinatorMessage, InstanceId}; +use tracing::{error, warn}; +use wasm_bindgen::JsCast; use wasmi_plugin_wasi::non_blocking_pipe::non_blocking_pipe; -use web_sys::js_sys::Reflect; -use web_sys::{MessageEvent, Worker, WorkerOptions, WorkerType, js_sys}; +use wasmi_plugin_wasi::wasi_ctx::{self, WasiReader}; +use web_sys::js_sys::WebAssembly; use crate::compile::Compiled; use crate::runtime::Runtime; +use crate::runtime::wasm_worker_pool::pool::{self, WorkerHandle, WorkerPool}; pub struct WorkerRuntime { inner: Arc>, } pub struct InnerWorkerRuntime { - coordinator: Worker, - instances: HashMap, - ready_tx: Option>, - shared_ready: Shared>, - _onmessage_closure: Option>, + // coordinator: Worker, + instances: HashMap>, + // ready_tx: Option>, + // shared_ready: Shared>, + // _onmessage_closure: Option>, } -struct PluginInstance { - stdout_writer: Box, - stderr_writer: Box, - run_plugin_resp_tx: Option>>, -} +// struct PluginInstance { +// stdout_writer: Box, +// stderr_writer: Box, +// run_plugin_resp_tx: Option>>, +// } #[derive(Debug, Error)] pub enum SpawnError { @@ -42,23 +38,37 @@ pub enum SpawnError { WorkerError(String), #[error("Oneshot cancelled")] OneshotCancelled, - #[error("Serde Error: {0}")] - Serde(#[from] serde_wasm_bindgen::Error), - #[error("Post Message Error: {0:?}")] - PostMessage(JsValue), - #[error("Reflect Error: {0:?}")] - Reflect(JsValue), + #[error("Pool error: {0}")] + Pool(#[from] pool::SpawnError), + // #[error("Serde Error: {0}")] + // Serde(#[from] serde_wasm_bindgen::Error), + // #[error("Post Message Error: {0:?}")] + // PostMessage(JsValue), + // #[error("Reflect Error: {0:?}")] + // Reflect(JsValue), +} + +#[derive(Debug, Error)] +enum RunError { + #[error("WASI Error: {0}")] + Wasi(#[from] wasi_ctx::WasiError), + #[error("Compile Error: {0}")] + Compile(#[from] wasmer::CompileError), + #[error("Wasmer Runtime Error: {0}")] + Runtime(#[from] wasmer::RuntimeError), + #[error("Wasmer Trap Error: {0}")] + Trap(String), } static RUNTIME: OnceLock = OnceLock::new(); -const WORKER_LOADER_JS: &str = include_str!("../../../wasmi-plugin-coordinator/src/worker.js"); -const WORKER_JS_GLUE: &str = include_str!("../../../wasmi-plugin-coordinator/assets/worker.js"); -const WORKER_WASM_BYTES: &[u8] = - include_bytes!("../../../wasmi-plugin-coordinator/assets/worker.wasm"); +// const WORKER_LOADER_JS: &str = include_str!("../../../wasmi-plugin-coordinator/src/worker.js"); +// const WORKER_JS_GLUE: &str = include_str!("../../../wasmi-plugin-coordinator/assets/worker.js"); +// const WORKER_WASM_BYTES: &[u8] = +// include_bytes!("../../../wasmi-plugin-coordinator/assets/worker.wasm"); -unsafe impl Send for InnerWorkerRuntime {} -unsafe impl Sync for InnerWorkerRuntime {} +// unsafe impl Send for InnerWorkerRuntime {} +// unsafe impl Sync for InnerWorkerRuntime {} impl WorkerRuntime { pub fn global() -> &'static WorkerRuntime { @@ -66,45 +76,45 @@ impl WorkerRuntime { } fn init() -> Self { - info!("Initializing WorkerRuntime"); - let options = WorkerOptions::new(); - options.set_name("coordinator"); - options.set_type(WorkerType::Module); - let worker = Worker::new_with_options(&worker_url().unwrap(), &options).unwrap(); + // info!("Initializing WorkerRuntime"); + // let options = WorkerOptions::new(); + // options.set_name("coordinator"); + // options.set_type(WorkerType::Module); + // let worker = Worker::new_with_options(&worker_url().unwrap(), &options).unwrap(); - let (ready_tx, ready_rx) = futures::channel::oneshot::channel(); - let shared_ready = ready_rx.map(|_| ()).boxed().shared(); + // let (ready_tx, ready_rx) = futures::channel::oneshot::channel(); + // let shared_ready = ready_rx.map(|_| ()).boxed().shared(); let inner = Arc::new(Mutex::new(InnerWorkerRuntime { - coordinator: worker, + // coordinator: worker, instances: HashMap::new(), - ready_tx: Some(ready_tx), - shared_ready, - _onmessage_closure: None, + // ready_tx: Some(ready_tx), + // shared_ready, + // _onmessage_closure: None, })); - let inner_clone = inner.clone(); - let onmessage = Closure::wrap(Box::new(move |e: MessageEvent| { - on_message(inner_clone.clone(), e); - }) as Box); + // let inner_clone = inner.clone(); + // let onmessage = Closure::wrap(Box::new(move |e: MessageEvent| { + // on_message(inner_clone.clone(), e); + // }) as Box); - inner - .lock() - .unwrap() - .coordinator - .set_onmessage(Some(onmessage.as_ref().unchecked_ref())); - inner.lock().unwrap()._onmessage_closure = Some(onmessage); + // inner + // .lock() + // .unwrap() + // .coordinator + // .set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + // inner.lock().unwrap()._onmessage_closure = Some(onmessage); Self { inner } } - async fn wait_ready(&self) { - let shared_ready = { - let inner = self.inner.lock().unwrap(); - inner.shared_ready.clone() - }; - shared_ready.await; - } + // async fn wait_ready(&self) { + // let shared_ready = { + // let inner = self.inner.lock().unwrap(); + // inner.shared_ready.clone() + // }; + // shared_ready.await; + // } } impl Runtime for &WorkerRuntime { @@ -122,215 +132,247 @@ impl Runtime for &WorkerRuntime { ), Self::Error, > { - self.wait_ready().await; - - let instance_id = InstanceId::new(); + let instance_id = getrandom::u64().expect("Failed to generate random instance ID"); let (stdin_reader, stdin_writer) = non_blocking_pipe(); let (stdout_reader, stdout_writer) = non_blocking_pipe(); let (stderr_reader, stderr_writer) = non_blocking_pipe(); - let (run_plugin_resp_tx, run_plugin_resp_rx) = futures::channel::oneshot::channel(); - - let plugin_instance = PluginInstance { - stdout_writer: Box::new(stdout_writer), - stderr_writer: Box::new(stderr_writer), - run_plugin_resp_tx: Some(run_plugin_resp_tx), - }; + let pool = WorkerPool::global(); + let res = pool.run_with(&compiled.js_module, move |extra| async move { + let res = run_instance(extra, stdin_reader, stdout_writer, stderr_writer); + if let Err(e) = res { + error!("Error running Wasm instance: {}", e); + } + })?; self.inner .lock() .unwrap() .instances - .insert(instance_id, plugin_instance); + .insert(instance_id, res); + Ok((instance_id, stdin_writer, stdout_reader, stderr_reader)) - // Start stdin proxy - stdin_proxy(stdin_reader, instance_id, self.inner.clone()); + // let (run_plugin_resp_tx, run_plugin_resp_rx) = futures::channel::oneshot::channel(); - // Notify the coordinator - let wasm_module_js = compiled.js_module.clone(); - let spawn_msg = CoordinatorMessage::RunPlugin { id: instance_id }; - let msg_value = serde_wasm_bindgen::to_value(&spawn_msg)?; - Reflect::set(&msg_value, &"wasm_module".into(), &wasm_module_js) - .map_err(SpawnError::Reflect)?; + // let plugin_instance = PluginInstance { + // stdout_writer: Box::new(stdout_writer), + // stderr_writer: Box::new(stderr_writer), + // run_plugin_resp_tx: Some(run_plugin_resp_tx), + // }; - self.inner - .lock() - .unwrap() - .coordinator - .post_message(&msg_value) - .map_err(SpawnError::PostMessage)?; - - // Await the response - match run_plugin_resp_rx.await { - Ok(Ok(())) => {} - Ok(Err(err_msg)) => return Err(SpawnError::WorkerError(err_msg)), - Err(_) => return Err(SpawnError::OneshotCancelled), - } + // self.inner + // .lock() + // .unwrap() + // .instances + // .insert(instance_id, plugin_instance); - Ok(( - instance_id.into(), - stdin_writer, - stdout_reader, - stderr_reader, - )) + // Start stdin proxy + // stdin_proxy(stdin_reader, instance_id, self.inner.clone()); + + // // Notify the coordinator + // let wasm_module_js = compiled.js_module.clone(); + // let spawn_msg = CoordinatorMessage::RunPlugin { id: instance_id }; + // let msg_value = serde_wasm_bindgen::to_value(&spawn_msg)?; + // Reflect::set(&msg_value, &"wasm_module".into(), &wasm_module_js) + // .map_err(SpawnError::Reflect)?; + + // self.inner + // .lock() + // .unwrap() + // .coordinator + // .post_message(&msg_value) + // .map_err(SpawnError::PostMessage)?; + + // // Await the response + // match run_plugin_resp_rx.await { + // Ok(Ok(())) => {} + // Ok(Err(err_msg)) => return Err(SpawnError::WorkerError(err_msg)), + // Err(_) => return Err(SpawnError::OneshotCancelled), + // } + + // Ok(( + // instance_id.into(), + // stdin_writer, + // stdout_reader, + // stderr_reader, + // )) } async fn terminate(&self, instance_id: u64) { - self.wait_ready().await; - - let instance_id: InstanceId = instance_id.into(); - self.inner.lock().unwrap().instances.remove(&instance_id); - - // Also notify the coordinator - let terminate_msg = CoordinatorMessage::Terminate { id: instance_id }; - let msg_value = serde_wasm_bindgen::to_value(&terminate_msg).unwrap(); - - self.inner - .lock() - .unwrap() - .coordinator - .post_message(&msg_value) - .unwrap(); + if let Some(handle) = self.inner.lock().unwrap().instances.remove(&instance_id) { + handle.terminate(); + } else { + warn!("No instance handle found for instance {}", instance_id); + } } } -fn worker_url() -> Result { - let options = web_sys::BlobPropertyBag::new(); - options.set_type("application/wasm"); - let wasm_blob = web_sys::Blob::new_with_u8_array_sequence_and_options( - &js_sys::Array::of1(&js_sys::Uint8Array::from(WORKER_WASM_BYTES)), - &options, - )?; - let wasm_url = web_sys::Url::create_object_url_with_blob(&wasm_blob)?; - - let options = web_sys::BlobPropertyBag::new(); - options.set_type("text/javascript"); - let sdk_script = format!("{}\n//# sourceURL=coordinator.js", WORKER_JS_GLUE); - let sdk_blob = web_sys::Blob::new_with_u8_array_sequence_and_options( - &js_sys::Array::of1(&js_sys::JsString::from(sdk_script)), - &options, - )?; - let sdk_url = web_sys::Url::create_object_url_with_blob(&sdk_blob)?; - - let loader_script = WORKER_LOADER_JS - .replace("{sdk_url}", &sdk_url) - .replace("{wasm_url}", &wasm_url); - - let options = web_sys::BlobPropertyBag::new(); - options.set_type("text/javascript"); - let blob = web_sys::Blob::new_with_u8_array_sequence_and_options( - &js_sys::Array::of1(&js_sys::JsString::from(loader_script)), - &options, - )?; - - web_sys::Url::create_object_url_with_blob(&blob) -} - -fn on_message(inner: Arc>, e: web_sys::MessageEvent) { - let data = e.data(); - - let Ok(msg) = serde_wasm_bindgen::from_value::(data.clone()) else { - error!("Worker: Failed to deserialize message"); - return; - }; - - match msg { - CoordinatorMessage::Ready => { - info!("Coordinator is ready"); - let mut inner = inner.lock().unwrap(); - if let Some(tx) = inner.ready_tx.take() { - let _ = tx.send(()); - } - } - CoordinatorMessage::Stdout { id } => { - let Ok(data) = Reflect::get(&data, &"data".into()) else { - error!("Failed to get stdout data from message"); - return; - }; - let data: Vec = data.unchecked_into::().to_vec(); - - let mut inner = inner.lock().unwrap(); - if let Some(instance) = inner.instances.get_mut(&id) { - if let Err(e) = instance.stdout_writer.write_all(&data) { - error!("Failed to write to stdout pipe: {}", e); - } - } else { - warn!("Received stdout for unknown instance ID {:?}", id); - } - } - CoordinatorMessage::Stderr { id } => { - let Ok(data) = Reflect::get(&data, &"data".into()) else { - error!("Failed to get stderr data from message"); - return; - }; - let data: Vec = data.unchecked_into::().to_vec(); - - let mut inner = inner.lock().unwrap(); - if let Some(instance) = inner.instances.get_mut(&id) { - if let Err(e) = instance.stderr_writer.write_all(&data) { - error!("Failed to write to stderr pipe: {}", e); - } +fn run_instance( + js_module: wasm_bindgen::JsValue, + stdin: impl WasiReader + 'static, + stdout: impl Write + Send + Sync + 'static, + stderr: impl Write + Send + Sync + 'static, +) -> Result<(), RunError> { + let mut store = wasmer::Store::default(); + + let js_module: WebAssembly::Module = js_module.dyn_into().unwrap(); + let module = js_module.into(); + + let wasi_ctx = wasi_ctx::WasiCtx::new() + .set_stdin(stdin) + .set_stdout(stdout) + .set_stderr(stderr); + + let start = wasi_ctx.into_fn(&mut store, &module)?; + match start.call(&mut store, &[]) { + Ok(_) => Ok(()), + Err(e) => { + if let Some(trap) = e.clone().to_trap() { + Err(RunError::Trap(trap.message().to_string())) } else { - warn!( - "Received stderr for unknown instance ID {:?}: {}", - id, - String::from_utf8_lossy(&data) - ); + Err(RunError::Runtime(e)) } } - CoordinatorMessage::RunPluginResp { id, status } => { - let mut inner = inner.lock().unwrap(); - if let Some(instance) = inner.instances.get_mut(&id) { - if let Some(tx) = instance.run_plugin_resp_tx.take() { - let _ = tx.send(status.map_err(|e| e.to_string())); - } else { - warn!("No response channel found for instance {:?}", id); - } - } else { - warn!("Received RunPluginResp for unknown instance ID {:?}", id); - } - } - CoordinatorMessage::Log { message } => { - info!("[Coordinator] {}", message); - } - _ => { - warn!("Unexpected message received from coordinator"); - } } } -fn stdin_proxy( - reader: impl AsyncRead + Unpin + Send + 'static, - id: InstanceId, - inner: Arc>, -) { - wasm_bindgen_futures::spawn_local(async move { - let mut reader = reader; - let mut buffer = [0u8; 1024]; - - loop { - match reader.read(&mut buffer).await { - Ok(0) => break, // EOF - Ok(n) => { - let data = js_sys::Uint8Array::from(&buffer[..n]); - let stdin_msg = CoordinatorMessage::Stdin { id }; - let msg_value = serde_wasm_bindgen::to_value(&stdin_msg).unwrap(); - Reflect::set(&msg_value, &"data".into(), &data).unwrap(); - let transfer_list = js_sys::Array::of1(&data.buffer()); - - let inner = inner.lock().unwrap(); - if let Err(e) = inner - .coordinator - .post_message_with_transfer(&msg_value, &transfer_list) - { - error!("Failed to post stdin message: {:?}", e); - break; - } - } - Err(e) => { - error!("Error reading from stdin: {}", e); - break; - } - } - } - }); -} +// fn worker_url() -> Result { +// let options = web_sys::BlobPropertyBag::new(); +// options.set_type("application/wasm"); +// let wasm_blob = web_sys::Blob::new_with_u8_array_sequence_and_options( +// &js_sys::Array::of1(&js_sys::Uint8Array::from(WORKER_WASM_BYTES)), +// &options, +// )?; +// let wasm_url = web_sys::Url::create_object_url_with_blob(&wasm_blob)?; + +// let options = web_sys::BlobPropertyBag::new(); +// options.set_type("text/javascript"); +// let sdk_script = format!("{}\n//# sourceURL=coordinator.js", WORKER_JS_GLUE); +// let sdk_blob = web_sys::Blob::new_with_u8_array_sequence_and_options( +// &js_sys::Array::of1(&js_sys::JsString::from(sdk_script)), +// &options, +// )?; +// let sdk_url = web_sys::Url::create_object_url_with_blob(&sdk_blob)?; + +// let loader_script = WORKER_LOADER_JS +// .replace("{sdk_url}", &sdk_url) +// .replace("{wasm_url}", &wasm_url); + +// let options = web_sys::BlobPropertyBag::new(); +// options.set_type("text/javascript"); +// let blob = web_sys::Blob::new_with_u8_array_sequence_and_options( +// &js_sys::Array::of1(&js_sys::JsString::from(loader_script)), +// &options, +// )?; + +// web_sys::Url::create_object_url_with_blob(&blob) +// } + +// fn on_message(inner: Arc>, e: web_sys::MessageEvent) { +// let data = e.data(); + +// let Ok(msg) = serde_wasm_bindgen::from_value::(data.clone()) else { +// error!("Worker: Failed to deserialize message"); +// return; +// }; + +// match msg { +// CoordinatorMessage::Ready => { +// info!("Coordinator is ready"); +// let mut inner = inner.lock().unwrap(); +// if let Some(tx) = inner.ready_tx.take() { +// let _ = tx.send(()); +// } +// } +// CoordinatorMessage::Stdout { id } => { +// let Ok(data) = Reflect::get(&data, &"data".into()) else { +// error!("Failed to get stdout data from message"); +// return; +// }; +// let data: Vec = data.unchecked_into::().to_vec(); + +// let mut inner = inner.lock().unwrap(); +// if let Some(instance) = inner.instances.get_mut(&id) { +// if let Err(e) = instance.stdout_writer.write_all(&data) { +// error!("Failed to write to stdout pipe: {}", e); +// } +// } else { +// warn!("Received stdout for unknown instance ID {:?}", id); +// } +// } +// CoordinatorMessage::Stderr { id } => { +// let Ok(data) = Reflect::get(&data, &"data".into()) else { +// error!("Failed to get stderr data from message"); +// return; +// }; +// let data: Vec = data.unchecked_into::().to_vec(); + +// let mut inner = inner.lock().unwrap(); +// if let Some(instance) = inner.instances.get_mut(&id) { +// if let Err(e) = instance.stderr_writer.write_all(&data) { +// error!("Failed to write to stderr pipe: {}", e); +// } +// } else { +// warn!( +// "Received stderr for unknown instance ID {:?}: {}", +// id, +// String::from_utf8_lossy(&data) +// ); +// } +// } +// CoordinatorMessage::RunPluginResp { id, status } => { +// let mut inner = inner.lock().unwrap(); +// if let Some(instance) = inner.instances.get_mut(&id) { +// if let Some(tx) = instance.run_plugin_resp_tx.take() { +// let _ = tx.send(status.map_err(|e| e.to_string())); +// } else { +// warn!("No response channel found for instance {:?}", id); +// } +// } else { +// warn!("Received RunPluginResp for unknown instance ID {:?}", id); +// } +// } +// CoordinatorMessage::Log { message } => { +// info!("[Coordinator] {}", message); +// } +// _ => { +// warn!("Unexpected message received from coordinator"); +// } +// } +// } + +// fn stdin_proxy( +// reader: impl AsyncRead + Unpin + Send + 'static, +// id: InstanceId, +// inner: Arc>, +// ) { +// wasm_bindgen_futures::spawn_local(async move { +// let mut reader = reader; +// let mut buffer = [0u8; 1024]; + +// loop { +// match reader.read(&mut buffer).await { +// Ok(0) => break, // EOF +// Ok(n) => { +// let data = js_sys::Uint8Array::from(&buffer[..n]); +// let stdin_msg = CoordinatorMessage::Stdin { id }; +// let msg_value = serde_wasm_bindgen::to_value(&stdin_msg).unwrap(); +// Reflect::set(&msg_value, &"data".into(), &data).unwrap(); +// let transfer_list = js_sys::Array::of1(&data.buffer()); + +// let inner = inner.lock().unwrap(); +// if let Err(e) = inner +// .coordinator +// .post_message_with_transfer(&msg_value, &transfer_list) +// { +// error!("Failed to post stdin message: {:?}", e); +// break; +// } +// } +// Err(e) => { +// error!("Error reading from stdin: {}", e); +// break; +// } +// } +// } +// }); +// } diff --git a/makefile b/makefile index 91ec171..2a5b882 100644 --- a/makefile +++ b/makefile @@ -1,8 +1,7 @@ # === Configuration === BROWSER := --firefox -ASSETS_DIR := crates/wasmi-plugin-coordinator/assets -.PHONY: help build-plugin build-coordinator build-all bench-wasm +.PHONY: help build-plugin build-all bench-wasm build-plugin: @echo "--- Building Plugin WASM ---" @@ -11,21 +10,7 @@ build-plugin: target/wasm32-wasip1/release/test-plugin.wasm \ -o target/wasm32-wasip1/release/test-plugin.wasm -build-coordinator: - @echo "--- Building Coordinator WASM ---" - cd crates/wasmi-plugin-coordinator && wasm-pack build \ - --target web \ - --out-dir pkg \ - --release - wasm-opt -O3 --debuginfo \ - crates/wasmi-plugin-coordinator/pkg/wasmi_plugin_coordinator_bg.wasm \ - -o crates/wasmi-plugin-coordinator/pkg/wasmi_plugin_coordinator_bg.wasm - - mkdir -p $(ASSETS_DIR) - @cp crates/wasmi-plugin-coordinator/pkg/wasmi_plugin_coordinator_bg.wasm $(ASSETS_DIR)/worker.wasm - @cp crates/wasmi-plugin-coordinator/pkg/wasmi_plugin_coordinator.js $(ASSETS_DIR)/worker.js - -build-all: build-plugin build-coordinator +build-all: build-plugin clippy-native: build-all @echo "--- Running Clippy Lints ---"