Skip to content

Commit f025bd7

Browse files
authored
Merge pull request #1381 from godot-rust/feature/autoload
Simple API to fetch autoloads
2 parents fd043d5 + a2d23de commit f025bd7

File tree

10 files changed

+305
-2
lines changed

10 files changed

+305
-2
lines changed

godot-core/src/init/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ fn gdext_on_level_deinit(level: InitLevel) {
232232
// No business logic by itself, but ensures consistency if re-initialization (hot-reload on Linux) occurs.
233233

234234
crate::task::cleanup();
235+
crate::tools::cleanup();
235236

236237
// Garbage-collect various statics.
237238
// SAFETY: this is the last time meta APIs are used.

godot-core/src/meta/error/convert_error.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,10 @@ impl fmt::Display for ErrorKind {
171171
Self::FromGodot(from_godot) => write!(f, "{from_godot}"),
172172
Self::FromVariant(from_variant) => write!(f, "{from_variant}"),
173173
Self::FromFfi(from_ffi) => write!(f, "{from_ffi}"),
174-
Self::Custom(cause) => write!(f, "{cause:?}"),
174+
Self::Custom(cause) => match cause {
175+
Some(c) => write!(f, "{c}"),
176+
None => write!(f, "custom error"),
177+
},
175178
}
176179
}
177180
}

