ThePhD / sol2

Sol3 (sol2 v3.0) - a C++ <-> Lua API wrapper with advanced features and top notch performance - is here, and it's great! Documentation:
http://sol2.rtfd.io/
MIT License
4.06k stars 493 forks source link

Getting the return type of a Lua function #1500

Closed Razakhel closed 1 year ago

Razakhel commented 1 year ago

Hey,

First of all, thanks for this excellent library!

To give some context for my issue/question, I'm using sol2 to add scripting capabilities to my game engine. I'm aiming to allow giving a Lua script containing a specific function returning a boolean, and said function would be run every frame. To check that it does actually return a boolean, I'm currently trying to get the return type of a Lua function, without having to execute it as it is not supposed to be at that point.

I've seen from the tutorials that the following is supposed to work (also shown here):

state.script("update = function () return true end");
// I actually do
//    state.script("function update() return true end");
// but I suppose it is the same thing; both give the same result anyway,
// likewise if giving C++ lambas as demonstrated below

if (!state["update"].is<std::function<bool()>>())
  // Error

However, is<std::function<T()>> seems to return true for any T:

sol::state lua;
lua.open_libraries(sol::lib::base);

lua["returns_void"]   = [] () {};
lua["returns_int"]    = [] () { return 100; };
lua["returns_bool"]   = [] () { return true; };
lua["returns_string"] = [] () { return std::string("test"); };

sol::function returns_void   = lua["returns_void"];
sol::function returns_int    = lua["returns_int"];
sol::function returns_bool   = lua["returns_bool"];
sol::function returns_string = lua["returns_string"];

std::cout << "returns_void:\n";
if (returns_void.is<std::function<void()>>()) std::cout << "\tReturns void\n";
if (returns_void.is<std::function<int()>>()) std::cout << "\tReturns int\n";
if (returns_void.is<std::function<bool()>>()) std::cout << "\tReturns bool\n";
if (returns_void.is<std::function<std::string()>>()) std::cout << "\tReturns string\n";

std::cout << "returns_int:\n";
if (returns_int.is<std::function<void()>>()) std::cout << "\tReturns void\n";
if (returns_int.is<std::function<int()>>()) std::cout << "\tReturns int\n";
if (returns_int.is<std::function<bool()>>()) std::cout << "\tReturns bool\n";
if (returns_int.is<std::function<std::string()>>()) std::cout << "\tReturns string\n";

std::cout << "returns_bool:\n";
if (returns_bool.is<std::function<void()>>()) std::cout << "\tReturns void\n";
if (returns_bool.is<std::function<int()>>()) std::cout << "\tReturns int\n";
if (returns_bool.is<std::function<bool()>>()) std::cout << "\tReturns bool\n";
if (returns_bool.is<std::function<std::string()>>()) std::cout << "\tReturns string\n";

std::cout << "returns_string:\n";
if (returns_string.is<std::function<void()>>()) std::cout << "\tReturns void\n";
if (returns_string.is<std::function<int()>>()) std::cout << "\tReturns int\n";
if (returns_string.is<std::function<bool()>>()) std::cout << "\tReturns bool\n";
if (returns_string.is<std::function<std::string()>>()) std::cout << "\tReturns string\n";
returns_void:
    Returns void
    Returns int
    Returns bool
    Returns string
returns_int:
    Returns void
    Returns int
    Returns bool
    Returns string
returns_bool:
    Returns void
    Returns int
    Returns bool
    Returns string
returns_string:
    Returns void
    Returns int
    Returns bool
    Returns string

(Tested with with and without SOL_ALL_SAFETIES_ON, using MinGW & MSVC under Windows, and GCC & Clang through WSL.)

Was this functionality working before and broke at some point?

Of course, I could simply execute the function and test its result type as follows:

sol::function_result res = state["update"]();
if (res.get_type() != sol::type::boolean)
  // Error

However, I'd really rather not do this, as it will imply testing it each frame; doing it only once at startup would be ideal.

Is there any other way, again without executing the function? If worse comes to worst, I'll simply test its result each frame with an assertion, or not at all.

Thanks!

Rochet2 commented 1 year ago

I dont think that is possible. Lua is a dynamically typed language, so if the code would for example be the following, would the evaluation determine that this functions if valid nor not?

function update()
    return global_var
end

It is not possible to do static analysis on the code to know the return value type. Any code anywhere (in lua or in c++) could assign global_var to be anything - boolean or not. And thus we can only analyze that the function does return a value, and that it returns one value instead of multiple, but we do now know what is the type of the value or its actual value. I think analyzing all of that information also proably requires one the interpret the bytecode or AST of the string.dump() output of the update function for example. Sol does not do any of this as far as I know.

You would need to make sure that the function is never changed (by taking a reference to it for example) and then analyze that the function indeed returns a constant boolean value instead of a variable - or that the variable is a constant value.

What is the point of checking if the value is a boolean? Why not accept any value and convert it to boolean using Lua's logic (nil and false are falsy, all other values are truthy)? It sounds as if you would be trying to make lua typed.

And on the example from Sol, it could work if Sol is able to peer into the value and see that it is a C++ function. Then extract the type of the function and compare the signatures. But that would only work for C++ functions and lambdas, not lua defined functions. So you would not be able to define the function in lua. It is possible that there is a bug or its not intended to check the signatures.

I do not know your requirements, but I would just use lua's boolean logic for evaluating the return value and ignore the type or have the runtime check for return type.

Razakhel commented 1 year ago

Any code anywhere (in lua or in c++) could assign global_var to be anything - boolean or not. And thus we can only analyze that the function does return a value, and that it returns one value instead of multiple, but we do now know what is the type of the value or its actual value. I think analyzing all of that information also proably requires one the interpret the bytecode or AST of the string.dump() output of the update function for example. Sol does not do any of this as far as I know.

I understand that. The fact that it was part of the tutorials was what made me wonder if Sol indeed did what you described last. Isn't it very misleading then? It may be worthwhile to remove it from the documentation, or even prevent using is<std::function<T>>() if it's never relevant 😕

And on the example from Sol, it could work if Sol is able to peer into the value and see that it is a C++ function. Then extract the type of the function and compare the signatures. But that would only work for C++ functions and lambdas, not lua defined functions. So you would not be able to define the function in lua. It is possible that there is a bug or its not intended to check the signatures.

I considered that, but I highly doubted the behavior differed according to the way it was set, and my example using C++ lambdas does show that it doesn't.

Why not accept any value and convert it to boolean using Lua's logic (nil and false are falsy, all other values are truthy)?

I do not know your requirements, but I would just use lua's boolean logic for evaluating the return value and ignore the type or have the runtime check for return type.

I thought about that a while after my post, and that seems like the best choice indeed. I guess I'll just go with Lua's boolean logic 😄

Thank you very much for your insights!