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

Check runtime and compiled Godot versions for compatibility #512

Merged
merged 1 commit into from
Nov 30, 2023
Merged
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
116 changes: 62 additions & 54 deletions godot-ffi/src/compat/compat_4_1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,9 @@ use crate::compat::BindingCompat;

pub type InitCompat = sys::GDExtensionInterfaceGetProcAddress;

#[cfg(not(target_family = "wasm"))]
#[repr(C)]
struct LegacyLayout {
version_major: u32,
version_minor: u32,
version_patch: u32,
version_string: *const std::ffi::c_char,
}

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.
// In WebAssembly, 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) won't work.
#[cfg(target_family = "wasm")]
fn ensure_static_runtime_compatibility(&self) {}

Expand Down Expand Up @@ -56,54 +46,59 @@ impl BindingCompat for sys::GDExtensionInterfaceGetProcAddress {
// As a result, we can try to interpret the function pointer as a legacy GDExtensionInterface data pointer and check if the
// first fields have values version_major=4 and version_minor=0. This might be deep in UB territory, but the alternative is
// to not be able to detect Godot 4.0.x at all, and run into UB anyway.

let get_proc_address = self.expect("get_proc_address unexpectedly null");
let data_ptr = get_proc_address as *const LegacyLayout; // crowbar it via `as` cast

// Assumption is that we have at least 8 bytes of memory to safely read from (for both the data and the function case).
let major = unsafe { data_ptr.read().version_major };
let minor = unsafe { data_ptr.read().version_minor };
let patch = unsafe { data_ptr.read().version_patch };

if major != 4 || minor != 0 {
// Technically, major should always be 4; loading Godot 3 will crash anyway.
return;
let static_version_str = crate::GdextBuild::godot_static_version_string();

// Strictly speaking, this is NOT the type GDExtensionGodotVersion but a 4.0 legacy version of it. They have the exact same
// layout, and due to GDExtension's compatibility promise, the 4.1+ struct won't change; so we can reuse the type.
// We thus read u32 pointers (field by field).
let data_ptr = get_proc_address as *const u32; // crowbar it via `as` cast

// SAFETY: borderline UB, but on Desktop systems, we should be able to reinterpret function pointers as data.
// On 64-bit systems, a function pointer is typically 8 bytes long, meaning we can interpret 8 bytes of it.
// On 32-bit systems, we can only read the first 4 bytes safely. If that happens to have value 4 (exceedingly unlikely for
// a function pointer), it's likely that it's the actual version and we run 4.0.x. In that case, read 4 more bytes.
let major = unsafe { data_ptr.read() };
if major == 4 {
// SAFETY: see above.
let minor = unsafe { data_ptr.offset(1).read() };
if minor == 0 {
// SAFETY: at this point it's reasonably safe to say that we are indeed dealing with that version struct; read the whole.
let data_ptr = get_proc_address as *const sys::GDExtensionGodotVersion;
let runtime_version_str = unsafe { read_version_string(&data_ptr.read()) };

panic!(
"gdext was compiled against a newer Godot version: {static_version_str}\n\
but loaded by legacy Godot binary, with version: {runtime_version_str}\n\
\n\
Update your Godot engine version, or read https://godot-rust.github.io/book/toolchain/compatibility.html.\n\
\n"
);
}
}

let static_version = crate::GdextBuild::godot_static_version_string();
let runtime_version = unsafe {
let char_ptr = data_ptr.read().version_string;
let c_str = std::ffi::CStr::from_ptr(char_ptr);

String::from_utf8_lossy(c_str.to_bytes())
.as_ref()
.strip_prefix("Godot Engine ")
.unwrap_or(&String::from_utf8_lossy(c_str.to_bytes()))
.to_string()
};

// Version 4.0.999 is used to signal that we're running Godot 4.1+ but loading extensions in legacy mode.
if patch == 999 {
// Godot 4.1+ loading the extension in legacy mode.
// Note: this can not happen as of June 2023 anymore, because Godot disallows loading 4.0 extensions now.
// TODO(bromeon): a while after 4.1 release, remove this branch.
//
// Instead of panicking, we could *theoretically* fall back to the legacy API at runtime, but then gdext would need to
// always ship two versions of gdextension_interface.h (+ generated code) and would encourage use of the legacy API.
panic!(
"gdext was compiled against a modern Godot version ({static_version}), but loaded in legacy (4.0.x) mode.\n\
In your .gdextension file, add `compatibility_minimum = 4.1` under the [configuration] section.\n"
)
} else {
// Truly a Godot 4.0 version.
// From here we can assume Godot 4.1+. We need to make sure that the runtime version is >= static version.
// Lexicographical tuple comparison does that.
let static_version = crate::GdextBuild::godot_static_version_triple();
let runtime_version_raw = self.runtime_version();

// SAFETY: Godot provides this version struct.
let runtime_version = (
runtime_version_raw.major as u8,
runtime_version_raw.minor as u8,
runtime_version_raw.patch as u8,
);

if runtime_version < static_version {
let runtime_version_str = read_version_string(&runtime_version_raw);

panic!(
"gdext was compiled against a newer Godot version ({static_version}),\n\
but loaded by a legacy Godot binary ({runtime_version}).\n\
\n\
Update your Godot engine version.\n\
"gdext was compiled against newer Godot version: {static_version_str}\n\
but loaded by older Godot binary, with version: {runtime_version_str}\n\
\n\
(If you _really_ need an older Godot version, recompile your Rust extension against that one\
(see `custom-godot` feature). However, that setup will not be supported for a long time.\n\
Update your Godot engine version, or compile gdext against an older version.\n\
For more information, read https://godot-rust.github.io/book/toolchain/compatibility.html.\n\
\n"
);
}
Expand All @@ -127,3 +122,16 @@ impl BindingCompat for sys::GDExtensionInterfaceGetProcAddress {
unsafe { sys::GDExtensionInterface::load(*self) }
}
}

fn read_version_string(version_ptr: &sys::GDExtensionGodotVersion) -> String {
let char_ptr = version_ptr.string;

// SAFETY: `version_ptr` points to a layout-compatible version struct.
let c_str = unsafe { std::ffi::CStr::from_ptr(char_ptr) };

String::from_utf8_lossy(c_str.to_bytes())
.as_ref()
.strip_prefix("Godot Engine ")
.unwrap_or(&String::from_utf8_lossy(c_str.to_bytes()))
.to_string()
}