Skip to content

Commit 0b164ab

Browse files
authored
Merge pull request #512 from godot-rust/qol/check-godot-version
Check runtime and compiled Godot versions for compatibility
2 parents e9177f2 + 5c9f898 commit 0b164ab

File tree

1 file changed

+62
-54
lines changed

1 file changed

+62
-54
lines changed

godot-ffi/src/compat/compat_4_1.rs

Lines changed: 62 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,9 @@ use crate::compat::BindingCompat;
1616

1717
pub type InitCompat = sys::GDExtensionInterfaceGetProcAddress;
1818

19-
#[cfg(not(target_family = "wasm"))]
20-
#[repr(C)]
21-
struct LegacyLayout {
22-
version_major: u32,
23-
version_minor: u32,
24-
version_patch: u32,
25-
version_string: *const std::ffi::c_char,
26-
}
27-
2819
impl BindingCompat for sys::GDExtensionInterfaceGetProcAddress {
29-
// Fundamentally in wasm function references and data pointers live in different memory
30-
// spaces so trying to read the "memory" at a function pointer (an index into a table) to
31-
// heuristically determine which API we have (as is done below) is not quite going to work.
20+
// In WebAssembly, function references and data pointers live in different memory spaces, so trying to read the "memory"
21+
// at a function pointer (an index into a table) to heuristically determine which API we have (as is done below) won't work.
3222
#[cfg(target_family = "wasm")]
3323
fn ensure_static_runtime_compatibility(&self) {}
3424

@@ -56,54 +46,59 @@ impl BindingCompat for sys::GDExtensionInterfaceGetProcAddress {
5646
// As a result, we can try to interpret the function pointer as a legacy GDExtensionInterface data pointer and check if the
5747
// first fields have values version_major=4 and version_minor=0. This might be deep in UB territory, but the alternative is
5848
// to not be able to detect Godot 4.0.x at all, and run into UB anyway.
59-
6049
let get_proc_address = self.expect("get_proc_address unexpectedly null");
61-
let data_ptr = get_proc_address as *const LegacyLayout; // crowbar it via `as` cast
62-
63-
// Assumption is that we have at least 8 bytes of memory to safely read from (for both the data and the function case).
64-
let major = unsafe { data_ptr.read().version_major };
65-
let minor = unsafe { data_ptr.read().version_minor };
66-
let patch = unsafe { data_ptr.read().version_patch };
6750

68-
if major != 4 || minor != 0 {
69-
// Technically, major should always be 4; loading Godot 3 will crash anyway.
70-
return;
51+
let static_version_str = crate::GdextBuild::godot_static_version_string();
52+
53+
// Strictly speaking, this is NOT the type GDExtensionGodotVersion but a 4.0 legacy version of it. They have the exact same
54+
// layout, and due to GDExtension's compatibility promise, the 4.1+ struct won't change; so we can reuse the type.
55+
// We thus read u32 pointers (field by field).
56+
let data_ptr = get_proc_address as *const u32; // crowbar it via `as` cast
57+
58+
// SAFETY: borderline UB, but on Desktop systems, we should be able to reinterpret function pointers as data.
59+
// On 64-bit systems, a function pointer is typically 8 bytes long, meaning we can interpret 8 bytes of it.
60+
// On 32-bit systems, we can only read the first 4 bytes safely. If that happens to have value 4 (exceedingly unlikely for
61+
// 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.
62+
let major = unsafe { data_ptr.read() };
63+
if major == 4 {
64+
// SAFETY: see above.
65+
let minor = unsafe { data_ptr.offset(1).read() };
66+
if minor == 0 {
67+
// SAFETY: at this point it's reasonably safe to say that we are indeed dealing with that version struct; read the whole.
68+
let data_ptr = get_proc_address as *const sys::GDExtensionGodotVersion;
69+
let runtime_version_str = unsafe { read_version_string(&data_ptr.read()) };
70+
71+
panic!(
72+
"gdext was compiled against a newer Godot version: {static_version_str}\n\
73+
but loaded by legacy Godot binary, with version: {runtime_version_str}\n\
74+
\n\
75+
Update your Godot engine version, or read https://godot-rust.github.io/book/toolchain/compatibility.html.\n\
76+
\n"
77+
);
78+
}
7179
}
7280

73-
let static_version = crate::GdextBuild::godot_static_version_string();
74-
let runtime_version = unsafe {
75-
let char_ptr = data_ptr.read().version_string;
76-
let c_str = std::ffi::CStr::from_ptr(char_ptr);
77-
78-
String::from_utf8_lossy(c_str.to_bytes())
79-
.as_ref()
80-
.strip_prefix("Godot Engine ")
81-
.unwrap_or(&String::from_utf8_lossy(c_str.to_bytes()))
82-
.to_string()
83-
};
84-
85-
// Version 4.0.999 is used to signal that we're running Godot 4.1+ but loading extensions in legacy mode.
86-
if patch == 999 {
87-
// Godot 4.1+ loading the extension in legacy mode.
88-
// Note: this can not happen as of June 2023 anymore, because Godot disallows loading 4.0 extensions now.
89-
// TODO(bromeon): a while after 4.1 release, remove this branch.
90-
//
91-
// Instead of panicking, we could *theoretically* fall back to the legacy API at runtime, but then gdext would need to
92-
// always ship two versions of gdextension_interface.h (+ generated code) and would encourage use of the legacy API.
93-
panic!(
94-
"gdext was compiled against a modern Godot version ({static_version}), but loaded in legacy (4.0.x) mode.\n\
95-
In your .gdextension file, add `compatibility_minimum = 4.1` under the [configuration] section.\n"
96-
)
97-
} else {
98-
// Truly a Godot 4.0 version.
81+
// From here we can assume Godot 4.1+. We need to make sure that the runtime version is >= static version.
82+
// Lexicographical tuple comparison does that.
83+
let static_version = crate::GdextBuild::godot_static_version_triple();
84+
let runtime_version_raw = self.runtime_version();
85+
86+
// SAFETY: Godot provides this version struct.
87+
let runtime_version = (
88+
runtime_version_raw.major as u8,
89+
runtime_version_raw.minor as u8,
90+
runtime_version_raw.patch as u8,
91+
);
92+
93+
if runtime_version < static_version {
94+
let runtime_version_str = read_version_string(&runtime_version_raw);
95+
9996
panic!(
100-
"gdext was compiled against a newer Godot version ({static_version}),\n\
101-
but loaded by a legacy Godot binary ({runtime_version}).\n\
102-
\n\
103-
Update your Godot engine version.\n\
97+
"gdext was compiled against newer Godot version: {static_version_str}\n\
98+
but loaded by older Godot binary, with version: {runtime_version_str}\n\
10499
\n\
105-
(If you _really_ need an older Godot version, recompile your Rust extension against that one\
106-
(see `custom-godot` feature). However, that setup will not be supported for a long time.\n\
100+
Update your Godot engine version, or compile gdext against an older version.\n\
101+
For more information, read https://godot-rust.github.io/book/toolchain/compatibility.html.\n\
107102
\n"
108103
);
109104
}
@@ -127,3 +122,16 @@ impl BindingCompat for sys::GDExtensionInterfaceGetProcAddress {
127122
unsafe { sys::GDExtensionInterface::load(*self) }
128123
}
129124
}
125+
126+
fn read_version_string(version_ptr: &sys::GDExtensionGodotVersion) -> String {
127+
let char_ptr = version_ptr.string;
128+
129+
// SAFETY: `version_ptr` points to a layout-compatible version struct.
130+
let c_str = unsafe { std::ffi::CStr::from_ptr(char_ptr) };
131+
132+
String::from_utf8_lossy(c_str.to_bytes())
133+
.as_ref()
134+
.strip_prefix("Godot Engine ")
135+
.unwrap_or(&String::from_utf8_lossy(c_str.to_bytes()))
136+
.to_string()
137+
}

0 commit comments

Comments
 (0)