首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

Lua 與 C++ 交互深度解析(三):C++ 調用 Lua 腳本全流程實戰

     編輯:關於C++

實際項目中常將 Lua 作為配置腳本(替代 INI/XML/JSON),利用其動態類型、表結構、函數定義能力,實現邏輯與數據分離。本文通過完整案例,演示 C++ 如何加載腳本、讀取全局變量/表/函數、修改表內容、創建新表、調用 Lua 函數、處理錯誤,並深度解析棧操作流程與類型映射規則。

一、核心通信協議回顧

「如果你想要什麼,告訴我,我來產生,放到棧上,你只能通過 API 操作;我只管我的世界。」

  • Lua 值由 Lua 創建、管理、GC;C++ 僅持有棧索引
  • C++ 值進入 Lua 世界 → lua_push* 創建對應 Lua 值並壓棧
  • Lua 值流出 → lua_to* 讀取並轉換為 C++ 原生類型
  • 無 直接指針共享、無 C++ 直接訪問 Lua 堆對象

二、案例腳本 hello.lua

str = "I am so cool"
tbl = {name = "shun", id = 20114442}
function add(a, b)
    return a + b
end

三、C++ 完整調用實現

#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;
}

四、棧操作流程深度解析

4.1 lua_getglobal 內部兩步

  1. 將全局變量名壓入棧
  2. 查找全局環境表 _G,獲取值並替換棧頂的變量名

結果:棧頂為變量值,變量名被消費。

4.2 lua_getfield(L, idx, k) 等價展開

lua_pushstring(L, k);   // 壓入鍵
lua_gettable(L, idx);   // 以 idx 處表為對象,彈出鍵,壓入值
// 注意:idx 為負數時,壓入鍵後表索引變為 idx-1,故實際常用 lua_gettable(L, idx-1)

4.3 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

4.4 函數調用棧變化圖解

調用前棧(自底向上):
[add函數] [10] [20]  ← 棧頂

lua_pcall 內部流程:
1. 彈出參數 20 → 保存
2. 彈出參數 10 → 保存
3. 彈出函數 add → 調用
4. 執行 Lua 函數
5. 將返回值壓入棧頂

調用後棧:
[30]  ← 棧頂(返回值)

4.5 關鍵原則:所有棧操作基於棧頂

例如 lua_setfield(L, 2, "name") 會:

  1. 彈出棧頂值(新值)
  2. 定位索引 2 處的表
  3. 設置表字段

因此操作順序極其重要:先壓值,再操作表/函數。

五、Lua 值與 C++ 類型映射表

C++ 類型Lua 類型壓入 API取出 API備註
—nillua_pushnil無無 C 對應
boolbooleanlua_pushbooleanlua_toboolean非 0 為 true
int/float/doublenumberlua_pushnumber / lua_pushintegerlua_tonumber / lua_tointeger整數存 n,不存 b
void*/指針lightuserdatalua_pushlightuserdatalua_touserdata無 GC,裸指針
std::string/char*stringlua_pushstring / lua_pushlstringlua_tostringLua 內部拷貝/驻留
—tablelua_newtable / lua_createtable無直接取出僅通過 API 操作
—functionlua_pushcclosure無直接取出C 函數封裝為閉包
自定義結構userdatalua_newuserdatalua_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 核心流程:

  1. luaL_newstate 創建狀態機
  2. luaL_loadfile + lua_pcall 加載執行腳本
  3. lua_getglobal 獲取全局變量/函數
  4. 表操作:lua_getfield / lua_setfield / lua_newtable
  5. 函數調用:函數入棧 → 參數入棧 → lua_pcall → 返回值出棧
  6. 類型轉換:lua_is* / lua_to* 族函數
  7. lua_close 釋放狀態機

掌握棧頂相對操作、索引管理、類型邊界、錯誤處理四大核心技能,即可構建穩健的 Lua-C++ 雙向交互層。進階主題(C++ 註冊模塊、用戶數據綁定類、元表實現面向對象、協程交互)將在後續文章展開。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved