Skip to content

Commit b5c85e5

Browse files
committed
spawn sync / bytes
1 parent f051c04 commit b5c85e5

File tree

7 files changed

+402
-47
lines changed

7 files changed

+402
-47
lines changed

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ members = [
1212
"crates/theater-handler-store",
1313
"crates/theater-handler-supervisor",
1414
"crates/theater-handler-tcp",
15+
"crates/theater-handler-assembler",
1516
"crates/theater-replay-experimenting",
1617
]
1718
# DEPRECATED: WASI handlers moved to crates/deprecated/
@@ -57,6 +58,7 @@ theater-handler-runtime = { version = "0.2.1", path = "crates/theater-handler-ru
5758
theater-handler-store = { version = "0.2.1", path = "crates/theater-handler-store" }
5859
theater-handler-supervisor = { version = "0.2.1", path = "crates/theater-handler-supervisor" }
5960
theater-handler-tcp = { version = "0.2.1", path = "crates/theater-handler-tcp" }
61+
theater-handler-assembler = { version = "0.2.1", path = "crates/theater-handler-assembler" }
6062
# DEPRECATED: WASI handlers (moved to crates/deprecated/)
6163
# theater-handler-environment = { version = "0.2.1", path = "crates/deprecated/theater-handler-environment" }
6264
# theater-handler-filesystem = { version = "0.2.1", path = "crates/deprecated/theater-handler-filesystem" }
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[package]
2+
name = "theater-handler-assembler"
3+
version.workspace = true
4+
edition.workspace = true
5+
authors.workspace = true
6+
license.workspace = true
7+
repository.workspace = true
8+
homepage.workspace = true
9+
keywords.workspace = true
10+
categories.workspace = true
11+
rust-version.workspace = true
12+
13+
[dependencies]
14+
# Core theater dependencies
15+
theater = { path = "../theater" }
16+
17+
# WAT to WASM conversion
18+
wat = "1"
19+
20+
# Async runtime
21+
tokio = { version = "1.0", features = ["full"] }
22+
23+
# Error handling
24+
anyhow = "1.0"
25+
26+
# Logging
27+
tracing = "0.1"
28+
29+
[dev-dependencies]
30+
test-log = "0.2"
31+
tracing-subscriber = "0.3"
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
//! # Assembler Handler
2+
//!
3+
//! Provides WAT to WASM assembly capabilities to WebAssembly actors in the Theater system.
4+
//! This handler allows actors to convert WebAssembly Text format (WAT) to binary WASM.
5+
6+
use std::future::Future;
7+
use std::pin::Pin;
8+
9+
use tracing::info;
10+
11+
use theater::actor::handle::ActorHandle;
12+
use theater::actor::store::ActorStore;
13+
use theater::handler::{Handler, HandlerContext, SharedActorInstance};
14+
use theater::shutdown::ShutdownReceiver;
15+
16+
// Pack integration
17+
use theater::pack_bridge::{Ctx, HostLinkerBuilder, LinkerError, Value, ValueType};
18+
19+
/// Handler for providing WAT to WASM assembly capabilities
20+
#[derive(Clone, Default)]
21+
pub struct AssemblerHandler;
22+
23+
impl AssemblerHandler {
24+
pub fn new() -> Self {
25+
Self
26+
}
27+
}
28+
29+
impl Handler for AssemblerHandler {
30+
fn create_instance(
31+
&self,
32+
_config: Option<&theater::config::actor_manifest::HandlerConfig>,
33+
) -> Box<dyn Handler> {
34+
Box::new(self.clone())
35+
}
36+
37+
fn start(
38+
&mut self,
39+
_actor_handle: ActorHandle,
40+
_actor_instance: SharedActorInstance,
41+
shutdown_receiver: ShutdownReceiver,
42+
) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send>> {
43+
info!("Starting assembler handler");
44+
45+
Box::pin(async {
46+
// Assembler handler doesn't need a background task, just wait for shutdown
47+
shutdown_receiver.wait_for_shutdown().await;
48+
info!("Assembler handler received shutdown signal");
49+
info!("Assembler handler shut down");
50+
Ok(())
51+
})
52+
}
53+
54+
fn setup_host_functions_composite(
55+
&mut self,
56+
builder: &mut HostLinkerBuilder<'_, ActorStore>,
57+
ctx: &mut HandlerContext,
58+
) -> Result<(), LinkerError> {
59+
info!("Setting up assembler host functions (Pack)");
60+
61+
// Check if the interface is already satisfied by another handler
62+
if ctx.is_satisfied("wisp:assembler/runtime") {
63+
info!("wisp:assembler/runtime already satisfied by another handler, skipping");
64+
return Ok(());
65+
}
66+
67+
builder
68+
.interface("wisp:assembler/runtime")?
69+
// wat-to-wasm: func(wat: string) -> result<list<u8>, string>
70+
.func_typed_result(
71+
"wat-to-wasm",
72+
|_ctx: &mut Ctx<'_, ActorStore>, input: Value| {
73+
let wat = match input {
74+
Value::String(s) => s,
75+
_ => {
76+
return Err(Value::String("expected string argument".to_string()));
77+
}
78+
};
79+
80+
info!("[ASSEMBLER] Converting {} bytes of WAT to WASM", wat.len());
81+
82+
match wat::parse_str(&wat) {
83+
Ok(wasm_bytes) => {
84+
info!("[ASSEMBLER] Success: {} bytes of WASM", wasm_bytes.len());
85+
let bytes: Vec<Value> =
86+
wasm_bytes.into_iter().map(Value::U8).collect();
87+
Ok(Value::List {
88+
elem_type: ValueType::U8,
89+
items: bytes,
90+
})
91+
}
92+
Err(e) => {
93+
info!("[ASSEMBLER] Error: {}", e);
94+
Err(Value::String(e.to_string()))
95+
}
96+
}
97+
},
98+
)?;
99+
100+
ctx.mark_satisfied("wisp:assembler/runtime");
101+
Ok(())
102+
}
103+
104+
fn supports_composite(&self) -> bool {
105+
true
106+
}
107+
108+
fn name(&self) -> &str {
109+
"assembler"
110+
}
111+
112+
fn imports(&self) -> Option<Vec<String>> {
113+
Some(vec!["wisp:assembler/runtime".to_string()])
114+
}
115+
116+
fn exports(&self) -> Option<Vec<String>> {
117+
None
118+
}
119+
}
120+
121+
#[cfg(test)]
122+
mod tests {
123+
use super::*;
124+
125+
#[test]
126+
fn test_assembler_handler_creation() {
127+
let handler = AssemblerHandler::new();
128+
assert_eq!(handler.name(), "assembler");
129+
assert_eq!(
130+
handler.imports(),
131+
Some(vec!["wisp:assembler/runtime".to_string()])
132+
);
133+
assert_eq!(handler.exports(), None);
134+
}
135+
136+
#[test]
137+
fn test_wat_to_wasm_basic() {
138+
// Test that the wat crate itself works
139+
let wat = "(module)";
140+
let result = wat::parse_str(wat);
141+
assert!(result.is_ok());
142+
}
143+
144+
#[test]
145+
fn test_wat_to_wasm_with_function() {
146+
let wat = r#"
147+
(module
148+
(func (export "add") (param i32 i32) (result i32)
149+
local.get 0
150+
local.get 1
151+
i32.add
152+
)
153+
)
154+
"#;
155+
let result = wat::parse_str(wat);
156+
assert!(result.is_ok());
157+
assert!(result.unwrap().len() > 0);
158+
}
159+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/// WIT world for the Assembler Handler
2+
///
3+
/// This world defines what the host provides to actors that import
4+
/// the wisp:assembler/runtime interface for converting WAT to WASM.
5+
6+
package wisp:assembler-handler;
7+
8+
world assembler-handler-host {
9+
/// Wisp assembler interface
10+
/// Provides WAT to WASM conversion functionality
11+
import wisp:assembler/runtime;
12+
}

crates/theater-handler-message-server/src/lib.rs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,8 @@ impl MessageServerHandler {
413413
params,
414414
)
415415
.await?;
416-
// Result is tuple<option<list<u8>>> - extract the optional response
416+
// Result is result<tuple<option<list<u8>>, tuple<option<list<u8>>>>, string>
417+
// Extract the optional response
417418
if let Some(response_data) = parse_option_bytes_from_tuple(&result) {
418419
let _ = response_tx.send(response_data);
419420
}
@@ -825,15 +826,42 @@ fn bytes_to_value(data: Vec<u8>) -> Value {
825826
}
826827
}
827828

828-
/// Parse an option<list<u8>> from a result tuple.
829-
/// The result from handle-request is tuple<option<list<u8>>>.
829+
/// Parse an option<list<u8>> from a handle-request result.
830+
/// The actual return type is:
831+
/// result<tuple<option<list<u8>>, tuple<option<list<u8>>>>, string>
832+
/// We need to:
833+
/// 1. Unwrap the Result (if Ok)
834+
/// 2. Get element 1 of the outer tuple (the response tuple)
835+
/// 3. Get element 0 of that tuple (the option<list<u8>>)
830836
fn parse_option_bytes_from_tuple(value: &Value) -> Option<Vec<u8>> {
831-
// Result is tuple<option<list<u8>>>
832-
let inner = match value {
837+
// First, unwrap the Result if present
838+
let inner_tuple = match value {
839+
Value::Result { value: Ok(inner), .. } => inner.as_ref(),
840+
Value::Result { value: Err(_), .. } => return None,
841+
// Fallback for simple tuple (backward compat)
842+
Value::Tuple(_) => value,
843+
_ => return None,
844+
};
845+
846+
// Now we have tuple<option<list<u8>>, tuple<option<list<u8>>>>
847+
// Element 0 is the new state, element 1 is the response tuple
848+
let response_tuple = match inner_tuple {
849+
Value::Tuple(items) if items.len() >= 2 => &items[1],
850+
// Fallback for old format: tuple<option<list<u8>>>
851+
Value::Tuple(items) if !items.is_empty() => {
852+
return parse_option_bytes(&items[0]);
853+
}
854+
_ => return None,
855+
};
856+
857+
// response_tuple is tuple<option<list<u8>>>
858+
// Get element 0 which is the option<list<u8>>
859+
let response_option = match response_tuple {
833860
Value::Tuple(items) if !items.is_empty() => &items[0],
834861
_ => return None,
835862
};
836-
parse_option_bytes(inner)
863+
864+
parse_option_bytes(response_option)
837865
}
838866

839867
/// Parse an option<list<u8>> Value into Option<Vec<u8>>

0 commit comments

Comments
 (0)