Skip to content

[GDExtension] Casting fails when virtual functions are added #610

Closed
@BastiaanOlij

Description

@BastiaanOlij

Further investigating why Ref<> isn't working properly I found a weird issue with casting. In C++ when inheriting it is possible that data structures shift and there is thus no guarantee that the pointer is the same. So:

BaseClass *base_class = (BaseClass *)sub_class;

Could result in the base_class pointer not being equal to the sub_class pointer.

The compiler deals with this nicely, so doing the following on our wrapper classes in GDExtensions:

Ref<XRInterfaceReference> interface;
interface.instantiate();

XRInterfaceReference *xr_interface_reference = interface.ptr();
XRInterfaceExtension *xr_interface_extension = (XRInterfaceExtension *)xr_interface_reference;
XRInterface *xr_interface = (XRInterface *)xr_interface_reference;
RefCounted *ref_counted = (RefCounted *)xr_interface_reference;
Object *object = (Object *)xr_interface_reference;

UtilityFunctions::print("XRInterfaceReference: ", Variant((uint64_t) xr_interface_reference),", owner: ", Variant((uint64_t) xr_interface_reference->_owner));
UtilityFunctions::print("XRInterfaceExtension: ", Variant((uint64_t) xr_interface_extension),", owner: ", Variant((uint64_t) xr_interface_extension->_owner));
UtilityFunctions::print("XRInterface: ", Variant((uint64_t) xr_interface),", owner: ", Variant((uint64_t) xr_interface->_owner));
UtilityFunctions::print("RefCounted: ", Variant((uint64_t) ref_counted),", owner: ", Variant((uint64_t) ref_counted->_owner));
UtilityFunctions::print("Object: ", Variant((uint64_t) object),", owner: ", Variant((uint64_t) object->_owner));

We can see this casts nicely:

XRInterfaceReference: 2226252139088, owner: 2226061244336
XRInterfaceExtension: 2226252139088, owner: 2226061244336
XRInterface: 2226252139096, owner: 2226061244336
RefCounted: 2226252139096, owner: 2226061244336
Object: 2226252139096, owner: 2226061244336

But also notice that from XRInterface our pointer is shifted 8 bytes, something is added to the XRInterfaceExtension data which the compiler puts in front of instead of after the data from the super class.

So for now not a problem, until we use Godots own casting logic:

RefCounted *ref_counted_casted = Object::cast_to<RefCounted>(xr_interface_reference);
UtilityFunctions::print("Casted: ", Variant((uint64_t) ref_counted_casted),", owner: ", Variant((uint64_t) ref_counted_casted->_owner));

Now our output is:

Casted: 2226252139088, owner: 140723262435304

We see that our original pointer is used but because this is incorrect for our RefCounted class we're reading the wrong memory for owner causing a crash if we attempt to use this object.

The reason for this is that our Object::cast_to<T> uses the owner to ask Godot to perform the cast, and then lookup the wrapper pointer belonging to that owner, which is still our subclass' pointer.

This approach to casting thus only works if there is no shift in the data.

So far my theory is that the shift is caused by the introduction of a vtable for the new virtual functions that were introduced and the pointer to this vtable precedes the subclass struct.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugThis has been identified as a bugtopic:gdextensionThis relates to the new Godot 4 extension implementation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions