Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions godot-core/src/init/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ fn gdext_on_level_deinit(level: InitLevel) {
// No business logic by itself, but ensures consistency if re-initialization (hot-reload on Linux) occurs.

crate::task::cleanup();
crate::tools::cleanup();

// Garbage-collect various statics.
// SAFETY: this is the last time meta APIs are used.
Expand Down
5 changes: 4 additions & 1 deletion godot-core/src/meta/error/convert_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,10 @@ impl fmt::Display for ErrorKind {
Self::FromGodot(from_godot) => write!(f, "{from_godot}"),
Self::FromVariant(from_variant) => write!(f, "{from_variant}"),
Self::FromFfi(from_ffi) => write!(f, "{from_ffi}"),
Self::Custom(cause) => write!(f, "{cause:?}"),
Self::Custom(cause) => match cause {
Some(c) => write!(f, "{c}"),
None => write!(f, "custom error"),
},
}
}
}
Expand Down
177 changes: 177 additions & 0 deletions godot-core/src/tools/autoload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
* Copyright (c) godot-rust; Bromeon and contributors.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

use std::cell::RefCell;
use std::collections::HashMap;

use sys::is_main_thread;

use crate::builtin::NodePath;
use crate::classes::{Engine, Node, SceneTree};
use crate::meta::error::ConvertError;
use crate::obj::{Gd, Inherits, Singleton};
use crate::sys;

/// Retrieves an autoload by name.
///
/// See [Godot docs] for an explanation of the autoload concept. Godot sometimes uses the term "autoload" interchangeably with "singleton";
/// we strictly refer to the former to separate from [`Singleton`][crate::obj::Singleton] objects.
///
/// If the autoload can be resolved, it will be cached and returned very quickly the second time.
///
/// [Godot docs]: https://docs.godotengine.org/en/stable/tutorials/scripting/singletons_autoload.html
///
/// # Panics
/// This is a convenience function that calls [`try_get_autoload_by_name()`]. Panics if that fails, e.g. not found or wrong type.
///
/// # Example
/// ```no_run
/// use godot::prelude::*;
/// use godot::tools::get_autoload_by_name;
///
/// #[derive(GodotClass)]
/// #[class(init, base=Node)]
/// struct GlobalStats {
/// base: Base<Node>,
/// }
///
/// // Assuming "Statistics" is registered as an autoload in `project.godot`,
/// // this returns the one instance of type Gd<GlobalStats>.
/// let stats = get_autoload_by_name::<GlobalStats>("Statistics");
/// ```
pub fn get_autoload_by_name<T>(autoload_name: &str) -> Gd<T>
where
T: Inherits<Node>,
{
try_get_autoload_by_name::<T>(autoload_name)
.unwrap_or_else(|err| panic!("Failed to get autoload `{autoload_name}`: {err}"))
}

/// Retrieves an autoload by name (fallible).
///
/// Autoloads are accessed via the `/root/{name}` path in the scene tree. The name is the one you used to register the autoload in
/// `project.godot`. By convention, it often corresponds to the class name, but does not have to.
///
/// If the autoload can be resolved, it will be cached and returned very quickly the second time.
///
/// See also [`get_autoload_by_name()`] for simpler function expecting the class name and non-fallible invocation.
///
/// This function returns `Err` if:
/// - No autoload is registered under `name`.
/// - The autoload cannot be cast to type `T`.
/// - There is an error fetching the scene tree.
///
/// # Example
/// ```no_run
/// use godot::prelude::*;
/// use godot::tools::try_get_autoload_by_name;
///
/// #[derive(GodotClass)]
/// #[class(init, base=Node)]
/// struct GlobalStats {
/// base: Base<Node>,
/// }
///
/// let result = try_get_autoload_by_name::<GlobalStats>("Statistics");
/// match result {
/// Ok(autoload) => { /* Use the Gd<GlobalStats>. */ }
/// Err(err) => eprintln!("Failed to get autoload: {err}"),
/// }
/// ```
pub fn try_get_autoload_by_name<T>(autoload_name: &str) -> Result<Gd<T>, ConvertError>
where
T: Inherits<Node>,
{
ensure_main_thread()?;

// Check cache first.
let cached = AUTOLOAD_CACHE.with(|cache| cache.borrow().get(autoload_name).cloned());

if let Some(cached_node) = cached {
return cast_autoload(cached_node, autoload_name);
}

// Cache miss - fetch from scene tree.
let main_loop = Engine::singleton()
.get_main_loop()
.ok_or_else(|| ConvertError::new("main loop not available"))?;

let scene_tree = main_loop
.try_cast::<SceneTree>()
.map_err(|_| ConvertError::new("main loop is not a SceneTree"))?;

let autoload_path = NodePath::from(&format!("/root/{autoload_name}"));

let root = scene_tree
.get_root()
.ok_or_else(|| ConvertError::new("scene tree root not available"))?;

let autoload_node = root
.try_get_node_as::<Node>(&autoload_path)
.ok_or_else(|| ConvertError::new(format!("autoload `{autoload_name}` not found")))?;

// Store in cache as Gd<Node>.
AUTOLOAD_CACHE.with(|cache| {
cache
.borrow_mut()
.insert(autoload_name.to_string(), autoload_node.clone());
});

// Cast to requested type.
cast_autoload(autoload_node, autoload_name)
}

// ----------------------------------------------------------------------------------------------------------------------------------------------
// Cache implementation

thread_local! {
/// Cache for autoloads. Maps autoload name to `Gd<Node>`.
///
/// Uses `thread_local!` because `Gd<T>` is not `Send`/`Sync`. Since all Godot objects must be accessed
/// from the main thread, this is safe. We enforce main-thread access via `ensure_main_thread()`.
static AUTOLOAD_CACHE: RefCell<HashMap<String, Gd<Node>>> = RefCell::new(HashMap::new());
}

/// Verifies that the current thread is the main thread.
///
/// Returns an error if called from a thread other than the main thread. This is necessary because `Gd<T>` is not thread-safe.
fn ensure_main_thread() -> Result<(), ConvertError> {
if is_main_thread() {
Ok(())
} else {
Err(ConvertError::new(
"Autoloads must be fetched from main thread, as Gd<T> is not thread-safe",
))
}
}

/// Casts an autoload node to the requested type, with descriptive error message on failure.
fn cast_autoload<T>(node: Gd<Node>, autoload_name: &str) -> Result<Gd<T>, ConvertError>
where
T: Inherits<Node>,
{
node.try_cast::<T>().map_err(|node| {
let expected = T::class_id();
let actual = node.get_class();

ConvertError::new(format!(
"autoload `{autoload_name}` has wrong type (expected {expected}, got {actual})",
))
})
}

/// Clears the autoload cache (called during shutdown).
///
/// # Panics
/// Panics if called from a thread other than the main thread.
pub(crate) fn clear_autoload_cache() {
ensure_main_thread().expect("clear_autoload_cache() must be called from the main thread");

AUTOLOAD_CACHE.with(|cache| {
cache.borrow_mut().clear();
});
}
8 changes: 8 additions & 0 deletions godot-core/src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@
//! Contains functionality that extends existing Godot classes and functions, to make them more versatile
//! or better integrated with Rust.

mod autoload;
mod gfile;
mod save_load;
mod translate;

pub use autoload::*;
pub use gfile::*;
pub use save_load::*;
pub use translate::*;

// ----------------------------------------------------------------------------------------------------------------------------------------------

pub(crate) fn cleanup() {
clear_autoload_cache();
}
14 changes: 14 additions & 0 deletions itest/godot/SpecialTests.gd
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,17 @@ func test_collision_object_2d_input_event():

window.queue_free()

func test_autoload():
var fetched = Engine.get_main_loop().get_root().get_node_or_null("/root/MyAutoload")
assert_that(fetched != null, "MyAutoload should be loaded")

var by_class: AutoloadClass = fetched
assert_eq(by_class.verify_works(), 787, "Autoload typed by class")

var by_class_symbol: AutoloadClass = MyAutoload
assert_eq(by_class_symbol.verify_works(), 787, "Autoload typed by class")

# Autoload in GDScript can be referenced by class name or autoload name, however autoload as a type is only available in Godot 4.3+.
# See https://github.com/godot-rust/gdext/pull/1381#issuecomment-3446111511.
# var by_name: MyAutoload = fetched
# assert_eq(by_name.verify_works(), 787, "Autoload typed by name")
2 changes: 1 addition & 1 deletion itest/godot/TestRunner.tscn
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://dgcj68l8n6wpb"]

[ext_resource type="Script" path="res://TestRunner.gd" id="1_wdbrf"]
[ext_resource type="Script" uid="uid://dcsm6ho05dipr" path="res://TestRunner.gd" id="1_wdbrf"]

[node name="TestRunner" type="Node"]
script = ExtResource("1_wdbrf")
3 changes: 3 additions & 0 deletions itest/godot/gdscript_tests/AutoloadScene.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[gd_scene format=3 uid="uid://csf04mj3dj8bn"]

[node name="AutoloadNode" type="AutoloadClass"]
4 changes: 4 additions & 0 deletions itest/godot/project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ run/main_scene="res://TestRunner.tscn"
config/features=PackedStringArray("4.2")
run/flush_stdout_on_print=true

[autoload]

MyAutoload="*res://gdscript_tests/AutoloadScene.tscn"

[debug]

gdscript/warnings/shadowed_variable=0
92 changes: 92 additions & 0 deletions itest/rust/src/engine_tests/autoload_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (c) godot-rust; Bromeon and contributors.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

use godot::classes::Node;
use godot::prelude::*;
use godot::tools::{get_autoload_by_name, try_get_autoload_by_name};

use crate::framework::{itest, quick_thread};

#[derive(GodotClass)]
#[class(init, base=Node)]
struct AutoloadClass {
base: Base<Node>,
#[var]
property: i32,
}

#[godot_api]
impl AutoloadClass {
#[func]
fn verify_works(&self) -> i32 {
787
}
}

#[itest]
fn autoload_get() {
let mut autoload = get_autoload_by_name::<AutoloadClass>("MyAutoload");
{
let mut guard = autoload.bind_mut();
assert_eq!(guard.verify_works(), 787);
assert_eq!(guard.property, 0, "still has default value");

guard.property = 42;
}

// Fetch same autoload anew.
let autoload2 = get_autoload_by_name::<AutoloadClass>("MyAutoload");
assert_eq!(autoload2.bind().property, 42);

// Reset for other tests.
autoload.bind_mut().property = 0;
}

#[itest]
fn autoload_try_get_named() {
let autoload = try_get_autoload_by_name::<AutoloadClass>("MyAutoload").expect("fetch autoload");

assert_eq!(autoload.bind().verify_works(), 787);
assert_eq!(autoload.bind().property, 0, "still has default value");
}

#[itest]
fn autoload_try_get_named_inexistent() {
let result = try_get_autoload_by_name::<AutoloadClass>("InexistentAutoload");
result.expect_err("non-existent autoload");
}

#[itest]
fn autoload_try_get_named_bad_type() {
let result = try_get_autoload_by_name::<Node2D>("MyAutoload");
result.expect_err("autoload of incompatible node type");
}

#[itest]
fn autoload_from_other_thread() {
use std::sync::{Arc, Mutex};

// We can't return the Result from the thread because Gd<T> is not Send, so we extract the error message instead.
let outer_error = Arc::new(Mutex::new(String::new()));
let inner_error = Arc::clone(&outer_error);

quick_thread(move || {
let result = try_get_autoload_by_name::<AutoloadClass>("MyAutoload");
match result {
Ok(_) => panic!("autoload access from non-main thread should fail"),
Err(err) => {
*inner_error.lock().unwrap() = err.to_string();
}
}
});

let msg = outer_error.lock().unwrap();
assert_eq!(
*msg,
"Autoloads must be fetched from main thread, as Gd<T> is not thread-safe"
);
}
1 change: 1 addition & 0 deletions itest/rust/src/engine_tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

mod async_test;
mod autoload_test;
mod codegen_enums_test;
mod codegen_test;
mod engine_enum_test;
Expand Down
Loading