Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -390,3 +390,5 @@ $RECYCLE.BIN/
*.msp
*.lnk
*.generated.props
.gdbinit
CLAUDE.md
5 changes: 5 additions & 0 deletions core/extension/extension_api_dump.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
#include "core/templates/pair.h"
#include "core/version.h"

// Forward declaration for struct type
class GDScriptStructInstance;

Comment on lines +43 to +45
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this methodology

#ifdef TOOLS_ENABLED
#include "editor/doc/editor_help.h"

Expand Down Expand Up @@ -210,6 +213,7 @@ Dictionary GDExtensionAPIDump::generate_extension_api(bool p_include_docs) {
{ Variant::NODE_PATH, ptrsize_32, ptrsize_64, ptrsize_32, ptrsize_64 },
{ Variant::RID, sizeof(uint64_t), sizeof(uint64_t), sizeof(uint64_t), sizeof(uint64_t) },
{ Variant::OBJECT, ptrsize_32, ptrsize_64, ptrsize_32, ptrsize_64 },
{ Variant::STRUCT, ptrsize_32, ptrsize_64, ptrsize_32, ptrsize_64 },
{ Variant::CALLABLE, sizeof(Callable), sizeof(Callable), sizeof(Callable), sizeof(Callable) }, // Hardcoded align.
{ Variant::SIGNAL, sizeof(Signal), sizeof(Signal), sizeof(Signal), sizeof(Signal) }, // Hardcoded align.
{ Variant::DICTIONARY, ptrsize_32, ptrsize_64, ptrsize_32, ptrsize_64 },
Expand Down Expand Up @@ -252,6 +256,7 @@ Dictionary GDExtensionAPIDump::generate_extension_api(bool p_include_docs) {
static_assert(type_size_array[Variant::NODE_PATH][sizeof(void *)] == sizeof(NodePath), "Size of NodePath mismatch");
static_assert(type_size_array[Variant::RID][sizeof(void *)] == sizeof(RID), "Size of RID mismatch");
static_assert(type_size_array[Variant::OBJECT][sizeof(void *)] == sizeof(Object *), "Size of Object mismatch");
static_assert(type_size_array[Variant::STRUCT][sizeof(void *)] == sizeof(GDScriptStructInstance *), "Size of Struct mismatch");
static_assert(type_size_array[Variant::CALLABLE][sizeof(void *)] == sizeof(Callable), "Size of Callable mismatch");
static_assert(type_size_array[Variant::SIGNAL][sizeof(void *)] == sizeof(Signal), "Size of Signal mismatch");
static_assert(type_size_array[Variant::DICTIONARY][sizeof(void *)] == sizeof(Dictionary), "Size of Dictionary mismatch");
Expand Down
10 changes: 10 additions & 0 deletions core/extension/gdextension_interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,11 @@ static GDExtensionPtrOperatorEvaluator gdextension_variant_get_ptr_operator_eval
return (GDExtensionPtrOperatorEvaluator)Variant::get_ptr_operator_evaluator(Variant::Operator(p_operator), Variant::Type(p_type_a), Variant::Type(p_type_b));
}
static GDExtensionPtrBuiltInMethod gdextension_variant_get_ptr_builtin_method(GDExtensionVariantType p_type, GDExtensionConstStringNamePtr p_method, GDExtensionInt p_hash) {
// STRUCT type doesn't support built-in methods
if (p_type == (int)Variant::STRUCT) {
return nullptr;
}

const StringName method = *reinterpret_cast<const StringName *>(p_method);
uint32_t hash = Variant::get_builtin_method_hash(Variant::Type(p_type), method);
if (hash != p_hash) {
Expand All @@ -827,6 +832,11 @@ static GDExtensionPtrBuiltInMethod gdextension_variant_get_ptr_builtin_method(GD
return (GDExtensionPtrBuiltInMethod)Variant::get_ptr_builtin_method(Variant::Type(p_type), method);
}
static GDExtensionPtrConstructor gdextension_variant_get_ptr_constructor(GDExtensionVariantType p_type, int32_t p_constructor) {
// STRUCT type doesn't support constructors (yet)
if (p_type == (int)Variant::STRUCT) {
return nullptr;
}

return (GDExtensionPtrConstructor)Variant::get_ptr_constructor(Variant::Type(p_type), p_constructor);
}
static GDExtensionPtrDestructor gdextension_variant_get_ptr_destructor(GDExtensionVariantType p_type) {
Expand Down
116 changes: 116 additions & 0 deletions core/io/json.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,40 @@
#include "core/config/engine.h"
#include "core/object/script_language.h"
#include "core/variant/container_type_validate.h"
#include "core/variant/variant_internal.h"

// GDScript struct serialization support
// Guarded with MODULE_GDSCRIPT_ENABLED since core/io can't depend on modules/gdscript
#ifdef MODULE_GDSCRIPT_ENABLED
// Forward declaration for GDScript struct serialization
class GDScriptStructInstance;
class GDScriptStruct;

// We need to declare these methods without including the GDScript headers
// This is a bit of a hack, but necessary due to layering (core/io can't include modules/gdscript)
// In the future, this should be replaced with a proper Variant-level API

extern "C" {
// These will be defined in gdscript.cpp with C linkage to avoid name mangling issues
void gdscript_struct_instance_serialize(const GDScriptStructInstance *p_instance, Dictionary &r_dict);
bool gdscript_struct_instance_get_type_name(const GDScriptStructInstance *p_instance, String &r_name);
}

// Helper function to serialize a struct
static Dictionary _serialize_struct(const GDScriptStructInstance *p_instance) {
ERR_FAIL_NULL_V(p_instance, Dictionary());

// Call the GDScript helper function
Dictionary dict;
gdscript_struct_instance_serialize(p_instance, dict);
return dict;
}
#else
// Stub implementation when GDScript module is disabled
static Dictionary _serialize_struct(const void *p_instance) {
ERR_FAIL_V_MSG(Dictionary(), "STRUCT serialization requires MODULE_GDSCRIPT_ENABLED.");
}
#endif // MODULE_GDSCRIPT_ENABLED

const char *JSON::tk_name[TK_MAX] = {
"'{'",
Expand Down Expand Up @@ -824,6 +858,50 @@ Variant JSON::_from_native(const Variant &p_variant, bool p_full_objects, int p_
return ret;
} break;

#ifdef MODULE_GDSCRIPT_ENABLED
case Variant::STRUCT: {
// Serialize struct as tagged dictionary with __type__ metadata
// This allows round-trip deserialization
const GDScriptStructInstance *struct_instance = reinterpret_cast<const GDScriptStructInstance *>(VariantInternal::get_struct(&p_variant));
ERR_FAIL_NULL_V(struct_instance, Variant());

Dictionary ret;
ret[TYPE] = Variant::get_type_name(p_variant.get_type());

// Get the serialized data from the struct instance using helper
Dictionary struct_data = _serialize_struct(struct_instance);

// Ensure __type__ field exists (it should from serialize())
if (!struct_data.has("__type__")) {
ERR_FAIL_V_MSG(Variant(), "Struct serialization failed: missing __type__ field.");
}

// Encode field values using _from_native to ensure proper JSON encoding
// This handles non-JSON-native types (nested structs, objects, typed arrays, etc.)
Dictionary encoded_struct_data;
encoded_struct_data["__type__"] = struct_data["__type__"]; // Copy type identifier as-is

for (const KeyValue<Variant, Variant> &kv : struct_data) {
if (kv.key == "__type__") {
continue; // Already copied above
}
// Encode the value using _from_native, keys are strings (field names) and don't need encoding
encoded_struct_data[kv.key] = _from_native(kv.value, p_full_objects, p_depth + 1);
}

// Wrap the encoded serialized data in the expected format
Array args;
args.push_back(encoded_struct_data);
ret[ARGS] = args;

return ret;
} break;
#else
case Variant::STRUCT: {
ERR_FAIL_V_MSG(Variant(), "STRUCT serialization requires MODULE_GDSCRIPT_ENABLED.");
} break;
#endif // MODULE_GDSCRIPT_ENABLED

case Variant::DICTIONARY: {
const Dictionary dict = p_variant;

Expand Down Expand Up @@ -1297,6 +1375,44 @@ Variant JSON::_to_native(const Variant &p_json, bool p_allow_objects, int p_dept
// Nothing to do at this stage. `Object` should be treated as a class, not as a built-in type.
} break;

#ifdef MODULE_GDSCRIPT_ENABLED
case Variant::STRUCT: {
// Deserialize struct from tagged dictionary
LOAD_ARGS();

ERR_FAIL_COND_V_MSG(args.size() != 1, Variant(), "Invalid struct data: expected single dictionary argument.");

Dictionary encoded_data = args[0];
ERR_FAIL_COND_V_MSG(!encoded_data.has("__type__"), Variant(), "Invalid struct data: missing __type__ field.");

String type_name = encoded_data["__type__"];
ERR_FAIL_COND_V_MSG(type_name.is_empty(), Variant(), "Invalid struct data: empty __type__ field.");

// Decode field values using _to_native to properly reconstruct non-JSON-native types
// This handles nested structs, objects, typed arrays, etc.
Dictionary decoded_struct_data;
decoded_struct_data["__type__"] = encoded_data["__type__"]; // Copy type identifier as-is

for (const KeyValue<Variant, Variant> &kv : encoded_data) {
if (kv.key == "__type__") {
continue; // Already copied above
}
// Decode the value using _to_native, keys are strings (field names) and don't need decoding
decoded_struct_data[kv.key] = _to_native(kv.value, p_allow_objects, p_depth + 1);
}

// Use ScriptServer to create the struct instance with decoded data
Variant struct_instance = ScriptServer::create_struct_instance(type_name, decoded_struct_data);
ERR_FAIL_COND_V_MSG(struct_instance.get_type() != Variant::STRUCT, Variant(), vformat("Failed to create struct instance for type '%s'.", type_name));

return struct_instance;
} break;
#else
case Variant::STRUCT: {
ERR_FAIL_V_MSG(Variant(), "STRUCT deserialization requires MODULE_GDSCRIPT_ENABLED.");
} break;
#endif // MODULE_GDSCRIPT_ENABLED

case Variant::DICTIONARY: {
LOAD_ARGS_CHECK_FACTOR(2);

Expand Down
34 changes: 34 additions & 0 deletions core/io/marshalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@
#include <climits>
#include <cstdio>

// GDScript struct serialization support
// Guarded with MODULE_GDSCRIPT_ENABLED since core/io can't depend on modules/gdscript
#ifdef MODULE_GDSCRIPT_ENABLED

extern "C" {
// These functions are defined in modules/gdscript/gdscript.cpp with C linkage
Error gdscript_variant_encode_struct(const Variant &p_variant, uint8_t *r_buffer, int &r_len);
Error gdscript_variant_decode_struct(const uint8_t *p_buffer, int p_len, int *r_len, Variant &r_variant);
}

#endif // MODULE_GDSCRIPT_ENABLED

void EncodedObjectAsID::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_object_id", "id"), &EncodedObjectAsID::set_object_id);
ClassDB::bind_method(D_METHOD("get_object_id"), &EncodedObjectAsID::get_object_id);
Expand Down Expand Up @@ -1296,6 +1308,17 @@ Error decode_variant(Variant &r_variant, const uint8_t *p_buffer, int p_len, int
r_variant = varray;

} break;
#ifdef MODULE_GDSCRIPT_ENABLED
case Variant::STRUCT: {
// Decode struct via GDScript helper function
Error err = gdscript_variant_decode_struct(buf, len, r_len, r_variant);
ERR_FAIL_COND_V(err, err);
} break;
#else
case Variant::STRUCT: {
ERR_FAIL_V(ERR_BUG);
} break;
#endif
Comment on lines +1311 to +1321
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: STRUCT encode/decode will return wrong r_len/used and corrupt container marshaling.
encode_variant() sets r_len = 0 at entry; decode_variant() sets *r_len = 4 at entry. The gdscript helpers call those functions internally, so passing the outer r_len/r_len pointer into the helpers will overwrite the outer accounting (missing the STRUCT header bytes), causing buffer mis-advancement in arrays/dicts.

Proposed fix (use a temporary payload size and add it)
 #ifdef MODULE_GDSCRIPT_ENABLED
   case Variant::STRUCT: {
-    // Decode struct via GDScript helper function
-    Error err = gdscript_variant_decode_struct(buf, len, r_len, r_variant);
+    int payload_used = 0;
+    Error err = gdscript_variant_decode_struct(buf, len, &payload_used, r_variant);
     ERR_FAIL_COND_V(err, err);
+    if (r_len) {
+      (*r_len) += payload_used;
+    }
   } break;
 #else
   case Variant::STRUCT: {
     ERR_FAIL_V(ERR_BUG);
   } break;
 #endif
 #ifdef MODULE_GDSCRIPT_ENABLED
   case Variant::STRUCT: {
-    // Encode struct via GDScript helper function
-    Error err = gdscript_variant_encode_struct(p_variant, buf, r_len);
+    int payload_len = 0;
+    Error err = gdscript_variant_encode_struct(p_variant, buf, payload_len);
     ERR_FAIL_COND_V(err, err);
+    r_len += payload_len;
   } break;
 #else
   case Variant::STRUCT: {
     ERR_FAIL_V(ERR_BUG);
   } break;
 #endif

Also applies to: 2150-2160

🤖 Prompt for AI Agents
In @core/io/marshalls.cpp around lines 1311 - 1321, The STRUCT branch is
overwriting the outer r_len/used when delegating to the GDScript helpers;
instead call the helpers with a temporary payload length (e.g., uint32_t
payload_len = 0) and after the helper returns add the STRUCT header size (4)
plus payload_len into the outer r_len/used, preserving error handling (return
err on failure). Apply this pattern for both decode
(gdscript_variant_decode_struct) and encode paths
(gdscript_variant_encode_struct) at the STRUCT case sites (including the other
occurrence around the 2150-2160 region) so the outer buffer accounting advances
correctly without being clobbered by the inner call.

default: {
ERR_FAIL_V(ERR_BUG);
}
Expand Down Expand Up @@ -2124,6 +2147,17 @@ Error encode_variant(const Variant &p_variant, uint8_t *r_buffer, int &r_len, bo
r_len += sizeof(real_t) * 4 * len;

} break;
#ifdef MODULE_GDSCRIPT_ENABLED
case Variant::STRUCT: {
// Encode struct via GDScript helper function
Error err = gdscript_variant_encode_struct(p_variant, buf, r_len);
ERR_FAIL_COND_V(err, err);
} break;
#else
case Variant::STRUCT: {
ERR_FAIL_V(ERR_BUG);
} break;
#endif
default: {
ERR_FAIL_V(ERR_BUG);
}
Expand Down
14 changes: 14 additions & 0 deletions core/object/object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,20 @@ Variant Object::callp(const StringName &p_method, const Variant **p_args, int p_
Variant ret;
OBJ_DEBUG_LOCK

#ifdef MODULE_GDSCRIPT_ENABLED
// Special case for GDScriptStructClass to support struct constructors
// This is needed because GDScriptStructClass::callp needs to be called directly,
// but callp is not virtual so Object::callp doesn't dispatch to it.
if (get_class_name() == StringName("GDScriptStructClass")) {
// Forward to GDScriptStructClass::callp by using a Callable
Callable callable = Callable(this, p_method);
if (callable.is_valid()) {
callable.callp(p_args, p_argcount, ret, r_error);
return ret;
}
}
#endif

if (script_instance) {
ret = script_instance->callp(p_method, p_args, p_argcount, r_error);
// Force jump table.
Expand Down
42 changes: 42 additions & 0 deletions core/object/script_language.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,48 @@ void ScriptServer::save_global_classes() {
ProjectSettings::get_singleton()->store_global_class_list(gcarr);
}

/***************** STRUCT SERIALIZATION *****************/

Variant ScriptServer::create_struct_instance(const String &p_fully_qualified_name, const Dictionary &p_data) {
MutexLock lock(languages_mutex);
if (!languages_ready) {
ERR_FAIL_V_MSG(Variant(), "Cannot create struct instance: languages not initialized.");
}

// Try each language to see if it can create the struct
for (int i = 0; i < _language_count; i++) {
ScriptLanguage *lang = _languages[i];
if (lang && lang->can_create_struct_by_name()) {
Variant result = lang->create_struct_by_name(p_fully_qualified_name, p_data);
if (result.get_type() != Variant::NIL) {
return result;
}
}
}

ERR_FAIL_V_MSG(Variant(), vformat("Cannot create struct instance: no language supports struct '%s'.", p_fully_qualified_name));
}

bool ScriptServer::global_struct_exists(const String &p_fully_qualified_name) {
MutexLock lock(languages_mutex);
if (!languages_ready) {
return false;
}

// Check if any language can create this struct
for (int i = 0; i < _language_count; i++) {
ScriptLanguage *lang = _languages[i];
if (lang && lang->can_create_struct_by_name()) {
// Try to create with empty data to check if it exists
Variant result = lang->create_struct_by_name(p_fully_qualified_name, Dictionary());
if (result.get_type() != Variant::NIL) {
return true;
}
}
}
return false;
}
Comment on lines +553 to +593
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid calling ScriptLanguage virtuals under languages_mutex (deadlock + long critical section risk).
create_struct_by_name() can reasonably call back into ScriptServer / load scripts / allocate, and keeping the global mutex held across that is risky.

Proposed fix (snapshot languages under lock, then call outside)
 Variant ScriptServer::create_struct_instance(const String &p_fully_qualified_name, const Dictionary &p_data) {
-	MutexLock lock(languages_mutex);
-	if (!languages_ready) {
-		ERR_FAIL_V_MSG(Variant(), "Cannot create struct instance: languages not initialized.");
-	}
-
-	// Try each language to see if it can create the struct
-	for (int i = 0; i < _language_count; i++) {
-		ScriptLanguage *lang = _languages[i];
-		if (lang && lang->can_create_struct_by_name()) {
-			Variant result = lang->create_struct_by_name(p_fully_qualified_name, p_data);
-			if (result.get_type() != Variant::NIL) {
-				return result;
-			}
-		}
-	}
+	Vector<ScriptLanguage *> langs;
+	{
+		MutexLock lock(languages_mutex);
+		if (!languages_ready) {
+			ERR_FAIL_V_MSG(Variant(), "Cannot create struct instance: languages not initialized.");
+		}
+		langs.resize(_language_count);
+		for (int i = 0; i < _language_count; i++) {
+			langs.write[i] = _languages[i];
+		}
+	}
+
+	for (int i = 0; i < langs.size(); i++) {
+		ScriptLanguage *lang = langs[i];
+		if (lang && lang->can_create_struct_by_name()) {
+			Variant result = lang->create_struct_by_name(p_fully_qualified_name, p_data);
+			if (result.get_type() != Variant::NIL) {
+				return result;
+			}
+		}
+	}

 	ERR_FAIL_V_MSG(Variant(), vformat("Cannot create struct instance: no language supports struct '%s'.", p_fully_qualified_name));
 }
 bool ScriptServer::global_struct_exists(const String &p_fully_qualified_name) {
-	MutexLock lock(languages_mutex);
-	if (!languages_ready) {
-		return false;
-	}
-
-	// Check if any language can create this struct
-	for (int i = 0; i < _language_count; i++) {
-		ScriptLanguage *lang = _languages[i];
-		if (lang && lang->can_create_struct_by_name()) {
-			// Try to create with empty data to check if it exists
-			Variant result = lang->create_struct_by_name(p_fully_qualified_name, Dictionary());
-			if (result.get_type() != Variant::NIL) {
-				return true;
-			}
-		}
-	}
+	Vector<ScriptLanguage *> langs;
+	{
+		MutexLock lock(languages_mutex);
+		if (!languages_ready) {
+			return false;
+		}
+		langs.resize(_language_count);
+		for (int i = 0; i < _language_count; i++) {
+			langs.write[i] = _languages[i];
+		}
+	}
+
+	for (int i = 0; i < langs.size(); i++) {
+		ScriptLanguage *lang = langs[i];
+		if (lang && lang->can_create_struct_by_name()) {
+			Variant result = lang->create_struct_by_name(p_fully_qualified_name, Dictionary());
+			if (result.get_type() != Variant::NIL) {
+				return true;
+			}
+		}
+	}
 	return false;
 }
🤖 Prompt for AI Agents
In @core/object/script_language.cpp around lines 553 - 593, The code currently
calls virtuals like ScriptLanguage::create_struct_by_name while holding
languages_mutex in create_struct_instance and global_struct_exists; instead,
under languages_mutex snapshot languages_ready, _language_count and copy
non-null _languages pointers into a local small vector, then release the lock
and iterate that snapshot calling
can_create_struct_by_name/create_struct_by_name; keep the initial
languages_ready check inside the lock and use the copied ScriptLanguage*
pointers (from the snapshot) for all subsequent checks/calls so the mutex is not
held during callbacks or long operations.


Vector<Ref<ScriptBacktrace>> ScriptServer::capture_script_backtraces(bool p_include_variables) {
if (is_program_exiting) {
return Vector<Ref<ScriptBacktrace>>();
Expand Down
11 changes: 11 additions & 0 deletions core/object/script_language.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ class ScriptServer {
static void get_inheriters_list(const StringName &p_base_type, List<StringName> *r_classes);
static void save_global_classes();

// Struct serialization utilities (for GDScript)
static Variant create_struct_instance(const String &p_fully_qualified_name, const Dictionary &p_data);
static bool global_struct_exists(const String &p_fully_qualified_name);

static Vector<Ref<ScriptBacktrace>> capture_script_backtraces(bool p_include_variables = false);

static void init_languages();
Expand Down Expand Up @@ -446,6 +450,13 @@ class ScriptLanguage : public Object {
virtual bool handles_global_class_type(const String &p_type) const { return false; }
virtual String get_global_class_name(const String &p_path, String *r_base_type = nullptr, String *r_icon_path = nullptr, bool *r_is_abstract = nullptr, bool *r_is_tool = nullptr) const { return String(); }

/* STRUCT SERIALIZATION */
// These methods allow serialization systems to create struct instances without knowing about GDScript
virtual bool can_create_struct_by_name() const { return false; }
virtual Variant create_struct_by_name(const String &p_fully_qualified_name, const Dictionary &p_data) { return Variant(); }
virtual Dictionary struct_to_dict(const Variant &p_struct) const { return Dictionary(); }
virtual void get_struct_property_list(const Variant &p_struct, List<PropertyInfo> *p_list) const {}

virtual ~ScriptLanguage() {}
};

Expand Down
Loading
Loading