diff --git a/Cargo.lock b/Cargo.lock index 2dfd579..b8c956e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2980,6 +2980,13 @@ dependencies = [ "web-sys", ] +[[package]] +name = "resident-probe-component" +version = "0.1.0" +dependencies = [ + "wit-bindgen 0.41.0", +] + [[package]] name = "ring" version = "0.17.14" diff --git a/Cargo.toml b/Cargo.toml index db8af12..c911c12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "components/spin", "components/clock-probe", "components/kv-probe", + "components/resident-probe", "components/wasi-clock-probe", ] default-members = ["crates/wasmeld-runtime"] diff --git a/components/resident-probe/Cargo.toml b/components/resident-probe/Cargo.toml new file mode 100644 index 0000000..5a4d3f7 --- /dev/null +++ b/components/resident-probe/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "resident-probe-component" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[package.metadata.wasmeld] +id = "resident-probe" +world = "component:resident-probe/resident-probe-component@0.1.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen.workspace = true diff --git a/components/resident-probe/src/lib.rs b/components/resident-probe/src/lib.rs new file mode 100644 index 0000000..80f6bfb --- /dev/null +++ b/components/resident-probe/src/lib.rs @@ -0,0 +1,72 @@ +use std::sync::atomic::{AtomicU64, Ordering}; + +mod bindings { + wit_bindgen::generate!({ + path: "wit", + world: "resident-probe-component", + generate_all, + }); +} + +use bindings::exports::wasmeld::resident::actor::{ + DatagramSend, Effect, Event, Guest as ResidentGuest, MessageAck, SourceCommand, StreamWrite, + TimerArm, +}; + +static EVENTS_HANDLED: AtomicU64 = AtomicU64::new(0); + +struct ResidentProbe; + +impl bindings::Guest for ResidentProbe { + fn init(_config: Vec) -> Result<(), bindings::ServiceError> { + EVENTS_HANDLED.store(0, Ordering::Relaxed); + Ok(()) + } + + fn invoke(_input: Vec) -> Result, bindings::ServiceError> { + Ok(EVENTS_HANDLED + .load(Ordering::Relaxed) + .to_le_bytes() + .to_vec()) + } +} + +impl ResidentGuest for ResidentProbe { + fn handle_event( + input: Event, + ) -> Result, bindings::exports::wasmeld::resident::actor::ResidentError> { + EVENTS_HANDLED.fetch_add(1, Ordering::Relaxed); + Ok(match input { + Event::StreamData(chunk) => vec![Effect::WriteStream(StreamWrite { + stream_id: chunk.stream_id, + bytes: chunk.bytes, + })], + Event::Datagram(datagram) => vec![Effect::SendDatagram(DatagramSend { + endpoint_id: datagram.endpoint_id, + peer: datagram.peer, + bytes: datagram.bytes, + })], + Event::Timer(timer) => vec![Effect::ArmTimer(TimerArm { + timer_id: timer.timer_id, + delay_ms: 10, + interval_ms: None, + })], + Event::Message(message) => vec![Effect::AcknowledgeMessage(MessageAck { + subscription_id: message.subscription_id, + message_id: message.message_id, + })], + Event::Source(source) => vec![Effect::SourceCommand(SourceCommand { + source_id: source.source_id, + command: source.kind, + payload: source.payload, + })], + Event::StreamOpened(_) + | Event::StreamWritable(_) + | Event::StreamHalfClosed(_) + | Event::StreamClosed(_) + | Event::Shutdown => Vec::new(), + }) + } +} + +bindings::export!(ResidentProbe with_types_in bindings); diff --git a/components/resident-probe/wasmeld.toml b/components/resident-probe/wasmeld.toml new file mode 100644 index 0000000..15d24ff --- /dev/null +++ b/components/resident-probe/wasmeld.toml @@ -0,0 +1,11 @@ +schema_version = 1 + +[dependencies] +"wasmeld:resident" = "0.1.0" +"wasmeld:service" = "0.1.0" + +[replace."wasmeld:resident"] +path = "../../wit/resident" + +[replace."wasmeld:service"] +path = "../../wit/service" diff --git a/components/resident-probe/wit.lock b/components/resident-probe/wit.lock new file mode 100644 index 0000000..ba2b6f5 --- /dev/null +++ b/components/resident-probe/wit.lock @@ -0,0 +1,15 @@ +schema_version = 1 + +[[package]] +name = "wasmeld:resident" +version = "0.1.0" +source = "path+../../wit/resident" +sha256 = "683a3c7196a6dd77b3f619227aa093430ff6995c097d39bb82d501b7c59a196c" +replaced = true + +[[package]] +name = "wasmeld:service" +version = "0.1.0" +source = "path+../../wit/service" +sha256 = "d5497307bbcd1e159f7707f385b488a6f2e26362d5256d5bc1080ac603a51305" +replaced = true diff --git a/components/resident-probe/wit/world.wit b/components/resident-probe/wit/world.wit new file mode 100644 index 0000000..e44e076 --- /dev/null +++ b/components/resident-probe/wit/world.wit @@ -0,0 +1,6 @@ +package component:resident-probe@0.1.0; + +world resident-probe-component { + include wasmeld:service/service-component@0.1.0; + export wasmeld:resident/actor@0.1.0; +} diff --git a/wit/resident/package.wit b/wit/resident/package.wit new file mode 100644 index 0000000..0e3d35d --- /dev/null +++ b/wit/resident/package.wit @@ -0,0 +1,123 @@ +package wasmeld:resident@0.1.0; + +/// Event/effect ABI for Components that remain active without monopolizing +/// their Actor thread. The Host owns timers, subscriptions, sockets, streams, +/// and all operating-system handles. A Component handles one bounded event and +/// returns effects before control goes back to the Runtime. +interface actor { + type resource-id = u64; + + record timer-fired { + timer-id: resource-id, + scheduled-at-ns: u64, + } + + record stream-opened { + endpoint-id: resource-id, + stream-id: resource-id, + peer: option, + } + + record stream-chunk { + stream-id: resource-id, + bytes: list, + } + + enum close-reason { + peer-closed, + host-closed, + idle-timeout, + protocol-error, + transport-error, + } + + record stream-closed { + stream-id: resource-id, + reason: close-reason, + } + + record datagram { + endpoint-id: resource-id, + peer: string, + bytes: list, + } + + record message { + subscription-id: resource-id, + message-id: u64, + bytes: list, + } + + /// Escape hatch for Host-owned event sources such as filesystem watchers, + /// serial devices, process signals, or platform-specific buses. `kind` and + /// `payload` are interpreted by the separately versioned Host capability + /// that created `source-id`. + record source-event { + source-id: resource-id, + kind: string, + payload: list, + } + + variant event { + timer(timer-fired), + stream-opened(stream-opened), + stream-data(stream-chunk), + stream-writable(resource-id), + stream-half-closed(resource-id), + stream-closed(stream-closed), + datagram(datagram), + message(message), + source(source-event), + shutdown, + } + + record stream-write { + stream-id: resource-id, + bytes: list, + } + + record datagram-send { + endpoint-id: resource-id, + peer: string, + bytes: list, + } + + record timer-arm { + timer-id: resource-id, + delay-ms: u64, + interval-ms: option, + } + + record message-ack { + subscription-id: resource-id, + message-id: u64, + } + + record source-command { + source-id: resource-id, + command: string, + payload: list, + } + + variant effect { + write-stream(stream-write), + close-stream(resource-id), + pause-stream(resource-id), + resume-stream(resource-id), + send-datagram(datagram-send), + arm-timer(timer-arm), + cancel-timer(resource-id), + acknowledge-message(message-ack), + source-command(source-command), + } + + variant resident-error { + event-failed(string), + } + + handle-event: func(input: event) -> result, resident-error>; +} + +world resident-component { + export actor; +}