Skip to content

Commit

Permalink
Merge pull request #493 from zecozephyr/wasm-mvp-for-merge
Browse files Browse the repository at this point in the history
Enable experimental wasm support
  • Loading branch information
Bromeon authored Nov 25, 2023
2 parents bc01b35 + 816c232 commit 9cca3e0
Show file tree
Hide file tree
Showing 11 changed files with 114 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
8 changes: 8 additions & 0 deletions godot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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::*;

Expand Down

0 comments on commit 9cca3e0

Please sign in to comment.