Skip to content

Commit

Permalink
Enable experimental wasm support
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 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 25, 2023
1 parent 9016ae2 commit 373bcea
Show file tree
Hide file tree
Showing 11 changed files with 109 additions and 1 deletion.
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"
9 changes: 9 additions & 0 deletions examples/dodge-the-creeps/rust/.cargo/config
Original file line number Diff line number Diff line change
@@ -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",
]
2 changes: 1 addition & 1 deletion examples/dodge-the-creeps/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
8 changes: 8 additions & 0 deletions examples/dodge-the-creeps/rust/build-wasm.sh
Original file line number Diff line number Diff line change
@@ -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 $@
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" }
8 changes: 8 additions & 0 deletions godot-ffi/src/compat/compat_4_1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
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
45 changes: 45 additions & 0 deletions godot-macros/src/gdextension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,57 @@ pub fn attribute_gdextension(decl: Declaration) -> ParseResult<TokenStream> {
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,
Expand Down
1 change: 1 addition & 0 deletions godot/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
3 changes: 3 additions & 0 deletions godot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,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::*;

Expand Down

0 comments on commit 373bcea

Please sign in to comment.