Skip to content

Commit 92dce9c

Browse files
authored
Merge pull request #542 from godot-rust/qol/better-double
Make `double-precision` feature more robust
2 parents 4ce4714 + c7000e5 commit 92dce9c

File tree

10 files changed

+161
-22
lines changed

10 files changed

+161
-22
lines changed

godot-codegen/src/central_generator.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ fn make_method_table(info: IndexedMethodTable) -> TokenStream {
463463
pub fn load() -> Self {
464464
// SAFETY: interface and lifecycle tables are initialized at this point, so we can get 'static references to them.
465465
let (interface, lifecycle_table) = unsafe {
466-
(crate::get_interface(), crate::method_table())
466+
(crate::get_interface(), crate::builtin_lifecycle_api())
467467
};
468468

469469
Self {

godot-codegen/src/interface_generator.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@ fn generate_proc_address_funcs(h_path: &Path) -> TokenStream {
7777

7878
// Do not derive Copy -- even though the struct is bitwise-copyable, this is rarely needed and may point to an error.
7979
let code = quote! {
80-
#[path = "../compat/compat_4_1.rs"]
81-
mod compat_4_1;
80+
#[path = "../compat/compat_4_1plus.rs"]
81+
mod compat_4_1plus;
8282

83-
pub use compat_4_1::InitCompat;
83+
pub use compat_4_1plus::InitCompat;
8484

8585
pub struct GDExtensionInterface {
8686
#( #fptr_decls )*

godot-codegen/src/special_cases.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,13 @@ fn is_class_experimental(class_name: &TyName) -> bool {
150150
/// Whether a method is available in the method table as a named accessor.
151151
#[rustfmt::skip]
152152
pub(crate) fn is_named_accessor_in_table(class_or_builtin_ty: &TyName, godot_method_name: &str) -> bool {
153+
// Hand-selected APIs.
154+
match (class_or_builtin_ty.godot_ty.as_str(), godot_method_name) {
155+
| ("OS", "has_feature")
156+
157+
=> return true, _ => {}
158+
}
159+
153160
// Generated methods made private are typically needed internally and exposed with a different API,
154161
// so make them accessible.
155162
is_method_private(class_or_builtin_ty, godot_method_name)

godot-core/src/engine/gfile.rs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,24 @@ impl GFile {
397397
/// See [`real`][type@real] type for more information.
398398
///
399399
/// Underlying Godot method:
400-
/// [`FileAccess::get_real`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-real).
400+
/// [`FileAccess::get_float`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-float) or
401+
/// [`FileAccess::get_double`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-double)
402+
/// (note that [`FileAccess::get_real`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-get-real)
403+
/// does not return an actual `real`).
404+
///
405+
/// <div class="warning">
406+
/// <strong>Warning:</strong>
407+
/// Since this involves a configuration-dependent type, you may not be able to read the value back if Godot uses different precision setting
408+
/// (single or double) than the one used to write the value.
409+
/// </div>
401410
#[doc(alias = "get_real")]
402411
pub fn read_real(&mut self) -> IoResult<real> {
403-
let val = self.fa.get_real();
412+
#[cfg(feature = "double-precision")]
413+
let val = self.fa.get_double();
414+
415+
#[cfg(not(feature = "double-precision"))]
416+
let val = self.fa.get_float();
417+
404418
self.check_error()?;
405419
Ok(val)
406420
}
@@ -410,7 +424,7 @@ impl GFile {
410424
/// If `allow_objects` is set to `true`, objects will be decoded.
411425
///
412426
/// <div class="warning">
413-
/// **Warning:** Deserialized objects can contain code which gets executed. Do not use this option if the serialized object
427+
/// <strong>Warning:</strong> Deserialized objects can contain code which gets executed. Do not use this option if the serialized object
414428
/// comes from untrusted sources, to avoid potential security threats such as remote code execution.
415429
/// </div>
416430
///
@@ -500,10 +514,26 @@ impl GFile {
500514
/// See [`real`][type@real] type for more information.
501515
///
502516
/// Underlying Godot method:
503-
/// [`FileAccess::store_real`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-real).
517+
/// [`FileAccess::store_float`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-float) or
518+
/// [`FileAccess::store_double`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-double)
519+
/// (note that [`FileAccess::store_real`](https://docs.godotengine.org/en/stable/classes/class_fileaccess.html#class-fileaccess-method-store-real)
520+
/// does not accept an actual `real`).
521+
///
522+
/// <div class="warning">
523+
/// <strong>Warning:</strong>
524+
/// Since this involves a configuration-dependent type, you may not be able to read the value back if Godot uses different precision setting
525+
/// (single or double) than the one used to write the value.
526+
/// </div>
504527
#[doc(alias = "store_real")]
505528
pub fn write_real(&mut self, value: real) -> IoResult<()> {
506-
self.fa.store_real(value);
529+
// FileAccess::store_real() does not accept an actual real_t; work around this.
530+
531+
#[cfg(feature = "double-precision")]
532+
self.fa.store_double(value);
533+
534+
#[cfg(not(feature = "double-precision"))]
535+
self.fa.store_float(value);
536+
507537
self.clear_file_length();
508538
self.check_error()?;
509539
Ok(())

godot-core/src/init/mod.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
use godot_ffi as sys;
99

1010
use std::cell;
11+
use sys::GodotFfi;
12+
13+
use crate::builtin::{GString, StringName};
14+
use crate::out;
1115

1216
pub use sys::GdextBuild;
1317

@@ -95,6 +99,7 @@ fn gdext_on_level_init(level: InitLevel) {
9599
}
96100
InitLevel::Scene => {
97101
sys::load_class_method_table(sys::ClassApiLevel::Scene);
102+
ensure_godot_features_compatible();
98103
}
99104
InitLevel::Editor => {
100105
sys::load_class_method_table(sys::ClassApiLevel::Editor);
@@ -244,3 +249,52 @@ impl InitLevel {
244249
}
245250
}
246251
}
252+
253+
// ----------------------------------------------------------------------------------------------------------------------------------------------
254+
255+
fn ensure_godot_features_compatible() {
256+
// The reason why we don't simply call Os::has_feature() here is that we might move the high-level engine classes out of godot-core
257+
// later, and godot-core would only depend on godot-sys. This makes future migrations easier. We still have access to builtins though.
258+
259+
out!("Check Godot precision setting...");
260+
261+
let os_class = StringName::from("OS");
262+
let single = GString::from("single");
263+
let double = GString::from("double");
264+
265+
// SAFETY: main thread, after initialize(), valid string pointers.
266+
let gdext_is_double = cfg!(feature = "double-precision");
267+
let godot_is_double = unsafe {
268+
let is_single = sys::godot_has_feature(os_class.string_sys(), single.sys_const());
269+
let is_double = sys::godot_has_feature(os_class.string_sys(), double.sys_const());
270+
271+
assert_ne!(
272+
is_single, is_double,
273+
"Godot has invalid configuration: single={is_single}, double={is_double}"
274+
);
275+
276+
is_double
277+
};
278+
279+
let s = |is_double: bool| -> &'static str {
280+
if is_double {
281+
"double"
282+
} else {
283+
"single"
284+
}
285+
};
286+
287+
out!(
288+
"Is double precision: Godot={}, gdext={}",
289+
s(godot_is_double),
290+
s(gdext_is_double)
291+
);
292+
293+
if godot_is_double != gdext_is_double {
294+
panic!(
295+
"Godot runs with {} precision, but gdext was compiled with {} precision.\n\
296+
Cargo feature `double-precision` must be used if and only if Godot is compiled with `precision=double`.\n",
297+
s(godot_is_double), s(gdext_is_double),
298+
);
299+
}
300+
}
File renamed without changes.

godot-ffi/src/lib.rs

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub(crate) mod gen {
3232
}
3333

3434
mod compat;
35-
mod gdextension_plus;
35+
mod extras;
3636
mod godot_ffi;
3737
mod opaque;
3838
mod plugins;
@@ -41,7 +41,6 @@ mod toolbox;
4141

4242
use compat::BindingCompat;
4343
use std::cell;
44-
use std::ffi::CStr;
4544

4645
// See https://github.com/dtolnay/paste/issues/69#issuecomment-962418430
4746
// and https://users.rust-lang.org/t/proc-macros-using-third-party-crate/42465/4
@@ -66,7 +65,7 @@ pub use gen::table_servers_classes::*;
6665
pub use gen::table_utilities::*;
6766

6867
// Other
69-
pub use gdextension_plus::*;
68+
pub use extras::*;
7069
pub use gen::central::*;
7170
pub use gen::gdextension_interface::*;
7271
pub use gen::interface::*;
@@ -188,12 +187,14 @@ pub unsafe fn initialize(
188187
out!("Loaded builtin method table (lazily).");
189188
}
190189

191-
println!(
192-
"Initialize GDExtension API for Rust: {}",
193-
CStr::from_ptr(version.string)
194-
.to_str()
195-
.expect("unknown Godot version")
196-
);
190+
print_preamble(version);
191+
}
192+
193+
fn print_preamble(version: GDExtensionGodotVersion) {
194+
let api_version: &'static str = GdextBuild::godot_static_version_string();
195+
let runtime_version = read_version_string(&version);
196+
197+
println!("Initialize godot-rust (API {api_version}, runtime {runtime_version})");
197198
}
198199

199200
/// # Safety
@@ -223,7 +224,7 @@ pub unsafe fn get_library() -> GDExtensionClassLibraryPtr {
223224
///
224225
/// The interface must have been initialised with [`initialize`] before calling this function.
225226
#[inline(always)]
226-
pub unsafe fn method_table() -> &'static BuiltinLifecycleTable {
227+
pub unsafe fn builtin_lifecycle_api() -> &'static BuiltinLifecycleTable {
227228
&unwrap_ref_unchecked(&BINDING).global_method_table
228229
}
229230

@@ -291,7 +292,7 @@ pub unsafe fn utility_function_table() -> &'static UtilityFunctionTable {
291292
/// # Safety
292293
///
293294
/// The interface must have been initialised with [`initialize`] before calling this function.
294-
#[inline(always)]
295+
#[inline]
295296
pub unsafe fn load_class_method_table(api_level: ClassApiLevel) {
296297
let binding = unwrap_ref_unchecked_mut(&mut BINDING);
297298

@@ -360,6 +361,35 @@ pub unsafe fn load_class_method_table(api_level: ClassApiLevel) {
360361
);
361362
}
362363

364+
/// # Safety
365+
///
366+
/// Must be accessed from the main thread, and the interface must have been initialized.
367+
/// `tag_string` must be a valid type pointer of a `String` instance.
368+
#[inline]
369+
pub unsafe fn godot_has_feature(
370+
os_class_sname: GDExtensionConstStringNamePtr,
371+
tag_string: GDExtensionConstTypePtr,
372+
) -> bool {
373+
// Issue a raw C call to OS.has_feature(tag_string).
374+
375+
let method_bind = class_scene_api().os__has_feature();
376+
let get_singleton = get_interface().global_get_singleton.unwrap();
377+
let class_ptrcall = get_interface().object_method_bind_ptrcall.unwrap();
378+
379+
let object_ptr = get_singleton(os_class_sname);
380+
let mut return_ptr = false;
381+
let type_ptrs = [tag_string];
382+
383+
class_ptrcall(
384+
method_bind,
385+
object_ptr,
386+
type_ptrs.as_ptr(),
387+
return_ptr.sys_mut(),
388+
);
389+
390+
return_ptr
391+
}
392+
363393
/// # Safety
364394
///
365395
/// Must be accessed from the main thread, and the interface must have been initialized.
@@ -383,15 +413,15 @@ pub unsafe fn config() -> &'static GdextConfig {
383413
#[doc(hidden)]
384414
macro_rules! builtin_fn {
385415
($name:ident $(@1)?) => {
386-
$crate::method_table().$name
416+
$crate::builtin_lifecycle_api().$name
387417
};
388418
}
389419

390420
#[macro_export]
391421
#[doc(hidden)]
392422
macro_rules! builtin_call {
393423
($name:ident ( $($args:expr),* $(,)? )) => {
394-
($crate::method_table().$name)( $($args),* )
424+
($crate::builtin_lifecycle_api().$name)( $($args),* )
395425
};
396426
}
397427

godot-ffi/src/toolbox.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,4 +304,18 @@ pub(crate) fn load_utility_function(
304304
})
305305
}
306306

307+
pub(crate) fn read_version_string(version_ptr: &sys::GDExtensionGodotVersion) -> String {
308+
let char_ptr = version_ptr.string;
309+
310+
// SAFETY: GDExtensionGodotVersion has the (manually upheld) invariant of a valid string field.
311+
let c_str = unsafe { std::ffi::CStr::from_ptr(char_ptr) };
312+
313+
let full_version = c_str.to_str().unwrap_or("(invalid UTF-8 in version)");
314+
315+
full_version
316+
.strip_prefix("Godot Engine ")
317+
.unwrap_or(full_version)
318+
.to_string()
319+
}
320+
307321
const INFO: &str = "\nMake sure gdext and Godot are compatible: https://godot-rust.github.io/book/gdext/advanced/compatibility.html";

godot/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ compile_error!("Thread safety for lazy function pointers is not yet implemented.
188188
#[cfg(all(target_family = "wasm", not(feature = "experimental-wasm")))]
189189
compile_error!("Must opt-in using `experimental-wasm` Cargo feature; keep in mind that this is work in progress");
190190

191+
// See also https://github.com/godotengine/godot/issues/86346.
192+
#[cfg(all(feature = "double-precision", not(feature = "custom-godot")))]
193+
compile_error!("The feature `double-precision` currently requires `custom-godot` due to incompatibilities in the GDExtension API JSON.");
194+
191195
pub mod init {
192196
pub use godot_core::init::*;
193197

0 commit comments

Comments
 (0)