Calling a Lua/C++ function from C++/Lua code

Another post in my series on how to use C++ with Lua; this time it’s about calling a function of one language from code of the other language.

Preface

After building Lua from source, and then creating a very simple C++ program that calls a Lua script as a starting point, let’s now delve deeper into “using C++ with Lua” by demonstrating how to call a Lua function from a C++ host program (as well as the other way around: how to call a C++ function from a Lua script).

Originally I wanted to cover first how to get (i.e. read-only) values from a Lua script into a C++ host program, because that will also come handy in the future:
One could use a Lua script as an advanced configuration file, so that one could test and play with settings and values without the need of constantly rebuilding C++ source code.
But to my surprise, it turned out that that task is a bit more complicated and extensive than I initally anticipated; therefore I need to spend a bit more time on it…

On the other hand, calling code from the the opposite language was not as hard as I expected. And while it was also not super-easy for me (being new to Lua and to the concept of data exchange between languages via a virtual stack), it went relatively smooth.

So that’s why this is now the subject matter here.


Calling a Lua function from C++

If one is using a C++ host program as the main driver of a project, that need may not appear too often; but on the hand: Why not? Right tool for the right job, right?! Sometimes things can be done much easier in a flexible secripting language instead with the very strict, verbose and rigid world of C++.

The Lua script

