godot: Virtual functions called on objects produced by GDExtension doesn't work on languages other than C++

Godot version

4.0.alpha12

System information

Windows 10

Issue description

When you register a new class from GDExtension, class_get_default_property_value instantiates it through calling create_instance_func passed into GDNativeExtensionClassCreationInfo. Then it gets casted to Object: https://github.com/godotengine/godot/blob/a9e4eac7b965450234c7a4fb2de1346a23ca67f2/core/object/class_db.cpp#L1429 After which ‘get_property_list’ gets called on this Object. And inside of it there are at least 2 calles to virtual functions. Like this one: https://github.com/godotengine/godot/blob/a9e4eac7b965450234c7a4fb2de1346a23ca67f2/core/object/object.cpp#L505 This calles would only makes sense if the object has __vfptr table, which is C++ specific. Pointof GDExtensions is to allow for other languages to interact with Godot, which this issue limits significantly.

Steps to reproduce

You can return C struct from create_instance_func in your GDExtension and see how Godot crashes because of access violation. I’m not sure how to describe steps to reproduce otherwise.

Minimal reproduction project

No response

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 4
  • Comments: 21 (16 by maintainers)

Commits related to this issue

Most upvoted comments

@pcting @VladoCC The PR attached to this should be ready for testing. Would you be able to take some time to see if it works for your use cases? There are still open questions about how these fields get added in 4.2 without breaking compatibility with 4.1 extensions.

This is also an issue for Dart (where I’ve used a template metaprogramming aproach to get around it. A similar problem is going to exist for me for GDExtensionInstanceBindingCallbacks as well, as I need to be able to wrap the binding funcitons to jump to the main Dart thread. A single piece of user data would help immensely.

Not sure if I should create a separate issue for the binding callbacks?

Opened a draft PR for this anyway, assuming we don’t want to add complexity to Object destruction.

My opinion is that compatability doesn’t matter in this case.

  1. This feature was supposed to be finished before beta. At that stage there wasn’t any comparability concerns.
  2. Coming straight from the 1. It’s been a year since creation of this issue. Any solution would be better than none.
  3. Compatability with what would be broken? As far as I know there’s not much of extensions finished right now, which would be affected.
  4. Even if compatability us a concern, this can be a feature for version 4.x. Major versions are expected to not be fully compatable with previous ones as far as I know.

but they still doesn’t know what functions they are expected to supply

Well I’ve encountered this as a challenge whilst writing a Go extension interface, it would be nice if GDNativeExtensionClassCallVirtual passed some sort of userdata argument. My workaround was to generate C functions callMethod1, callMethod2 up to callMethod255 since C doesn’t have closures, this allows me to identify which method is being called (limited to having 255 methods attached to an exported type at the moment).

Ie. like this:

extern uint8_t goClassGetVirtual(void *classID, const char *p_name);
extern void goMethodCallDirect(uintptr_t methodIndex, uintptr_t instance, const GDNativeTypePtr *p_args, GDNativeTypePtr r_ret);

void goClassCallVirtual1(GDExtensionClassInstancePtr p_instance, const GDNativeTypePtr *p_args, GDNativeTypePtr r_ret) {goMethodCallDirect((uintptr_t)1, (uintptr_t)p_instance,  p_args, r_ret);}
void goClassCallVirtual2(GDExtensionClassInstancePtr p_instance, const GDNativeTypePtr *p_args, GDNativeTypePtr r_ret) {goMethodCallDirect((uintptr_t)2, (uintptr_t)p_instance,  p_args, r_ret);}
void goClassCallVirtual255(GDExtensionClassInstancePtr p_instance, const GDNativeTypePtr *p_args, GDNativeTypePtr r_ret) {goMethodCallDirect((uintptr_t)255, (uintptr_t)p_instance,  p_args, r_ret);}

GDNativeExtensionClassCallVirtual get_virtual_func(void *p_userdata, const char *p_name) {
  switch (goClassGetVirtual(p_userdata, p_name)) {
	  case 1: return goClassCallVirtual1;
	  case 2: return goClassCallVirtual2;
	  ...
	case 255: return goClassCallVirtual255;
	default: return NULL;
  }
}

Which language are you working with?

I’ve also run into the exact same issue in chat a couple weeks back

In summary, you cannot call a go method directly from C as only functions can be exported to C. So, you have to workaround this by setting up virtual function callback in a non-idiomatic go way.

I was able to implement an example _ready() function shown here in my godot-go library:

//export Example_Ready
func Example_Ready(inst unsafe.Pointer) {
	log.Info("Example_Ready called")

	...
}

as opposed to a method “attached” to a go struct… something like this:

func (e *Example) Ready() {
	log.Info("Example_Ready called")

	...
}

this effectively requires users of the library to be exposed to cgo as part of the library DSL, which is not ideal for the average go developer. the average go developer won’t know the intricacies of working in cgo.

To get this working, you also have to wrap the callback in C code:

void cgo_callback_example_ready(GDExtensionClassInstancePtr p_instance, const GDExtensionTypePtr *p_args, GDExtensionTypePtr r_ret) {
	Example_Ready((void*)(p_instance));
}

this functions can now be registered in go code when initializing the custom GDClass:

gdextension.ClassDBBindMethodVirtual(t, "_ready", (gdextensionffi.GDExtensionClassCallVirtual)(C.cgo_callback_example_ready))

if there was a way to pass a user_data as a parameter, we can hide all this behind the scenes and abstracted away from users of the godot-go library. 💯

maybe a “virtual function call mode” can be provided when registering the GDExtension with Godot… either to keep the current behavior or add in a user_data parameter for GDExtensions that can benefit from it?

@reduz for my use case, a userdata param would provide the flexibility for me to store a pointer to the go function as the userdata so that i can have a single go function exported to C as a gateway to call into go