Skip to content

feat: LuaCallable#224

Merged
gilzoide merged 12 commits into
gilzoide:mainfrom
illarn:main
Apr 20, 2026
Merged

feat: LuaCallable#224
gilzoide merged 12 commits into
gilzoide:mainfrom
illarn:main

Conversation

@illarn
Copy link
Copy Markdown
Contributor

@illarn illarn commented Apr 9, 2026

Goal

Make it possible to create callables for godot's signals from lua

Changes

Known limitations

  • Binding arguments does nothing
  • No return values

These features do not contribute to this PR goal and are redundant since you can just use lua functions if you need this

TODO

  • Right now explicitly calling :call() on a LuaCallable freezes the game. While it's not intended to be used this way adding some error and/or a guard for this sounds reasonable. Or maybe a comment in the lua definition will be enough, not really sure

@illarn
Copy link
Copy Markdown
Contributor Author

illarn commented Apr 9, 2026

Builded and tested in a linux environment only

I didn't use include guards like other scripts I've seen and may've broken some other styling rules, so pls tell me if I have to change it

Also I've tried using a LuaCoroutine to solve a call freezing issue, but either I didn't understand how it's supposed to be used or did something wrong, anyways, it didn't work

And I did a somewhat hacky thing with the lua definition, since I didn't really have time to delve into this part, if I should have done it differently pls tell/change

Comment thread src/utils/LuaCallable.cpp Outdated
Comment on lines +7 to +9
LuaCallable::~LuaCallable() {
_lua_func.unref();
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

You don't really need to manually call unref, the Ref's destructor already does that, so a default destructor would be enough here.

Comment thread src/utils/LuaCallable.hpp Outdated
class LuaCallable : public CallableCustom {
Ref<LuaFunction> _lua_func;
public:
explicit LuaCallable(sol::function func) : _lua_func{memnew(LuaFunction(func))} {};
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Please use sol::protected_function here instead of sol::function, just like everywhere else in the code.

Also do not create a new LuaFunction here, use LuaObject::wrap_object<LuaFunction>(sol::protected_function f) instead.

Comment thread src/utils/VariantType.cpp Outdated
#include "Class.hpp"
#include "VariantArguments.hpp"
#include "convert_godot_lua.hpp"
#include "godot_cpp/variant/callable.hpp"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Includes for 3rd party headers live in the block below and use angled brackets #include <godot_cpp/...>. In any case, I don't think you need this include here, Godot's Variant types are reachable by the other headers already included. But feel free to keep it, since it's used directly, just put it in the other block.

Copy link
Copy Markdown
Contributor Author

@illarn illarn Apr 12, 2026

Choose a reason for hiding this comment

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

My bad, it's all auto-incudes from clangd. It's probably a good idea to add .clang-format for auto sorting and correct brackets, I can add it if you want

Comment thread src/utils/VariantType.cpp Outdated
#include <godot_cpp/classes/resource.hpp>
#include <godot_cpp/classes/script.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include "LuaCallable.hpp"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Includes for Lua GDExtension's own headers live in the block above and are sorted alphabetically.

Comment thread src/utils/VariantType.cpp Outdated
return dictionary;
}
} else if (first_arg_type == sol::type::function) {
return LuaCallable::construct(args.get<sol::function>());
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Use sol::protected_function instead of sol::function to match the rest of the code.

Comment thread src/utils/VariantType.cpp Outdated
Comment on lines +36 to +37
#include "sol/forward.hpp"
#include "sol/types.hpp"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

You also don't really need these, VariantType.hpp already includes sol.hpp. It's ok to leave them here, but use angled brackets<> for 3rd party code instead of double quotes "" at least.

Comment thread src/utils/LuaCallable.hpp Outdated
virtual CompareLessFunc get_compare_less_func() const override;
uint32_t hash() const override;
void call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, GDExtensionCallError &r_call_error) const override;
static void register_lua(lua_State *L);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Why does it have this register_lua method? I don't really see this being called or even implemented in LuaCallable.cpp. Seems like it should be removed.

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.

Yeah, you're right, it's leftovers from when I tested, I'll remove it

Comment thread src/utils/LuaCallable.hpp
void call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, GDExtensionCallError &r_call_error) const override;
static void register_lua(lua_State *L);
int get_argument_count(bool &r_is_valid) const override;
static Variant construct(sol::function func);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Also please use sol::protected_function here instead of sol::function.