godot-core/src/tools/autoload.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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+
8+
use std::cell::RefCell;
9+
use std::collections::HashMap;
10+
11+
use sys::is_main_thread;
12+
13+
use crate::builtin::NodePath;
14+
use crate::classes::{Engine, Node, SceneTree};
15+
use crate::meta::error::ConvertError;
16+
use crate::obj::{Gd, Inherits, Singleton};
17+
use crate::sys;
18+
19+
/// Retrieves an autoload by name.
20+
///
21+
/// See [Godot docs] for an explanation of the autoload concept. Godot sometimes uses the term "autoload" interchangeably with "singleton";
22+
/// we strictly refer to the former to separate from [`Singleton`][crate::obj::Singleton] objects.
23+
///
24+
/// If the autoload can be resolved, it will be cached and returned very quickly the second time.
25+
///
26+
/// [Godot docs]: https://docs.godotengine.org/en/stable/tutorials/scripting/singletons_autoload.html
27+
///
28+
/// # Panics
29+
/// This is a convenience function that calls [`try_get_autoload_by_name()`]. Panics if that fails, e.g. not found or wrong type.
30+
///
31+
/// # Example
32+
/// ```no_run
33+
/// use godot::prelude::*;
34+
/// use godot::tools::get_autoload_by_name;
35+
///
36+
/// #[derive(GodotClass)]
37+
/// #[class(init, base=Node)]
38+
/// struct GlobalStats {
39+
/// base: Base<Node>,
40+
/// }
41+
///
42+
/// // Assuming "Statistics" is registered as an autoload in `project.godot`,
43+
/// // this returns the one instance of type Gd<GlobalStats>.
44+
/// let stats = get_autoload_by_name::<GlobalStats>("Statistics");
45+
/// ```
46+
pub fn get_autoload_by_name<T>(autoload_name: &str) -> Gd<T>
47+
where
48+
T: Inherits<Node>,
49+
{
50+
try_get_autoload_by_name::<T>(autoload_name)
51+
.unwrap_or_else(|err| panic!("Failed to get autoload `{autoload_name}`: {err}"))
52+
}
53+
54+
/// Retrieves an autoload by name (fallible).
55+
///
56+
/// Autoloads are accessed via the `/root/{name}` path in the scene tree. The name is the one you used to register the autoload in
57+
/// `project.godot`. By convention, it often corresponds to the class name, but does not have to.
58+
///
59+
/// If the autoload can be resolved, it will be cached and returned very quickly the second time.
60+
///
61+
/// See also [`get_autoload_by_name()`] for simpler function expecting the class name and non-fallible invocation.
62+
///
63+
/// This function returns `Err` if:
64+
/// - No autoload is registered under `name`.
65+
/// - The autoload cannot be cast to type `T`.
66+
/// - There is an error fetching the scene tree.
67+
///
68+
/// # Example
69+
/// ```no_run
70+
/// use godot::prelude::*;
71+
/// use godot::tools::try_get_autoload_by_name;
72+
///
73+
/// #[derive(GodotClass)]
74+
/// #[class(init, base=Node)]
75+
/// struct GlobalStats {
76+
/// base: Base<Node>,
77+
/// }
78+
///
79+
/// let result = try_get_autoload_by_name::<GlobalStats>("Statistics");
80+
/// match result {
81+
/// Ok(autoload) => { /* Use the Gd<GlobalStats>. */ }
82+
/// Err(err) => eprintln!("Failed to get autoload: {err}"),
83+
/// }
84+
/// ```
85+
pub fn try_get_autoload_by_name<T>(autoload_name: &str) -> Result<Gd<T>, ConvertError>
86+
where
87+
T: Inherits<Node>,
88+
{
89+
ensure_main_thread()?;
90+
91+
// Check cache first.
92+
let cached = AUTOLOAD_CACHE.with(|cache| cache.borrow().get(autoload_name).cloned());
93+
94+
if let Some(cached_node) = cached {
95+
return cast_autoload(cached_node, autoload_name);
96+
}
97+
98+
// Cache miss - fetch from scene tree.
99+
let main_loop = Engine::singleton()
100+
.get_main_loop()
101+
.ok_or_else(|| ConvertError::new("main loop not available"))?;
102+
103+
let scene_tree = main_loop
104+
.try_cast::<SceneTree>()
105+
.map_err(|_| ConvertError::new("main loop is not a SceneTree"))?;
106+
107+
let autoload_path = NodePath::from(&format!("/root/{autoload_name}"));
108+
109+
let root = scene_tree
110+
.get_root()
111+
.ok_or_else(|| ConvertError::new("scene tree root not available"))?;
112+
113+
let autoload_node = root
114+
.try_get_node_as::<Node>(&autoload_path)
115+
.ok_or_else(|| ConvertError::new(format!("autoload `{autoload_name}` not found")))?;
116+
117+
// Store in cache as Gd<Node>.
118+
AUTOLOAD_CACHE.with(|cache| {
119+
cache
120+
.borrow_mut()
121+
.insert(autoload_name.to_string(), autoload_node.clone());
122+
});
123+
124+
// Cast to requested type.
125+
cast_autoload(autoload_node, autoload_name)
126+
}
127+
128+
// ----------------------------------------------------------------------------------------------------------------------------------------------
129+
// Cache implementation
130+
131+
thread_local! {
132+
/// Cache for autoloads. Maps autoload name to `Gd<Node>`.
133+
///
134+
/// Uses `thread_local!` because `Gd<T>` is not `Send`/`Sync`. Since all Godot objects must be accessed
135+
/// from the main thread, this is safe. We enforce main-thread access via `ensure_main_thread()`.
136+
static AUTOLOAD_CACHE: RefCell<HashMap<String, Gd<Node>>> = RefCell::new(HashMap::new());
137+
}
138+
139+
/// Verifies that the current thread is the main thread.
140+
///
141+
/// Returns an error if called from a thread other than the main thread. This is necessary because `Gd<T>` is not thread-safe.
142+
fn ensure_main_thread() -> Result<(), ConvertError> {
143+
if is_main_thread() {
144+
Ok(())
145+
} else {
146+
Err(ConvertError::new(
147+
"Autoloads must be fetched from main thread, as Gd<T> is not thread-safe",
148+
))
149+
}
150+
}
151+
152+
/// Casts an autoload node to the requested type, with descriptive error message on failure.
153+
fn cast_autoload<T>(node: Gd<Node>, autoload_name: &str) -> Result<Gd<T>, ConvertError>
154+
where
155+
T: Inherits<Node>,
156+
{
157+
node.try_cast::<T>().map_err(|node| {
158+
let expected = T::class_id();
159+
let actual = node.get_class();
160+
161+
ConvertError::new(format!(
162+
"autoload `{autoload_name}` has wrong type (expected {expected}, got {actual})",
163+
))
164+
})
165+
}
166+
167+
/// Clears the autoload cache (called during shutdown).
168+
///
169+
/// # Panics
170+
/// Panics if called from a thread other than the main thread.
171+
pub(crate) fn clear_autoload_cache() {
172+
ensure_main_thread().expect("clear_autoload_cache() must be called from the main thread");
173+
174+
AUTOLOAD_CACHE.with(|cache| {
175+
cache.borrow_mut().clear();
176+
});
177+
}

godot-core/src/tools/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,18 @@
1010
//! Contains functionality that extends existing Godot classes and functions, to make them more versatile
1111
//! or better integrated with Rust.
1212
13+
mod autoload;
1314
mod gfile;
1415
mod save_load;
1516
mod translate;
1617

18+
pub use autoload::*;
1719
pub use gfile::*;
1820
pub use save_load::*;
1921
pub use translate::*;
22+
23+
// ----------------------------------------------------------------------------------------------------------------------------------------------
24+
25+
pub(crate) fn cleanup() {
26+
clear_autoload_cache();
27+
}

itest/godot/SpecialTests.gd

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,17 @@ func test_collision_object_2d_input_event():
5757

5858
window.queue_free()
5959

