Skip to content

Commit

Permalink
Minimal proof-of-concept for running dodge-the-creeps targeting wasm
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
zecozephyr committed Nov 22, 2023
1 parent 13ab375 commit 47aec42
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 0 deletions.
2 changes: 2 additions & 0 deletions examples/dodge-the-creeps/godot/DodgeTheCreeps.gdextension
Original file line number Diff line number Diff line change
Expand Up @@ -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"
7 changes: 7 additions & 0 deletions examples/dodge-the-creeps/rust/.cargo/config
Original file line number Diff line number Diff line change
@@ -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"
]
3 changes: 3 additions & 0 deletions godot-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
7 changes: 7 additions & 0 deletions godot-ffi/src/compat/compat_4_1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down
4 changes: 4 additions & 0 deletions godot-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions godot-ffi/src/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -60,6 +82,9 @@ macro_rules! plugin_add_inner {
}
__inner_init
};

#[cfg(target_family = "wasm")]
$crate::gensym! { $crate::plugin_add_inner_wasm!() }
};
};
}
Expand Down
43 changes: 43 additions & 0 deletions godot-macros/src/gdextension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,45 @@ pub fn attribute_gdextension(decl: Declaration) -> ParseResult<TokenStream> {
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]
Expand All @@ -42,6 +81,10 @@ pub fn attribute_gdextension(decl: Declaration) -> ParseResult<TokenStream> {
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,
Expand Down

0 comments on commit 47aec42

Please sign in to comment.