The Lua script defines a function that can be called from Lua itself, as well as from a C++ host program (see also Additional notes #2 below):

-- Definition of the Lua function that will be called from the C++ code:

function Add (a, b)
    print("[Lua] Called Add(" .. a .. ", " .. b .. ")")
    return a + b
end

The C++ host program

The C++ code here is the host: It provides the main function of the program, which loads and runs the code from the Lua script; the script’s filepath is provided to main() as an argument:

// The Lua-provided function will be called in main():

#include <iostream>
#include <format>

extern "C"
{
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}

int main (int argc, char* argv[])
{    
    const auto L = luaL_newstate();
    
    // Opening (only) the basic Lua library (for print() in the Lua script).
    // (See comment on the next section for more details on this.)
    luaL_requiref(L, "_G", luaopen_base, 1);
    lua_pop(L, 1);
    
    // Load & Run the Lua code:
    //
    // Note: Loading alone won't make any globals or functions from a Lua file available;
    //       we must first also execute the loaded chunk (Lua has helper to combine these steps).
    //
    // Again: I left out any checks of the results; recommended only for demonstration purposes!
    luaL_loadfile(L, argv[1]);       // Load (argument = filepath).
    lua_pcall(L, 0, LUA_MULTRET, 0); // Run loaded chunk.
    
    // Find (and push onto the stack) the Lua function that should be called later:
    const char* LuaFunctionName = "Add";
    int type = lua_getglobal(L, LuaFunctionName);

    // Check and call the function (maybe):
    if (lua_isnoneornil(L, -1))
    {
        std::cout << std::format("Nothing with the name '{0}' was found!", LuaFunctionName) << std::endl;
    }
    else if (lua_isfunction(L, -1))
    {
        // Define the argument values:
        double FirstValue  = 2.4;
        double SecondValue = 3.1;
        
        // Push the values as Lua numbers on the stack, so that the Lua function can pop it as
        // its parameters 'a' and 'b' (pay attention to the order, it's a stack, after all!).
        lua_pushnumber(L, FirstValue);
        lua_pushnumber(L, SecondValue);
        
        // Call the Lua function: 2 Parameters, 1 Return Value expected, 0 (error) message handler:
        int ParamCount  = 2;
        int RetValCount = 1;

        if (lua_pcall(L, ParamCount, RetValCount, 0) == LUA_OK)
        {
            std::cout << std::format("[C++] Called Lua function '{0}({1}, {2})' -> Return Value: {3}",\
                "Add", FirstValue, SecondValue, (double)lua_tonumber(L, -1)) << std::endl;
            
            lua_pop(L, RetValCount); // Keep the stack tidy: Pop all returned values from the stack again.
        }
        else
        {
            std::string em = lua_tostring(L, -1);
            std::cout << em << std::endl;
        }
    }
    else
    {
        std::cout << std::format("Something with the name '{0}' was found, but it's not a Lua function, but of type '{1}'!",\
            LuaFunctionName, lua_typename(L, type)) << std::endl;
    }

    lua_close(L);

    return 0;
}

The call of the Lua function from the C++ host program should output something like this:

> .\LuaCpp-x64.exe C:\Path\To\script.lua
[Lua] Called Add(2.4, 3.1)
[C++] Called Lua function 'Add(2.4, 3.1)' -> Return Value: 5.5

Calling a C++ function from Lua

Another approach: A C++ codebase/library offers functions to a Lua script for the heavy lifting:
Maybe due to performance reasons or because the task is too specialized or too low-level for Lua.

Also a good reason: Flexibility in testing, designing or modifying things like workflows, gameplay and similiar, often changing actions and jobs.
With a scripting language as an extension, one can more easily and way faster adjust things, without waiting for the compilation and linking to be done after a each modification step.

The Lua script

Here we are calling a C++ function (provided as Sum()) from within a Lua script:

-- Lua script that calls the C++-provided function:

arguments = { 1, 2, 3 }
result    = Sum(arguments[1], arguments[2], arguments[3])

print("[Lua] Calling a C++ function with these arguments: (" .. table.concat(arguments, ", ") .. ")")
print("[Lua] The result of the C++ function call is: " .. result)

The C++ host program

Here we are defining the C++ function that the Lua script will call.
(Again, the C++ code is in this example also the main driver of the program, from which the executable will be generated.)

Rough summary of the steps

  1. The C++ function must be defined, following the prototype/signature that Lua demands (see lua.h: lua_CFunction):

    • The function must only have one parameter: The current Lua state.
    • The function must return an integer; representing the number of values it will return (= push onto the virtual Lua stack).
    • All other parameters will be transferred via the virtual Lua stack.
  2. The C++ function must then be registered with the Lua state; the name can be different between both worlds:
    The registration takes care of the mapping.

// The C++ program that defines the function for the Lua code
// (and also invokes the Lua script which then calls that function...)

#include <iostream>

extern "C"
{
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}

//------------------------------
// Definition of the C++ function that can be called from the Lua script:

int CppSumFunction (lua_State* L)
{
    // Get the index of the top of the stack (which is also the current size of the stack and
    // the number of arguments passed to the function (remember that Lua starts counting with 1!).
    int NumberOfArguments = lua_gettop(L);
    
    double sum = 0;
    
    // Get every argument from the stack and add it to the sum:
    // (Once more: Remember that Lua starts counting [its stack elements] with 1!)
    for (int i = 1; i <= NumberOfArguments; i++)
    {
        // Imagine that we start with a fresh/empty stack, using the absolute index
        // (i.e. from the bottom, so using 1, 2, etc. is correct):
        sum += lua_tonumber(L, i); 
    }
    
    // Push the return value(s) onto the stack:
    // The count of pushes must match the number of returned values by this function!
    lua_pushnumber(L, sum); 
    
    std::cout << "[C++] Host function has been called (by Lua)" << std::endl;
    
    // The return value of the function must be the amount of actual return values
    // that will be pushed onto the stack for Lua (= number of arguments passed back to Lua):
    return 1; // In this case it was 1: The value of 'sum'.
}

//------------------------------
// Setting up the Lua state and invoking the Lua script (provided as an filepath argument):

int main (int argc, char* argv[])
{    
    const auto L = luaL_newstate();
    
    // Open (specific) Lua libraries, so that a Lua script can use functions
    // like print() or table-related functions.
    // 
    // Note: One reason to be so specific (instead of simply calling luaL_openlibs()),
    // is to reduce the possible attack vector/surface by preventing that a user script can
    // do or call things that we don't want it (e.g. access to the filesystem).
    // 
    // Things have changed with Lua 5.1 and many of the older tips I found on the internet
    // didn't work anymore with Lua 5.4.7.
    // This post was helpful: https://stackoverflow.com/a/57190353
    
    luaL_requiref(L, "_G", luaopen_base, 1);            // Lua's Basic Library.
    lua_pop(L, 1);
    
    luaL_requiref(L, LUA_TABLIBNAME, luaopen_table, 1); // Lua's Table library.
    lua_pop(L, 1);
    
    // Register the C++ function with Lua (under a different name for Lua):
    lua_register(L, "Sum", CppSumFunction);
    
    // Call the Lua code (first argument to main = filepath to the script):
    luaL_dofile (L, argv[1]);

    lua_close(L);
	
	return 0;
}

The call to to the C++-based executable should output something like this:

> .\LuaCpp-x64.exe C:\Path\To\script.lua
[Lua] Calling C++ function with arguments: (1, 2, 3)
[C++] Host function has been called (by Lua)
[Lua] The result of the C++ function is: 6.0

Additional Notes

  1. In the sample code above, I left out most checks of return values, status codes and/or error handling, so that it would not distract from the code of the core mechanisms.

    But sadly that also means that things can fail silently, without any hints, why…

    For example: I initally forgot to load Lua’s table library in the second part, therefore the call of table.concat() in script.lua wasn’t possible; and that resulted in no output at all from the EXE.

  2. Lua variables and functions must not be declared “local” in a Lua script if you want to use those items (easily) from C++!
    Otherwise the C++ host program won’t be able to see or call them; at least not without hassle.

  3. Next to many, many individual (older) posts on blogs, discussion forums and mailing list archives, these links were very helpful in my general understanding of Lua and its C-API (in comparison, the offical Lua Reference Manual is not that great, but a necessary evil…):