Skip to content

Commit 861c325

Browse files
committed
Drop Send/Sync from Actor + ActorBehavior + ActorState on wasm32
The actor framework required `Send + Sync` everywhere because the native runtime drives actors on tokio's multi-threaded executor. On `wasm32-unknown-unknown` the runtime is single-threaded (`spawn_local`) and the bound is gratuitous — but it was rejecting every browser-only type that holds raw JS handles (`*mut u8`): - `wgpu::backend::webgpu::WebQueue/WebTexture/WebBuffer` - `reqwest::wasm::AbortGuard` - `web_sys::WebSocket`, `web_sys::GpuDevice`, … Fixes: - Introduce `MaybeSend` / `MaybeSync` marker traits. Both collapse to `Send` / `Sync` on native and to nothing on wasm32. Use these for trait supertraits (`Actor`, `ActorState`). - For `dyn Future + Send` trait objects, marker traits don't help (Rust forbids combining a custom trait with a non-auto trait in a dyn). Cfg-split `ActorBehavior`, `Actor::create_process`, `ActorProcess::into_future`, and `Network::init_process` to drop `+ Send` on wasm. - Tidy reflow_components: gate `crate::io::FileLoadActor/FileSaveActor` imports + registry entries to native (already done at the module level, but the registry imports were unconditional). Document gpu module as "feature-gated wgpu pieces are native-only — sdf path utilities and font atlas math stay available to wasm consumers". Result: `cargo check --target wasm32-unknown-unknown -p reflow_components` is clean. The wgpu/openh264/chromiumoxide-feature actors remain native-only — those need separate work for the wgpu+wasm async init story (no `pollster::block_on` in browser).
1 parent 64be11c commit 861c325

5 files changed

Lines changed: 113 additions & 11 deletions

File tree

crates/reflow_actor/src/lib.rs

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,52 @@ use crate::{
2929
types::GraphNode,
3030
};
3131