60+
func test_autoload():
61+
var fetched = Engine.get_main_loop().get_root().get_node_or_null("/root/MyAutoload")
62+
assert_that(fetched != null, "MyAutoload should be loaded")
63+
64+
var by_class: AutoloadClass = fetched
65+
assert_eq(by_class.verify_works(), 787, "Autoload typed by class")
66+
67+
var by_class_symbol: AutoloadClass = MyAutoload
68+
assert_eq(by_class_symbol.verify_works(), 787, "Autoload typed by class")
69+
70+
# Autoload in GDScript can be referenced by class name or autoload name, however autoload as a type is only available in Godot 4.3+.
71+
# See https://github.com/godot-rust/gdext/pull/1381#issuecomment-3446111511.
72+
# var by_name: MyAutoload = fetched
73+
# assert_eq(by_name.verify_works(), 787, "Autoload typed by name")

itest/godot/TestRunner.tscn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[gd_scene load_steps=2 format=3 uid="uid://dgcj68l8n6wpb"]
22

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

55
[node name="TestRunner" type="Node"]
66
script = ExtResource("1_wdbrf")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[gd_scene format=3 uid="uid://csf04mj3dj8bn"]
2+
3+
[node name="AutoloadNode" type="AutoloadClass"]

itest/godot/project.godot

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ run/main_scene="res://TestRunner.tscn"
1515
config/features=PackedStringArray("4.2")
1616
run/flush_stdout_on_print=true
1717

18+
[autoload]
19+
20+
MyAutoload="*res://gdscript_tests/AutoloadScene.tscn"
21+
1822
[debug]
1923

2024
gdscript/warnings/shadowed_variable=0
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
8+
use godot::classes::Node;
9+
use godot::prelude::*;
10+
use godot::tools::{get_autoload_by_name, try_get_autoload_by_name};
11+
12+
use crate::framework::{itest, quick_thread};
13+
14+
#[derive(GodotClass)]
15+
#[class(init, base=Node)]
16+
struct AutoloadClass {
17+
base: Base<Node>,
18+
#[var]
19+
property: i32,
20+
}
21+
22+
#[godot_api]
23+
impl AutoloadClass {
24+
#[func]
25+
fn verify_works(&self) -> i32 {
26+
787
27+
}
28+
}
29+
30+
#[itest]
31+
fn autoload_get() {
32+
let mut autoload = get_autoload_by_name::<AutoloadClass>("MyAutoload");
33+
{
34+
let mut guard = autoload.bind_mut();
35+
assert_eq!(guard.verify_works(), 787);
36+
assert_eq!(guard.property, 0, "still has default value");
37+
38+
guard.property = 42;
39+
}
40+
41+
// Fetch same autoload anew.
42+
let autoload2 = get_autoload_by_name::<AutoloadClass>("MyAutoload");
43+
assert_eq!(autoload2.bind().property, 42);
44+
45+
// Reset for other tests.
46+
autoload.bind_mut().property = 0;
47+
}
48+
49+
#[itest]
50+
fn autoload_try_get_named() {
51+
let autoload = try_get_autoload_by_name::<AutoloadClass>("MyAutoload").expect("fetch autoload");
52+
53+
assert_eq!(autoload.bind().verify_works(), 787);
54+
assert_eq!(autoload.bind().property, 0, "still has default value");
55+
}
56+
57+
#[itest]
58+
fn autoload_try_get_named_inexistent() {
59+
let result = try_get_autoload_by_name::<AutoloadClass>("InexistentAutoload");
60+
result.expect_err("non-existent autoload");
61+
}
62+
63+
#[itest]
64+
fn autoload_try_get_named_bad_type() {
65+
let result = try_get_autoload_by_name::<Node2D>("MyAutoload");
66+
result.expect_err("autoload of incompatible node type");
67+
}
68+
69+
#[itest]
70+
fn autoload_from_other_thread() {
71+
use std::sync::{Arc, Mutex};
72+
73+
// We can't return the Result from the thread because Gd<T> is not Send, so we extract the error message instead.
74+
let outer_error = Arc::new(Mutex::new(String::new()));
75+
let inner_error = Arc::clone(&outer_error);
76+
77+
quick_thread(move || {
78+
let result = try_get_autoload_by_name::<AutoloadClass>("MyAutoload");
79+
match result {
80+
Ok(_) => panic!("autoload access from non-main thread should fail"),
81+
Err(err) => {
82+
*inner_error.lock().unwrap() = err.to_string();
83+
}
84+
}
85+
});
86+
87+
let msg = outer_error.lock().unwrap();
88+
assert_eq!(
89+
*msg,
90+
"Autoloads must be fetched from main thread, as Gd<T> is not thread-safe"
91+
);
92+
}

itest/rust/src/engine_tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
mod async_test;
9+
mod autoload_test;
910
mod codegen_enums_test;
1011
mod codegen_test;
1112
mod engine_enum_test;

0 commit comments

Comments
 (0)