cacari.co

Embedding Lua in C++: Give Your Code a Scriptable Brain

header-image

In the same way Neovim broke from the legacy of Vim by adopting Lua, I wanted to decouple configuration and logic from compiled C++ in a new environment: Hyprland. But this post isn’t about Hyprland—it’s about how to give your C++ project a runtime scripting layer without bloating it or handing your soul to Python.

You want to expose internal logic. You want extensibility. You want Lua. Why Lua?

  • Because it’s lean.
  • Because it doesn’t ship with batteries—instead, it gives you matches.
  • Because embedding Python is a mess, and writing config parsers for your C++ app is boring as hell.

Lua was designed to be embedded. Its C API is low-level, but dead simple. You keep full control while your users get a sandbox to do whatever weird thing they want.

Step 1: Set Up the Lua Runtime

You need the Lua headers and library. On Arch or Fedora:

sudo pacman -S lua        # Arch
sudo dnf install lua-devel # Fedora

Now start with a minimal integration:

#include <lua.hpp>

int main() {
    lua_State* L = luaL_newstate();     // create a Lua state
    luaL_openlibs(L);                   // load Lua standard libs

    luaL_dofile(L, "example.lua");      // run a script

    lua_close(L);                       // clean up
    return 0;
}

That’s all it takes to boot Lua from C++. You now have a programmable brain inside your C++ app.

Step 2: Calling Lua Functions from C++

You want your Lua script to define functions you can call from C++. Say your example.lua has:

function add(a, b)
  return a + b
end

From C++:

lua_getglobal(L, "add");
lua_pushnumber(L, 2);
lua_pushnumber(L, 3);
lua_pcall(L, 2, 1, 0);

double result = lua_tonumber(L, -1);
lua_pop(L, 1);

Boom. Lua did the math, and you still own the stack. Step 3: Calling C++ from Lua

This is where it gets powerful. You write C++ functions that Lua can call.

int cpp_say_hello(lua_State* L) {
    printf("Hello from C++!\n");
    return 0;
}

// Register the function:

lua_register(L, "say_hello", cpp_say_hello);

Now Lua can:

say_hello()

You’ve just extended Lua with native code.

Step 4: Write a C++ Module for Lua

Instead of polluting the global Lua namespace, it’s cleaner to define a full module and register functions inside a table.

Here’s a basic module that mimics what lfs.currentdir() does: it calls getcwd() from C and returns the current working directory to Lua.

Module: fs.cpp

#include <lua.hpp>
#include <unistd.h>   // for getcwd
#include <limits.h>   // for PATH_MAX

int lua_get_cwd(lua_State* L) {
    char buffer[PATH_MAX];
    if (getcwd(buffer, sizeof(buffer)) != NULL) {
        lua_pushstring(L, buffer);
        return 1;
    } else {
        return luaL_error(L, "failed to get current directory");
    }
}

static const luaL_Reg fslib[] = {
    {"cwd", lua_get_cwd},
    {NULL, NULL}
};

extern "C" int luaopen_fs(lua_State* L) {
    luaL_newlib(L, fslib);
    return 1;
}

🧠 What’s Happening:

  1. We use getcwd() to retrieve the current directory.
  2. If it works, we push the result onto the Lua stack.
  3. If it fails, we raise a Lua error (cleaner than returning nil and expecting the user to check).
  4. luaL_newlib() creates a Lua table with your native functions—this is the idiomatic way to write a Lua module in C or C++.

In Lua:

local fs = require("fs")
print("Current directory is:", fs.cwd())

🔧 Build it:

Compile the module as a shared library (fs.so, fs.dylib, or fs.dll, depending on OS):

g++ -shared -fPIC -o fs.so fs.cpp -llua

Make sure fs.so is in your package.cpath or the current working directory when you run your Lua script.

Embedding Lua is not unsafe, but it’s what you expose that matters.

  • Use luaL_check* functions to validate input types.
  • Don’t expose system calls you wouldn’t want triggered in a script.
  • You can sandbox Lua by not loading all libs or replacing os.execute with a no-op.
  • You can monitor execution by setting instruction limits with lua_sethook.

If you’re giving users access to Lua, you owe it to yourself to lock down what they can touch. TL;DR

If your C++ project has any kind of dynamic behavior, Lua is your best bet to expose a clean, scriptable API without turning your codebase into a graveyard of config parsing and command-line flags.

It’s easy to embed, lightning-fast, and battle-tested in gaming engines, editors, and now… your own stuff.

Want to see this in action? I wrote HyprLua to bring Lua to Hyprland. But you can build your own runtime layer for any project. Lua gives you power without taking control.