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

|Lua 簡介

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

某些程序經常須要修改內容,而修改的內容不只僅是數據,更要修改不少函數的行爲。ios

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

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

它爲程序大大地提供了靈活的擴展和定製功能,減小了修改的成本。github

而遊戲程序每每會選擇性能高的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 }

 

測試結果: