From 816c2328e95b3e242cf1cdda5a18e3ebaf91e4e7 Mon Sep 17 00:00:00 2001 From: Jon B <1441856+zecozephyr@users.noreply.github.com> Date: Fri, 24 Nov 2023 05:04:28 -0800 Subject: [PATCH] Enable experimental wasm support 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 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 | 9 ++++ examples/dodge-the-creeps/rust/Cargo.toml | 2 +- examples/dodge-the-creeps/rust/build-wasm.sh | 8 ++++ godot-ffi/Cargo.toml | 3 ++ godot-ffi/src/compat/compat_4_1.rs | 8 ++++ godot-ffi/src/lib.rs | 4 ++ godot-ffi/src/plugins.rs | 25 +++++++++++ godot-macros/src/gdextension.rs | 45 +++++++++++++++++++ godot/Cargo.toml | 1 + godot/src/lib.rs | 8 ++++ 11 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 examples/dodge-the-creeps/rust/.cargo/config create mode 100755 examples/dodge-the-creeps/rust/build-wasm.sh 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::*;