實際項目中常將 Lua 作為配置腳本(替代 INI/XML/JSON),利用其動態類型、表結構、函數定義能力,實現邏輯與數據分離。本文通過完整案例,演示 C++ 如何加載腳本、讀取全局變量/表/函數、修改表內容、創建新表、調用 Lua 函數、處理錯誤,並深度解析棧操作流程與類型映射規則。
「如果你想要什麼,告訴我,我來產生,放到棧上,你只能通過 API 操作;我只管我的世界。」
lua_push* 創建對應 Lua 值並壓棧lua_to* 讀取並轉換為 C++ 原生類型hello.luastr = "I am so cool"
tbl = {name = "shun", id = 20114442}
function add(a, b)
return a + b
end
#include
#include
extern "C" {
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
}
int main() {
// 1. 創建 Lua 狀態機
lua_State* L = luaL_newstate();
if (!L) return 1;
// 2. 打開標準庫(可選,但推薦)
luaL_openlibs(L);
// 3. 加載並編譯腳本文件
int loadRet = luaL_loadfile(L, "hello.lua");
if (loadRet != LUA_OK) {
std::cerr << "Load file error: " << lua_tostring(L, -1) << "\n";
lua_pop(L, 1);
lua_close(L);
return 1;
}
// 4. 執行腳本(定義全局變量/函數)
int runRet = lua_pcall(L, 0, 0, 0);
if (runRet != LUA_OK) {
std::cerr << "Run script error: " << lua_tostring(L, -1) << "\n";
lua_pop(L, 1);
lua_close(L);
return 1;
}
// ========== 5. 讀取全局字符串 ==========
lua_getglobal(L, "str"); // 壓入 str 值
if (lua_isstring(L, -1)) {
std::string str = lua_tostring(L, -1); // 拷貝到 C++ string
std::cout << "str = " << str << "\n"; // str = I am so cool
}
lua_pop(L, 1); // 彈出字符串值
// ========== 6. 讀取/修改表 ==========
lua_getglobal(L, "tbl"); // 壓入表 tbl(索引 -1)
// 6.1 讀取字段 name
lua_getfield(L, -1, "name"); // 等價:push "name" + gettable(-2)
std::string name = lua_tostring(L, -1);
std::cout << "tbl.name = " << name << "\n"; // shun
lua_pop(L, 1); // 彈出 name 值,表仍在 -1
// 6.2 讀取字段 id
lua_getfield(L, -1, "id");
int id = lua_tointeger(L, -1);
std::cout << "tbl.id = " << id << "\n"; // 20114442
lua_pop(L, 1);
// 6.3 修改表字段 name
lua_pushstring(L, "我是一個好人~"); // 壓入新值
lua_setfield(L, -2, "name"); // tbl["name"] = 新值,彈出新值,表仍在 -2
// 驗證修改
lua_getfield(L, -1, "name");
std::cout << "modified name = " << lua_tostring(L, -1) << "\n";
lua_pop(L, 1);
lua_pop(L, 1); // 彈出表 tbl
// ========== 7. 創建新表並設置字段 ==========
lua_newtable(L); // 創建並壓入新表 {}
lua_pushstring(L, "Give me a girl friend !");
lua_setfield(L, -2, "str"); // 新表["str"] = 值,彈出值
// 新表仍在棧頂,可繼續操作或賦值給全局
lua_setglobal(L, "new_tbl"); // 彈出表並設置全局 new_tbl
// ========== 8. 調用 Lua 函數 ==========
lua_getglobal(L, "add"); // 1. 函數壓棧
lua_pushnumber(L, 10); // 2. 參數 1
lua_pushnumber(L, 20); // 3. 參數 2
// 棧狀態:底 [-3] add函數, [-2] 10, [-1] 20(棧頂)
int callRet = lua_pcall(L, 2, 1, 0); // 調用:2參數,1返回值
if (callRet != LUA_OK) {
std::cerr << "Call add error: " << lua_tostring(L, -1) << "\n";
lua_pop(L, 1);
} else {
double sum = lua_tonumber(L, -1);
std::cout << "add(10, 20) = " << sum << "\n"; // 30
lua_pop(L, 1); // 彈出返回值
}
lua_close(L);
return 0;
}
lua_getglobal 內部兩步_G,獲取值並替換棧頂的變量名結果:棧頂為變量值,變量名被消費。
lua_getfield(L, idx, k) 等價展開lua_pushstring(L, k); // 壓入鍵
lua_gettable(L, idx); // 以 idx 處表為對象,彈出鍵,壓入值
// 注意:idx 為負數時,壓入鍵後表索引變為 idx-1,故實際常用 lua_gettable(L, idx-1)
lua_setfield(L, idx, k) 等價展開lua_pushstring(L, k); // 壓入鍵
lua_insert(L, -2); // 將值移到鍵下方:... table, value, key -> ... table, key, value
lua_settable(L, idx); // 彈出 key, value;設置 table[key]=value
調用前棧(自底向上):
[add函數] [10] [20] ← 棧頂
lua_pcall 內部流程:
1. 彈出參數 20 → 保存
2. 彈出參數 10 → 保存
3. 彈出函數 add → 調用
4. 執行 Lua 函數
5. 將返回值壓入棧頂
調用後棧:
[30] ← 棧頂(返回值)
例如 lua_setfield(L, 2, "name") 會:
因此操作順序極其重要:先壓值,再操作表/函數。
| C++ 類型 | Lua 類型 | 壓入 API | 取出 API | 備註 |
|---|---|---|---|---|
| — | nil | lua_pushnil | 無 | 無 C 對應 |
| bool | boolean | lua_pushboolean | lua_toboolean | 非 0 為 true |
| int/float/double | number | lua_pushnumber / lua_pushinteger | lua_tonumber / lua_tointeger | 整數存 n,不存 b |
| void*/指針 | lightuserdata | lua_pushlightuserdata | lua_touserdata | 無 GC,裸指針 |
| std::string/char* | string | lua_pushstring / lua_pushlstring | lua_tostring | Lua 內部拷貝/驻留 |
| — | table | lua_newtable / lua_createtable | 無直接取出 | 僅通過 API 操作 |
| — | function | lua_pushcclosure | 無直接取出 | C 函數封裝為閉包 |
| 自定義結構 | userdata | lua_newuserdata | lua_touserdata | 需元表支持方法調用 |
| 錯誤現象 | 原因 | 修正 |
|---|---|---|
| 棧溢出/崩潰 | 壓入過多未彈出,超過棧限制(默認 ~100萬) | 嚴格遵守「壓幾個彈幾個」,善用 lua_settop 還原棧點 |
| 讀取字符串後內容錯亂 | 直接保存 lua_tostring 返回指針,後續棧操作導致緩衝區失效 | 立即構造 std::string(lua_tostring(L, -1)) 拷貝內容 |
| 調用報錯「attempt to call a nil value」 | lua_getglobal 找不到名稱,壓入 nil | 調用前 lua_isfunction(L, -1) 校驗 |
| 表字段讀取為 nil | 鍵名拼寫錯誤、大小寫敏感、表未在預期索引 | 打印棧頂類型調試,使用 lua_getfield 而非手動組合 |
| 內存泄漏 | C++ 分配內存指針作 lightuserdata 傳給 Lua,Lua 不負責釋放 | 改用 full userdata + __gc 元方法,或 C++ 單獨管理生命周期 |
C++ 調用 Lua 核心流程:
luaL_newstate 創建狀態機luaL_loadfile + lua_pcall 加載執行腳本lua_getglobal 獲取全局變量/函數lua_getfield / lua_setfield / lua_newtablelua_is* / lua_to* 族函數lua_close 釋放狀態機掌握棧頂相對操作、索引管理、類型邊界、錯誤處理四大核心技能,即可構建穩健的 Lua-C++ 雙向交互層。進階主題(C++ 註冊模塊、用戶數據綁定類、元表實現面向對象、協程交互)將在後續文章展開。