32-
// #[cfg(not(target_arch = "wasm32"))]
32+
/// Cross-target `Send`/`Sync` markers.
33+
///
34+
/// On native, `MaybeSend` ≡ `Send`, `MaybeSync` ≡ `Sync` — the actor
35+
/// runtime uses tokio's multi-threaded executor and needs both.
36+
///
37+
/// On `wasm32-unknown-unknown` the executor is single-threaded
38+
/// (`spawn_local`), so we drop both bounds. That unblocks browser-only
39+
/// types that hold raw JS handles (`*mut u8`) under the hood —
40+
/// `wgpu::WebQueue`, `reqwest`'s `AbortGuard`, `web_sys::WebSocket`,
41+
/// `web_sys::GpuDevice`, … — none of which can satisfy `Send`.
42+
///
43+
/// This is the same pattern tokio, gloo, and async-std use.
44+
#[cfg(not(target_arch = "wasm32"))]
45+
pub trait MaybeSend: Send {}
46+
#[cfg(not(target_arch = "wasm32"))]
47+
impl<T: Send + ?Sized> MaybeSend for T {}
48+
49+
#[cfg(target_arch = "wasm32")]
50+
pub trait MaybeSend {}
51+
#[cfg(target_arch = "wasm32")]
52+
impl<T: ?Sized> MaybeSend for T {}
53+
54+
#[cfg(not(target_arch = "wasm32"))]
55+
pub trait MaybeSync: Sync {}
56+
#[cfg(not(target_arch = "wasm32"))]
57+
impl<T: Sync + ?Sized> MaybeSync for T {}
58+
59+
#[cfg(target_arch = "wasm32")]
60+
pub trait MaybeSync {}
61+
#[cfg(target_arch = "wasm32")]
62+
impl<T: ?Sized> MaybeSync for T {}
63+
64+
/// Type-erased actor body. The closure produces a future that resolves
65+
/// to the actor's port outputs.
66+
///
67+
/// On native, the future + closure are `Send + Sync` so the network can
68+
/// drive them via tokio's multi-threaded executor. On
69+
/// `wasm32-unknown-unknown` the executor is single-threaded
70+
/// (`spawn_local`), so we drop the auto-trait bounds — that unblocks
71+
/// browser-only types (`wgpu::WebQueue`, `reqwest::AbortGuard`,
72+
/// `web_sys::WebSocket`, …) which hold raw JS handles and are `!Send`.
73+
///
74+
/// Two cfg-split type aliases is the only way: trait objects can't
75+
/// combine a custom marker trait with a non-auto trait like `Future`,
76+
/// so a `MaybeSend` shim doesn't apply to `dyn Future + Send`.
77+
#[cfg(not(target_arch = "wasm32"))]
3378
pub type ActorBehavior = Box<
3479
dyn Fn(
3580
ActorContext,
@@ -44,6 +89,18 @@ pub type ActorBehavior = Box<
4489
+ 'static,
4590
>;
4691

92+
#[cfg(target_arch = "wasm32")]
93+
pub type ActorBehavior = Box<
94+
dyn Fn(
95+
ActorContext,
96+
) -> std::pin::Pin<
97+
Box<
98+
dyn std::future::Future<Output = Result<HashMap<String, Message>, anyhow::Error>>
99+
+ 'static,
100+
>,
101+
> + 'static,
102+
>;
103+
47104
pub type ActorPayload = HashMap<String, Message>;
48105
pub type ActorChannel = (
49106
flume::Sender<crate::message::Message>,
@@ -223,8 +280,10 @@ impl ActorConfig {
223280
}
224281
}
225282

226-
// #[cfg(not(target_arch = "wasm32"))]
227-
pub trait Actor: Send + Sync + 'static {
283+
// `MaybeSend` + `MaybeSync` collapse to `Send + Sync` on native and to
284+
// nothing on wasm32. See the marker definitions near the top of this
285+
// module.
286+
pub trait Actor: MaybeSend + MaybeSync + 'static {
228287
/// The actor's reaction to incoming data. This is the only thing an actor
229288
/// *must* define beyond port declarations.
230289
fn get_behavior(&self) -> ActorBehavior;
@@ -302,6 +361,7 @@ pub trait Actor: Send + Sync + 'static {
302361
///
303362
/// Override only when you need a fundamentally different execution
304363
/// model (e.g. SubgraphActor's inner-network routing).
364+
#[cfg(not(target_arch = "wasm32"))]
305365
fn create_process(
306366
&self,
307367
config: ActorConfig,
@@ -323,6 +383,28 @@ pub trait Actor: Send + Sync + 'static {
323383
.into_future()
324384
}
325385

386+
#[cfg(target_arch = "wasm32")]
387+
fn create_process(
388+
&self,
389+
config: ActorConfig,
390+
tracing_integration: Option<TracingIntegration>,
391+
) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + 'static>> {
392+
crate::process::ActorProcess::new(
393+
config.get_node_id().to_string(),
394+
self.get_behavior(),
395+
self.inport_names(),
396+
self.await_all_inports(),
397+
self.required_inports(),
398+
self.get_inports().1,
399+
self.get_outports(),
400+
self.create_state(),
401+
self.load_count(),
402+
config,
403+
tracing_integration,
404+
)
405+
.into_future()
406+
}
407+
326408
/// Shutdown the actor, waiting for all processes to finish
327409
fn shutdown(&self) {
328410
while self.load_count().get() > 0 {
@@ -734,7 +816,7 @@ impl BrowserActorContext {
734816
}
735817
}
736818

737-
pub trait ActorState: Send + Sync + 'static {
819+
pub trait ActorState: MaybeSend + MaybeSync + 'static {
738820
fn as_any(&self) -> &dyn Any;
739821
fn as_mut_any(&mut self) -> &mut dyn Any;
740822
}
@@ -1321,7 +1403,7 @@ impl Actor for JsBrowserActor {
13211403
&self,
13221404
config: ActorConfig,
13231405
tracing_integration: Option<TracingIntegration>,
1324-
) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + 'static + Send>> {
1406+
) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + 'static>> {
13251407
self.actor.create_process(config, tracing_integration)
13261408
}
13271409
}
@@ -1446,7 +1528,7 @@ impl Actor for BrowserActor {
14461528
&self,
14471529
actor_config: ActorConfig,
14481530
_tracing_integration: Option<TracingIntegration>,
1449-
) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + 'static + Send>> {
1531+
) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + 'static>> {
14501532
use futures::StreamExt;
14511533
use serde_json::json;
14521534

