godot_core/init/mod.rs
1/*
2 * Copyright (c) godot-rust; Bromeon and contributors.
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
6 */
7
8use std::sync::atomic::{AtomicBool, Ordering};
9
10use godot_ffi as sys;
11use sys::GodotFfi;
12
13use crate::builtin::{GString, StringName};
14use crate::out;
15
16mod reexport_pub {
17 #[cfg(not(wasm_nothreads))] #[cfg_attr(published_docs, doc(cfg(not(wasm_nothreads))))]
18 pub use super::sys::main_thread_id;
19 pub use super::sys::{is_main_thread, GdextBuild, InitStage};
20}
21pub use reexport_pub::*;
22
23#[repr(C)]
24struct InitUserData {
25 library: sys::GDExtensionClassLibraryPtr,
26 #[cfg(since_api = "4.5")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.5")))]
27 main_loop_callbacks: sys::GDExtensionMainLoopCallbacks,
28}
29
30#[cfg(since_api = "4.5")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.5")))]
31unsafe extern "C" fn startup_func<E: ExtensionLibrary>() {
32 E::on_stage_init(InitStage::MainLoop);
33}
34
35#[cfg(since_api = "4.5")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.5")))]
36unsafe extern "C" fn frame_func<E: ExtensionLibrary>() {
37 E::on_main_loop_frame();
38}
39
40#[cfg(since_api = "4.5")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.5")))]
41unsafe extern "C" fn shutdown_func<E: ExtensionLibrary>() {
42 E::on_stage_deinit(InitStage::MainLoop);
43}
44
45#[doc(hidden)]
46#[deny(unsafe_op_in_unsafe_fn)]
47pub unsafe fn __gdext_load_library<E: ExtensionLibrary>(
48 get_proc_address: sys::GDExtensionInterfaceGetProcAddress,
49 library: sys::GDExtensionClassLibraryPtr,
50 init: *mut sys::GDExtensionInitialization,
51) -> sys::GDExtensionBool {
52 let init_code = || {
53 // Make sure the first thing we do is check whether hot reloading should be enabled or not. This is to ensure that if we do anything to
54 // cause TLS-destructors to run then we have a setting already for how to deal with them. Otherwise, this could cause the default
55 // behavior to kick in and disable hot reloading.
56 #[cfg(target_os = "linux")] #[cfg_attr(published_docs, doc(cfg(target_os = "linux")))]
57 sys::linux_reload_workaround::default_set_hot_reload();
58
59 let tool_only_in_editor = match E::editor_run_behavior() {
60 EditorRunBehavior::ToolClassesOnly => true,
61 EditorRunBehavior::AllClasses => false,
62 };
63
64 let config = sys::GdextConfig::new(tool_only_in_editor);
65
66 // SAFETY: no custom code has run yet + no other thread is accessing global handle.
67 unsafe {
68 sys::initialize(get_proc_address, library, config);
69 }
70
71 // With experimental-features enabled, we can always print panics to godot_print!
72 #[cfg(feature = "experimental-threads")] #[cfg_attr(published_docs, doc(cfg(feature = "experimental-threads")))]
73 crate::private::set_gdext_hook(|| true);
74
75 // Without experimental-features enabled, we can only print panics with godot_print! if the panic occurs on the main (Godot) thread.
76 #[cfg(not(feature = "experimental-threads"))]
77 {
78 let main_thread = std::thread::current().id();
79 crate::private::set_gdext_hook(move || std::thread::current().id() == main_thread);
80 }
81
82 // Currently no way to express failure; could be exposed to E if necessary.
83 // No early exit, unclear if Godot still requires output parameters to be set.
84 let success = true;
85 // Leak the userdata. It will be dropped in core level deinitialization.
86 let userdata = Box::into_raw(Box::new(InitUserData {
87 library,
88 #[cfg(since_api = "4.5")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.5")))]
89 main_loop_callbacks: sys::GDExtensionMainLoopCallbacks {
90 startup_func: Some(startup_func::<E>),
91 frame_func: Some(frame_func::<E>),
92 shutdown_func: Some(shutdown_func::<E>),
93 },
94 }));
95
96 let godot_init_params = sys::GDExtensionInitialization {
97 minimum_initialization_level: E::min_level().to_sys(),
98 userdata: userdata.cast::<std::ffi::c_void>(),
99 initialize: Some(ffi_initialize_layer::<E>),
100 deinitialize: Some(ffi_deinitialize_layer::<E>),
101 };
102
103 // SAFETY: Godot is responsible for passing us a valid pointer.
104 unsafe {
105 *init = godot_init_params;
106 }
107
108 success as u8
109 };
110
111 // Use std::panic::catch_unwind instead of handle_panic: handle_panic uses TLS, which
112 // calls `thread_atexit` on linux, which sets the hot reloading flag on linux.
113 // Using std::panic::catch_unwind avoids this, although we lose out on context information
114 // for debugging.
115 let is_success = std::panic::catch_unwind(init_code);
116
117 is_success.unwrap_or(0)
118}
119
120static LEVEL_SERVERS_CORE_LOADED: AtomicBool = AtomicBool::new(false);
121
122unsafe extern "C" fn ffi_initialize_layer<E: ExtensionLibrary>(
123 userdata: *mut std::ffi::c_void,
124 init_level: sys::GDExtensionInitializationLevel,
125) {
126 let userdata = userdata.cast::<InitUserData>().as_ref().unwrap();
127 let level = InitLevel::from_sys(init_level);
128 let ctx = || format!("failed to initialize GDExtension level `{level:?}`");
129
130 fn try_load<E: ExtensionLibrary>(level: InitLevel, userdata: &InitUserData) {
131 // Workaround for https://github.com/godot-rust/gdext/issues/629:
132 // When using editor plugins, Godot may unload all levels but only reload from Scene upward.
133 // Manually run initialization of lower levels.
134
135 // TODO: Remove this workaround once after the upstream issue is resolved.
136 if level == InitLevel::Scene {
137 if !LEVEL_SERVERS_CORE_LOADED.load(Ordering::Relaxed) {
138 try_load::<E>(InitLevel::Core, userdata);
139 try_load::<E>(InitLevel::Servers, userdata);
140 }
141 } else if level == InitLevel::Core {
142 // When it's normal initialization, the `Servers` level is normally initialized.
143 LEVEL_SERVERS_CORE_LOADED.store(true, Ordering::Relaxed);
144 }
145
146 // SAFETY: Godot will call this from the main thread, after `__gdext_load_library` where the library is initialized,
147 // and only once per level.
148 unsafe { gdext_on_level_init(level, userdata) };
149 E::on_stage_init(level.to_stage());
150 }
151
152 // Swallow panics. TODO consider crashing if gdext init fails.
153 let _ = crate::private::handle_panic(ctx, || {
154 try_load::<E>(level, userdata);
155 });
156}
157
158unsafe extern "C" fn ffi_deinitialize_layer<E: ExtensionLibrary>(
159 userdata: *mut std::ffi::c_void,
160 init_level: sys::GDExtensionInitializationLevel,
161) {
162 let level = InitLevel::from_sys(init_level);
163 let ctx = || format!("failed to deinitialize GDExtension level `{level:?}`");
164
165 // Swallow panics.
166 let _ = crate::private::handle_panic(ctx, || {
167 if level == InitLevel::Core {
168 // Once the CORE api is unloaded, reset the flag to initial state.
169 LEVEL_SERVERS_CORE_LOADED.store(false, Ordering::Relaxed);
170
171 // Drop the userdata.
172 drop(Box::from_raw(userdata.cast::<InitUserData>()));
173 }
174
175 E::on_stage_deinit(level.to_stage());
176 gdext_on_level_deinit(level);
177 });
178}
179
180/// Tasks needed to be done by gdext internally upon loading an initialization level. Called before user code.
181///
182/// # Safety
183///
184/// - Must be called from the main thread.
185/// - The interface must have been initialized.
186/// - Must only be called once per level.
187#[deny(unsafe_op_in_unsafe_fn)]
188unsafe fn gdext_on_level_init(level: InitLevel, userdata: &InitUserData) {
189 // TODO: in theory, a user could start a thread in one of the early levels, and run concurrent code that messes with the global state
190 // (e.g. class registration). This would break the assumption that the load_class_method_table() calls are exclusive.
191 // We could maybe protect globals with a mutex until initialization is complete, and then move it to a directly-accessible, read-only static.
192
193 // SAFETY: we are in the main thread, initialize has been called, has never been called with this level before.
194 unsafe { sys::load_class_method_table(level) };
195
196 match level {
197 InitLevel::Core => {
198 #[cfg(since_api = "4.5")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.5")))]
199 unsafe {
200 sys::interface_fn!(register_main_loop_callbacks)(
201 userdata.library,
202 &raw const userdata.main_loop_callbacks,
203 )
204 };
205 }
206 InitLevel::Servers => {
207 // SAFETY: called from the main thread, sys::initialized has already been called.
208 unsafe { sys::discover_main_thread() };
209 }
210 InitLevel::Scene => {
211 // SAFETY: On the main thread, api initialized, `Scene` was initialized above.
212 unsafe { ensure_godot_features_compatible() };
213 }
214 InitLevel::Editor => {
215 #[cfg(all(since_api = "4.3", feature = "register-docs"))]
216 // SAFETY: Godot binding is initialized, and this is called from the main thread.
217 unsafe {
218 crate::docs::register();
219 }
220 }
221 }
222
223 crate::registry::class::auto_register_classes(level);
224}
225
226/// Tasks needed to be done by gdext internally upon unloading an initialization level. Called after user code.
227fn gdext_on_level_deinit(level: InitLevel) {
228 crate::registry::class::unregister_classes(level);
229
230 if level == InitLevel::Core {
231 // If lowest level is unloaded, call global deinitialization.
232 // No business logic by itself, but ensures consistency if re-initialization (hot-reload on Linux) occurs.
233
234 crate::task::cleanup();
235 crate::tools::cleanup();
236
237 // Garbage-collect various statics.
238 // SAFETY: this is the last time meta APIs are used.
239 unsafe {
240 crate::meta::cleanup();
241 }
242
243 // SAFETY: called after all other logic, so no concurrent access.
244 // TODO: multithreading must make sure other threads are joined/stopped here.
245 unsafe {
246 sys::deinitialize();
247 }
248 }
249}
250
251// ----------------------------------------------------------------------------------------------------------------------------------------------
252
253/// Defines the entry point for a GDExtension Rust library.
254///
255/// Every library should have exactly one implementation of this trait. It is always used in combination with the
256/// [`#[gdextension]`][gdextension] proc-macro attribute.
257///
258/// # Example
259/// The simplest usage is as follows. This will automatically perform the necessary init and cleanup routines, and register
260/// all classes marked with `#[derive(GodotClass)]`, without needing to mention them in a central list. The order in which
261/// classes are registered is not specified.
262///
263/// ```
264/// use godot::init::*;
265///
266/// // This is just a type tag without any functionality.
267/// // Its name is irrelevant.
268/// struct MyExtension;
269///
270/// #[gdextension]
271/// unsafe impl ExtensionLibrary for MyExtension {}
272/// ```
273///
274/// # Custom entry symbol
275/// There is usually no reason to, but you can use a different entry point (C function in the dynamic library). This must match the key
276/// that you specify in the `.gdextension` file. Let's say your `.gdextension` file has such a section:
277/// ```toml
278/// [configuration]
279/// entry_symbol = "custom_name"
280/// ```
281/// then you can implement the trait like this:
282/// ```no_run
283/// # use godot::init::*;
284/// struct MyExtension;
285///
286/// #[gdextension(entry_symbol = custom_name)]
287/// unsafe impl ExtensionLibrary for MyExtension {}
288/// ```
289/// Note that this only changes the name. You cannot provide your own function -- use the [`on_level_init()`][ExtensionLibrary::on_level_init]
290/// hook for custom startup logic.
291///
292/// # Availability of Godot APIs during init and deinit
293// Init order: see also special_cases.rs > classify_codegen_level().
294/// Godot loads functionality gradually during its startup routines, and unloads it during shutdown. As a result, Godot classes are only
295/// available above a certain level. Trying to access a class API when it's not available will panic (if not, please report it as a bug).
296///
297/// A few singletons (`Engine`, `Os`, `Time`, `ProjectSettings`) are available from the `Core` level onward and can be used inside
298/// this method. Most other singletons are **not available during init** at all, and will only become accessible once the first frame has
299/// run.
300///
301/// The exact time a class is available depends on the Godot initialization logic, which is quite complex and may change between versions.
302/// To get an up-to-date view, inspect the Godot source code of [main.cpp], particularly `Main::setup()`, `Main::setup2()` and
303/// `Main::cleanup()` methods. Make sure to look at the correct version of the file.
304///
305/// In case of doubt, do not rely on classes being available during init/deinit.
306///
307/// [main.cpp]: https://github.com/godotengine/godot/blob/master/main/main.cpp
308///
309/// # Safety
310/// The library cannot enforce any safety guarantees outside Rust code, which means that **you as a user** are
311/// responsible to uphold them: namely in GDScript code or other GDExtension bindings loaded by the engine.
312/// Violating this may cause undefined behavior, even when invoking _safe_ functions.
313///
314/// If you use the `disengaged` [safeguard level], you accept that UB becomes possible even **in safe Rust APIs**, if you use them wrong
315/// (e.g. accessing a destroyed object).
316///
317/// [gdextension]: attr.gdextension.html
318/// [safety]: https://godot-rust.github.io/book/gdext/advanced/safety.html
319/// [safeguard level]: ../index.html#safeguard-levels
320// FIXME intra-doc link
321#[doc(alias = "entry_symbol", alias = "entry_point")]
322pub unsafe trait ExtensionLibrary {
323 /// Determines if and how an extension's code is run in the editor.
324 fn editor_run_behavior() -> EditorRunBehavior {
325 EditorRunBehavior::ToolClassesOnly
326 }
327
328 /// Determines the initialization level at which the extension is loaded (`Scene` by default).
329 ///
330 /// If the level is lower than [`InitLevel::Scene`], the engine needs to be restarted to take effect.
331 fn min_level() -> InitLevel {
332 InitLevel::Scene
333 }
334
335 /// Custom logic when a certain initialization stage is loaded.
336 ///
337 /// This will be invoked for stages >= [`Self::min_level()`], in ascending order. Use `if` or `match` to hook to specific stages.
338 ///
339 /// The stages are loaded in order: `Core` → `Servers` → `Scene` → `Editor` (if in editor) → `MainLoop` (4.5+). \
340 /// The `MainLoop` stage represents the fully initialized state of Godot, after all initialization levels and classes have been loaded.
341 ///
342 /// See also [`on_main_loop_frame()`][Self::on_main_loop_frame] for per-frame processing.
343 ///
344 /// # Panics
345 /// If the overridden method panics, an error will be printed, but GDExtension loading is **not** aborted.
346 #[allow(unused_variables)]
347 #[expect(deprecated)] // Fall back to older API.
348 fn on_stage_init(stage: InitStage) {
349 stage
350 .try_to_level()
351 .inspect(|&level| Self::on_level_init(level));
352
353 #[cfg(since_api = "4.5")] // Compat layer.
354 if stage == InitStage::MainLoop {
355 Self::on_main_loop_startup();
356 }
357 }
358
359 /// Custom logic when a certain initialization stage is unloaded.
360 ///
361 /// This will be invoked for stages >= [`Self::min_level()`], in descending order. Use `if` or `match` to hook to specific stages.
362 ///
363 /// The stages are unloaded in reverse order: `MainLoop` (4.5+) → `Editor` (if in editor) → `Scene` → `Servers` → `Core`. \
364 /// At the time `MainLoop` is deinitialized, all classes are still available.
365 ///
366 /// # Panics
367 /// If the overridden method panics, an error will be printed, but GDExtension unloading is **not** aborted.
368 #[allow(unused_variables)]
369 #[expect(deprecated)] // Fall back to older API.
370 fn on_stage_deinit(stage: InitStage) {
371 #[cfg(since_api = "4.5")] // Compat layer.
372 if stage == InitStage::MainLoop {
373 Self::on_main_loop_shutdown();
374 }
375
376 stage
377 .try_to_level()
378 .inspect(|&level| Self::on_level_deinit(level));
379 }
380
381 /// Old callback before [`on_stage_init()`][Self::on_stage_deinit] was added. Does not support `MainLoop` stage.
382 #[deprecated = "Use `on_stage_init()` instead, which also includes the MainLoop stage."]
383 #[allow(unused_variables)]
384 fn on_level_init(level: InitLevel) {
385 // Nothing by default.
386 }
387
388 /// Old callback before [`on_stage_deinit()`][Self::on_stage_deinit] was added. Does not support `MainLoop` stage.
389 #[deprecated = "Use `on_stage_deinit()` instead, which also includes the MainLoop stage."]
390 #[allow(unused_variables)]
391 fn on_level_deinit(level: InitLevel) {
392 // Nothing by default.
393 }
394
395 #[cfg(since_api = "4.5")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.5")))]
396 #[deprecated = "Use `on_stage_init(InitStage::MainLoop)` instead."]
397 #[doc(hidden)] // Added by mistake -- works but don't advertise.
398 fn on_main_loop_startup() {
399 // Nothing by default.
400 }
401
402 #[cfg(since_api = "4.5")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.5")))]
403 #[deprecated = "Use `on_stage_deinit(InitStage::MainLoop)` instead."]
404 #[doc(hidden)] // Added by mistake -- works but don't advertise.
405 fn on_main_loop_shutdown() {
406 // Nothing by default.
407 }
408
409 /// Callback invoked for every process frame.
410 ///
411 /// This is called during the main loop, after Godot is fully initialized. It runs after all
412 /// [`process()`][crate::classes::INode::process] methods on Node, and before the Godot-internal `ScriptServer::frame()`.
413 /// This is intended to be the equivalent of [`IScriptLanguageExtension::frame()`][`crate::classes::IScriptLanguageExtension::frame()`]
414 /// for GDExtension language bindings that don't use the script API.
415 ///
416 /// # Example
417 /// To hook into startup/shutdown of the main loop, use [`on_stage_init()`][Self::on_stage_init] and
418 /// [`on_stage_deinit()`][Self::on_stage_deinit] and watch for [`InitStage::MainLoop`].
419 ///
420 /// ```no_run
421 /// # use godot::init::*;
422 /// # struct MyExtension;
423 /// #[gdextension]
424 /// unsafe impl ExtensionLibrary for MyExtension {
425 /// fn on_stage_init(stage: InitStage) {
426 /// if stage == InitStage::MainLoop {
427 /// // Startup code after fully initialized.
428 /// }
429 /// }
430 ///
431 /// fn on_main_loop_frame() {
432 /// // Per-frame logic.
433 /// }
434 ///
435 /// fn on_stage_deinit(stage: InitStage) {
436 /// if stage == InitStage::MainLoop {
437 /// // Cleanup code before shutdown.
438 /// }
439 /// }
440 /// }
441 /// ```
442 ///
443 /// # Panics
444 /// If the overridden method panics, an error will be printed, but execution continues.
445 #[cfg(since_api = "4.5")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.5")))]
446 fn on_main_loop_frame() {
447 // Nothing by default.
448 }
449
450 /// Whether to override the Wasm binary filename used by your GDExtension which the library should expect at runtime. Return `None`
451 /// to use the default where gdext expects either `{YourCrate}.wasm` (default binary name emitted by Rust) or
452 /// `{YourCrate}.threads.wasm` (for builds producing separate single-threaded and multi-threaded binaries).
453 ///
454 /// Upon exporting a game to the web, the library has to know at runtime the exact name of the `.wasm` binary file being used to load
455 /// each GDExtension. By default, Rust exports the binary as `cratename.wasm`, so that is the name checked by godot-rust by default.
456 ///
457 /// However, if you need to rename that binary, you can make the library aware of the new binary name by returning
458 /// `Some("newname.wasm")` (don't forget to **include the `.wasm` extension**).
459 ///
460 /// For example, to have two simultaneous versions, one supporting multi-threading and the other not, you could add a suffix to the
461 /// filename of the Wasm binary of the multi-threaded version in your build process. If you choose the suffix `.threads.wasm`,
462 /// you're in luck as godot-rust already accepts this suffix by default, but let's say you want to use a different suffix, such as
463 /// `-with-threads.wasm`. For this, you can have a `"nothreads"` feature which, when absent, should produce a suffixed binary,
464 /// which can be informed to gdext as follows:
465 ///
466 /// ```no_run
467 /// # use godot::init::*;
468 /// struct MyExtension;
469 ///
470 /// #[gdextension]
471 /// unsafe impl ExtensionLibrary for MyExtension {
472 /// fn override_wasm_binary() -> Option<&'static str> {
473 /// // Binary name unchanged ("mycrate.wasm") without thread support.
474 /// #[cfg(feature = "nothreads")]
475 /// return None;
476 ///
477 /// // Tell godot-rust we add a custom suffix to the binary with thread support.
478 /// // Please note that this is not needed if "mycrate.threads.wasm" is used.
479 /// // (You could return `None` as well in that particular case.)
480 /// #[cfg(not(feature = "nothreads"))]
481 /// Some("mycrate-with-threads.wasm")
482 /// }
483 /// }
484 /// ```
485 /// Note that simply overriding this method won't change the name of the Wasm binary produced by Rust automatically: you'll still
486 /// have to rename it by yourself in your build process, as well as specify the updated binary name in your `.gdextension` file.
487 /// This is just to ensure gdext is aware of the new name given to the binary, avoiding runtime errors.
488 fn override_wasm_binary() -> Option<&'static str> {
489 None
490 }
491}
492
493/// Determines if and how an extension's code is run in the editor.
494///
495/// By default, Godot 4 runs all virtual lifecycle callbacks (`_ready`, `_process`, `_physics_process`, ...)
496/// for extensions. This behavior is different from Godot 3, where extension classes needed to be explicitly
497/// marked as "tool".
498///
499/// In many cases, users write extension code with the intention to run in games, not inside the editor.
500/// This is why the default behavior in gdext deviates from Godot: lifecycle callbacks are disabled inside the
501/// editor (see [`ToolClassesOnly`][Self::ToolClassesOnly]). It is possible to configure this.
502///
503/// See also [`ExtensionLibrary::editor_run_behavior()`].
504#[derive(Copy, Clone, Debug)]
505#[non_exhaustive]
506pub enum EditorRunBehavior {
507 /// Only runs `#[class(tool)]` classes in the editor.
508 ///
509 /// All classes are registered, and calls from GDScript to Rust are possible. However, virtual lifecycle callbacks
510 /// (`_ready`, `_process`, `_physics_process`, ...) are not run unless the class is annotated with `#[class(tool)]`.
511 ToolClassesOnly,
512
513 /// Runs the extension with full functionality in editor.
514 ///
515 /// Ignores any `#[class(tool)]` annotations.
516 AllClasses,
517}
518
519// ----------------------------------------------------------------------------------------------------------------------------------------------
520
521pub use sys::InitLevel;
522
523// ----------------------------------------------------------------------------------------------------------------------------------------------
524
525/// # Safety
526///
527/// - Must be called from the main thread.
528/// - The interface must be initialized.
529/// - The `Scene` api level must have been initialized.
530#[deny(unsafe_op_in_unsafe_fn)]
531unsafe fn ensure_godot_features_compatible() {
532 // 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
533 // later, and godot-core would only depend on godot-sys. This makes future migrations easier. We still have access to builtins though.
534
535 out!("Check Godot precision setting...");
536
537 #[cfg(feature = "debug-log")] // Display safeguards level in debug log.
538 let safeguards_level = if cfg!(safeguards_strict) {
539 "strict"
540 } else if cfg!(safeguards_balanced) {
541 "balanced"
542 } else {
543 "disengaged"
544 };
545 out!("Safeguards: {safeguards_level}");
546
547 let os_class = StringName::from("OS");
548 let single = GString::from("single");
549 let double = GString::from("double");
550
551 let gdext_is_double = cfg!(feature = "double-precision");
552
553 // SAFETY: main thread, after initialize(), valid string pointers, `Scene` initialized.
554 let godot_is_double = unsafe {
555 let is_single = sys::godot_has_feature(os_class.string_sys(), single.sys());
556 let is_double = sys::godot_has_feature(os_class.string_sys(), double.sys());
557
558 assert_ne!(
559 is_single, is_double,
560 "Godot has invalid configuration: single={is_single}, double={is_double}"
561 );
562
563 is_double
564 };
565
566 let s = |is_double: bool| -> &'static str {
567 if is_double {
568 "double"
569 } else {
570 "single"
571 }
572 };
573
574 out!(
575 "Is double precision: Godot={}, gdext={}",
576 s(godot_is_double),
577 s(gdext_is_double)
578 );
579
580 if godot_is_double != gdext_is_double {
581 panic!(
582 "Godot runs with {} precision, but gdext was compiled with {} precision.\n\
583 Cargo feature `double-precision` must be used if and only if Godot is compiled with `precision=double`.\n",
584 s(godot_is_double), s(gdext_is_double),
585 );
586 }
587}