Martin Helmut Fieber

Part 5 of 9 → show all

C++ and Lua

Posted on — Updated

Showing a VSCodium window in dark mode, a C++ file open with some Lua library C-API code. At the bottom the CMake file for the project. The sidebar on the left, showing the contained project files that get built in this part of the series.
Part of the example for setting up a Lua module via C++.

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>
}
Avoid the compiler name-mangling via 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
The $ 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
width = 42
height = 23
ids = { 1, 2, 3 }

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
This will print 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")
labels = {
  hello = "Hello",
  reader = "Reader"
}
{
  hello = "Hello",
  reader = "Reader"
}
1 -3
Stack bottom
labels = {
  hello = "Hello",
  reader = "Reader"
}

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")
numbers = {7, 13, 23, 42}
{7, 13, 23, 42}
1 -3
Stack bottom
numbers = {7, 13, 23, 42}

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.

lua_pcall error codes
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
local first, second = compute(2, 10)

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)
Prints 12 and 20.

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)
This will print 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;
}
Full 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 👋🏻

← Show all posts | Show all in series