diff --git a/.github/workflows/build-nightly.yml b/.github/workflows/build-nightly.yml index 248a741..208dabc 100644 --- a/.github/workflows/build-nightly.yml +++ b/.github/workflows/build-nightly.yml @@ -125,13 +125,21 @@ jobs: working-directory: generator run: cargo build -vv + - name: "Run generator tests" + working-directory: generator + run: cargo test + - name: "Move generated files + rename according to platform" env: BUILD_CONFIG: linux run: | - mv .generated/* output/ - mv output/gdextension_interface.rs output/gdextension_interface_${BUILD_CONFIG}.rs - rm output/*stats* + mv .generated/gdextension_interface.h output/ + # Rename 64-bit and 32-bit bindings with platform prefix + mv .generated/gdextension_interface_64.rs output/gdextension_interface_${BUILD_CONFIG}_64.rs + mv .generated/gdextension_interface_32.rs output/gdextension_interface_${BUILD_CONFIG}_32.rs + rm -f .generated/*stats* output/*stats* + echo "Output directory:" + ls -la output/ # ----------------------------------------------------------------------------------------------------------------------------------------- # JSON spec diff --git a/generator/build.rs b/generator/build.rs index 4fecfaf..4d52ef9 100644 --- a/generator/build.rs +++ b/generator/build.rs @@ -14,16 +14,36 @@ use std::path::Path; -fn main() { - let mut watch = godot_bindings::StopWatch::start(); +use godot_bindings::TargetPointerWidth; +fn main() { let gen_path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../.generated")); let header_c_path = gen_path.join("gdextension_interface.h"); - let header_rs_path = gen_path.join("gdextension_interface.rs"); + + // Generate bindings for both 64-bit and 32-bit targets. + // This allows proper layout tests on both architectures without runtime stripping. + // See: https://github.com/godot-rust/gdext/issues/347 + + // Output paths - CI workflow will rename these with platform prefix (e.g., linux_64, linux_32). + // Windows and macOS only need 64-bit; Linux/wasm needs both. + let header_rs_64_path = gen_path.join("gdextension_interface_64.rs"); + let header_rs_32_path = gen_path.join("gdextension_interface_32.rs"); // Note: do not call clear_dir(), as we pass in the C header into the gen directory. - //godot_bindings::clear_dir(gen_path, &mut watch); - godot_bindings::write_gdextension_headers_from_c(&header_c_path, &header_rs_path, &mut watch); - watch.write_stats_to(&gen_path.join("godot-ffi-stats.txt")); + // Generate 64-bit bindings. + godot_bindings::write_gdextension_headers_for_target( + &header_c_path, + &header_rs_64_path, + TargetPointerWidth::Bits64, + ); + println!("Generated 64-bit bindings: {}", header_rs_64_path.display()); + + // Generate 32-bit bindings. + godot_bindings::write_gdextension_headers_for_target( + &header_c_path, + &header_rs_32_path, + TargetPointerWidth::Bits32, + ); + println!("Generated 32-bit bindings: {}", header_rs_32_path.display()); } diff --git a/generator/src/lib.rs b/generator/src/lib.rs index de9c0de..c7b918a 100644 --- a/generator/src/lib.rs +++ b/generator/src/lib.rs @@ -6,6 +6,11 @@ */ // Output of generated code. Mimics the file structure, symbols are re-exported. +// We now generate separate bindings for 64-bit and 32-bit targets (Linux/wasm only for 32-bit). +// See: https://github.com/godot-rust/gdext/issues/347 + +// The generator runs on 64-bit Linux and produces both _64.rs and _32.rs files. +// For local builds, we select based on host pointer width. #[rustfmt::skip] #[allow( dead_code, @@ -15,7 +20,21 @@ non_upper_case_globals, clippy::redundant_static_lifetimes, )] -#[path = "../../.generated/gdextension_interface.rs"] +#[cfg(target_pointer_width = "64")] +#[path = "../../.generated/gdextension_interface_64.rs"] +mod gdextension_interface; + +#[rustfmt::skip] +#[allow( + dead_code, + deref_nullptr, + non_camel_case_types, + non_snake_case, + non_upper_case_globals, + clippy::redundant_static_lifetimes, +)] +#[cfg(target_pointer_width = "32")] +#[path = "../../.generated/gdextension_interface_32.rs"] mod gdextension_interface; @@ -27,3 +46,57 @@ trait Distinct {} impl Distinct for gdextension_interface::GDExtensionVariantPtr {} impl Distinct for gdextension_interface::GDExtensionTypePtr {} impl Distinct for gdextension_interface::GDExtensionConstTypePtr {} + +#[cfg(test)] +mod tests { + use super::gdextension_interface::*; + + /// Verify that key FFI types are exported and have the expected properties. + #[test] + fn test_ffi_types_exist() { + // These types should exist in the generated bindings + let _: GDExtensionVariantPtr; + let _: GDExtensionTypePtr; + let _: GDExtensionConstTypePtr; + let _: GDExtensionBool; + let _: GDExtensionInt; + } + + /// Verify pointer types are distinct (type safety through patch). + #[test] + fn test_pointer_types_are_distinct() { + use std::any::TypeId; + + // These should be different types (ensured by the Distinct trait above, + // but we also verify with TypeId for good measure) + let variant_ptr_id = TypeId::of::(); + let type_ptr_id = TypeId::of::(); + let const_type_ptr_id = TypeId::of::(); + + assert_ne!(variant_ptr_id, type_ptr_id, "VariantPtr and TypePtr should be distinct"); + assert_ne!(variant_ptr_id, const_type_ptr_id, "VariantPtr and ConstTypePtr should be distinct"); + assert_ne!(type_ptr_id, const_type_ptr_id, "TypePtr and ConstTypePtr should be distinct"); + } + + /// Verify that GDExtensionBool has the expected size (1 byte / u8). + #[test] + fn test_gdextension_bool_size() { + assert_eq!( + std::mem::size_of::(), + 1, + "GDExtensionBool should be 1 byte" + ); + } + + /// Verify that GDExtensionInt has pointer-width size (platform-dependent). + #[test] + fn test_gdextension_int_size() { + // GDExtensionInt is int64_t on 64-bit and int32_t on 32-bit (pointer-sized) + let expected_size = std::mem::size_of::(); + assert_eq!( + std::mem::size_of::(), + expected_size, + "GDExtensionInt should be pointer-sized" + ); + } +} diff --git a/prebuilt-template/src/lib.rs b/prebuilt-template/src/lib.rs index 6de13c8..39d62d1 100644 --- a/prebuilt-template/src/lib.rs +++ b/prebuilt-template/src/lib.rs @@ -26,8 +26,60 @@ pub const fn load_gdextension_header_h() -> CowStr { CowStr::Borrowed(include_str!("../res/gdextension_interface.h")) } -/// Returns the contents of the header file `gdextension_interface.rs`, generated for the corresponding platform. +/// Returns the contents of the header file `gdextension_interface.rs`, generated for the corresponding platform and pointer width. +/// +/// The bindings are now generated separately for 32-bit and 64-bit targets to ensure +/// correct layout tests on both architectures. See: https://github.com/godot-rust/gdext/issues/347 +/// +/// **Important**: This function uses compile-time `#[cfg]` to select bindings, which means it +/// evaluates based on the *current compilation target*, not the cross-compilation target. +/// For cross-compilation scenarios (e.g., build scripts), use [`load_gdextension_header_rs_32`] +/// or [`load_gdextension_header_rs_64`] with `CARGO_CFG_TARGET_POINTER_WIDTH` instead. +// Kept for backward compatibility. New code should use load_gdextension_header_rs_32() or _64(). +#[deprecated( + since = "0.3.1", + note = "Use load_gdextension_header_rs_32() or load_gdextension_header_rs_64() with CARGO_CFG_TARGET_POINTER_WIDTH for cross-compilation support" +)] pub const fn load_gdextension_header_rs() -> CowStr { + // 64-bit platforms: use platform-specific bindings + #[cfg(all(windows, target_pointer_width = "64"))] + let s = include_str!("../res/gdextension_interface_windows.rs"); + + #[cfg(all(target_os = "macos", target_pointer_width = "64"))] + let s = include_str!("../res/gdextension_interface_macos.rs"); + + #[cfg(all(unix, not(target_os = "macos"), target_pointer_width = "64"))] + let s = include_str!("../res/gdextension_interface_linux_64.rs"); + + // 32-bit platforms: all use the same bindings (Linux, wasm32, etc.) + // + // Note: with this, in practce, we only support wasm32 and linux i686. + // Godot *has* support for other 32-bit platforms. However + // maintaining and offering *tested* exports for corner cases such + // as win32 is quite costly. Wasm32 is a real-world use case though. + #[cfg(target_pointer_width = "32")] + let s = include_str!("../res/gdextension_interface_linux_32.rs"); + + CowStr::Borrowed(s) +} + +/// Returns the 32-bit bindings, regardless of current compilation target. +/// +/// Use this in build scripts with `CARGO_CFG_TARGET_POINTER_WIDTH` to select +/// the correct bindings for cross-compilation. +pub const fn load_gdextension_header_rs_32() -> CowStr { + CowStr::Borrowed(include_str!("../res/gdextension_interface_linux_32.rs")) +} + +/// Returns the 64-bit bindings for the current platform. +/// +/// Use this in build scripts with `CARGO_CFG_TARGET_POINTER_WIDTH` to select +/// the correct bindings for cross-compilation. +/// +/// Note: Returns platform-specific bindings (Windows/macOS/Linux) based on compile-time cfg. +/// For cross-compilation to a different OS, this may not be accurate, but struct layouts +/// are identical across 64-bit platforms. +pub const fn load_gdextension_header_rs_64() -> CowStr { #[cfg(windows)] let s = include_str!("../res/gdextension_interface_windows.rs"); @@ -35,7 +87,11 @@ pub const fn load_gdextension_header_rs() -> CowStr { let s = include_str!("../res/gdextension_interface_macos.rs"); #[cfg(all(unix, not(target_os = "macos")))] - let s = include_str!("../res/gdextension_interface_linux.rs"); + let s = include_str!("../res/gdextension_interface_linux_64.rs"); + + // Fallback for non-unix, non-windows, non-macos (unlikely but safe) + #[cfg(not(any(windows, unix)))] + let s = include_str!("../res/gdextension_interface_linux_64.rs"); CowStr::Borrowed(s) }