header-image

Da mesma forma que o Neovim rompeu com o legado do Vim ao adotar Lua, eu queria desacoplar configuração e lógica do C++ compilado em um novo ambiente: o Hyprland. Mas este post não é sobre o Hyprland — é sobre como dar ao seu projeto C++ uma camada de scripting em tempo de execução sem inchá-lo ou entregar sua alma ao Python.

Você quer expor lógica interna. Você quer extensibilidade. Você quer Lua.

Por que Lua?

  • Porque é leve.
  • Porque não vem com pilhas incluídas — em vez disso, te dá fósforos.
  • Porque embutir Python é uma bagunça, e escrever parsers de config para seu app C++ é entediante pra caramba.

Lua foi projetada para ser embutida. Sua API C é de baixo nível, mas incrivelmente simples. Você mantém controle total enquanto seus usuários ganham uma sandbox para fazer o que quiserem.

Passo 1: Configurar o Runtime Lua

Você precisa dos headers e da biblioteca Lua. No Arch ou Fedora:

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

Agora comece com uma integração mínima:

#include <lua.hpp>

int main() {
    lua_State* L = luaL_newstate();     // cria um estado Lua
    luaL_openlibs(L);                   // carrega as libs padrão do Lua

    luaL_dofile(L, "example.lua");      // executa um script

    lua_close(L);                       // limpa
    return 0;
}

É tudo que você precisa para iniciar o Lua a partir do C++. Agora você tem um cérebro programável dentro do seu app C++.

Passo 2: Chamar Funções Lua a Partir do C++

Você quer que seu script Lua defina funções que você pode chamar do C++. Digamos que seu example.lua tenha:

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

Do 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);

Pronto. O Lua fez a conta e você ainda é dono da stack.

Passo 3: Chamar C++ a Partir do Lua

É aqui que fica poderoso. Você escreve funções C++ que o Lua pode chamar.

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

// Registra a função:
lua_register(L, "say_hello", cpp_say_hello);

Agora o Lua pode:

say_hello()

Você acabou de estender o Lua com código nativo.

Passo 4: Escrever um Módulo C++ para Lua

Em vez de poluir o namespace global do Lua, é mais limpo definir um módulo completo e registrar as funções dentro de uma tabela.

Aqui está um módulo básico que imita o que lfs.currentdir() faz: chama getcwd() do C e retorna o diretório de trabalho atual para o Lua.

Módulo: fs.cpp

#include <lua.hpp>
#include <unistd.h>   // para getcwd
#include <limits.h>   // para 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;
}

O que está acontecendo:

  1. Usamos getcwd() para recuperar o diretório atual.
  2. Se funcionar, empurramos o resultado para a stack do Lua.
  3. Se falhar, levantamos um erro Lua (mais limpo do que retornar nil e esperar que o usuário verifique).
  4. luaL_newlib() cria uma tabela Lua com suas funções nativas — essa é a forma idiomática de escrever um módulo Lua em C ou C++.

Em Lua:

local fs = require("fs")
print("Diretório atual:", fs.cwd())

Compilar:

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

Garanta que fs.so esteja no seu package.cpath ou no diretório de trabalho atual ao rodar seu script Lua.

Embutir Lua não é inseguro, mas o que você expõe é que importa.

  • Use funções luaL_check* para validar tipos de entrada.
  • Não exponha chamadas de sistema que você não gostaria que fossem acionadas por um script.
  • Você pode fazer sandbox no Lua não carregando todas as libs ou substituindo os.execute por um no-op.
  • Você pode monitorar a execução definindo limites de instrução com lua_sethook.

Se você está dando acesso ao Lua para usuários, você se deve o trabalho de travar o que eles podem tocar.

TL;DR

Se seu projeto C++ tem qualquer tipo de comportamento dinâmico, Lua é sua melhor aposta para expor uma API limpa e scriptável sem transformar seu codebase em um cemitério de parsers de config e flags de linha de comando.

É fácil de embutir, incrivelmente rápido e battle-tested em engines de games, editores e agora… no seu próprio projeto.

Quer ver isso em ação? Escrevi o HyprLua para trazer Lua ao Hyprland. Mas você pode construir sua própria camada de runtime para qualquer projeto. Lua te dá poder sem tirar o controle.