C++與Lua交互之配置&交互原理&示例


|Lua 簡介

Lua 是一種輕量小巧的腳本語言,也是號稱性能最高的腳本語言,它用C語言編寫並以源代碼形式開放。

某些程序常常需要修改內容,而修改的內容不僅僅是數據,更要修改很多函數的行為。

而修改函數行為這種事,很難用簡單的更改數據的方式來實現,若在源代碼層面上改又得重新編譯生成,導致修改成本高。

而腳本語言先通過更改數據,並加了一層對數據解釋成運行代碼的步驟,從而使程序能在運行時更改復雜的函數行為而無需重新編譯。

它為程序大大地提供了靈活的擴展和定制功能,減少了修改的成本。

而游戲程序往往會選擇性能高的LUA作為腳本,來應對某些經常修改的模塊。

 

|編譯、配置 Lua動態鏈接庫

  (本文使用Lua-5.3.5版本)

此外不建議編譯配置Lua靜態鏈接庫,不然用到某些函數缺少dll會導致運行時錯誤

Lua庫C源碼:https://www.lua.org/download.html

下載lua-5.3.x.tar.gz文件,解壓。

創建DLL項目,選擇Release模式

將解壓后的src文件夾下所有.h和.c文件(lua.c,luac.c和其他格式文件都不要)拖進項目,

預處理器定義(宏定義)加上LUA_BULD_AS_DLL

然后項目生成dll文件和lib文件,這兩個文件就是編譯好出來的動態鏈接庫。

 

最后,在自己的工程項目里,

dll文件復制過來放在生成文件夾(第一次編譯項目會在項目根目錄生成的Debug/Release文件夾)里,

lib文件復制過來放在項目里某個目錄,

那堆.h文件.c文件lua.c,luac.c和其他格式文件都不要)也要復制過來放在項目里某個目錄,

配置好項目的包含目錄(放.h.c文件的那里)和庫目錄(放lib文件的那里)

至此,項目配置Lua庫完成

 

然后可以在工程項目里如下代碼包含lua庫:

#pragma comment(lib, "lua.lib")  
extern "C"  
{  
#include <lua.h>  
#include <lualib.h>  
#include <lauxlib.h>  
};

 

|Lua 基本語法(部分)

完整的語法教程->Lua編程參考文檔:http://book.luaer.cn/

 

部分Lua的基本變量類型:

nil 無效值
boolean 只有兩個值:false和true
number 雙精度類型的實浮點數(Lua的數字類型只有雙精度浮點數,並無整形單精度之分)
string 字符串由一對雙引號或單引號來表示
function 由C或Lua編寫的函數
table

Lua 中的表(table)其實是一個"關聯數組",數組的索引可以是數字或者是字符串。

 

 

 

 

 

 

 

Lua在定義一個變量時,無需聲明它的類型:

a = 12

b = 250.520

c = "hello world"

d = {name = "asd",id = 2333}

 

條件:

if xxx then

  xxxx

else

  xxxx

end

 

Lua的函數可以返還多個返還值

函數格式:

function xxx(xxxx)

end

 

|C與Lua的交互機制

在用C/C++使用Lua庫前,有必要理解它們的交互機制。

C與Lua交互的基礎是虛擬棧:

 

(如圖所示)

此外,為了方便找到棧底棧頂元素的位置,這個虛擬棧還提供兩種索引:

正數索引和負數索引,從而使-1總是代表棧頂元素的索引,1總是代表棧底元素的索引

 

交互基本原理:

當C要調用Lua數據時,Lua把值壓入棧中,C再從棧中取值;

當Lua調用C數據時,C要將數據壓入棧中,讓Lua從棧中取值。

 

交互值時大部分可以按上面的互相傳輸,但是交互函數稍微更復雜:

當C要調用Lua函數時,Lua先將Lua函數壓入棧中,C再將數據(作為參數)繼續壓入棧中,

然后用API調用棧上的lua函數+參數,調用完后,Lua函數和參數都會出棧,而函數計算后的返還值會壓入棧中。

 

 

 

 

當Lua要調用C函數時,需要通過API注冊符合lua規范的C函數,來讓Lua知道該C函數的定義。

 

