Skip to content

Commit c374a76

Browse files
committed
Proof of concept for a hot-reload test case
1 parent 5910c0e commit c374a76

6 files changed

Lines changed: 126 additions & 16 deletions

File tree

test/project/example.gdextension

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
entry_symbol = "example_library_init"
44
compatibility_minimum = "4.1"
5+
reloadable = true
56

67
[libraries]
78

test/project/reload.gd

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@tool
2+
extends Example
3+
4+
func _notification( what : int ):
5+
if what == NOTIFICATION_EXTENSION_RELOADED:
6+
print("gdscript: NOTIFICATION_EXTENSION_RELOADED")
7+
8+
func _ready() -> void:
9+
custom_signal.connect(func( msg, value) -> void:
10+
print("GSExtension Reloaded : %s, %d" % [msg, value])
11+
get_tree().quit(0))
12+
await get_tree().create_timer(5).timeout
13+
print("Timed Out.")
14+
get_tree().quit(1)

test/project/reload.tscn

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[gd_scene load_steps=2 format=3 uid="uid://b4wrd36npp8ov"]
2+
3+
[ext_resource type="Script" uid="uid://cfsxgbu2p7sl3" path="res://reload.gd" id="1_icw2l"]
4+
5+
[node name="Reload" type="Example"]
6+
script = ExtResource("1_icw2l")

test/src/example.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ void Example::_notification(int p_what) {
114114
rpc_config("test_rpc", opts);
115115
}
116116
//UtilityFunctions::print("Notification: ", String::num(p_what));
117+
if ( p_what == NOTIFICATION_EXTENSION_RELOADED) {
118+
UtilityFunctions::print( "NOTIFICATION_EXTENSION_RELOADED" );
119+
emit_custom_signal("NOTIFICATION_EXTENSION_RELOADED ",0);
120+
reloaded_count++;
121+
}
117122
}
118123