crates/reflow_actor/src/process.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,11 +179,21 @@ impl ActorProcess {
179179

180180
/// Consume self and return a boxed, pinned future suitable for
181181
/// `tokio::spawn` or the Actor trait's `create_process` return type.
182+
#[cfg(not(target_arch = "wasm32"))]
182183
pub fn into_future(
183184
self,
184185
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'static>> {
185186
Box::pin(self.run())
186187
}
188+
189+
/// wasm32 variant — no `Send` because the runtime is single-threaded
190+
/// (`spawn_local`) and browser-only types are typically `!Send`.
191+
#[cfg(target_arch = "wasm32")]
192+
pub fn into_future(
193+
self,
194+
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + 'static>> {
195+
Box::pin(self.run())
196+
}
187197
}
188198

189199
/// Merge-aware accumulation for fan-in synchronization.

crates/reflow_components/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ pub use reflow_api_services::api;
4747
pub mod assets;
4848
mod display;
4949
pub mod flow_control;
50+
// `gpu/` exposes both pure-Rust modules (path/sdf-IR consumers,
51+
// font atlas math) and wgpu-backed renderers. Wasm-incompatible
52+
// pieces are gated inside `gpu/mod.rs` behind `feature = "gpu"`,
53+
// which is native-only — see Cargo.toml. The browser-friendly
54+
// subset (sdf path utilities, font atlas data) stays available so
55+
// `procedural::tube_mesh` and friends still link.
5056
pub mod gpu;
5157
#[cfg(feature = "window-events")]
5258
pub mod input;

crates/reflow_components/src/registry.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,12 @@ use crate::input::{
5959
use crate::integration::BrowserScreencastActor;
6060
use crate::integration::HttpRequestActor;
6161
use crate::io::{
62-
FbxImportActor, FileLoadActor, FileSaveActor, GltfExportActor, GltfImportActor,
63-
MeshImportActor, ObjExportActor, ObjImportActor, SceneImportActor, StlExportActor,
64-
StlImportActor,
62+
FbxImportActor, GltfExportActor, GltfImportActor, MeshImportActor, ObjExportActor,
63+
ObjImportActor, SceneImportActor, StlExportActor, StlImportActor,
6564
};
65+
// File load/save actors are native-only (require tokio::fs).
66+
#[cfg(not(target_arch = "wasm32"))]
67+
use crate::io::{FileLoadActor, FileSaveActor};
6668
use crate::logic::RulesEngineActor;
6769
use crate::math::{
6870
Mat4IdentityActor,
@@ -321,8 +323,10 @@ pub fn get_actor_for_template(template_id: &str) -> Option<Arc<dyn Actor>> {
321323
"tpl_image_decode" => Some(Arc::new(ImageDecodeActor::new())),
322324
"tpl_image_encode" => Some(Arc::new(ImageEncodeActor::new())),
323325

324-
// File I/O
326+
// File I/O — native-only (no filesystem in browser).
327+
#[cfg(not(target_arch = "wasm32"))]
325328
"tpl_file_load" => Some(Arc::new(FileLoadActor::new())),
329+
#[cfg(not(target_arch = "wasm32"))]
326330
"tpl_file_save" => Some(Arc::new(FileSaveActor::new())),
327331

328332
// Stream Display

crates/reflow_network/src/network.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,7 @@ impl Network {
660660
}
661661
#[cfg(target_arch = "wasm32")]
662662
pub(crate) fn init_process(
663-
actor_process: std::pin::Pin<Box<dyn futures::Future<Output = ()> + 'static + Send>>,
663+
actor_process: std::pin::Pin<Box<dyn futures::Future<Output = ()> + 'static>>,
664664
) {
665665
spawn_local(actor_process);
666666
}

0 commit comments

Comments
 (0)