@@ -16,19 +16,9 @@ use crate::compat::BindingCompat;
1616
1717pub 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-
2819impl 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