Skip to content

Commit de7f73d

Browse files
committed
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_<sig>` in the generated `invoke_<sig>` methods (i.e. when an invoke_<sig> 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)
1 parent 13ab375 commit de7f73d

File tree

11 files changed

+108
-1
lines changed

11 files changed

+108
-1
lines changed

examples/dodge-the-creeps/godot/DodgeTheCreeps.gdextension

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ macos.debug = "res://../../../target/debug/libdodge_the_creeps.dylib"
1111
macos.release = "res://../../../target/release/libdodge_the_creeps.dylib"
1212
macos.debug.arm64 = "res://../../../target/debug/libdodge_the_creeps.dylib"
1313
macos.release.arm64 = "res://../../../target/release/libdodge_the_creeps.dylib"
14+
web.debug.wasm32 = "res://../../../target/wasm32-unknown-emscripten/debug/dodge_the_creeps.wasm"
15+
web.release.wasm32 = "res://../../../target/wasm32-unknown-emscripten/release/dodge_the_creeps.wasm"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# The cargo flag "-Zbuild-std" is also required but this cannot yet be specified for specific
2+
# targets: https://github.com/rust-lang/cargo/issues/8733
3+
[target.wasm32-unknown-emscripten]
4+
rustflags = [
5+
"-C", "link-args=-sSIDE_MODULE=2",
6+
"-C", "link-args=-sUSE_PTHREADS=1",
7+
"-C", "target-feature=+atomics,+bulk-memory,+mutable-globals",
8+
"-Zlink-native-libraries=no",
9+
]

examples/dodge-the-creeps/rust/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ publish = false
99
crate-type = ["cdylib"]
1010

1111
[dependencies]
12-
godot = { path = "../../../godot", default-features = false }
12+
godot = { path = "../../../godot", default-features = false, features = ["experimental-wasm"] }
1313
rand = "0.8"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/sh
2+
3+
# Must be in dodge-the-creep's rust directory in order to pick up the .cargo/config
4+
cd `dirname "$0"`
5+
6+
# We build the host gdextension first so that the godot editor doesn't complain.
7+
cargo +nightly build --package dodge-the-creeps $@ &&
8+
cargo +nightly build --package dodge-the-creeps --target wasm32-unknown-emscripten -Zbuild-std $@

godot-ffi/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ trace = []
1717
[dependencies]
1818
paste = "1"
1919

20+
[target.'cfg(target_family = "wasm")'.dependencies]
21+
gensym = "0.1.1"
22+
2023
[build-dependencies]
2124
godot-bindings = { path = "../godot-bindings" }
2225
godot-codegen = { path = "../godot-codegen" }

godot-ffi/src/compat/compat_4_1.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ struct LegacyLayout {
2626

2727
impl BindingCompat for sys::GDExtensionInterfaceGetProcAddress {
2828
fn ensure_static_runtime_compatibility(&self) {
29+
// Fundamentally in wasm function references and data pointers live in different memory
30+
// spaces so trying to read the "memory" at a function pointer (an index into a table) to
31+
// heuristically determine which API we have (as is done below) is not quite going to work.
32+
if cfg!(target_family = "wasm") {
33+
return;
34+
}
35+
2936
// In Godot 4.0.x, before the new GetProcAddress mechanism, the init function looked as follows.
3037
// In place of the `get_proc_address` function pointer, the `p_interface` data pointer was passed.
3138
//

godot-ffi/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ use std::ffi::CStr;
4747
#[doc(hidden)]
4848
pub use paste;
4949

50+
#[doc(hidden)]
51+
#[cfg(target_family = "wasm")]
52+
pub use gensym::gensym;
53+
5054
pub use crate::godot_ffi::{
5155
from_sys_init_or_init_default, GodotFfi, GodotNullableFfi, PrimitiveConversionError,
5256
PtrcallType,

godot-ffi/src/plugins.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,28 @@ macro_rules! plugin_registry {
2626
};
2727
}
2828

29+
#[doc(hidden)]
30+
#[macro_export]
31+
#[allow(clippy::deprecated_cfg_attr)]
32+
#[cfg_attr(rustfmt, rustfmt::skip)]
33+
// ^ skip: paste's [< >] syntax chokes fmt
34+
// cfg_attr: workaround for https://github.com/rust-lang/rust/pull/52234#issuecomment-976702997
35+
macro_rules! plugin_add_inner_wasm {
36+
($gensym:ident,) => {
37+
// Rust presently requires that statics with a custom `#[link_section]` must be a simple
38+
// list of bytes on the wasm target (with no extra levels of indirection such as references).
39+
//
40+
// As such, instead we export a fn with a random name of predictable format to be used
41+
// by the embedder.
42+
$crate::paste::paste! {
43+
#[no_mangle]
44+
extern "C" fn [< rust_gdext_registrant_ $gensym >] () {
45+
__init();
46+
}
47+
}
48+
};
49+
}
50+
2951
#[doc(hidden)]
3052
#[macro_export]
3153
#[allow(clippy::deprecated_cfg_attr)]
@@ -60,6 +82,9 @@ macro_rules! plugin_add_inner {
6082
}
6183
__inner_init
6284
};
85+
86+
#[cfg(target_family = "wasm")]
87+
$crate::gensym! { $crate::plugin_add_inner_wasm!() }
6388
};
6489
};
6590
}