|C/C++調用Lua腳本

先編寫一個測試用的Lua腳本文件,

(由於博主新裝電腦,暫時直接用記事本編輯,但是沒語法檢查容易出錯,這里推薦使用其它專業的lua編輯器,例如vsc,lua studio等)

 

打開lua腳本文件:

char lua_filename[] = "test.lua";
lua_State *L = load_lua(lua_filename);
if (NULL == L) {
  return -1;
}

讀取lua文件的一般變量:

lua_getglobal(L, "str");
printf("str:%s\n",lua_tostring(L, -1));
lua_getglobal(L, "number");
printf("number:%f\n", lua_tonumber(L, -1));

讀取lua文件的table里的變量:

lua_getglobal(L, "table");
//記錄table的索引
int tableIndex = lua_gettop(L);

//對-1位置的table取name變量壓入棧頂
lua_getfield(L, -1, "name");
printf("table:name:%s\n",lua_tostring(L, -1));

//對tableIndex位置的table取table2變量壓入棧頂
lua_getfield(L, tableIndex, "table2");
//對-1位置的table2取name2變量壓入棧頂
lua_getfield(L, -1, "name2");
printf("table:table2:name2:%s\n", lua_tostring(L, -1));

讀取lua文件的函數,並調用之:

lua_getglobal(L, "add");//讀取函數到棧頂
lua_pushnumber(L, 10); //壓入參數 10
lua_pushnumber(L, 20); //壓入參數 20
//調用函數,若失敗返還非0
//lua_pcall第二個參數是指參數的數量,第三個參數是指返還值的數量
if (lua_pcall(L, 2, 1, 0) != 0) {
    printf("lua_pcall failed: %s\n", lua_tostring(L, -1));
    return -1;
}
//讀取目前棧頂的元素,也就是返還值
double result = lua_tonumber(L, -1);
printf("add result:%f\n",result);

 

執行上述代碼,我們便能看到如下結果

 

調用lua API簡單總結:

將Lua腳本里的變量壓入棧中

//根據name獲取某個全局變量,壓入棧頂
int lua_getglobal(lua_State *L, const char *name);
//根據name獲取index索引的table元素里的某個變量,壓入棧頂
int lua_getfield(lua_State *L, int index, const char *name);

將C變量壓入棧中

//將數字壓入棧頂
void lua_pushnumber(lua_State *L,double number);
//將字符串壓入棧頂
const char *lua_pushstring(lua_State *L, const char *str);

將棧中某個位置的元素提取成C變量

//將index索引的元素以數字的形式提取
double lua_tonumber(lua_State *L, int index);
//將index索引的元素以字符串的形式提取,返還
const char* lua_tostring(lua_State *L, int index);

利用棧調用lua函數

//調用lua函數,arguNum是參數的個數,returnNum是返還值的個數,errorHandleIndex是函數調用錯誤時會另外調用的錯誤處理函數的索引(0視為無)
//調用前要求:依次壓入 lua函數元素,第1個參數元素,第2個參數元素....
//調用后:調用的lua函數元素和所有參數元素 會在棧里被清理掉,並且若干個返還值元素將壓入棧頂
int lua_pcall(lua_State *L, int arguNum, int returnNum, int errorHandleIndex);

 

|Lua腳本調用C函數

首先編寫好要調用的C函數,

但是這個C函數並不會像我們往常編寫的“正宗C函數”。

首先該函數格式應為:

static int xxxxx(lua_State *L) {
  //balabala隨便做點事什么
    return 一個數字;
}

xxxxx的返還值 代表 注冊后該函數返還值的個數

那如何接受參數呢?這得通過上面介紹過的“將棧中某個位置的元素提取成C變量”方法獲取參數。

那如何返還返還值呢?同樣通過"將C變量壓入棧中"方法將返還值壓入棧頂。

 

例如1個參數、無返還值的print_num函數

static int print_num(lua_State *L) {
    double a = lua_tonumber(L, -1);
    printf("This num is %f", a);
return 0; }

有3個參數、1個返還值的add_three函數

static int add_three(lua_State *L) {
    int a = lua_tonumber(L, -1);
int b = lua_tonumber(L, -2);
int c = lua_tonumber(L, -3);
int sum = a + b + c; lua_pushnumber(L, sum); return 1; }

 

 

