C++ and Lua
The Little Language That Could
One of the great strengths of Lua is the relative ease of integrating it into another language or application, especially when written in C or C++. With the now better understanding of how to work with Lua, including debugging and profiling from the last article, it is time to leverage use cases for the language outside its own language scope.
About Versions
The latest Lua version as of writing is 5.5 (released ), though everything in this article series will work with Lua 5.1 (released ) and up. If this is not the case, a callout like the following will signal the required version.
An overview of all versions and their main features can be found on the official Lua version history page.
Lua Integration
Focusing on adding Lua to a C++ project, I will use CMake to show different ways to integrate Lua into a codebase. Covering all the different ways outside CMake would end in a book called "The 1000 Ways to Add External Dependencies to C++", and I won't submit to that.
Nevertheless, the integration via a Git submodule should be something that works with many different kinds of build and package tooling, so I'll show that too.
As the examples are based on the usage in C++, the include header
will be
lua.hpp, not lua.h. This is because the
C++ header is defined as follows:
extern "C" {
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
}
extern "C".
CMake find_package
To have CMake's find_package function work, Lua needs
to be installed somewhere on the system. The
first article of the Lua series covered the installation of
Lua
on all three major operating systems.
A small CMake configuration via CMakeLists.txt could
look as follows.
cmake_minimum_required(VERSION 3.18)
project(
LuaWithCMakeFindPackage
VERSION 1.0
LANGUAGES CXX)
add_executable(ProgramName main.cpp)
The first line sets the minimum required CMake version. Support for Lua 5.4 was added in CMake version 3.18; see the FindLua page in the official CMake documentation.
Via project the project is defined with a name, a
version, and the fact that the C++ language is used.
CMake's add_executable defines the program name and the
used sources, in this case just a main.cpp file.
#include <lua.hpp>
int main() { return 0; }
There are three parts to adding Lua to the CMake project: first, find the installed Lua version.
find_package(Lua)
Second, if Lua was found and no target is defined yet, add a new CMake library and set its target properties to find the include directory and link necessary libraries.
if(Lua_FOUND AND NOT TARGET Lua::Lua)
add_library(Lua::Lua
INTERFACE IMPORTED)
set_target_properties(
Lua::Lua
PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${LUA_INCLUDE_DIR}"
INTERFACE_LINK_LIBRARIES "${LUA_LIBRARIES}"
)
endif()
And third, linking the Lua library to the executable.
target_link_libraries(
ProgramName
PRIVATE Lua::Lua)
The whole CMake CMakeLists.txt now looks like this:
# src/cpp/cmake_find_package/CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(
LuaWithCMakeFindPackage
VERSION 1.0
LANGUAGES CXX)
add_executable(ProgramName main.cpp)
find_package(Lua)
if(Lua_FOUND AND NOT TARGET Lua::Lua)
add_library(Lua::Lua
INTERFACE IMPORTED)
set_target_properties(
Lua::Lua
PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${LUA_INCLUDE_DIR}"
INTERFACE_LINK_LIBRARIES "${LUA_LIBRARIES}"
)
endif()
target_link_libraries(
ProgramName
PRIVATE Lua::Lua)
The project is ready to be configured — from inside the
folder the
CMakeLists.txt is defined, run the CMake
configuration generator*.
$ cmake -B build
$ is used to show a command will be entered.
This will create the configuration inside the folder
build/, from which the project then can be built.
$ cmake --build build
Lua can now be included and used via
#include <lua.hpp>.
CMake External Project
If the dependency is not guaranteed to be installed on the system, a
specific version is needed, or the project is planned to be
self-contained, Lua can be included as an external dependency via
CMake's
FetchContent module.
The basic CMakeLists.txt starts like the previous
example: setting a minimum CMake version, defining the project's
name, version, and used language, and defining an executable and its
sources.
cmake_minimum_required(VERSION 3.18)
project(
LuaWithCMakeExternalProject
VERSION 1.0
LANGUAGES CXX)
add_executable(ProgramName main.cpp)
The FetchContent module needs to be included.
include(FetchContent)
Then Lua can be fetched from a Git repository, for example, the GitHub mirror.
FetchContent_Declare(
lua
GIT_REPOSITORY "https://github.com/lua/lua.git"
GIT_TAG v5.5.0
)
After every dependency that uses FetchContent is
declared, FetchContent_MakeAvailable can be called with
a list of those dependencies, in this case only Lua.
FetchContent_MakeAvailable(lua)
Next, set up the Lua CMake library and define the include directory on the library target.
add_library(Lua::Lua
INTERFACE IMPORTED)
target_include_directories(
Lua::Lua
INTERFACE "${lua_SOURCE_DIR}")
What's left is to link the Lua library to the executable.
target_link_libraries(
ProgramName
PRIVATE Lua::Lua)
The whole CMake CMakeLists.txt should now look like
this:
# src/cpp/cmake_external_project/CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(
LuaWithCMakeExternalProject
VERSION 1.0
LANGUAGES CXX)
add_executable(ProgramName main.cpp)
include(FetchContent)
FetchContent_Declare(
lua
GIT_REPOSITORY "https://github.com/lua/lua.git"
GIT_TAG v5.5.0
)
FetchContent_MakeAvailable(lua)
add_library(Lua::Lua
INTERFACE IMPORTED)
target_include_directories(
Lua::Lua
INTERFACE "${lua_SOURCE_DIR}")
target_link_libraries(
ProgramName
PRIVATE Lua::Lua)
From the folder where the CMakeLists.txt is defined,
the CMake configuration generator can now be run*.
$ cmake -B build
This will create the configuration files inside the folder
build/, from which the project then can be built.
$ cmake --build build
Lua can now be included and used via
#include <lua.hpp>.
Git Submodule
Another way to integrate Lua is as a submodule when using Git; again, using CMake*.
Same as in the other examples, the
CMakeLists.txt starts with setting a minimum CMake
version, the project name, the version, the language used, and
defining an executable with its sources.
cmake_minimum_required(VERSION 3.18)
project(
LuaWithGitSubmodules
VERSION 1.0
LANGUAGES CXX)
add_executable(ProgramName main.cpp)
The GitHub mirror of Lua can be used as a Git submodule. The git
command will take a URL and add the module under a folder named
lua/.
$ git submodule add https://github.com/lua/lua.git
The CMake library and its include path are set up via
add_library and
target_include_directories.
add_library(Lua::Lua
INTERFACE IMPORTED)
target_include_directories(
Lua::Lua
INTERFACE "lua")
The last step is linking the library to the executable.
target_link_libraries(
ProgramName
PRIVATE Lua::Lua)
The whole CMake CMakeLists.txt should now look like
this:
# src/cpp/cmake_git_submodule/CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(
LuaWithGitSubmodules
VERSION 1.0
LANGUAGES CXX)
add_executable(ProgramName main.cpp)
add_library(Lua::Lua
INTERFACE IMPORTED)
target_include_directories(
Lua::Lua
INTERFACE "lua")
target_link_libraries(
ProgramName
PRIVATE Lua::Lua)
From the folder where the CMakeLists.txt is defined,
the CMake configuration generator can be run*.
$ cmake -B build
This will create the configuration inside the folder `build`, from which the project then can be built.
$ cmake --build build
Lua can now be included and used via
#include <lua.hpp>.
The Lua State
With the integration done and Lua usable from C++, a Lua environment
can be created. Lua will not define any global C++ variables; this
is all managed through the state lua_State. All Lua C++
functions will get a pointer to this state structure.
The state will serve as the environment for the Lua runtime, handle
the garbage collector, hold loaded libraries, and
manage the stack. By default, it will not contain
any libraries or functions, not even print.
The Lua standard library and its functions come as standalone packages that can be loaded all at once or only as needed. This gives full control over the environment on state creation. In its simplest form, this means to create a new state and open all standard library modules on that state.
lua_State* L = luaL_newstate();
luaL_openlibs(L);
The Lua Stack
The Lua state is the environment giving context for running Lua from C++. The question is, how does a statically typed language with manual memory management communicate with a dynamically typed language with automatic memory management and a garbage collector?
Answer: a virtual stack. This is the way C++ talks to Lua, and vice versa. Each slot in the stack can hold any Lua value. When accessing, e.g., a global Lua variable from C++, Lua will push the variable onto the stack to be read from C++. The same goes the other way, where transferring a value to Lua from C++ is pushing the value onto the stack, where Lua will pop it off the stack.
Lua uses LIFO (Last In, First Out), while C++ can manipulate the stack with full freedom, accessing, adding, or deleting elements on the stack anywhere.
There are functions in the Lua C library to push and read any Lua
value. The top of the stack can be accessed on index
-1, the bottom of the stack is at index 1.
To make this easier to understand, here's an example of how the Lua stack works.
| C++ | Lua | Stack | Index | |
|---|---|---|---|---|
| Stack top | ||||
lua_getglobal(L, "width") |
width = 42 |
42 |
3 | -1 |
lua_getglobal(L, "height") |
height = 23 |
23 |
2 | -2 |
lua_getglobal(L, "ids") |
ids = { 1, 2, 3 } |
{ 1, 2, 3 } |
1 | -3 |
| Stack bottom |
|
|||
Access Lua Data
Quick Start
It's best to head straight into a practical example by reading data from a Lua file. This is a very common usage when adding Lua to an existing application. Being able to simply read data from a Lua file enables using Lua as a dynamic configuration format.
For the example I use C++23 and CMake with
find_package, the
full example can be found in the companion repository on
GitHub.
As mentioned earlier, a new state needs to be created before Lua can
be accessed. It is also necessary to close this state when done with
lua_close.
// src/cpp/example-read-config/main.cpp
#include <print>
#include <lua.hpp>
int main() {
lua_State* L = luaL_newstate();
// Code comes here ...
lua_close(L);
return 0;
}
As this is an example of using Lua as a configuration format, there is no need to initialise any standard library modules.
Via luaL_dofile a Lua file can be loaded and executed
at once. If there was a problem, an error will be pushed to the
stack that can be accessed with lua_tostring(L, -1),
where -1 is the topmost stack item.
if (luaL_dofile(L, "./config.lua") == LUA_OK) {
// Upcoming code here ...
} else {
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
When the script file is successfully loaded and run, a global
variable can be pushed to the stack to access its value. In this
case, getting the variable
width from the top of the stack.
lua_getglobal(L, "width");
if (lua_isnumber(L, -1)) {
lua_Number width = lua_tonumber(L, -1);
std::print("width = {}\n", width);
}
The full example looks like this:
// src/cpp/example-read-config/main.cpp
#include <print>
#include <lua.hpp>
int main() {
lua_State* L = luaL_newstate();
if (luaL_dofile(L, "./config.lua") == LUA_OK) {
lua_getglobal(L, "width");
if (lua_isnumber(L, -1)) {
lua_Number width = lua_tonumber(L, -1);
std::print("width = {}\n", width);
}
} else {
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
lua_close(L);
return 0;
}
And the mentioned Lua file:
-- config.lua
width = 42
height = 23
Configuring and running CMake:
$ cmake -B build && make --build build
And finally, running the program.
$ ./build/ProgramName
width = 42.Numbers
Numbers on the stack can be checked with
lua_isnumber and read via lua_tonumber.
lua_getglobal(L, "width");
if (lua_isnumber(L, -1)) {
lua_Number width = lua_tonumber(L, -1);
}
lua_Number is a type definition of
double by default.
Integer
Integers on the stack can be checked with
lua_isinteger and read via lua_tointeger.
lua_getglobal(L, "height");
if (lua_isinteger(L, -1)) {
lua_Integer height = lua_tointeger(L, -1);
}
lua_Integer is by default long long.
Boolean
Booleans on the stack can be checked with
lua_isboolean and read via lua_toboolean.
lua_getglobal(L, "isResizable");
if (lua_isboolean(L, -1)) {
int isResizable = lua_toboolean(L, -1);
}
lua_toboolean will return an int with
0 or 1 as boolean value.
Strings
Strings on the stack can be checked with
lua_isstring and read via lua_tostring.
lua_getglobal(L, "name");
if (lua_isstring(L, -1)) {
const char* name = lua_tostring(L, -1);
}
Tables
Given the following table in Lua:
labels = {
hello = "Hello",
reader = "Reader"
}
Tables are a little more involved, but not by much. Getting the
table on top of the stack via lua_getglobal and doing a
check if it's actually a table with lua_istable.
lua_getglobal(L, "labels");
if (lua_istable(L, -1)) {
// More here ...
}
From the table now at the top of the stack at -1, a
field can be pushed to the top of the stack with
lua_getfield.
lua_getglobal(L, "labels");
if (lua_istable(L, -1)) {
// Table is at -1, push field to top
lua_getfield(L, -1, "hello");
// Now field is on top at -1
const char* hello = lua_tostring(L, -1);
}
With the field on top of the stack at -1, the table
moved one position down to -2. This is where the stack
needs to be accessed to get another field from the table.
lua_getglobal(L, "labels");
if (lua_istable(L, -1)) {
// First field code ...
// Table is now at -2, get next field on top of stack
lua_getfield(L, -2, "reader");
// New pushed field is now on top at -1
const char* hello = lua_tostring(L, -1);
}
To make this easier to understand, here another example of how the stack works when reading data from a Lua table.
| C++ | Lua | Stack | Index | |
|---|---|---|---|---|
| Stack top | ||||
lua_getfield(L, -2, "reader") |
reader = "Reader" |
"Reader" |
3 | -1 |
lua_getfield(L, -1, "hello") |
hello = "Hello" |
"Hello" |
2 | -2 |
lua_getglobal(L, "labels") |
|
|
1 | -3 |
| Stack bottom |
|
|||
Arrays
Given the following array in Lua:
numbers = {7, 13, 23, 42}
Arrays are indexed tables in Lua. Therefore, when
retrieving an array from the stack, lua_istable needs
to be used to validate the value same as is done for tables.
lua_getglobal(L, "numbers");
if (lua_istable(L, -1)) {
// More here ...
}
With the array on top of the stack, the first index can be retrieved
via
lua_geti.
// Array is at -1, push first index 1 to top
lua_geti(L, -1, 1);
// Get value from top of the stack
lua_Number first = lua_tonumber(L, -1);
With the first index pushed to the top of the stack, the table moved
down to index
-2.
// Get the second index, from the
// table at stack index -2
lua_geti(L, -2, 2);
// Get the value from the second index
// pushed to the top of the stack
lua_Number second = lua_tonumber(L, -1);
Again, an example should help to get a better intuition for the stack behaviour.
| C++ | Lua | Stack | Index | |
|---|---|---|---|---|
| Stack top | ||||
lua_tonumber(L, -1) |
7 |
7 |
3 | -1 |
lua_geti(L, -1, 1) |
7 |
7 |
2 | -2 |
lua_getglobal(L, "numbers") |
|
|
1 | -3 |
| Stack bottom |
|
|||
Call Lua Function from C++
To call a global Lua function from C++ the function needs to be put on the stack first. This is not yet calling it, only making it accessible to pass arguments to it if needed. Those need to be pushed on the stack from C++ too. When everything is ready the function can be called on the stack.
Given the following Lua code:
function some_fn(arg1, arg2)
local result = arg1 + arg2;
-- Multiple return values
return result, true
end
From C++ the function needs to be pushed on the stack.
lua_getglobal(L, "some_fn");
If this was successful, checked via lua_isfunction,
function arguments can be pushed on the stack via
lua_pushnumber.
if (lua_isfunction(L, -1)) {
lua_pushnumber(L, 13);
lua_pushnumber(L, 23);
// ...
}
The function gets two arguments and returns two results. To call the
now on the stack prepared function a protected call can be made with
lua_pcall.
Sparing some details, protected call means any error on call can be
handled. The
lua_pcall function will return the call result and put
a potential error on top of the stack.
if (lua_isfunction(L, -1)) {
lua_pushnumber(L, 13);
lua_pushnumber(L, 23);
constexpr int arguments_count = 2;
constexpr int returnvalues_count = 2;
// Ignore the `0` as last argument for now,
// the next section will explain more.
int status = lua_pcall(
L,
arguments_count,
returnvalues_count,
0
);
}
The different error codes from lua_pcall are shown in
the following table.
| Return code | Description |
|---|---|
LUA_OK |
Successful |
LUA_ERRRUN |
Runtime error |
LUA_ERRMEM |
Failed to allocate memory |
LUA_ERRERR |
Failed to call the message handler |
LUA_ERRGCMM |
Failed to run __gc metamethod.
Removed in Lua 5.4.
|
Let's say everything went splendid; multiple return values get
pushed onto the stack in order. That means, the
first return value goes on top of the stack at -1, then
the second. Making the first return value move down one position on
the stack to -2.
if (status == LUA_OK) {
// First return value at -2
lua_Number result1 = lua_tonumber(L, -2);
// Second return value on -1
bool result2 = lua_toboolean(L, -1);
}
Protected Call Message Handler
The function lua_pcall alone will not give detailed
error information if something goes wrong. At the time
lua_pcall returns the stack is unwound and potential
tracing information is lost.
To solve this, a message handler can be referenced via stack index
as last argument of lua_pcall. Given an error handler
function called on_error.
int on_error(lua_State* L) {
// More to come ...
return 1;
}
To get access to the stack after a faulty call the current top of
the stack index is needed, this can be done with
lua_gettop. After that the error message handler can be
pushed and its index retrieved.
const int top_index = lua_gettop(L);
lua_pushcfunction(L, on_error);
const int handler_index = lua_gettop(L);
To test the protected call it needs a function that will throw. Setting up such a function by pushing a faulty argument to a stack prepared function.
lua_getglobal(L, "some_fn");
if (lua_isfunction(L, -1)) {
lua_pushnumber(L, 13);
lua_pushliteral(L, "23"); // Faulty argument
constexpr int arguments_count = 2;
constexpr int returnvalues_count = 2;
// Call next ...
}
Instead of 0 the last argument to
lua_pcall is the
"message handler stack index".
int status = lua_pcall(L,
arguments_count,
returnvalues_count,
// Handler index from earlier:
handler_index
);
On error, the on_error message handler is called.
Testing for the correct lua_pcall return value
LUA_ERRRUN, the error message can be retrieved from the
top of the stack. Before it was put there it went through the
on_error handler.
if (status == LUA_ERRRUN) {
const char* err = lua_tostring(L, -1);
std::print("Error = {}\n", err);
lua_pop(L, 1);
}
All that's left is cleaning up the stack.
lua_settop(L, top_index);
There was nothing done initially with the error message in the
on_error function, but this would be the right place to
add traceback information via
luaL_traceback for a proper stacktrace of the error.
int on_error(lua_State* L) {
const char* err = lua_tostring(L, -1);
lua_remove(L, -1);
luaL_traceback(L, L, err, 1);
return 1;
}
The full example code to call a function from Lua and handle potential errors:
// src/cpp/example-functions/main.cpp
#include <print>
#include <lua.hpp>
// Message handler to extend error message
int on_error(lua_State* L) {
const char* err = lua_tostring(L, -1);
lua_remove(L, -1);
luaL_traceback(L, L, err, 1);
return 1;
}
int main() {
lua_State* L = luaL_newstate();
if (luaL_dofile(L, "./script.lua") == LUA_OK) {
const int top_index = lua_gettop(L);
// Set up message handler
lua_pushcfunction(L, on_error);
const int handler_index = lua_gettop(L);
lua_getglobal(L, "some_fn");
if (lua_isfunction(L, -1)) {
lua_pushnumber(L, 13);
// Faulty argument:
lua_pushliteral(L, "23");
int status = lua_pcall(L,
2, // Arguments count
2, // Return value count
handler_index
);
if (status == LUA_ERRRUN) {
const char* err = lua_tostring(L, -1);
std::print("Error = {}\n", err);
lua_pop(L, 1);
}
lua_settop(L, top_index);
}
} else {
luaL_error(L,
"Error: %s\n",
lua_tostring(L, -1));
}
lua_close(L);
return 0;
}
Call C++ Function from Lua
To get a function defined in C++ to Lua it needs to be on the stack before the Lua code calling this function is executed. Every of those defined functions follows the same interface.
int function_name(lua_State* L);
It's a function returning an int, this is the number of
arguments the function will return in Lua. The single argument is
always the Lua state to get access to the stack where function
arguments are and return-values can be pushed to.
Given a function compute that takes two arguments and
returns two values. The function arguments are on the stack in
reverse order, as they get pushed from left to
right when calling a Lua function.
| C++ | Lua | Stack | Index | |
|---|---|---|---|---|
| Stack top | ||||
lua_tonumber(L, -2) |
2 |
2 |
2 | -1 |
lua_tonumber(L, -1) |
10 |
10 |
1 | -2 |
| Stack bottom |
|
|||
The return values are pushed onto the stack in the order they are retrieved.
This is how the function will be called in Lua:
local first, second = compute(2, 10)
And this is the function definition in C++:
int lua_compute(lua_State* L) {
lua_Number arg_2 = lua_tonumber(L, -1);
lua_Number arg_1 = lua_tonumber(L, -2);
lua_pushnumber(L, arg_2 + arg_1);
lua_pushnumber(L, arg_2 * arg_1);
return 2;
}
After the state is created, the function needs to be pushed to the stack and a name defined for the global access in Lua.
lua_pushcfunction(L, lua_compute);
lua_setglobal(L, "compute");
After that the Lua script using the new function can be loaded and
run, for example via luaL_dofile.
Full Lua example code:
local first, second = compute(2, 10)
print("Result 1 = " .. first)
print("Result 2 = " .. second)
The full C++ code to define a new function in C++ callable by Lua:
// src/cpp/example-cpp-to-lua/main.cpp
#include <print>
#include <lua.hpp>
int lua_compute(lua_State* L) {
lua_Number arg_2 = lua_tonumber(L, -1);
lua_Number arg_1 = lua_tonumber(L, -2);
lua_pushnumber(L, arg_2 + arg_1);
lua_pushnumber(L, arg_2 * arg_1);
return 2;
}
int main() {
lua_State* L = luaL_newstate();
luaL_openlibs(L);
lua_pushcfunction(L, compute);
lua_setglobal(L, "compute");
if (luaL_dofile(L, "./script.lua") == LUA_OK) {
std::print("Done\n");
} else {
luaL_error(L,
"Error: %s\n",
lua_tostring(L, -1));
}
lua_close(L);
return 0;
}
User Data
… or how to get C++ data to Lua. This is a little more involved, and it helps to know how to define a Lua function through C++. Generally, it needs a way to construct a new object and setting up getter and setter for data entries*.
Doing this is by creating a new module that needs to be pushed to the stack before a Lua script is run that will use it. It also needs a new meta table, meta methods, and a factory to create a new instance of the object.
The example will define a GameData module, that can be
used in Lua like this:
local GameData = require "GameData"
A factory function usually named new will allow
creating new instances, taking a name and
amount value as arguments.
local data = GameData.new("Dwarves", 7)
Accessing data from the object:
print(data.amount .. " " .. data.name)
7 Dwarves.
The example setup will register a new module named
"GameData" based on the GameData struct via
luaL_requiref in the package table for use with
require. The last parameter 0 tells it to
not create an equally named global variable called
GameData (1 would create it).
struct GameData {
const char* name;
int amount{0};
};
int main() {
lua_State* L = luaL_newstate();
luaL_openlibs(L);
// Add "GameData" module
luaL_requiref(L, "GameData", open_GameData, 0);
if (luaL_dofile(L, "./script.lua") == LUA_OK) {
std::print("Done\n");
} else {
luaL_error(L,
"Error: %s\n",
lua_tostring(L, -1));
}
lua_close(L);
return 0;
}
The function open_GameData is where the module will be
set up. The minimal version of this, defined outside
main, will only define a meta table with the same name.
int open_GameData(lua_State* L) {
luaL_newmetatable(L, "GameData");
return 1;
}
Even though the name can be different, it is a good practice to keep
it the same as the module or object name, as this name will be used
for the
__name meta field.
The new factory function will create a new user data
object via lua_newuserdata from GameData,
assign the given function parameters, and set its meta table to be
"GameData".
int GameData_new(lua_State* L) {
auto* game_data = static_cast<GameData*>(
lua_newuserdata(L, sizeof(GameData))
);
game_data->amount = lua_tonumber(L, -2);
game_data->name = lua_tostring(L, -3);
luaL_setmetatable(L, "GameData");
return 1;
}
The new library gets created with luaL_newlib, and
requires a registry of functions to be part of; this is where the
GameData_new will go. The registry is a null terminated
array of type luaL_Reg.
const luaL_Reg game_data_lib[] = {
{"new", GameData_new},
{nullptr, nullptr},
};
The setup is done in open_GameData.
int open_GameData(lua_State* L) {
luaL_newmetatable(L, "GameData");
luaL_newlib(L, game_data_lib);
return 1;
}
This will create the new Lua module called "GameData", and
define a new function to create instances of the
object.
local GameData = require "GameData"
local data = GameData.new("Dwarves", 7)
For read and write access to the table data it needs both meta
methods
__index and __newindex. The meta method
__index is for reading fields from the table. The C++
function will need to retrieve the user data and field name from the
stack and return the associated value. It also needs to manage the
access to not defined fields, either via returning
nil or giving an error.
int GameData_index(lua_State* L) {
// Get the object from the stack
auto* game_data = static_cast<GameData*>(
lua_touserdata(L, 1)
);
// Get the field name
const std::string key = luaL_checkstring(L, 2);
// Access to "name" field
if (key == "name") {
lua_pushstring(L, game_data->name);
return 1;
}
// Access to "amount" field
if (key == "amount") {
lua_pushinteger(L, game_data->amount);
return 1;
}
// Throw if field is not defined.
luaL_error(L,
"Unknown property access: %s",
key.c_str()
);
return 1;
}
Writing fields to the table is very similar, defined by the meta
method
__newindex — retrieving the user data from the
stack, checking the field to access, and assigning a new value to
it.
int GameData_newindex(lua_State* L) {
// Get the object from the stack
auto* game_data = static_cast<GameData*>(
lua_touserdata(L, 1)
);
// Get the field name
const std::string key = luaL_checkstring(L, 2);
// Write to "name" field
if (key == "name") {
game_data->name = luaL_checkstring(L, 3);
return 1;
}
// Write to "amount" field
if (key == "amount") {
game_data->amount = luaL_checkinteger(L, 3);
return 1;
}
luaL_error(L,
"Unknown property access: %s",
key.c_str()
);
return 1;
}
The GameData_index and
GameData_newindex are not yet doing anything. Similar
to new they need to be registered, but this time to the
objects meta table.
const luaL_Reg game_data_meta[] = {
{"__index", GameData_index},
{"__newindex", GameData_newindex},
{nullptr, nullptr},
};
In open_GameData the meta method registry can be
assigned to the created meta table via luaL_setfuncs.
int open_GameData(lua_State* L) {
luaL_newmetatable(L, "GameData");
luaL_setfuncs(L, game_data_meta, 0);
luaL_newlib(L, game_data_lib);
return 1;
}
Here the full C++ code:
// src/cpp/example-custom-module/main.cpp
#include <print>
#include <lua.hpp>
struct GameData {
const char* name;
int amount{0};
};
int GameData_new(lua_State* L) {
auto* game_data = static_cast<GameData*>(
lua_newuserdata(L, sizeof(GameData))
);
game_data->amount = lua_tonumber(L, -2);
game_data->name = lua_tostring(L, -3);
luaL_setmetatable(L, "GameData");
return 1;
}
int GameData_index(lua_State* L) {
auto* game_data = static_cast<GameData*>(
lua_touserdata(L, 1)
);
const std::string key = luaL_checkstring(L, 2);
if (key == "name") {
lua_pushstring(L, game_data->name);
return 1;
}
if (key == "amount") {
lua_pushinteger(L, game_data->amount);
return 1;
}
luaL_error(L,
"Unknown property access: %s",
key.c_str()
);
return 1;
}
int GameData_newindex(lua_State* L) {
auto* game_data = static_cast<GameData*>(
lua_touserdata(L, 1)
);
const std::string key = luaL_checkstring(L, 2);
if (key == "name") {
game_data->name = luaL_checkstring(L, 3);
return 1;
}
if (key == "amount") {
game_data->amount = luaL_checkinteger(L, 3);
return 1;
}
luaL_error(L,
"Unknown property access: %s",
key.c_str()
);
return 1;
}
const luaL_Reg game_data_meta[] = {
{"__index", GameData_index},
{"__newindex", GameData_newindex},
{nullptr, nullptr},
};
const luaL_Reg game_data_lib[] = {
{"new", GameData_new},
{nullptr, nullptr},
};
int open_GameData(lua_State* L) {
luaL_newmetatable(L, "GameData");
luaL_setfuncs(L, game_data_meta, 0);
luaL_newlib(L, game_data_lib);
return 1;
}
int main() {
lua_State* L = luaL_newstate();
luaL_openlibs(L);
luaL_requiref(L, "GameData", open_GameData, 0);
if (luaL_dofile(L, "./script.lua") == LUA_OK) {
std::print("Done\n");
} else {
std::print("Error reading configuration file:\n");
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
lua_close(L);
return 0;
}
And the Lua that is enabled through the C++:
local GameData = require "GameData"
local data = GameData.new("Dwarves", 7)
print(data.amount .. " " .. data.name)
--> "7 Dwarves"
data.name = "Gnomes"
print(data.amount .. " " .. data.name)
--> "7 Gnomes"
Override Library Functions
Overriding a built-in library function is a handy way to provide a
custom version of a function, extend functionality, or implement a
preferred alternative. One of my go-to overrides I use is extending
the Lua
type function to handle user data types in a, for me,
more useful way.
Taking the earlier GameData as example, when printing
the type via Lua, the result will always be
"userdata" for any user data.
local GameData = require "GameData"
local data = GameData.new("Dwarves", 7)
print(type(data)) --> "userdata"
Therefore, I usually override the type function in C++
to take the meta table name as type, accessible under the meta field
__name.
Taking the
earlier example of setting up the user data
GameData, leaving out the setup code in this example.
int main() {
lua_State* L = luaL_newstate();
luaL_openlibs(L);
luaL_requiref(L, "GameData", open_GameData, 0);
if (luaL_dofile(L, "./script.lua") == LUA_OK) {
std::print("Done\n");
} else {
std::print("Error reading configuration file:\n");
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
lua_close(L);
return 0;
}
The idea is to get a reference to the built-in Lua function
type, and then register a new
type function handling user data. When the argument is
not a user data object, take the original built-in
type function.
I'll keep it simple and take a static variable for the reference to
the Lua
type function.
static int type_ref;
With a setup function setup_type_override the
type global is retrieved. If not nil its
reference gets stored in the static type_ref variable.
The lua_register will then register a new
type function, lua_type.
static int type_ref;
void setup_type_override(lua_State* L) {
if (lua_getglobal(L, "type") != LUA_TNIL) {
type_ref = luaL_ref(L, LUA_REGISTRYINDEX);
lua_register(L, "type", lua_type);
}
}
The new function has the common signature,
returning an int for the number of return values and
taking the Lua state as an argument. The implementation will test if
user data got passed to the type call and if not, call
the built-in type function through the previously
stored reference.
int lua_type(lua_State* L) {
if (lua_isuserdata(L, -1)) {
// More here ...
return 1;
}
// Get and call original `type` function.
lua_rawgeti(L, LUA_REGISTRYINDEX, type_ref);
lua_pushvalue(L, -1);
lua_call(L, -1, -1);
return 1;
}
When user data gets passed, it is on top of the stack at
-1. The meta field __name needs to be
pushed to the stack, and if not nil, the string value
retrieved. If a name is defined, it can get pushed back as a
function argument via lua_pushlstring.
if (luaL_getmetafield(L, -1, "__name") != LUA_TNIL) {
const std::string name = lua_tostring(L, -1);
if (!name.empty()) {
lua_pushlstring(L, name.c_str(), name.size());
return 1;
}
}
The full function replacement is only a couple of lines and will
give, if available, the name for all defined user data when
type is called.
int lua_type(lua_State* L) {
if (lua_isuserdata(L, -1)) {
if (luaL_getmetafield(L, -1, "__name") != LUA_TNIL) {
const std::string name = lua_tostring(L, -1);
if (!name.empty()) {
lua_pushlstring(L, name.c_str(), name.size());
return 1;
}
}
}
lua_rawgeti(L, LUA_REGISTRYINDEX, type_ref);
lua_pushvalue(L, -1);
lua_call(L, -1, -1);
return 1;
}
type function replacement.
One important part is that the
setup_type_override function gets called after
luaL_openlibs that will bring the
type function into the registry before overriding it.
lua_State* L = luaL_newstate();
luaL_openlibs(L);
setup_type_override(L);
And here's the full setup, excluding the definition of
GameData as it was
defined earlier.
// src/cpp/example-override-builtin/main.cpp
#include <print>
#include <lua.hpp>
static int type_ref;
// The new type function override
int lua_type(lua_State* L) {
// Override "userdata" type
if (lua_isuserdata(L, -1)) {
if (luaL_getmetafield(L, -1, "__name") != LUA_TNIL) {
const std::string name = lua_tostring(L, -1);
if (!name.empty()) {
lua_pushlstring(L, name.c_str(), name.size());
return 1;
}
}
}
// Call built-in function
lua_rawgeti(L, LUA_REGISTRYINDEX, type_ref);
lua_pushvalue(L, -1);
lua_call(L, -1, -1);
return 1;
}
// The setup helper
void setup_type_override(lua_State* L) {
if (lua_getglobal(L, "type") != LUA_TNIL) {
type_ref = luaL_ref(L, LUA_REGISTRYINDEX);
lua_register(L, "type", lua_type);
}
}
int main() {
lua_State* L = luaL_newstate();
luaL_openlibs(L);
// Set up type override
setup_type_override(L);
// Example custom module
luaL_requiref(L, "GameData", open_GameData, 0);
if (luaL_dofile(L, "./script.lua") == LUA_OK) {
std::print("Done\n");
} else {
std::print("Error reading configuration file:\n");
luaL_error(L, "Error: %s\n", lua_tostring(L, -1));
}
lua_close(L);
return 0;
}
And the example Lua code this enables:
local GameData = require "GameData"
local data = GameData.new()
print(type(data)) --> "GameData"
More About the Lua C-API
There is, of course, a lot more to the Lua C-API than any of my examples could cover. My main resources for this are mainly the official Programming in Lua book and the online reference manual. One other non-official guide is the Lua Integration Guide website.
What Comes Next
As usual, all the code shown in this article is also available in the companion repository on GitHub, branch part-5.
Next comes a little excursion in building a CLI in Lua. Setup, dependencies, how to package, and ultimately distribute it.
Until then 👋🏻