Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable experimental wasm support #493

Merged
merged 1 commit into from
Nov 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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