然后我們在代碼里用API將上述C函數注冊到Lua環境里:

第二個參數為在Lua腳本里要注冊的函數名字,第三個參數為要注冊的C函數指針

lua_register(L, "print1", print_num);
lua_register(L, "add3", add_three);

 

 接下來修改腳本文件內容:

 

我們看看在C調用Lua腳本的callCFunc(),這個函數里面能不能正確調用回2個注冊的C函數。

 1     char lua_filename[] = "test.lua";
 2     lua_State *L = load_lua(lua_filename);
 3     if (NULL == L) {
 4         return -1;
 5     }
 6     // 注冊函數
 7     lua_register(L, "print1", print_num);
 8     lua_register(L, "add3", add_three);
 9 
10     //調用Lua腳本的callCFunc函數
11     lua_getglobal(L, "callCFunc");
12     lua_pcall(L, 0, 0, 0);

 

結果如我們所料:

 

通過上面C與Lua的交互,我們發現它們的交互機制並不簡單,

而且尚不支持C++這種更復雜的更多特性與Lua交互(類/對象/等)。

實際工程中我們往往不想將注意力放在交互的底層過程,而是想如何方便的直接使用交互。

於是可以使用github現有的庫以已達到C++與Lua方便交互的作用。

 

|使用Kaguya C++ binding庫

kaguya c++ binding下載地址:https://github.com/satoren/kaguya

kaguya是一個很易用的庫,它github的使用說明也十分淺顯易懂,就連它的配置也是十分簡單的:

首先確保你的項目已經包含了lua5.1~lua5.3的環境,

然后只需在你的項目添加"kaguya/include"目錄到項目的"頭文件包含目錄"即可。

 

本文就只簡單示范它的幾個用法(因為github的說明足夠詳細了,可自行查閱):

testkaguya.lua文件:

str = "Im dont know what to write"
number = 250.520

table = {
name = "Ezio",
id = 123456,
table2 = {name2 = "Auditore",id2 = 23333}
}

function useCppClass(obj)
    obj:a()
end

C++測試用代碼:

 1 #include <iostream>
 2 #include <string> 
 3 #include "kaguya/kaguya.hpp"
 4 
 5 using namespace std;
 6 
 7 class Base {
 8 private:
 9     int shit;
10 public:
11     virtual void a() {cout << "Base::a()";}
12 };
13 
14 class Derived : public Base {
15 public:
16     virtual void a() { cout << "Derived::b()"; }
17 };
18 
19 int main()
20 {
21     //----初始化-----//
22     kaguya::State state;
23     state.dofile("testkaguya.lua");
24 
25     //-----執行lua代碼-----//
26     state("number2 = 233");
27     state("str2 = 'ok'");
28 
29     //-----值交互----//
30     std::string value0 = state["str"];
31     cout << value0 << endl << endl;
32 
33     std::string value1 = state["str2"];
34     cout << value1 << endl << endl;
35 
36     double value2 = state["number"];
37     cout << value2 << endl << endl;
38 
39     std::string value3 = state["table"]["name"];
40     cout << value3 << endl << endl;
41 
42     state["tbl"] = kaguya::NewTable();
43     state["tbl"]["value"] = 1;
44     double value4 = state["tbl"]["value"];
45     cout << value4 << endl << endl;
46 
47     //-----函數交互-----//
48     int funcReturn1 = state["math"]["abs"](-32);
49     assert(funcReturn1 == 32);
50 
51     auto funcReturn2 = state["math"]["abs"].call<int>(-32);
52     assert(funcReturn2 == 32);
53     //-----類部分-----//
54     state["Base"].setClass(kaguya::UserdataMetatable<Base>()
55         .addFunction("a", &Base::a)
56     );
57     state["Derived"].setClass(kaguya::UserdataMetatable<Derived, Base>()
58         .addFunction("a", &Derived::a)
59     );
60 
61     Base obj1;
62     Derived obj2;
63     state["useCppClass"](obj1);
64     state["useCppClass"](obj2);
65 
66     system("pause");
67     return 0;
68 }

 

測試結果:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM