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..51472916f --- /dev/null +++ b/examples/dodge-the-creeps/rust/.cargo/config @@ -0,0 +1,7 @@ +[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/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..2b843d06e 100644 --- a/godot-ffi/src/compat/compat_4_1.rs +++ b/godot-ffi/src/compat/compat_4_1.rs @@ -26,6 +26,13 @@ struct LegacyLayout { impl BindingCompat for sys::GDExtensionInterfaceGetProcAddress { fn ensure_static_runtime_compatibility(&self) { + // 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. + if cfg!(target_family = "wasm") { + return; + } + // 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..6421b6168 100644 --- a/godot-macros/src/gdextension.rs +++ b/godot-macros/src/gdextension.rs @@ -34,6 +34,45 @@ pub fn attribute_gdextension(decl: Declaration) -> ParseResult { let impl_ty = &impl_decl.self_ty; Ok(quote! { + #[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()); } + } + #impl_decl #[no_mangle] @@ -42,6 +81,10 @@ pub fn attribute_gdextension(decl: Declaration) -> ParseResult { 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,