From f3b44b5d699e15ad4358ae70a7e0fd2a57daa922 Mon Sep 17 00:00:00 2001 From: Zachery Aaron Shores-Chmielewski Date: Fri, 6 Feb 2026 23:45:58 +0700 Subject: [PATCH] feat: basic wasm Unfeatured wasm module. Mostly to test that wasm builds --- .gitignore | 2 +- wasm/Cargo.lock | 138 ++++++++++++++++++++++++++++++++++++++++++++++++ wasm/Cargo.toml | 13 +++++ wasm/src/lib.rs | 113 +++++++++++++++++++++++++++++++++++++++ wasm/test.mjs | 102 +++++++++++++++++++++++++++++++++++ 5 files changed, 367 insertions(+), 1 deletion(-) create mode 100644 wasm/Cargo.lock create mode 100644 wasm/Cargo.toml create mode 100644 wasm/src/lib.rs create mode 100644 wasm/test.mjs diff --git a/.gitignore b/.gitignore index 0bd9521..eff640b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/target +**/target tools/depgraph/target/ node_modules/ .vscode/ diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock new file mode 100644 index 0000000..a99a116 --- /dev/null +++ b/wasm/Cargo.lock @@ -0,0 +1,138 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[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 = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "swactor" +version = "0.1.0" +dependencies = [ + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "swactor-wasm" +version = "0.1.0" +dependencies = [ + "swactor", + "wasm-bindgen", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml new file mode 100644 index 0000000..dfc5e24 --- /dev/null +++ b/wasm/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] + +[package] +name = "swactor-wasm" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +swactor = { path = "..", default-features = false, features = ["no_random"] } +wasm-bindgen = "0.2" diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs new file mode 100644 index 0000000..221a38d --- /dev/null +++ b/wasm/src/lib.rs @@ -0,0 +1,113 @@ +use wasm_bindgen::prelude::*; + +use swactor::actor::{ActorAddress, ActorInterface}; +use swactor::runtime::{Ctx, Inbox, Runtime, RuntimeConfig}; + +// --------------------------------------------------------------------------- +// Actors (private — only exposed through the wasm API) +// --------------------------------------------------------------------------- + +struct Counter { + total: u32, + report_to: ActorAddress, +} + +impl ActorInterface for Counter { + type Incoming = u32; + type Response = (); + + fn handle(&mut self, ctx: &Ctx, msg: u32) { + self.total += msg; + let _ = ctx.send(self.report_to, self.total); + } +} + +struct Relay { + target: ActorAddress, +} + +impl ActorInterface for Relay { + type Incoming = u32; + type Response = (); + + fn handle(&mut self, ctx: &Ctx, msg: u32) { + let _ = ctx.send(self.target, msg); + } +} + +// --------------------------------------------------------------------------- +// JS-facing runtime wrapper +// --------------------------------------------------------------------------- + +#[wasm_bindgen] +pub struct SwactorRuntime { + rt: Runtime, + inbox: Inbox, + actors: Vec, +} + +#[wasm_bindgen] +impl SwactorRuntime { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + let rt = Runtime::new(RuntimeConfig { + num_threads: 1, + ..RuntimeConfig::default() + }); + let inbox = rt.new_inbox().unwrap(); + Self { + rt, + inbox, + actors: Vec::new(), + } + } + + /// Spawn a counter actor. Returns its index (used with `send`). + pub fn spawn_counter(&mut self) -> usize { + let addr = self + .rt + .spawn(Counter { + total: 0, + report_to: *self.inbox.addr(), + }) + .expect("spawn counter"); + let idx = self.actors.len(); + self.actors.push(addr); + idx + } + + /// Spawn a relay that forwards every message to `target_idx`. + pub fn spawn_relay(&mut self, target_idx: usize) -> usize { + let target = self.actors[target_idx]; + let addr = self + .rt + .spawn(Relay { target }) + .expect("spawn relay"); + let idx = self.actors.len(); + self.actors.push(addr); + idx + } + + /// Send a u32 to the actor at `actor_idx`. + pub fn send(&self, actor_idx: usize, value: u32) -> bool { + if actor_idx >= self.actors.len() { + return false; + } + self.rt.send_to(self.actors[actor_idx], value).is_ok() + } + + /// Drive one tick of the single-threaded runtime. + pub fn tick(&self) { + self.rt.tick(); + } + + /// Try to read the next result from the inbox. Returns `undefined` when empty. + pub fn try_recv(&self) -> Option { + self.inbox.try_recv() + } + + /// Number of actors the runtime knows about. + pub fn actor_count(&self) -> usize { + self.rt.stats().actors.len() + } +} diff --git a/wasm/test.mjs b/wasm/test.mjs new file mode 100644 index 0000000..b23438a --- /dev/null +++ b/wasm/test.mjs @@ -0,0 +1,102 @@ +import { SwactorRuntime } from "./pkg/swactor_wasm.js"; + +let passed = 0; +let failed = 0; + +function assert(cond, msg) { + if (!cond) { + console.error(` FAIL: ${msg}`); + failed++; + } else { + passed++; + } +} + +function assertEq(a, b, msg) { + const aj = JSON.stringify(a); + const bj = JSON.stringify(b); + if (aj !== bj) { + console.error(` FAIL: ${msg} (got ${aj}, expected ${bj})`); + failed++; + } else { + passed++; + } +} + +function drain(rt) { + const results = []; + let v; + while ((v = rt.try_recv()) !== undefined) results.push(v); + return results; +} + +// ---- accumulator ---------------------------------------------------------- +{ + console.log("test: accumulator processes messages"); + const rt = new SwactorRuntime(); + const c = rt.spawn_counter(); + rt.send(c, 1); + rt.send(c, 2); + rt.send(c, 10); + rt.tick(); + assertEq(drain(rt), [1, 3, 13], "running totals"); + rt.free(); +} + +// ---- relay ---------------------------------------------------------------- +{ + console.log("test: relay forwards to counter"); + const rt = new SwactorRuntime(); + const c = rt.spawn_counter(); + const r = rt.spawn_relay(c); + rt.send(r, 5); + rt.send(r, 7); + // tick 1: relay receives and forwards (cross-actor, same worker → pending_local) + // tick 2: counter receives forwarded messages + rt.tick(); + rt.tick(); + assertEq(drain(rt), [5, 12], "relayed totals"); + rt.free(); +} + +// ---- multiple counters ---------------------------------------------------- +{ + console.log("test: multiple independent counters"); + const rt = new SwactorRuntime(); + const a = rt.spawn_counter(); + const b = rt.spawn_counter(); + rt.send(a, 10); + rt.send(b, 100); + rt.tick(); + const results = drain(rt); + // order depends on HashMap iteration, so just check set equality + assert( + results.includes(10) && results.includes(100) && results.length === 2, + "both counters report" + ); + rt.free(); +} + +// ---- actor_count ---------------------------------------------------------- +{ + console.log("test: actor_count tracks spawns"); + const rt = new SwactorRuntime(); + rt.spawn_counter(); + rt.spawn_counter(); + rt.spawn_counter(); + rt.tick(); // drain spawn queue + assertEq(rt.actor_count(), 3, "three actors"); + rt.free(); +} + +// ---- send to invalid index returns false ---------------------------------- +{ + console.log("test: send to bad index returns false"); + const rt = new SwactorRuntime(); + assert(!rt.send(999, 1), "out-of-bounds send"); + rt.free(); +} + +// ---- results -------------------------------------------------------------- +console.log(`\n${passed} passed, ${failed} failed`); +if (failed > 0) process.exit(1); -- 2.45.2