Skip to content

Commit c620427

Browse files
Support for non-browser wasm (#17499)
# Objective - Contributes to #15460 - Supersedes #8520 - Fixes #4906 ## Solution - Added a new `web` feature to `bevy`, and several of its crates. - Enabled new `web` feature automatically within crates without `no_std` support. ## Testing - `cargo build --no-default-features --target wasm32v1-none` --- ## Migration Guide When using Bevy crates which _don't_ automatically enable the `web` feature, please enable it when building for the browser. ## Notes - I added [`cfg_if`](https://crates.io/crates/cfg-if) to help manage some of the feature gate gore that this extra feature introduces. It's still pretty ugly, but I think much easier to read. - Certain `wasm` targets (e.g., [wasm32-wasip1](https://doc.rust-lang.org/nightly/rustc/platform-support/wasm32-wasip1.html#wasm32-wasip1)) provide an incomplete implementation for `std`. I have not tested these platforms, but I suspect Bevy's liberal use of usually unsupported features (e.g., threading) will cause these targets to fail. As such, consider `wasm32-unknown-unknown` as the only `wasm` platform with support from Bevy for `std`. All others likely will need to be treated as `no_std` platforms.
1 parent edba54a commit c620427

File tree

29 files changed

+499
-393
lines changed

29 files changed

+499
-393
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,9 @@ critical-section = ["bevy_internal/critical-section"]
513513
# Uses the `libm` maths library instead of the one provided in `std` and `core`.
514514
libm = ["bevy_internal/libm"]
515515

516+
# Enables use of browser APIs. Note this is currently only applicable on `wasm32` architectures.
517+
web = ["bevy_internal/web"]
518+
516519
[dependencies]
517520
bevy_internal = { path = "crates/bevy_internal", version = "0.16.0-dev", default-features = false }
518521
tracing = { version = "0.1", default-features = false, optional = true }

crates/bevy_animation/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ smallvec = "1"
4444
tracing = { version = "0.1", default-features = false, features = ["std"] }
4545

4646
[target.'cfg(target_arch = "wasm32")'.dependencies]
47+
# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption.
4748
uuid = { version = "1.13.1", default-features = false, features = ["js"] }
4849

4950
[lints]

crates/bevy_app/Cargo.toml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ critical-section = [
6161
"bevy_reflect?/critical-section",
6262
]
6363

64+
## Enables use of browser APIs.
65+
## Note this is currently only applicable on `wasm32` architectures.
66+
web = [
67+
"bevy_platform_support/web",
68+
"bevy_tasks/web",
69+
"bevy_reflect?/web",
70+
"dep:wasm-bindgen",
71+
"dep:web-sys",
72+
"dep:console_error_panic_hook",
73+
]
74+
6475
[dependencies]
6576
# bevy
6677
bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" }
@@ -78,14 +89,15 @@ thiserror = { version = "2", default-features = false }
7889
variadics_please = "1.1"
7990
tracing = { version = "0.1", default-features = false, optional = true }
8091
log = { version = "0.4", default-features = false }
92+
cfg-if = "1.0.0"
8193

8294
[target.'cfg(any(unix, windows))'.dependencies]
8395
ctrlc = { version = "3.4.4", optional = true }
8496

8597
[target.'cfg(target_arch = "wasm32")'.dependencies]
86-
wasm-bindgen = { version = "0.2" }
87-
web-sys = { version = "0.3", features = ["Window"] }
88-
console_error_panic_hook = "0.1.6"
98+
wasm-bindgen = { version = "0.2", optional = true }
99+
web-sys = { version = "0.3", features = ["Window"], optional = true }
100+
console_error_panic_hook = { version = "0.1.6", optional = true }
89101

90102
[dev-dependencies]
91103
crossbeam-channel = "0.5.0"

crates/bevy_app/src/app.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1353,7 +1353,7 @@ type RunnerFn = Box<dyn FnOnce(App) -> AppExit>;
13531353

13541354
fn run_once(mut app: App) -> AppExit {
13551355
while app.plugins_state() == PluginsState::Adding {
1356-
#[cfg(not(target_arch = "wasm32"))]
1356+
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
13571357
bevy_tasks::tick_global_task_pools_on_main_thread();
13581358
}
13591359
app.finish();

crates/bevy_app/src/panic_handler.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,16 @@ impl Plugin for PanicHandlerPlugin {
4343
{
4444
static SET_HOOK: std::sync::Once = std::sync::Once::new();
4545
SET_HOOK.call_once(|| {
46-
#[cfg(target_arch = "wasm32")]
47-
{
48-
// This provides better panic handling in JS engines (displays the panic message and improves the backtrace).
49-
std::panic::set_hook(alloc::boxed::Box::new(console_error_panic_hook::hook));
50-
}
51-
#[cfg(not(target_arch = "wasm32"))]
52-
{
53-
#[cfg(feature = "error_panic_hook")]
54-
{
46+
cfg_if::cfg_if! {
47+
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
48+
// This provides better panic handling in JS engines (displays the panic message and improves the backtrace).
49+
std::panic::set_hook(alloc::boxed::Box::new(console_error_panic_hook::hook));
50+
} else if #[cfg(feature = "error_panic_hook")] {
5551
let current_hook = std::panic::take_hook();
5652
std::panic::set_hook(alloc::boxed::Box::new(
5753
bevy_ecs::error::bevy_error_panic_hook(current_hook),
5854
));
5955
}
60-
6156
// Otherwise use the default target panic hook - Do nothing.
6257
}
6358
});

crates/bevy_app/src/schedule_runner.rs

Lines changed: 49 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
use bevy_platform_support::time::Instant;
77
use core::time::Duration;
88

9-
#[cfg(target_arch = "wasm32")]
9+
#[cfg(all(target_arch = "wasm32", feature = "web"))]
1010
use {
1111
alloc::{boxed::Box, rc::Rc},
1212
core::cell::RefCell,
@@ -77,7 +77,7 @@ impl Plugin for ScheduleRunnerPlugin {
7777
let plugins_state = app.plugins_state();
7878
if plugins_state != PluginsState::Cleaned {
7979
while app.plugins_state() == PluginsState::Adding {
80-
#[cfg(not(target_arch = "wasm32"))]
80+
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
8181
bevy_tasks::tick_global_task_pools_on_main_thread();
8282
}
8383
app.finish();
@@ -118,58 +118,56 @@ impl Plugin for ScheduleRunnerPlugin {
118118
Ok(None)
119119
};
120120

121-
#[cfg(not(target_arch = "wasm32"))]
122-
{
123-
loop {
124-
match tick(&mut app, wait) {
125-
Ok(Some(_delay)) => {
126-
#[cfg(feature = "std")]
127-
std::thread::sleep(_delay);
128-
}
129-
Ok(None) => continue,
130-
Err(exit) => return exit,
121+
cfg_if::cfg_if! {
122+
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
123+
fn set_timeout(callback: &Closure<dyn FnMut()>, dur: Duration) {
124+
web_sys::window()
125+
.unwrap()
126+
.set_timeout_with_callback_and_timeout_and_arguments_0(
127+
callback.as_ref().unchecked_ref(),
128+
dur.as_millis() as i32,
129+
)
130+
.expect("Should register `setTimeout`.");
131131
}
132-
}
133-
}
134-
135-
#[cfg(target_arch = "wasm32")]
136-
{
137-
fn set_timeout(callback: &Closure<dyn FnMut()>, dur: Duration) {
138-
web_sys::window()
139-
.unwrap()
140-
.set_timeout_with_callback_and_timeout_and_arguments_0(
141-
callback.as_ref().unchecked_ref(),
142-
dur.as_millis() as i32,
143-
)
144-
.expect("Should register `setTimeout`.");
145-
}
146-
let asap = Duration::from_millis(1);
147-
148-
let exit = Rc::new(RefCell::new(AppExit::Success));
149-
let closure_exit = exit.clone();
150-
151-
let mut app = Rc::new(app);
152-
let moved_tick_closure = Rc::new(RefCell::new(None));
153-
let base_tick_closure = moved_tick_closure.clone();
154-
155-
let tick_app = move || {
156-
let app = Rc::get_mut(&mut app).unwrap();
157-
let delay = tick(app, wait);
158-
match delay {
159-
Ok(delay) => set_timeout(
160-
moved_tick_closure.borrow().as_ref().unwrap(),
161-
delay.unwrap_or(asap),
162-
),
163-
Err(code) => {
164-
closure_exit.replace(code);
132+
let asap = Duration::from_millis(1);
133+
134+
let exit = Rc::new(RefCell::new(AppExit::Success));
135+
let closure_exit = exit.clone();
136+
137+
let mut app = Rc::new(app);
138+
let moved_tick_closure = Rc::new(RefCell::new(None));
139+
let base_tick_closure = moved_tick_closure.clone();
140+
141+
let tick_app = move || {
142+
let app = Rc::get_mut(&mut app).unwrap();
143+
let delay = tick(app, wait);
144+
match delay {
145+
Ok(delay) => set_timeout(
146+
moved_tick_closure.borrow().as_ref().unwrap(),
147+
delay.unwrap_or(asap),
148+
),
149+
Err(code) => {
150+
closure_exit.replace(code);
151+
}
152+
}
153+
};
154+
*base_tick_closure.borrow_mut() =
155+
Some(Closure::wrap(Box::new(tick_app) as Box<dyn FnMut()>));
156+
set_timeout(base_tick_closure.borrow().as_ref().unwrap(), asap);
157+
158+
exit.take()
159+
} else {
160+
loop {
161+
match tick(&mut app, wait) {
162+
Ok(Some(_delay)) => {
163+
#[cfg(feature = "std")]
164+
std::thread::sleep(_delay);
165+
}
166+
Ok(None) => continue,
167+
Err(exit) => return exit,
165168
}
166169
}
167-
};
168-
*base_tick_closure.borrow_mut() =
169-
Some(Closure::wrap(Box::new(tick_app) as Box<dyn FnMut()>));
170-
set_timeout(base_tick_closure.borrow().as_ref().unwrap(), asap);
171-
172-
exit.take()
170+
}
173171
}
174172
}
175173
}

crates/bevy_app/src/task_pool_plugin.rs

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,19 @@ use bevy_tasks::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool, TaskPoolBuil
66
use core::{fmt::Debug, marker::PhantomData};
77
use log::trace;
88

9-
#[cfg(not(target_arch = "wasm32"))]
10-
use {crate::Last, bevy_ecs::prelude::NonSend};
11-
12-
#[cfg(not(target_arch = "wasm32"))]
13-
use bevy_tasks::tick_global_task_pools_on_main_thread;
9+
cfg_if::cfg_if! {
10+
if #[cfg(not(all(target_arch = "wasm32", feature = "web")))] {
11+
use {crate::Last, bevy_ecs::prelude::NonSend, bevy_tasks::tick_global_task_pools_on_main_thread};
12+
13+
/// A system used to check and advanced our task pools.
14+
///
15+
/// Calls [`tick_global_task_pools_on_main_thread`],
16+
/// and uses [`NonSendMarker`] to ensure that this system runs on the main thread
17+
fn tick_global_task_pools(_main_thread_marker: Option<NonSend<NonSendMarker>>) {
18+
tick_global_task_pools_on_main_thread();
19+
}
20+
}
21+
}
1422

1523
/// Setup of default task pools: [`AsyncComputeTaskPool`], [`ComputeTaskPool`], [`IoTaskPool`].
1624
#[derive(Default)]
@@ -24,22 +32,13 @@ impl Plugin for TaskPoolPlugin {
2432
// Setup the default bevy task pools
2533
self.task_pool_options.create_default_pools();
2634

27-
#[cfg(not(target_arch = "wasm32"))]
35+
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
2836
_app.add_systems(Last, tick_global_task_pools);
2937
}
3038
}
3139
/// A dummy type that is [`!Send`](Send), to force systems to run on the main thread.
3240
pub struct NonSendMarker(PhantomData<*mut ()>);
3341

34-
/// A system used to check and advanced our task pools.
35-
///
36-
/// Calls [`tick_global_task_pools_on_main_thread`],
37-
/// and uses [`NonSendMarker`] to ensure that this system runs on the main thread
38-
#[cfg(not(target_arch = "wasm32"))]
39-
fn tick_global_task_pools(_main_thread_marker: Option<NonSend<NonSendMarker>>) {
40-
tick_global_task_pools_on_main_thread();
41-
}
42-
4342
/// Defines a simple way to determine how many threads to use given the number of remaining cores
4443
/// and number of total cores
4544
#[derive(Clone)]
@@ -176,20 +175,21 @@ impl TaskPoolOptions {
176175
remaining_threads = remaining_threads.saturating_sub(io_threads);
177176

178177
IoTaskPool::get_or_init(|| {
179-
#[cfg_attr(target_arch = "wasm32", expect(unused_mut))]
180-
let mut builder = TaskPoolBuilder::default()
178+
let builder = TaskPoolBuilder::default()
181179
.num_threads(io_threads)
182180
.thread_name("IO Task Pool".to_string());
183181

184-
#[cfg(not(target_arch = "wasm32"))]
185-
{
182+
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
183+
let builder = {
184+
let mut builder = builder;
186185
if let Some(f) = self.io.on_thread_spawn.clone() {
187186
builder = builder.on_thread_spawn(move || f());
188187
}
189188
if let Some(f) = self.io.on_thread_destroy.clone() {
190189
builder = builder.on_thread_destroy(move || f());
191190
}
192-
}
191+
builder
192+
};
193193

194194
builder.build()
195195
});
@@ -205,20 +205,21 @@ impl TaskPoolOptions {
205205
remaining_threads = remaining_threads.saturating_sub(async_compute_threads);
206206

207207
AsyncComputeTaskPool::get_or_init(|| {
208-
#[cfg_attr(target_arch = "wasm32", expect(unused_mut))]
209-
let mut builder = TaskPoolBuilder::default()
208+
let builder = TaskPoolBuilder::default()
210209
.num_threads(async_compute_threads)
211210
.thread_name("Async Compute Task Pool".to_string());
212211

213-
#[cfg(not(target_arch = "wasm32"))]
214-
{
212+
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
213+
let builder = {
214+
let mut builder = builder;
215215
if let Some(f) = self.async_compute.on_thread_spawn.clone() {
216216
builder = builder.on_thread_spawn(move || f());
217217
}
218218
if let Some(f) = self.async_compute.on_thread_destroy.clone() {
219219
builder = builder.on_thread_destroy(move || f());
220220
}
221-
}
221+
builder
222+
};
222223

223224
builder.build()
224225
});
@@ -234,20 +235,21 @@ impl TaskPoolOptions {
234235
trace!("Compute Threads: {}", compute_threads);
235236

236237
ComputeTaskPool::get_or_init(|| {
237-
#[cfg_attr(target_arch = "wasm32", expect(unused_mut))]
238-
let mut builder = TaskPoolBuilder::default()
238+
let builder = TaskPoolBuilder::default()
239239
.num_threads(compute_threads)
240240
.thread_name("Compute Task Pool".to_string());
241241

242-
#[cfg(not(target_arch = "wasm32"))]
243-
{
242+
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
243+
let builder = {
244+
let mut builder = builder;
244245
if let Some(f) = self.compute.on_thread_spawn.clone() {
245246
builder = builder.on_thread_spawn(move || f());
246247
}
247248
if let Some(f) = self.compute.on_thread_destroy.clone() {
248249
builder = builder.on_thread_destroy(move || f());
249250
}
250-
}
251+
builder
252+
};
251253

252254
builder.build()
253255
});

crates/bevy_asset/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ tracing = { version = "0.1", default-features = false, features = ["std"] }
5757
bevy_window = { path = "../bevy_window", version = "0.16.0-dev" }
5858

5959
[target.'cfg(target_arch = "wasm32")'.dependencies]
60+
# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption.
6061
wasm-bindgen = { version = "0.2" }
6162
web-sys = { version = "0.3", features = [
6263
"Window",
@@ -66,6 +67,15 @@ web-sys = { version = "0.3", features = [
6667
wasm-bindgen-futures = "0.4"
6768
js-sys = "0.3"
6869
uuid = { version = "1.13.1", default-features = false, features = ["js"] }
70+
bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [
71+
"web",
72+
] }
73+
bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [
74+
"web",
75+
] }
76+
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [
77+
"web",
78+
] }
6979

7080
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
7181
notify-debouncer-full = { version = "0.5.0", optional = true }

crates/bevy_audio/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,16 @@ tracing = { version = "0.1", default-features = false, features = ["std"] }
2626
cpal = { version = "0.15", optional = true }
2727

2828
[target.'cfg(target_arch = "wasm32")'.dependencies]
29+
# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption.
2930
rodio = { version = "0.20", default-features = false, features = [
3031
"wasm-bindgen",
3132
] }
33+
bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [
34+
"web",
35+
] }
36+
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [
37+
"web",
38+
] }
3239

3340
[features]
3441
mp3 = ["rodio/mp3"]

0 commit comments

Comments
 (0)