Skip to content

Allow ScriptExtension implementations to override callp#118595

Open
Naros wants to merge 1 commit intogodotengine:masterfrom
Naros:scriptextension-callp-override
Open

Allow ScriptExtension implementations to override callp#118595
Naros wants to merge 1 commit intogodotengine:masterfrom
Naros:scriptextension-callp-override

Conversation

@Naros
Copy link
Copy Markdown
Contributor

@Naros Naros commented Apr 15, 2026

Fixes godotengine/godot-proposals#14614

Description

Introduces a virtual override handler on ScriptExtension for hierarchy calls via callp, delegating them to the underlying script implementation and enabling GDExtension-based scripting languages to handle calls from GDScript or CSharp that invoke statically declared script functions.

@Naros Naros requested a review from a team as a code owner April 15, 2026 02:56
@Naros
Copy link
Copy Markdown
Contributor Author

Naros commented Apr 15, 2026

@Ivorforce @dsnopek here's my first pass at the implementation from today's GDExtension call. I used the Dictionary approach as we had discussed, and chose to handle it by using the following mapping:

  1. Expects a Dictionary return
  2. Must supply a value key, even if that points to a Variant() for errors.
  3. Optional error key that is a dictionary, with sub-dictionary keys that map to error, argument, expected struct components of the Callable::CallError engine side struct.

Let me know what you think.

@Naros Naros force-pushed the scriptextension-callp-override branch from 7b5440f to 7a17cd2 Compare April 15, 2026 03:00
@AThousandShips AThousandShips added this to the 4.x milestone Apr 15, 2026
@Naros Naros force-pushed the scriptextension-callp-override branch from 7a17cd2 to 4bbc139 Compare April 15, 2026 08:45
@Naros Naros requested a review from a team as a code owner April 15, 2026 08:45
Comment thread core/object/script_language_extension.h Outdated
if (ret.has("result")) {
Variant result = ret["result"];
if (ret.has("error")) {
Dictionary err = ret["error"];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So, the structure is:

{
  'value': <variant>,
  'error': {
    'error': 0, // CALL_OK
    'argument': 1, // just example values...
    'expected': 1,
  }
}

I'm not sure about having the nested Dictionary. Is there any "prior art" for this layout?

Alternatively, we could do this with a flat structure like:

{
  'value': <variant>,
  'error': 0, // CALL_OK
  'argument': 1, // just example values...
  'expected': 1,
} 

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I looked across the code base, and there isn't any precedent for returning the CallError this way, so I can certainly make it a flat structure; that's not an issue.

@Naros Naros force-pushed the scriptextension-callp-override branch from 4bbc139 to bc114c5 Compare April 15, 2026 14:20

Dictionary ret;
if (GDVIRTUAL_CALL(_callp, p_method, args, ret)) {
if (ret.has("result")) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So, if the dictionary doesn't have a "result" key, then this falls back on Script::callp(). Is that what we want?

I could see an argument that this should could as either (a) CALL_OK for a static method with no return value, or (b) an error because we're requiring all GDExtensions to return a "result" here

Copy link
Copy Markdown
Contributor Author

@Naros Naros Apr 16, 2026

Choose a reason for hiding this comment

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

My reasoning here is that normally I would implement _callp like this:

Variant OScript::_callp_internal(const StringName& p_method, const Variant** p_args, int p_argcount, GDExtensionCallError& r_error) {
  HashMap<StringName, OScriptFunc*>::ConstIterator E = _member_functions.find(p_method);
  if (E) {
    ERR_FAIL_COND_V_MSG(!E->value->is_static(), Variant(), "Cannot call non-static function '" + p_method + "' in script.");
    return E->value->call(nullptr, p_args, p_argcount, r_error);
  }
  if (native.is_valid()) {
    return native->callp(p_method, p_args, p_argcount, r_error);
  }
  r_error.error = GDEXTENSION_CALL_ERROR_INVALID_METHOD;
  return Variant();
}

Dictionary OScript::_callp(const StringName& p_method, const Array& p_args) {
  Vector<const Variant*> argptrs;
  argptrs.resize(p_args.size());
  for (int i = 0; i < p_args.size(); i++) {
    argptrs.write[i] = &p_args[i];
  }

  Dictionary ret;
  GDExtensionCallError r_error;
  ret["result"] = _callp_internal(p_method, argsptr, argptrs.size(), r_error);
  
  if (r_error.error != GDEXTENSION_CALL_OK) {
    ret["error"] = r_error.error;
    ret["expected"] = r_error.expected;
    ret["argument"] = r_error.argument;
  }

  return ret;
}

The compiled function always returns an uninitialized Variant if the script function is void or fails with an error, and I get an initialized Variant if the function call succeeds, because to the VM it does not care whether the function's entry call is or is not static.

So my assumption here is I pass "result" and the optional "error" all the way back up the chain from the VM output. This way, for certain Godot functions that have a precedent for returning a String as the "result" to provide an error reason, while setting "error" to the generic error code, this avoids my _callp override from having to check this use case.

While I could do this, I don't like how it looks and I think passing result up if the VM was called logically makes more functional sense from a Godot PoV.

Variant result = _callp_internal(...);
if (result != Variant()) {
  ret["result"] = result;
}

I can see both ways, so I guess it's up to what everyone prefers as the "Godot way".

I left an empty Dictionary as a way for implementations, if they really want it to delegate up to Object, they have that option as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Script Static Function Calls via ScriptExtension

3 participants