119124
bool Example::_set(const StringName &p_name, const Variant &p_value) {
@@ -302,6 +307,12 @@ void Example::_bind_methods() {
302307

303308
BIND_CONSTANT(CONSTANT_WITHOUT_ENUM);
304309
BIND_ENUM_CONSTANT(OUTSIDE_OF_CLASS);
310+
311+
312+
// Test extension reloading
313+
ClassDB::bind_method(D_METHOD("get_reload_count"), &Example::get_reload_count);
314+
ClassDB::bind_method(D_METHOD("set_reload_count"), &Example::set_reload_count);
315+
ADD_PROPERTY(PropertyInfo(Variant::INT, "reload_count"), "set_reload_count", "get_reload_count");
305316
}
306317

307318
bool Example::has_object_instance_binding() const {
@@ -323,6 +334,15 @@ Example::~Example() {
323334
}
324335

325336
// Methods.
337+
338+
auto Example::get_reload_count() -> int {
339+
return reloaded_count;
340+
}
341+
342+
auto Example::set_reload_count(const int value ) -> void {
343+
reloaded_count = value;
344+
}
345+
326346
void Example::simple_func() {
327347
emit_custom_signal("simple_func", 3);
328348
}

test/src/example.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class Example : public Control {
8080
String _to_string() const;
8181

8282
private:
83+
int reloaded_count = 0;
8384
Vector2 custom_position;
8485
Vector3 property_from_list;
8586
Vector2 dprop[3];
@@ -107,6 +108,9 @@ class Example : public Control {
107108
Example();
108109
~Example();
109110

111+
auto get_reload_count() -> int;
112+
auto set_reload_count(int value) -> void;
113+
110114
// Functions.
111115
void simple_func();
112116
void simple_const_func() const;

test/src/register_types.cpp

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,38 +12,103 @@
1212
#include <godot_cpp/godot.hpp>
1313

1414
#include "example.h"
15+
#include "godot_cpp/classes/dir_access.hpp"
16+
#include "godot_cpp/classes/editor_interface.hpp"
17+
#include "godot_cpp/classes/engine.hpp"
18+
#include "godot_cpp/classes/file_access.hpp"
19+
#include "godot_cpp/classes/gd_extension_manager.hpp"
1520
#include "tests.h"
1621

1722
using namespace godot;
1823

24+
// Global flag to control the timer thread
25+
std::atomic keep_running(true);
26+
27+
// Function to simulate extension reload
28+
void trigger_extension_reload() {
29+
if (Engine::get_singleton()->is_editor_hint()) {
30+
if (const EditorInterface *ei = EditorInterface::get_singleton(); ei->get_edited_scene_root()) {
31+
UtilityFunctions::print("Editor has a loaded scene: ", ei->get_edited_scene_root()->get_name());
32+
} else {
33+
return;
34+
}
35+
}
36+
37+
keep_running = false;
38+
const auto gdextension_path = String{"res://example.gdextension"};
39+
UtilityFunctions::print("Simulating GDExtension reload...");
40+
41+
// Reload all extensions
42+
if (GDExtensionManager *ext_manager = GDExtensionManager::get_singleton()) {
43+
ext_manager->call_deferred("reload_extension", gdextension_path );
44+
UtilityFunctions::print("GDExtensionManager::reload_extension(...) called successfully.");
45+
} else {
46+
UtilityFunctions::print("Error: GDExtensionManager singleton not available.");
47+
}
48+
}
49+
50+
// Timer thread function
51+
void timer_thread_function(const float interval_seconds) {
52+
// Single cycle for testing: sleep once, trigger reload, then exit (keep_running=false in trigger).
53+
while (keep_running) {
54+
std::this_thread::sleep_for(std::chrono::seconds(static_cast<int>(interval_seconds)));
55+
trigger_extension_reload();
56+
}
57+
}
58+
1959
void initialize_example_module(ModuleInitializationLevel p_level) {
20-
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
21-
return;
60+
if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) {
61+
UtilityFunctions::print( "Initializing Integration Testing Extension" );
62+
GDREGISTER_CLASS(ExampleRef);
63+
GDREGISTER_CLASS(ExampleMin);
64+
GDREGISTER_CLASS(Example);
65+
GDREGISTER_VIRTUAL_CLASS(ExampleVirtual);
66+
GDREGISTER_ABSTRACT_CLASS(ExampleAbstractBase);
67+
GDREGISTER_CLASS(ExampleConcrete);
68+
GDREGISTER_CLASS(ExampleBase);
69+
GDREGISTER_CLASS(ExampleChild);
70+
GDREGISTER_RUNTIME_CLASS(ExampleRuntime);
71+
GDREGISTER_CLASS(ExamplePrzykład);
72+
GDREGISTER_INTERNAL_CLASS(ExampleInternal);
2273
}
74+
else if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) {
75+
const String lock_path = "user://extension_timer_started";
76+
for ( const auto &arg : OS::get_singleton()->get_cmdline_args() ) {
77+
if (arg.begins_with("reload") && !FileAccess::file_exists(lock_path)) {
78+
79+
constexpr float reload_interval = 3.0f;
80+
UtilityFunctions::print("No lock file found; starting timer thread for hot-reload test.");
81+
std::thread timer_thread(timer_thread_function, reload_interval);
82+
timer_thread.detach(); // Detach the thread to run independently
2383

24-
GDREGISTER_CLASS(ExampleRef);
25-
GDREGISTER_CLASS(ExampleMin);
26-
GDREGISTER_CLASS(Example);
27-
GDREGISTER_VIRTUAL_CLASS(ExampleVirtual);
28-
GDREGISTER_ABSTRACT_CLASS(ExampleAbstractBase);
29-
GDREGISTER_CLASS(ExampleConcrete);
30-
GDREGISTER_CLASS(ExampleBase);
31-
GDREGISTER_CLASS(ExampleChild);
32-
GDREGISTER_RUNTIME_CLASS(ExampleRuntime);
33-
GDREGISTER_CLASS(ExamplePrzykład);
34-
GDREGISTER_INTERNAL_CLASS(ExampleInternal);
84+
// Create the lock file to prevent future inits from starting duplicates.
85+
if (const Ref<FileAccess> lock_file = FileAccess::open(lock_path, FileAccess::WRITE); lock_file.is_valid()) {
86+
lock_file->close();
87+
} else {
88+
UtilityFunctions::print("Warning: Failed to create lock file.");
89+
}
90+
} else {
91+
UtilityFunctions::print("Lock file exists; skipping timer thread start (already triggered reload).");
92+
}
93+
}
94+
}
3595
}
3696

3797
void uninitialize_example_module(ModuleInitializationLevel p_level) {
38-
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
39-
return;
98+
if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) {
99+
UtilityFunctions::print( "Uninitializing Integration Testing Extension" );
100+
}
101+
else if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) {
102+
// Stop the timer thread when deinitializing
103+
keep_running = false;
40104
}
41105
}
42106

107+
43108
extern "C" {
44109
// Initialization.
45110
GDExtensionBool GDE_EXPORT example_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) {
46-
godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization);
111+
GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization);
47112

48113
init_obj.register_initializer(initialize_example_module);
49114
init_obj.register_terminator(uninitialize_example_module);

0 commit comments

Comments
 (0)