Skip to content
Closed
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
19 changes: 19 additions & 0 deletions .github/workflows/full-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }})
Expand Down Expand Up @@ -493,6 +511,7 @@ jobs:
- unit-test
- miri-test
- proptest
- wasm-check
- godot-itest
- cargo-deny-machete
- license-guard
Expand Down
148 changes: 148 additions & 0 deletions godot-bindings/src/header_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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::<usize>() * 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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -128,3 +227,52 @@ fn apple_include_path() -> Result<String, std::io::Error> {
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"));
}
}
45 changes: 43 additions & 2 deletions godot-bindings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down Expand Up @@ -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");
Expand Down
2 changes: 1 addition & 1 deletion itest/rust/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ fn generate_rust_methods(inputs: &[Input]) -> Vec<TokenStream> {
.collect::<Vec<_>>();

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();
Expand Down