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/// [gdextension]: attr.gdextension.html
315/// [safety]: https://godot-rust.github.io/book/gdext/advanced/safety.html
316// FIXME intra-doc link
317#[doc(alias = "entry_symbol", alias = "entry_point")]
318pub unsafe trait ExtensionLibrary {
319    /// Determines if and how an extension's code is run in the editor.
320    fn editor_run_behavior() -> EditorRunBehavior {
321        EditorRunBehavior::ToolClassesOnly
322    }
323
324    /// Determines the initialization level at which the extension is loaded (`Scene` by default).
325    ///
326    /// If the level is lower than [`InitLevel::Scene`], the engine needs to be restarted to take effect.
327    fn min_level() -> InitLevel {
328        InitLevel::Scene
329    }
330
331    /// Custom logic when a certain initialization stage is loaded.
332    ///
333    /// This will be invoked for stages >= [`Self::min_level()`], in ascending order. Use `if` or `match` to hook to specific stages.
334    ///
335    /// The stages are loaded in order: `Core` → `Servers` → `Scene` → `Editor` (if in editor) → `MainLoop` (4.5+).  \
336    /// The `MainLoop` stage represents the fully initialized state of Godot, after all initialization levels and classes have been loaded.
337    ///
338    /// See also [`on_main_loop_frame()`][Self::on_main_loop_frame] for per-frame processing.
339    ///
340    /// # Panics
341    /// If the overridden method panics, an error will be printed, but GDExtension loading is **not** aborted.
342    #[allow(unused_variables)]
343    #[expect(deprecated)] // Fall back to older API.
344    fn on_stage_init(stage: InitStage) {
345        stage
346            .try_to_level()
347            .inspect(|&level| Self::on_level_init(level));
348
349        #[cfg(since_api = "4.5")] // Compat layer.
350        if stage == InitStage::MainLoop {
351            Self::on_main_loop_startup();
352        }
353    }
354
355    /// Custom logic when a certain initialization stage is unloaded.
356    ///
357    /// This will be invoked for stages >= [`Self::min_level()`], in descending order. Use `if` or `match` to hook to specific stages.
358    ///
359    /// The stages are unloaded in reverse order: `MainLoop` (4.5+) → `Editor` (if in editor) → `Scene` → `Servers` → `Core`.  \
360    /// At the time `MainLoop` is deinitialized, all classes are still available.
361    ///
362    /// # Panics
363    /// If the overridden method panics, an error will be printed, but GDExtension unloading is **not** aborted.
364    #[allow(unused_variables)]
365    #[expect(deprecated)] // Fall back to older API.
366    fn on_stage_deinit(stage: InitStage) {
367        #[cfg(since_api = "4.5")] // Compat layer.
368        if stage == InitStage::MainLoop {
369            Self::on_main_loop_shutdown();
370        }
371
372        stage
373            .try_to_level()
374            .inspect(|&level| Self::on_level_deinit(level));
375    }
376
377    /// Old callback before [`on_stage_init()`][Self::on_stage_deinit] was added. Does not support `MainLoop` stage.
378    #[deprecated = "Use `on_stage_init()` instead, which also includes the MainLoop stage."]
379    #[allow(unused_variables)]
380    fn on_level_init(level: InitLevel) {
381        // Nothing by default.
382    }
383
384    /// Old callback before [`on_stage_deinit()`][Self::on_stage_deinit] was added. Does not support `MainLoop` stage.
385    #[deprecated = "Use `on_stage_deinit()` instead, which also includes the MainLoop stage."]
386    #[allow(unused_variables)]
387    fn on_level_deinit(level: InitLevel) {
388        // Nothing by default.
389    }
390
391    #[cfg(since_api = "4.5")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.5")))]
392    #[deprecated = "Use `on_stage_init(InitStage::MainLoop)` instead."]
393    #[doc(hidden)] // Added by mistake -- works but don't advertise.
394    fn on_main_loop_startup() {
395        // Nothing by default.
396    }
397
398    #[cfg(since_api = "4.5")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.5")))]
399    #[deprecated = "Use `on_stage_deinit(InitStage::MainLoop)` instead."]
400    #[doc(hidden)] // Added by mistake -- works but don't advertise.
401    fn on_main_loop_shutdown() {
402        // Nothing by default.
403    }
404
405    /// Callback invoked for every process frame.
406    ///
407    /// This is called during the main loop, after Godot is fully initialized. It runs after all
408    /// [`process()`][crate::classes::INode::process] methods on Node, and before the Godot-internal `ScriptServer::frame()`.
409    /// This is intended to be the equivalent of [`IScriptLanguageExtension::frame()`][`crate::classes::IScriptLanguageExtension::frame()`]
410    /// for GDExtension language bindings that don't use the script API.
411    ///
412    /// # Example
413    /// To hook into startup/shutdown of the main loop, use [`on_stage_init()`][Self::on_stage_init] and
414    /// [`on_stage_deinit()`][Self::on_stage_deinit] and watch for [`InitStage::MainLoop`].
415    ///
416    /// ```no_run
417    /// # use godot::init::*;
418    /// # struct MyExtension;
419    /// #[gdextension]
420    /// unsafe impl ExtensionLibrary for MyExtension {
421    ///     fn on_stage_init(stage: InitStage) {
422    ///         if stage == InitStage::MainLoop {
423    ///             // Startup code after fully initialized.
424    ///         }
425    ///     }
426    ///
427    ///     fn on_main_loop_frame() {
428    ///         // Per-frame logic.
429    ///     }
430    ///
431    ///     fn on_stage_deinit(stage: InitStage) {
432    ///         if stage == InitStage::MainLoop {
433    ///             // Cleanup code before shutdown.
434    ///         }
435    ///     }
436    /// }
437    /// ```
438    ///
439    /// # Panics
440    /// If the overridden method panics, an error will be printed, but execution continues.
441    #[cfg(since_api = "4.5")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.5")))]
442    fn on_main_loop_frame() {
443        // Nothing by default.
444    }
445
446    /// Whether to override the Wasm binary filename used by your GDExtension which the library should expect at runtime. Return `None`
447    /// to use the default where gdext expects either `{YourCrate}.wasm` (default binary name emitted by Rust) or
448    /// `{YourCrate}.threads.wasm` (for builds producing separate single-threaded and multi-threaded binaries).
449    ///
450    /// 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
451    /// each GDExtension. By default, Rust exports the binary as `cratename.wasm`, so that is the name checked by godot-rust by default.
452    ///
453    /// However, if you need to rename that binary, you can make the library aware of the new binary name by returning
454    /// `Some("newname.wasm")` (don't forget to **include the `.wasm` extension**).
455    ///
456    /// For example, to have two simultaneous versions, one supporting multi-threading and the other not, you could add a suffix to the
457    /// filename of the Wasm binary of the multi-threaded version in your build process. If you choose the suffix `.threads.wasm`,
458    /// 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
459    /// `-with-threads.wasm`. For this, you can have a `"nothreads"` feature which, when absent, should produce a suffixed binary,
460    /// which can be informed to gdext as follows:
461    ///
462    /// ```no_run
463    /// # use godot::init::*;
464    /// struct MyExtension;
465    ///
466    /// #[gdextension]
467    /// unsafe impl ExtensionLibrary for MyExtension {
468    ///     fn override_wasm_binary() -> Option<&'static str> {
469    ///         // Binary name unchanged ("mycrate.wasm") without thread support.
470    ///         #[cfg(feature = "nothreads")]
471    ///         return None;
472    ///
473    ///         // Tell godot-rust we add a custom suffix to the binary with thread support.
474    ///         // Please note that this is not needed if "mycrate.threads.wasm" is used.
475    ///         // (You could return `None` as well in that particular case.)
476    ///         #[cfg(not(feature = "nothreads"))]
477    ///         Some("mycrate-with-threads.wasm")
478    ///     }
479    /// }
480    /// ```
481    /// Note that simply overriding this method won't change the name of the Wasm binary produced by Rust automatically: you'll still
482    /// have to rename it by yourself in your build process, as well as specify the updated binary name in your `.gdextension` file.
483    /// This is just to ensure gdext is aware of the new name given to the binary, avoiding runtime errors.
484    fn override_wasm_binary() -> Option<&'static str> {
485        None
486    }
487}
488
489/// Determines if and how an extension's code is run in the editor.
490///
491/// By default, Godot 4 runs all virtual lifecycle callbacks (`_ready`, `_process`, `_physics_process`, ...)
492/// for extensions. This behavior is different from Godot 3, where extension classes needed to be explicitly
493/// marked as "tool".
494///
495/// In many cases, users write extension code with the intention to run in games, not inside the editor.
496/// This is why the default behavior in gdext deviates from Godot: lifecycle callbacks are disabled inside the
497/// editor (see [`ToolClassesOnly`][Self::ToolClassesOnly]). It is possible to configure this.
498///
499/// See also [`ExtensionLibrary::editor_run_behavior()`].
500#[derive(Copy, Clone, Debug)]
501#[non_exhaustive]
502pub enum EditorRunBehavior {
503    /// Only runs `#[class(tool)]` classes in the editor.
504    ///
505    /// All classes are registered, and calls from GDScript to Rust are possible. However, virtual lifecycle callbacks
506    /// (`_ready`, `_process`, `_physics_process`, ...) are not run unless the class is annotated with `#[class(tool)]`.
507    ToolClassesOnly,
508
509    /// Runs the extension with full functionality in editor.
510    ///
511    /// Ignores any `#[class(tool)]` annotations.
512    AllClasses,
513}
514
515// ----------------------------------------------------------------------------------------------------------------------------------------------
516
517pub use sys::InitLevel;
518
519// ----------------------------------------------------------------------------------------------------------------------------------------------
520
521/// # Safety
522///
523/// - Must be called from the main thread.
524/// - The interface must be initialized.
525/// - The `Scene` api level must have been initialized.
526#[deny(unsafe_op_in_unsafe_fn)]
527unsafe fn ensure_godot_features_compatible() {
528    // 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
529    // later, and godot-core would only depend on godot-sys. This makes future migrations easier. We still have access to builtins though.
530
531    out!("Check Godot precision setting...");
532
533    let os_class = StringName::from("OS");
534    let single = GString::from("single");
535    let double = GString::from("double");
536
537    let gdext_is_double = cfg!(feature = "double-precision");
538
539    // SAFETY: main thread, after initialize(), valid string pointers, `Scene` initialized.
540    let godot_is_double = unsafe {
541        let is_single = sys::godot_has_feature(os_class.string_sys(), single.sys());
542        let is_double = sys::godot_has_feature(os_class.string_sys(), double.sys());
543
544        assert_ne!(
545            is_single, is_double,
546            "Godot has invalid configuration: single={is_single}, double={is_double}"
547        );
548
549        is_double
550    };
551
552    let s = |is_double: bool| -> &'static str {
553        if is_double {
554            "double"
555        } else {
556            "single"
557        }
558    };
559
560    out!(
561        "Is double precision: Godot={}, gdext={}",
562        s(godot_is_double),
563        s(gdext_is_double)
564    );
565
566    if godot_is_double != gdext_is_double {
567        panic!(
568            "Godot runs with {} precision, but gdext was compiled with {} precision.\n\
569            Cargo feature `double-precision` must be used if and only if Godot is compiled with `precision=double`.\n",
570            s(godot_is_double), s(gdext_is_double),
571        );
572    }
573}