Comment thread src/utils/LuaCallable.cpp Outdated
Comment on lines +35 to +38
int LuaCallable::get_argument_count(bool &r_is_valid) const {
r_is_valid = true;
return 0;
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I don't think you need to override get_argument_count. CallableCustomBase::get_argument_count already does pretty much the same thing, although setting r_is_valid to false, which I think is the correct thing to do in this case (this argument count of 0 isn't really valid, the function may have more than 0 parameters, you could only know with a LuaDebug in Lua 5.2+).

@gilzoide
Copy link
Copy Markdown
Owner

Hi @illarn, thanks for the contribution!

I only checked the code for now and added some comments.

About these "known limitations", did you test if they only happen in Lua or if it happens in GDScript as well? I really don't think it's ok to have Callables that don't properly bind arguments or return values.

Also, the freezing issue should most likely be fixed before we let people use this functionality. I'll take a look into it here and let you know if I find the cause in my tests. Just by reading the code everything seems fine.

Comment thread test/lua_tests/callable.lua Outdated
Comment on lines +3 to +8
local a = 1
local custom_callable = Callable(function(val)
a = a + val
print(a == 6 and "Passed" or "Failed")
end)
custom_callable:call_deferred(5)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Automated tests should assert instead of only printing, the test runner only understands thrown errors and do not look into printed text.

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.

Yeah, this test is not very good, I'll change it. I completely missed the Makefile, so I've run tests wrong, I'll do it properly now

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

There's no documentation of the test setup at all, so don't worry! Maybe I should have told you here as well, sorry about that. Just run make test and there you go (or run the command line from Makefile if you don't have make installed) 😄

@gilzoide
Copy link
Copy Markdown
Owner

I tested calling the LuaCallable here without :call, using the regular function call style (custom_callable(5) instead of custom_callable:call(5)) and it works fine. Using :call gives an error because it's calling Variant::call instead of Callable::call, which expects the method name as first argument, this error is likely what makes your game freeze. Lua uses Variants for simplicity instead of defining each variant type separately.

@gilzoide
Copy link
Copy Markdown
Owner

gilzoide commented Apr 12, 2026

Also tested bind and returning values and it's all working fine:

local bound_callable = custom_callable:bind(5)
local result = bound_callable()
print(result)

@illarn
Copy link
Copy Markdown
Contributor Author

illarn commented Apr 12, 2026

Thank you for the feedback, I'll go fix it now

Also tested bind and returning values and it's all working fine:

Yeah, for me it also started working, I was sleepy and probably messed something up when testing it initially

@illarn
Copy link
Copy Markdown
Contributor Author

illarn commented Apr 12, 2026

I've pushed fixes, fixed everything from the comments, fixed the test and added a few more cases. I've also added callable from function to the README. Calling via () doesn't freeze, returning and binding arguments works, so I think everything should be ready now

@illarn
Copy link
Copy Markdown
Contributor Author

illarn commented Apr 13, 2026

I've added .clang-format, tried to make it as non-obstructive as possible, so it follows existing code style almost 1/1

@gilzoide
Copy link
Copy Markdown
Owner

Hey @illarn, thanks for the updates.

Everything looks fine now, I'll just let the CI do its thing before merging.

Just wanted to say that the bind you are making in the test doesn't really test anything. Callable.bind returns a copy of the callable with the bound arguments, you have to make the call from the returned Callable and not the original one!
Now that I'm rethinking, the last assert in the test might be broken 🤔

.clang-format is one of those little organization things that all projects should have, and yet I never have setup in my projects 😅 Thanks for bringing that in!

@illarn
Copy link
Copy Markdown
Contributor Author

illarn commented Apr 14, 2026

Just wanted to say that the bind you are making in the test doesn't really test anything. Callable.bind returns a copy of the callable with the bound arguments, you have to make the call from the returned Callable and not the original one!

fixed, forgot to commit this and lost in autostash when switching branches, lol (sorry for the wasted CI run)

@gilzoide
Copy link
Copy Markdown
Owner

Nice, now everything should work fine. Again, I'll just wait for the CI to do its thing and if everything is fine, merge afterwards.

Thanks again for the contribution!

@gilzoide gilzoide merged commit c41f076 into gilzoide:main Apr 20, 2026
68 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants