diff --git a/.github/workflows/full-ci.yml b/.github/workflows/full-ci.yml index 71aa01eea..436dd1f8c 100644 --- a/.github/workflows/full-ci.yml +++ b/.github/workflows/full-ci.yml @@ -213,6 +213,24 @@ jobs: - name: "Test" run: cargo test -p godot-cell --features="proptest" + # Verifies that the library compiles for wasm32, which is a 32-bit target. + # This catches regressions in 32-bit support (e.g., pointer width assumptions). + # See https://github.com/godot-rust/gdext/issues/347. + wasm-check: + name: wasm-check + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: "Install Rust" + uses: ./.github/composite/rust + + - name: "Add wasm target" + run: rustup target add wasm32-unknown-emscripten + + - name: "Check wasm32 compilation (32-bit target)" + run: cargo check --target wasm32-unknown-emscripten -p godot --features experimental-wasm,experimental-wasm-nothreads + # For complex matrix workflow, see https://stackoverflow.com/a/65434401 godot-itest: name: godot-itest (${{ matrix.name }}) @@ -493,6 +511,7 @@ jobs: - unit-test - miri-test - proptest + - wasm-check - godot-itest - cargo-deny-machete - license-guard diff --git a/godot-bindings/src/header_gen.rs b/godot-bindings/src/header_gen.rs index d999d383e..3b639c6a0 100644 --- a/godot-bindings/src/header_gen.rs +++ b/godot-bindings/src/header_gen.rs @@ -8,6 +8,31 @@ use std::env; use std::path::Path; +/// Target pointer width for binding generation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TargetPointerWidth { + /// 32-bit target (e.g., wasm32, i686) + Bits32, + /// 64-bit target (e.g., x86_64, aarch64) + Bits64, +} + +impl TargetPointerWidth { + /// Returns the clang target triple for this pointer width. + fn clang_target(&self) -> &'static str { + match self { + // Use wasm32-unknown-emscripten as the 32-bit target since that's the primary + // 32-bit platform supported by Godot/gdext. + TargetPointerWidth::Bits32 => "wasm32-unknown-emscripten", + TargetPointerWidth::Bits64 => "x86_64-unknown-linux-gnu", + } + } +} + +/// Generate Rust bindings from a C header file. +/// +/// This is the standard function that determines whether to enable layout tests +/// based on cross-compilation detection (host vs target pointer width). pub(crate) fn generate_rust_binding(in_h_path: &Path, out_rs_path: &Path) { let c_header_path = in_h_path.display().to_string(); @@ -23,10 +48,22 @@ pub(crate) fn generate_rust_binding(in_h_path: &Path, out_rs_path: &Path) { // If you have an idea to address this without too invasive changes, please comment on that issue. let cargo_cfg = bindgen::CargoCallbacks::new().rerun_on_header_files(false); + // Only disable layout tests when cross-compiling between different pointer widths. + // Layout tests are generated based on the host architecture but validated on the target, + // which causes failures when cross-compiling (e.g., from 64-bit host to 32-bit target) + // because struct sizes differ. See: https://github.com/godot-rust/gdext/issues/347. + let host_pointer_width = std::mem::size_of::() * 8; + let target_pointer_width: usize = env::var("CARGO_CFG_TARGET_POINTER_WIDTH") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(host_pointer_width); + let enable_layout_tests = host_pointer_width == target_pointer_width; + let builder = bindgen::Builder::default() .header(c_header_path) .parse_callbacks(Box::new(cargo_cfg)) .prepend_enum_name(false) + .layout_tests(enable_layout_tests) // Bindgen can generate wrong size checks for types defined as `__attribute__((aligned(__alignof__(struct {...}))))`, // which is how clang defines max_align_t: https://clang.llvm.org/doxygen/____stddef__max__align__t_8h_source.html. // Size checks seems to be fine on all the targets but `wasm32-unknown-emscripten`, disallowing web builds. @@ -63,6 +100,68 @@ pub(crate) fn generate_rust_binding(in_h_path: &Path, out_rs_path: &Path) { }); } +/// Generate Rust bindings from a C header file for a specific target pointer width. +/// +/// This function is intended for use by the prebuilt artifact generator (godot4-prebuilt) +/// to generate bindings for both 32-bit and 64-bit targets from a single host machine. +/// +/// Unlike [`generate_rust_binding`], this function: +/// - Explicitly targets a specific pointer width via clang's `--target` flag +/// - Always enables layout tests (since the target is explicitly specified) +/// +/// # Arguments +/// * `in_h_path` - Path to the input C header file +/// * `out_rs_path` - Path where the generated Rust bindings will be written +/// * `target_width` - The target pointer width to generate bindings for +pub fn generate_rust_binding_for_target( + in_h_path: &Path, + out_rs_path: &Path, + target_width: TargetPointerWidth, +) { + let c_header_path = in_h_path.display().to_string(); + + // We don't need cargo rerun-if-changed since this is for prebuilt generation. + let cargo_cfg = bindgen::CargoCallbacks::new().rerun_on_header_files(false); + + let builder = bindgen::Builder::default() + .header(c_header_path) + .parse_callbacks(Box::new(cargo_cfg)) + .prepend_enum_name(false) + // Enable layout tests - they will be valid for the specified target. + .layout_tests(true) + // Blocklist max_align_t due to bindgen issues. + // See: https://github.com/rust-lang/rust-bindgen/issues/3295. + .blocklist_type("max_align_t") + // Target the specified architecture for correct pointer sizes. + .clang_arg(format!("--target={}", target_width.clang_target())); + + std::fs::create_dir_all( + out_rs_path + .parent() + .expect("bindgen output file has parent dir"), + ) + .expect("create bindgen output dir"); + + let bindings = builder.generate().unwrap_or_else(|err| { + panic!( + "bindgen generate failed\n c: {}\n rs: {}\n target: {:?}\n err: {}\n", + in_h_path.display(), + out_rs_path.display(), + target_width, + err + ) + }); + + bindings.write_to_file(out_rs_path).unwrap_or_else(|err| { + panic!( + "bindgen write failed\n c: {}\n rs: {}\n err: {}\n", + in_h_path.display(), + out_rs_path.display(), + err + ) + }); +} + //#[cfg(target_os = "macos")] fn configure_platform_specific(builder: bindgen::Builder) -> bindgen::Builder { // On macOS arm64 architecture, we currently get the following error. Tried using different LLVM versions. @@ -128,3 +227,52 @@ fn apple_include_path() -> Result { println!("cargo:rerun-if-changed={}", path.display()); } }*/ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_target_pointer_width_clang_targets() { + // Verify 32-bit target produces wasm32 triple (primary 32-bit target for Godot) + assert_eq!( + TargetPointerWidth::Bits32.clang_target(), + "wasm32-unknown-emscripten" + ); + + // Verify 64-bit target produces x86_64 triple + assert_eq!( + TargetPointerWidth::Bits64.clang_target(), + "x86_64-unknown-linux-gnu" + ); + } + + #[test] + fn test_target_pointer_width_equality() { + // Test PartialEq derive + assert_eq!(TargetPointerWidth::Bits32, TargetPointerWidth::Bits32); + assert_eq!(TargetPointerWidth::Bits64, TargetPointerWidth::Bits64); + assert_ne!(TargetPointerWidth::Bits32, TargetPointerWidth::Bits64); + } + + #[test] + fn test_target_pointer_width_clone_copy() { + // Test Clone and Copy derives + let width = TargetPointerWidth::Bits64; + let cloned = width.clone(); + let copied = width; // Copy + + assert_eq!(width, cloned); + assert_eq!(width, copied); + } + + #[test] + fn test_target_pointer_width_debug() { + // Test Debug derive produces meaningful output + let debug_32 = format!("{:?}", TargetPointerWidth::Bits32); + let debug_64 = format!("{:?}", TargetPointerWidth::Bits64); + + assert!(debug_32.contains("Bits32") || debug_32.contains("32")); + assert!(debug_64.contains("Bits64") || debug_64.contains("64")); + } +} diff --git a/godot-bindings/src/lib.rs b/godot-bindings/src/lib.rs index 464fd3983..04b187bdb 100644 --- a/godot-bindings/src/lib.rs +++ b/godot-bindings/src/lib.rs @@ -69,6 +69,38 @@ mod depend_on_custom { godot_exe::write_gdextension_headers(h_path, rs_path, true, watch); } + // Re-export types for prebuilt artifact generation. + #[cfg(feature = "api-custom-extheader")] + pub use header_gen::TargetPointerWidth; + + /// Generate Rust bindings for a specific target pointer width. + /// + /// This function is intended for the prebuilt artifact generator (godot4-prebuilt) + /// to produce bindings for both 32-bit and 64-bit architectures from a single host. + /// + /// # Example (in godot4-prebuilt generator) + /// ```ignore + /// use godot_bindings::{write_gdextension_headers_for_target, TargetPointerWidth}; + /// + /// // Generate 64-bit bindings + /// write_gdextension_headers_for_target(h_path, rs_64_path, TargetPointerWidth::Bits64); + /// + /// // Generate 32-bit bindings + /// write_gdextension_headers_for_target(h_path, rs_32_path, TargetPointerWidth::Bits32); + /// ``` + #[cfg(feature = "api-custom-extheader")] + pub fn write_gdextension_headers_for_target( + h_path: &Path, + rs_path: &Path, + target_width: header_gen::TargetPointerWidth, + ) { + // Patch the C header first (same as write_gdextension_headers_from_c). + godot_exe::patch_c_header(h_path, h_path); + + // Generate bindings for the specified target. + header_gen::generate_rust_binding_for_target(h_path, rs_path, target_width); + } + pub(crate) fn get_godot_version() -> GodotVersion { godot_exe::read_godot_version(&godot_exe::locate_godot_binary()) } @@ -123,13 +155,22 @@ mod depend_on_prebuilt { } pub fn write_gdextension_headers(h_path: &Path, rs_path: &Path, watch: &mut StopWatch) { - // Note: prebuilt artifacts just return a static str. let h_contents = prebuilt::load_gdextension_header_h(); std::fs::write(h_path, h_contents.as_ref()) .unwrap_or_else(|e| panic!("failed to write gdextension_interface.h: {e}")); watch.record("write_header_h"); - let rs_contents = prebuilt::load_gdextension_header_rs(); + // Use CARGO_CFG_TARGET_POINTER_WIDTH to select the correct bindings for cross-compilation. + // This is set by Cargo to the target's pointer width, not the host's. + let target_pointer_width = + std::env::var("CARGO_CFG_TARGET_POINTER_WIDTH").unwrap_or_else(|_| "64".to_string()); + + let rs_contents = if target_pointer_width == "32" { + prebuilt::load_gdextension_header_rs_32() + } else { + prebuilt::load_gdextension_header_rs_64() + }; + std::fs::write(rs_path, rs_contents.as_ref()) .unwrap_or_else(|e| panic!("failed to write gdextension_interface.rs: {e}")); watch.record("write_header_rs"); diff --git a/itest/rust/build.rs b/itest/rust/build.rs index 40a5f6268..c1dd2e9b6 100644 --- a/itest/rust/build.rs +++ b/itest/rust/build.rs @@ -369,7 +369,7 @@ fn generate_rust_methods(inputs: &[Input]) -> Vec { .collect::>(); let manual_methods = quote! { - #[allow(clippy::suspicious_else_formatting)] // `quote!` might output whole file as one big line. + #[allow(clippy::suspicious_else_formatting, clippy::possible_missing_else)] // `quote!` might output whole file as one big line. #[func] fn check_last_notrace(last_method_name: String, expected_callconv: String) -> bool { let last = godot::private::trace::pop();