引言
通過前幾篇,我們已經對Lua的C API有了一定的了解,如lua_push*、lua_is*、lua_to*等等。用C++調用Lua數據時,我們主要運用lua_getglobal與lua_push*配合以達到目的。現在我們來試試用Lua調用C++數據。
C++數據類型映射到Lua
C++中數據類型有這么幾種:1、內建的int、float等;2、指針,如void *、int *、int (*fun)(int, int)等;3、用戶自定義的class、strcut等。Lua中C API支持操作的數據類型有如下:
見名知意,從他們的參數,我們就可以看出來他們的作用。比如:lua_pushlightuserdata用來將指針壓棧,lua_pushcclosure用來將函數壓棧,不一而足。通過這些API,我們可以將C++中的數據結構一一映射到Lua中。
Lua調用C++內置常用數據類型與函數
我們要將一個值,從C++傳入Lua,必須有兩個步驟:1、值是多少?通過lua_push*將值壓入棧頂,此時該值的類型與值的大小已確定;2、用什么名字來引用該值?通過lua_setglobal來用一個名字引用棧頂的值。我們來按照這個步驟,嘗試一下將一個變量傳入Lua,代碼如下:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
extern "C"
{
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
};
void TestLua2();
int main()
{
TestLua2();
return 0;
}
void TestLua2()
{
lua_State *L = luaL_newstate();
luaopen_base(L); //
luaopen_table(L); //
luaopen_package(L); //
luaopen_io(L); //
luaopen_string(L); //
luaL_openlibs(L); //打開以上所有的lib
int valueCPP = 1;
// 將a值壓入棧頂
lua_pushnumber(L, valueCPP);
// 命名棧頂的值
lua_setglobal(L, "valueCPP");
string str;
while (true)
{
cout << "輸入lua文件路徑:" << endl;
getline(cin, str, '\n');
if (luaL_loadfile(L, str.c_str())
|| lua_pcall(L, 0, 0, 0) )
{
const char * error = lua_tostring(L, -1) ;
cout << string(error) << endl;
}
}
}
Lua文件中代碼如下:
運行結果如下:
將函數傳入Lua有點復雜,如何確定函數的參數?如何確定函數的返回值及其個數?我們一起去查閱Lua文檔,看看文檔怎么說。
翻譯如下:將一個C函數入棧。這個函數接受一個C函數指針,將其壓入作為一個Lua的function類型值壓入棧,當這個值被調用時,Lua將調用該值對應的C函數。所有注冊在Lua中的C函數必須遵守一個正確的協議來接受參數和返回返回值(查看lua_CFunction)。
翻譯如下:為了正確的與Lua交互,必須遵守如下協議,該協議定義了參數和返回值的傳遞方式:一個C函數從Lua的棧中以直接(左邊的參數先入棧)順序來接收參數。因此,當該函數的調用開始時,lua_gettop(L) 得到用來調用該函數的參數個數(注:lua_gettop的作用是得到棧的棧頂元素的索引,即棧的長度)。第一個參數(如果有的話)的值索引為1,最后一個參數的值索引為lua_gettop(L)。為了將返回值返回給Lua,C函數將所有的返回值以直接的順序(第一個返回值先入棧,以此類推)全部壓入棧中,然后將返回值的個數作為值返回(注:此處的返回是return之意,不是傳遞到Lua中)。所有棧中其他的在返回值個數之下的索引對應的值會被Lua忽略掉,被Lua調用的C函數能傳遞多個值返回給Lua。例子我就不翻譯了。
按照其描述,我們先來試試無參無返回值的函數:
lua文件內容如下:
結果如下:
現在,我們定義一個函數,接收兩個int類型的參數,返回兩個值:和與差。代碼如下:
Lua文件內容如下:
結果如下:
以上,即可完成從Lua中調用C++函數。這里有個技巧,我們可以在Lua中改變在C++里定義的函數名,即調用lua_setglobal時指定該函數的名字。如果不想改變,我們可以定義一個宏來省略掉這些代碼,如下:
這樣,即可用一行代碼,將函數在Lua中聲明。
Lua調用C++中自定義類型
我低估了這么做的復雜度,這個內容我想准備一整個篇幅來描述。請靜待下一篇。(請點擊這里)
將C++里的數據打包成dll供Lua調用
前面的操作,本質上還是Lua寄宿在C++中的。現在,我們將C++寄宿在Lua中。我們先回顧一下luaopen_*這一系列的API:
這些API的作用是打開相應的庫,比如luaopen_string,打開lua的字符串操作庫。如果不打開這個庫,那么我們就無法使用string.format之類的操作,因為這些操作的定義是在這些庫中實現的。我們先來看一下string庫這個文件,它位於src目錄下lstrlib.c。詳細內容我就不貼出來了,主要看看最下面的代碼:
如果我們要為Lua寫擴展模塊,我們必須遵循下面幾條規定:1、必須有一個luaL_Reg結構,以便將庫中所有的函數映射到Lua中,如上圖。2、必須定義一個luaopen_*函數並且導出它,*為dll的名字(注意,函數的luaopen_后面的字符必須與dll的命名一致)。3、映射入Lua的函數,其參數的傳遞,必須按照Lua的規定(就是上文面描述的)。我們先來試試,首先修改CMakeLists.txt,新添加一個項目,用來構建Dll:
project(LuaTest)
include_directories(AFTER ${CMAKE_SOURCE_DIR})
##########lua靜態庫
set(LIB_FILES lapi.c lcode.c lctype.c ldebug.c ldo.c ldump.c lfunc.c lgc.c llex.c lmem.c lobject.c lopcodes.c lparser.c lstate.c lstring.c ltable.c ltm.c lundump.c lvm.c lzio.c lauxlib.c lbaselib.c lbitlib.c lcorolib.c ldblib.c liolib.c lmathlib.c loslib.c lstrlib.c ltablib.c loadlib.c linit.c)
source_group("\\libFiles" FILES ${LIB_FILES})
add_library (LuaLib STATIC ${LIB_FILES})
###########c++與lua交互###################
add_executable(LuaWithCPPTest source.cpp)
###########source_group("\\headFiles" FILES source.cpp)
target_link_libraries(LuaWithCPPTest LuaLib)
############lua解釋器###########
add_executable(LuaInterpreter lua.c ${LIB_FILES})
############DLL#################
add_library(LuaDll SHARED sourceDll.cpp)
target_link_libraries(LuaDll LuaLib )
#ADD_DEFINITIONS(-DLUA_LIB -DLUA_BUILD_AS_DLL)
######################################define LUA_LIB##################################################
######################################define LUA_BUILD_AS_DLL#########################################
其中的DLL即是我們新添加的項目,sourceDll.cpp文件內容如下:
extern "C"
{
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
};
static int Test(lua_State *L)
{
lua_pushnumber(L, 1);
printf("This is a message from c++ dll\n");
return 0;
}
static const struct luaL_Reg mydlllib[] = {
{"Test", Test},
{NULL,NULL},
};
extern "C" int __declspec(dllexport)luaopen_LuaDll(lua_State *L)
{
printf("luaopen_LuaDll invoked\n");
luaL_newlib(L, mydlllib); // 5.2之前使用luaL_register(L, "modulename", modulename);
return 1;
}
得到的項目結構如下:
構建好LuaDll后,我們可以在其目錄下,運行LuaInterpreter,在解釋器中require,具體如下:
運行,得到結果如下:
我們觀察它的輸出,第一句是:“luaopen_LuaDll invoked”,這句話表明程序確實走到了我們構建的DLL中,接着輸出“multiple Lua VMs detected”,這句話翻譯過來就是:檢測到多個Lua虛擬機。這就是我前面說的那個坑了!!!網上搜資料,然后自己摸索,總結出的經驗如下:自定義擴展Lua的DLL時,要求其DLL構建時,引用的是Lua動態庫,而不是靜態庫(在這里,我們現在引用的是靜態庫)。如果我們自己的宿主也要支持擴展DLL的話,宿主也必須是引用Lua的動態庫。生成Lua動態庫時要定義宏LUA_LIB和LUA_BUILD_AS_DLL,以便導出Lua函數符號,如果沒定義這兩個宏,那么生成DLL時,將沒有.lib文件生成。為什么是這兩個宏呢?請以這兩個宏為關鍵字搜索src目錄下luaconf.h文件,即可找到答案。既然知道原因了,我們來修改CMakeLists.txt,使之生成動態庫,並且在其中定義這兩個宏,結構如下:
重新生成即可,然后在我們的項目中導入,具體步驟及效果如下圖:
tst.lua內容如下:
至此,我們便可以通過DLL的形式擴展Lua了。
總結
通過DLL,我們可以很方便的擴展Lua,從Lua中調用C++的函數,使Lua更加強大了。C++中的類映射到Lua時,比較復雜,要涉及到Lua的metatable。下一篇,我們將一起探索metatable。