From 47aec4251dd5c0fef8972cc35086254a6318f019 Mon Sep 17 00:00:00 2001 From: Jon B <1441856+zecozephyr@users.noreply.github.com> Date: Wed, 22 Nov 2023 09:36:33 -0800 Subject: [PATCH] Minimal proof-of-concept for running dodge-the-creeps targeting wasm Instead of messing around with godot export templates or emscripten in order to either: 1. dlopen the gdextension lib with global flag (which may come with unforeseen problems from broadly exposing miscellaneous new symbols from the dso). 2. Reconsider the lookup scope of `dynCall_` in the generated `invoke_` methods (i.e. when an invoke_ is generated, also make it it remember the originating dso and fall back to lookup in the dso exports if the `dynCall` is not found globally) I instead opt to simply promote the selected troublesome symbols from the dso to Module scope as early as possible at the gdextension entry point, whilst also searching for and executing the constructor methods which set up state for the subsequent class registrations. ----------- Tested With: Godot Engine v4.1.3.stable.official [f06b6836a] (default export templates, dlink variant) rustc 1.75.0-nightly (2f1bd0729 2023-10-27) emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.47 (431685f05c67f0424c11473cc16798b9587bb536) Chrome Version 120.0.6093.0 (Official Build) canary (arm64) --- .../godot/DodgeTheCreeps.gdextension | 2 + examples/dodge-the-creeps/rust/.cargo/config | 7 +++ godot-ffi/Cargo.toml | 3 ++ godot-ffi/src/compat/compat_4_1.rs | 7 +++ godot-ffi/src/lib.rs | 4 ++ godot-ffi/src/plugins.rs | 25 +++++++++++ godot-macros/src/gdextension.rs | 43 +++++++++++++++++++ 7 files changed, 91 insertions(+) create mode 100644 examples/dodge-the-creeps/rust/.cargo/config 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,