(Part 3 of my little series on “Lua”)
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
-
The C++ function must be defined, following the prototype/signature that Lua demands (see
lua_CFunctionin lua.h):- The function must only have one parameter: The current Lua state.
- The function must return an integer: The count of returned values (which are pushed onto the virtual Lua stack).
- All other parameters will be transferred via the virtual Lua stack (see sample code below for details).
-
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);
// But: lua_to* is type unsafe; prefer luaL_check* functions.
}
// 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's just 1 value: 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 several functions
(For example: print() requires Lua's basic standard library to be loaded.)
Helpful:
- https://stackoverflow.com/a/57190353
- https://www.lua.org/manual/5.4/manual.html#6 ("6 – The Standard Libraries"):
"[...] Alternatively, the host program can open them individually by using luaL_requiref [...]"
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).
Note: Things changed with Lua 5.1; many older tips on the internet don't work anymore with version 5.4.7.
*/
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
Registering functions from a C/C++ library with Lua
In the example above, we registered only specific, single C++ functions with Lua, but if you want to register a library of multiple functions with Lua, the approach is slightly different (based on How to add your C Library in Lua).
Note: I won’t go into details on how to build a library (the nativefunc.dll in the example below), I just assume it works…
The name of the file and the name of the loading function must follow some conventions set up by Lua:
- require (Reference Manual · Programming in Lua: )
- require is the preferred function in Lua for loading libraries; guided by the table package.searchers:
- package.searchers is a table used by require to control how to find modules.
Searches among others in the paths specified in package.path and package.cpath.In the library, a C function with a specific name(!) will be looked up as the loader:
The name of this C function must beluaopen_<filename>(dots will be replaced by underscores).If the filename has a hyphen, everything after (and including) the first hyphen is removed.
For instance, if the module name is a.b.c-v2.1, the function name will be luaopen_a_b_c.
C++: Define, export & register functions
Here’s the C/C++ part, where the actual native function in the library and the helper function (for loading/registering with Lua) are defined:
#include <lua.h>
#include <lauxlib.h>
static int l_multiply_by_ten (lua_State* L)
{
double number = luaL_checknumber(L, 1);
lua_pushnumber(L, number * 10);
return 1;
}
// ...
int luaopen_nativefunc (lua_State* L)
{
// A C struct array to hold the info about the functions to be exported:
// (luaL_Reg is defined by Lua)
static const struct luaL_Reg NativeFunctions [] =
{
{ "multiply_by_ten", l_multiply_by_ten }, // = function name (in Lua); pointer to the function.
// ...
{ NULL, NULL } // A luaL_Reg array must end with a sentinel entry: both values must be NULL.
};
luaL_register(L, "nativelib", NativeFunctions);
// "nativelib" will be a global variable for holding the functions (a Lua callback table).
return 1;
}
Lua: Import library & use functions
And here’s the Lua script that imports the C/C++ library and calls one of its functions:
require "nativefunc" -- Import (see note below!)
-- Alternatives (not recommended! https://www.lua.org/manual/5.4/manual.html#pdf-package.loadlib)
-- Not ISO C; not available on all platforms; insecure; low-level; bypasses package/module system.
-- lib = package.loadlib("./nativefunc.dll", "luaopen_nativefunc")
-- lib();
-- or:
-- package.loadlib("./nativefunc.dll", "luaopen_nativefunc")()
r = nativelib.multiply_by_ten(2) -- Use
print(r)
The Lua instruction require searches specific paths for modules/packages.
One can override the search paths be setting the variables package.path and package.cpath:
package.path = package.path .. ";../?.lua"
package.cpath = package.cpath .. ";../?.lua"
Additional Notes
-
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()inscript.luawasn’t possible; and that resulted in no output at all from the EXE. -
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. -
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…):
- javidx9: Embedding Lua in C++ #1 (YouTube video!)
- StackOverflow: LuaL_openlibs() and sandboxing scripts, but especially this answer from that thread.
- A Lua C API Cheat Sheet
- Calling C++ Functions From Lua
Film & Television (57)
How To (70)
Journal (18)
Miscellaneous (4)
News & Announcements (21)
On Software (12)
Projects (26)