为什么

把 Lua 当配置语言 + 脚本插件层塞进 C/C++ 程序。这是 Lua 的"原产业"——魔兽、Adobe Lightroom、VLC、Roblox 都这么做。

读者画像:你已经在写 C/C++ 程序,想加可热更的脚本能力。

链接 Lua

brew install lua / apt install liblua5.4-dev 后:

gcc main.c -llua -lm -ldl -o myapp
# 或
gcc main.c $(pkg-config --cflags --libs lua5.4) -o myapp

LuaJIT:-lluajit-5.1

最小 host:跑一段 Lua 字符串

#include <stdio.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>

int main(void) {
    lua_State *L = luaL_newstate();   // 新建 Lua 状态机
    luaL_openlibs(L);                 // 打开标准库

    if (luaL_dostring(L, "print('Hello from Lua!')") != LUA_OK) {
        fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1));
    }

    lua_close(L);
    return 0;
}

luaL_newstate 创建独立 Lua 虚拟机。可以同进程开多个 lua_State——互不干扰。

加载脚本文件

if (luaL_dofile(L, "script.lua") != LUA_OK) {
    fprintf(stderr, "%s\n", lua_tostring(L, -1));
}

script.lua

print("loaded from C")
function greet(name) return "Hello, " .. name end

从 C 调 Lua 函数

luaL_dofile(L, "script.lua");        // 加载,注册 greet 到全局

lua_getglobal(L, "greet");           // 推函数到栈
lua_pushstring(L, "World");          // 推参数
if (lua_pcall(L, 1, 1, 0) != LUA_OK) {  // 1 入参,1 返回值
    fprintf(stderr, "%s\n", lua_tostring(L, -1));
} else {
    printf("got: %s\n", lua_tostring(L, -1));    // "Hello, World"
}
lua_pop(L, 1);                       // 弹返回值

心智模型:Lua C API 全是基于一个操作——参数、返回值都通过推 / 弹来传递。

从 Lua 调 C 函数

static int l_sum(lua_State *L) {
    double a = luaL_checknumber(L, 1);
    double b = luaL_checknumber(L, 2);
    lua_pushnumber(L, a + b);
    return 1;     // 返回值个数
}

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    lua_pushcfunction(L, l_sum);
    lua_setglobal(L, "csum");        // Lua 现在能调 csum()

    luaL_dostring(L, "print(csum(3, 4))");   // 7

    lua_close(L);
}

配置文件模式

config.lua

return {
    server = { host = "0.0.0.0", port = 8080 },
    debug = true,
    plugins = {"auth", "rate-limit", "metrics"},
}

C 端:

luaL_dofile(L, "config.lua");        // 配置返回一个表,留在栈顶

lua_getfield(L, -1, "server");       // 推 server 子表
lua_getfield(L, -1, "host");
const char *host = lua_tostring(L, -1);
lua_pop(L, 1);

lua_getfield(L, -1, "port");
int port = (int)lua_tointeger(L, -1);
lua_pop(L, 2);                       // 弹 port 和 server

printf("listening on %s:%d\n", host, port);

配置文件用 Lua 比 JSON/YAML 灵活:能写计算、条件、引用、注释、共享变量。

安全:沙箱

让脚本只能用部分 API:

lua_State *L = luaL_newstate();
// 不打开所有标准库——只挑安全的
luaopen_base(L);     lua_pop(L, 1);
luaopen_string(L);   lua_setglobal(L, "string");
luaopen_math(L);     lua_setglobal(L, "math");
// 不开 io / os / debug / package

或加载脚本前清掉危险全局:

luaL_dostring(L, "io = nil; os.execute = nil; package = nil");

更严格的方案用 lua_setfenv (5.1) / _ENV (5.2+) 做按脚本的环境隔离——见 Lua sandbox 教程

错误处理

int r = lua_pcall(L, nargs, nresults, 0);
if (r != LUA_OK) {
    const char *err = lua_tostring(L, -1);
    fprintf(stderr, "Lua error (%d): %s\n", r, err);
    lua_pop(L, 1);
}

lua_call 直接调(出错程序整个崩溃);lua_pcall 是受保护的(拿到错误字符串,宿主决定怎么处理)。

内存与 GC

lua_gc(L, LUA_GCCOLLECT, 0);          // 强制 full GC
int kb = lua_gc(L, LUA_GCCOUNT, 0);   // 当前内存 (KB)

长跑程序里:定期主动触发 GC 或调 lua_gc(L, LUA_GCSTEP, 0) 渐进式收。

替代:sol2(C++ 友好封装)

直接用 C API 啰嗦。C++ 项目用 sol2

#include <sol/sol.hpp>

sol::state lua;
lua.open_libraries(sol::lib::base, sol::lib::string);
lua.script("function add(a, b) return a + b end");

int result = lua["add"](3, 4);    // 干净的语法
std::cout << result << "\n";       // 7

lua.set_function("greet", [](const std::string& name) {
    return "Hello, " + name;
});
lua.script("print(greet('Sol'))");

C++14 + 模板魔法,几乎零运行时开销。强烈推荐 C++ 项目用。

→ 下一篇 Lua 路线图