diff --git a/examples/dodge-the-creeps/godot/DodgeTheCreeps.gdextension b/examples/dodge-the-creeps/godot/DodgeTheCreeps.gdextension index 7bbaf48e8..2b6887ab7 100644 --- a/examples/dodge-the-creeps/godot/DodgeTheCreeps.gdextension +++ b/examples/dodge-the-creeps/godot/DodgeTheCreeps.gdextension @@ -11,3 +11,5 @@ macos.debug = "res://../../../target/debug/libdodge_the_creeps.dylib" macos.release = "res://../../../target/release/libdodge_the_creeps.dylib" macos.debug.arm64 = "res://../../../target/debug/libdodge_the_creeps.dylib" macos.release.arm64 = "res://../../../target/release/libdodge_the_creeps.dylib" +web.debug.wasm32 = "res://../../../target/wasm32-unknown-emscripten/debug/dodge_the_creeps.wasm" +web.release.wasm32 = "res://../../../target/wasm32-unknown-emscripten/release/dodge_the_creeps.wasm" diff --git a/examples/dodge-the-creeps/rust/.cargo/config b/examples/dodge-the-creeps/rust/.cargo/config new file mode 100644 index 000000000..ce9dcc3e7 --- /dev/null +++ b/examples/dodge-the-creeps/rust/.cargo/config @@ -0,0 +1,9 @@ +# The cargo flag "-Zbuild-std" is also required but this cannot yet be specified for specific +# targets: https://github.com/rust-lang/cargo/issues/8733 +[target.wasm32-unknown-emscripten] +rustflags = [ + "-C", "link-args=-sSIDE_MODULE=2", + "-C", "link-args=-sUSE_PTHREADS=1", + "-C", "target-feature=+atomics,+bulk-memory,+mutable-globals", + "-Zlink-native-libraries=no", +] diff --git a/examples/dodge-the-creeps/rust/Cargo.toml b/examples/dodge-the-creeps/rust/Cargo.toml index 8f650fb82..c45f9d413 100644 --- a/examples/dodge-the-creeps/rust/Cargo.toml +++ b/examples/dodge-the-creeps/rust/Cargo.toml @@ -9,5 +9,5 @@ publish = false crate-type = ["cdylib"] [dependencies] -godot = { path = "../../../godot", default-features = false } +godot = { path = "../../../godot", default-features = false, features = ["experimental-wasm"] } rand = "0.8" diff --git a/examples/dodge-the-creeps/rust/build-wasm.sh b/examples/dodge-the-creeps/rust/build-wasm.sh new file mode 100755 index 000000000..54a8e05c9 --- /dev/null +++ b/examples/dodge-the-creeps/rust/build-wasm.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +# Must be in dodge-the-creep's rust directory in order to pick up the .cargo/config +cd `dirname "$0"` + +# We build the host gdextension first so that the godot editor doesn't complain. +cargo +nightly build --package dodge-the-creeps && +cargo +nightly build --package dodge-the-creeps --target wasm32-unknown-emscripten -Zbuild-std $@ diff --git a/godot-ffi/Cargo.toml b/godot-ffi/Cargo.toml index 4eb04920a..5e6b46b23 100644 --- a/godot-ffi/Cargo.toml +++ b/godot-ffi/Cargo.toml @@ -17,6 +17,9 @@ trace = [] [dependencies] paste = "1" +[target.'cfg(target_family = "wasm")'.dependencies] +gensym = "0.1.1" + [build-dependencies] godot-bindings = { path = "../godot-bindings" } godot-codegen = { path = "../godot-codegen" } diff --git a/godot-ffi/src/compat/compat_4_1.rs b/godot-ffi/src/compat/compat_4_1.rs index e258e5870..e0eb7aad2 100644 --- a/godot-ffi/src/compat/compat_4_1.rs +++ b/godot-ffi/src/compat/compat_4_1.rs @@ -16,6 +16,7 @@ use crate::compat::BindingCompat; pub type InitCompat = sys::GDExtensionInterfaceGetProcAddress; +#[cfg(not(target_family = "wasm"))] #[repr(C)] struct LegacyLayout { version_major: u32, @@ -25,6 +26,13 @@ struct LegacyLayout { } impl BindingCompat for sys::GDExtensionInterfaceGetProcAddress { + // Fundamentally in wasm function references and data pointers live in different memory + // spaces so trying to read the "memory" at a function pointer (an index into a table) to + // heuristically determine which API we have (as is done below) is not quite going to work. + #[cfg(target_family = "wasm")] + fn ensure_static_runtime_compatibility(&self) {} + + #[cfg(not(target_family = "wasm"))] fn ensure_static_runtime_compatibility(&self) { // In Godot 4.0.x, before the new GetProcAddress mechanism, the init function looked as follows. // In place of the `get_proc_address` function pointer, the `p_interface` data pointer was passed. diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index db9a693b8..91c7f53b9 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -47,6 +47,10 @@ use std::ffi::CStr; #[doc(hidden)] pub use paste; +#[doc(hidden)] +#[cfg(target_family = "wasm")] +pub use gensym::gensym; + pub use crate::godot_ffi::{ from_sys_init_or_init_default, GodotFfi, GodotNullableFfi, PrimitiveConversionError, PtrcallType, diff --git a/godot-ffi/src/plugins.rs b/godot-ffi/src/plugins.rs index 1dfb181a7..a369fbdc0 100644 --- a/godot-ffi/src/plugins.rs +++ b/godot-ffi/src/plugins.rs @@ -26,6 +26,28 @@ macro_rules! plugin_registry { }; } +#[doc(hidden)] +#[macro_export] +#[allow(clippy::deprecated_cfg_attr)] +#[cfg_attr(rustfmt, rustfmt::skip)] +// ^ skip: paste's [< >] syntax chokes fmt +// cfg_attr: workaround for https://github.com/rust-lang/rust/pull/52234#issuecomment-976702997 +macro_rules! plugin_add_inner_wasm { + ($gensym:ident,) => { + // Rust presently requires that statics with a custom `#[link_section]` must be a simple + // list of bytes on the wasm target (with no extra levels of indirection such as references). + // + // As such, instead we export a fn with a random name of predictable format to be used + // by the embedder. + $crate::paste::paste! { + #[no_mangle] + extern "C" fn [< rust_gdext_registrant_ $gensym >] () { + __init(); + } + } + }; +} + #[doc(hidden)] #[macro_export] #[allow(clippy::deprecated_cfg_attr)] @@ -60,6 +82,9 @@ macro_rules! plugin_add_inner { } __inner_init }; + + #[cfg(target_family = "wasm")] + $crate::gensym! { $crate::plugin_add_inner_wasm!() } }; }; } diff --git a/godot-macros/src/gdextension.rs b/godot-macros/src/gdextension.rs index f0e489b34..b111135ef 100644 --- a/godot-macros/src/gdextension.rs +++ b/godot-macros/src/gdextension.rs @@ -36,12 +36,57 @@ pub fn attribute_gdextension(decl: Declaration) -> ParseResult { Ok(quote! { #impl_decl + // This cfg cannot be checked from the outer proc-macro since its 'target' is the build + // host. See: https://github.com/rust-lang/rust/issues/42587 + #[cfg(target_os = "emscripten")] + fn emscripten_preregistration() { + // Module is documented here[1] by emscripten so perhaps we can consider it a part + // of its public API? In any case for now we mutate global state directly in order + // to get things working. + // [1] https://emscripten.org/docs/api_reference/module.html + // + // Warning: It may be possible that in the process of executing the code leading up + // to `emscripten_run_script` that we might trigger usage of one of the symbols we + // wish to monkey patch? It seems fairly unlikely, especially as long as no i64 are + // involved, but I don't know what guarantees we have here. + // + // We should keep an eye out for these sorts of failures! + let script = std::ffi::CString::new(concat!( + "var pkgName = '", env!("CARGO_PKG_NAME"), "';", r#" + var libName = pkgName.replaceAll('-', '_') + '.wasm'; + var dso = LDSO.loadedLibsByName[libName]["module"]; + var registrants = []; + for (sym in dso) { + if (sym.startsWith("dynCall_")) { + if (!(sym in Module)) { + console.log(`Patching Module with ${sym}`); + Module[sym] = dso[sym]; + } + } else if (sym.startsWith("rust_gdext_registrant_")) { + registrants.push(sym); + } + } + for (sym of registrants) { + console.log(`Running registrant ${sym}`); + dso[sym](); + } + console.log("Added", registrants.length, "plugins to registry!"); + "#)).expect("Unable to create CString from script"); + + extern "C" { fn emscripten_run_script(script: *const std::ffi::c_char); } + unsafe { emscripten_run_script(script.as_ptr()); } + } + #[no_mangle] unsafe extern "C" fn #entry_point( interface_or_get_proc_address: ::godot::sys::InitCompat, library: ::godot::sys::GDExtensionClassLibraryPtr, init: *mut ::godot::sys::GDExtensionInitialization, ) -> ::godot::sys::GDExtensionBool { + // Required due to the lack of a constructor facility such as .init_array in rust wasm + #[cfg(target_os = "emscripten")] + emscripten_preregistration(); + ::godot::init::__gdext_load_library::<#impl_ty>( interface_or_get_proc_address, library, diff --git a/godot/Cargo.toml b/godot/Cargo.toml index 5bf01e05c..26cd60126 100644 --- a/godot/Cargo.toml +++ b/godot/Cargo.toml @@ -16,6 +16,7 @@ serde = ["godot-core/serde"] lazy-function-tables = ["godot-core/codegen-lazy-fptrs"] experimental-threads = ["godot-core/experimental-threads"] experimental-godot-api = ["godot-core/experimental-godot-api"] +experimental-wasm = [] # Private features, they are under no stability guarantee codegen-full = ["godot-core/codegen-full"] diff --git a/godot/src/lib.rs b/godot/src/lib.rs index f7bab98ba..414101781 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -146,6 +146,11 @@ //! Access to `godot::engine` APIs that Godot marks "experimental". These are under heavy development and may change at any time. //! If you opt in to this feature, expect breaking changes at compile and runtime. //! +//! * **`experimental-wasm`** +//! +//! Support for WebAssembly exports is still a work-in-progress and is not yet well tested. This feature is in place for users +//! to explicitly opt-in to any instabilities or rough edges that may result. +//! //! * **`lazy-function-tables`** //! //! Instead of loading all engine function pointers at startup, load them lazily on first use. This reduces startup time and RAM usage, but @@ -178,6 +183,9 @@ pub use godot_core::sys; #[cfg(all(feature = "lazy-function-tables", feature = "experimental-threads"))] compile_error!("Thread safety for lazy function pointers is not yet implemented."); +#[cfg(all(target_family = "wasm", not(feature = "experimental-wasm")))] +compile_error!("Must opt-in using `experimental-wasm` Cargo feature; keep in mind that this is work in progress"); + pub mod init { pub use godot_core::init::*;