feat: basic wasm (#12)
Unfeatured wasm module. Mostly to test that wasm builds Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
This commit is contained in:
parent
a01c3eb5f9
commit
c9d1383d1c
5 changed files with 367 additions and 1 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
/target
|
**/target
|
||||||
tools/depgraph/target/
|
tools/depgraph/target/
|
||||||
node_modules/
|
node_modules/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
|
||||||
138
wasm/Cargo.lock
generated
Normal file
138
wasm/Cargo.lock
generated
Normal file
|
|
@ -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",
|
||||||
|
]
|
||||||
13
wasm/Cargo.toml
Normal file
13
wasm/Cargo.toml
Normal file
|
|
@ -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"
|
||||||
113
wasm/src/lib.rs
Normal file
113
wasm/src/lib.rs
Normal file
|
|
@ -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<u32>,
|
||||||
|
actors: Vec<ActorAddress>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<u32> {
|
||||||
|
self.inbox.try_recv()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of actors the runtime knows about.
|
||||||
|
pub fn actor_count(&self) -> usize {
|
||||||
|
self.rt.stats().actors.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
102
wasm/test.mjs
Normal file
102
wasm/test.mjs
Normal file
|
|
@ -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);
|
||||||
Loading…
Reference in a new issue