Skip to content

Commit 08c09d7

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

File tree

7 files changed

+91
-0
lines changed

7 files changed

+91
-0
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[target.wasm32-unknown-emscripten]
2+
rustflags = [
3+
"-C", "link-args=-sSIDE_MODULE=2",
4+
"-C", "link-args=-sUSE_PTHREADS=1",
5+
"-C", "target-feature=+atomics,+bulk-memory,+mutable-globals",
6+
"-Zlink-native-libraries=no"
7+
]

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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,45 @@ pub fn attribute_gdextension(decl: Declaration) -> ParseResult<TokenStream> {
3434
let impl_ty = &impl_decl.self_ty;
3535

3636
Ok(quote! {
37+
#[cfg(target_os = "emscripten")]
38+
fn emscripten_preregistration() {
39+
// Module is documented here[1] by emscripten so perhaps we can consider it a part of
40+
// its public API? In any case for now we mutate global state directly in order to get
41+
// things working.
42+
// [1] https://emscripten.org/docs/api_reference/module.html
43+
//
44+
// Warning: It may be possible that in the process of executing the code leading up to
45+
// `emscripten_run_script` that we might trigger usage of one of the symbols we wish to
46+
// monkey patch? It seems fairly unlikely, especially as long as no i64 are involved,
47+
// but I don't know what guarantees we have here.
48+
//
49+
// We should keep an eye out for these sorts of failures!
50+
let script = std::ffi::CString::new(concat!(
51+
"var pkgName = '", env!("CARGO_PKG_NAME"), "';", r#"
52+
var libName = pkgName.replaceAll('-', '_') + '.wasm';
53+
var dso = LDSO.loadedLibsByName[libName]["module"];
54+
var registrants = [];
55+
for (sym in dso) {
56+
if (sym.startsWith("dynCall_")) {
57+
if (!(sym in Module)) {
58+
console.log(`Patching Module with ${sym}`);
59+
Module[sym] = dso[sym];
60+
}
61+
} else if (sym.startsWith("rust_gdext_registrant_")) {
62+
registrants.push(sym);
63+
}
64+
}
65+
for (sym of registrants) {
66+
console.log(`Running registrant ${sym}`);
67+
dso[sym]();
68+
}
69+
console.log("Added", registrants.length, "plugins to registry!");
70+
"#)).expect("Unable to create CString from script");
71+
72+
extern "C" { fn emscripten_run_script(script: *const std::ffi::c_char); }
73+
unsafe { emscripten_run_script(script.as_ptr()); }
74+
}
75+
3776
#impl_decl
3877

3978
#[no_mangle]
@@ -42,6 +81,10 @@ pub fn attribute_gdextension(decl: Declaration) -> ParseResult<TokenStream> {
4281
library: ::godot::sys::GDExtensionClassLibraryPtr,
4382
init: *mut ::godot::sys::GDExtensionInitialization,
4483
) -> ::godot::sys::GDExtensionBool {
84+
// Required due to the lack of a constructor facility such as .init_array in rust wasm
85+
#[cfg(target_os = "emscripten")]
86+
emscripten_preregistration();
87+
4588
::godot::init::__gdext_load_library::<#impl_ty>(
4689
interface_or_get_proc_address,
4790
library,

0 commit comments

Comments
 (0)