godot-macros/src/gdextension.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,57 @@ pub fn attribute_gdextension(decl: Declaration) -> ParseResult<TokenStream> {
3636
Ok(quote! {
3737
#impl_decl
3838

39+
// This cfg cannot be checked from the outer proc-macro since its 'target' is the build
40+
// host. See: https://github.com/rust-lang/rust/issues/42587
41+
#[cfg(target_os = "emscripten")]
42+
fn emscripten_preregistration() {
43+
// Module is documented here[1] by emscripten so perhaps we can consider it a part
44+
// of its public API? In any case for now we mutate global state directly in order
45+
// to get things working.
46+
// [1] https://emscripten.org/docs/api_reference/module.html
47+
//
48+
// Warning: It may be possible that in the process of executing the code leading up
49+
// to `emscripten_run_script` that we might trigger usage of one of the symbols we
50+
// wish to monkey patch? It seems fairly unlikely, especially as long as no i64 are
51+
// involved, but I don't know what guarantees we have here.
52+
//
53+
// We should keep an eye out for these sorts of failures!
54+
let script = std::ffi::CString::new(concat!(
55+
"var pkgName = '", env!("CARGO_PKG_NAME"), "';", r#"
56+
var libName = pkgName.replaceAll('-', '_') + '.wasm';
57+
var dso = LDSO.loadedLibsByName[libName]["module"];
58+
var registrants = [];
59+
for (sym in dso) {
60+
if (sym.startsWith("dynCall_")) {
61+
if (!(sym in Module)) {
62+
console.log(`Patching Module with ${sym}`);
63+
Module[sym] = dso[sym];
64+
}
65+
} else if (sym.startsWith("rust_gdext_registrant_")) {
66+
registrants.push(sym);
67+
}
68+
}
69+
for (sym of registrants) {
70+
console.log(`Running registrant ${sym}`);
71+
dso[sym]();
72+
}
73+
console.log("Added", registrants.length, "plugins to registry!");
74+
"#)).expect("Unable to create CString from script");
75+
76+
extern "C" { fn emscripten_run_script(script: *const std::ffi::c_char); }
77+
unsafe { emscripten_run_script(script.as_ptr()); }
78+
}
79+
3980
#[no_mangle]
4081
unsafe extern "C" fn #entry_point(
4182
interface_or_get_proc_address: ::godot::sys::InitCompat,
4283
library: ::godot::sys::GDExtensionClassLibraryPtr,
4384
init: *mut ::godot::sys::GDExtensionInitialization,
4485
) -> ::godot::sys::GDExtensionBool {
86+
// Required due to the lack of a constructor facility such as .init_array in rust wasm
87+
#[cfg(target_os = "emscripten")]
88+
emscripten_preregistration();
89+
4590
::godot::init::__gdext_load_library::<#impl_ty>(
4691
interface_or_get_proc_address,
4792
library,

godot/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ serde = ["godot-core/serde"]
1616
lazy-function-tables = ["godot-core/codegen-lazy-fptrs"]
1717
experimental-threads = ["godot-core/experimental-threads"]
1818
experimental-godot-api = ["godot-core/experimental-godot-api"]
19+
experimental-wasm = []
1920

2021
# Private features, they are under no stability guarantee
2122
codegen-full = ["godot-core/codegen-full"]

godot/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ pub use godot_core::sys;
178178
#[cfg(all(feature = "lazy-function-tables", feature = "experimental-threads"))]
179179
compile_error!("Thread safety for lazy function pointers is not yet implemented.");
180180

181+
#[cfg(all(target_family = "wasm", not(feature = "experimental-wasm")))]
182+
compile_error!("Must opt-in using `experimental-wasm` Cargo feature; keep in mind that this is work in progress");
183+
181184
pub mod init {
182185
pub use godot_core::init::*;
183186

0 commit comments

Comments
 (0)