0.簡介
Lua(念“魯啊”)作為一門發展成熟的腳本語言,正在變得越來越流行。它也可以作為和C/C++執行腳本交互的語言。並且Lua的整個庫很小,我安裝了最新的正式版Lua 5.1版本,而整個靜態鏈接的lua.dll才164KB,所以Lua很輕量,特別適合輕量級腳本嵌入。
看文章名就知道,我們要講Lua和C/C++的交互,這期講Lua如何使用C/C++的東西——Lua通過C/C++導出的dll來調用。OK,Let’s Go!首先,你得去Lua官網http://www.lua.org獲取最新版本的Lua。
1.准備工作
安裝完Lua,需要在Visual Studio中配置Lua路徑,使得你的編譯器能搜尋到。關於VS2010的配置,見我的博文《VS2010 C++目錄配置》一文。完成後新建一個Dll工程便可以了。
我們用一個在Lua中顯示Windows對話框的程序來簡要介紹一下,程序雖小,但五髒俱全。程序如下:
//
// 將一些有用的Win32特性導出.
// 以便在Lua中使用.
//
extern "C"
{
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#pragma comment(lib, "lua.lib")
};
#include <Windows.h>
#include <iostream>
using namespace std;
static const char* const ERROR_ARGUMENT_COUNT = "參數數目錯誤!";
static const char* const ERROR_ARGUMENT_TYPE = "參數類型錯誤!";
//
// 發生錯誤,報告錯誤.
//
void ErrorMsg(lua_State* luaEnv, const char* const pszErrorInfo)
{
lua_pushstring(luaEnv, pszErrorInfo);
lua_error(luaEnv);
}
//
// 檢測函數調用參數個數是否正常.
//
void CheckParamCount(lua_State* luaEnv, int paramCount)
{
// lua_gettop獲取棧中元素個數.
if (lua_gettop(luaEnv) != paramCount)
{
ErrorMsg(luaEnv, ERROR_ARGUMENT_COUNT);
}
}
//
// 顯示Windows對話框.
// @param [in] pszMessage string 1
// @param [in] pszCaption string 2
//
extern "C" int ShowMsgBox(lua_State* luaEnv)
{
const char* pszMessage = 0;
const char* pszCaption = 0;
// 檢測參數個數是否正確.
CheckParamCount(luaEnv, 2);
// 提取參數.
pszMessage = luaL_checkstring(luaEnv, 1);
pszCaption = luaL_checkstring(luaEnv, 2);
if (pszCaption && pszMessage)
{
::MessageBox(
NULL,
pszMessage,
pszCaption,
MB_OK | MB_ICONINFORMATION
);
}
else
{
ErrorMsg(luaEnv, ERROR_ARGUMENT_TYPE);
}
// 返回值個數為0個.
return 0;
}
//
// 導出函數列表.
//
static luaL_Reg luaLibs[] =
{
{"ShowMsgBox", ShowMsgBox},
{NULL, NULL}
};
//
// Dll入口函數,Lua調用此Dll的入口函數.
//
extern "C" __declspec(dllexport)
int luaopen_WinFeature(lua_State* luaEnv)
{
const char* const LIBRARY_NAME = "WinFeature";
luaL_register(luaEnv, LIBRARY_NAME, luaLibs);
return 1;
}
//:-)
2.程序解析
首先我們包含Lua的頭文件,並鏈入庫文件。注意:Lua的頭文件為C風格,所以用external “C”來含入。在此例中,我們最終的導出函數為“ShowMsgBox”。
每一個導出函數的格式都為:
extern “C”int Export_Proc_Name(luaState* luaEnv);
其中,luaState*所指的結構中包含了Lua調用此Dll時必備的Lua環境。那麼Lua向函數傳遞參數該怎麼辦呢?實際上是用luaL_check[type]函數來完成的。如下:
const char* pHelloStr = luaL_checkstring(luaEnv, 1);
double value = luaL_checknumber(luaEnv, 2);
int ivalue = luaL_checkint(luaEnv, 3);
luaL_check系列函數的第二個參數是Lua調用該函數時傳遞參數從坐到右的順序(從1開始)。
然後我們看到,static的一個luaL_Reg的結構數組中包含了所有要導出的函數列表。最後通過luaopen_YourDllName的一個導出函數來完成一系列操作。YourDllName就是你最終的Dll的名字(不含擴展名)。因為你在Lua中調用此Dll時,Lua會根據此Dll名字找luaopen_YourDllName對應的函數,然後從此函數加載該Dll。
Dll入口函數格式如下:
extern "C" __declspec(dllexport)
int luaopen_WinFeature(lua_State* luaEnv)
{
const char* const LIBRARY_NAME = "WinFeature";
luaL_register(luaEnv, LIBRARY_NAME, luaLibs);
return 1;
}
我們通過luaL_register將LIBRARY_NAME對應的庫名,以及luaL_Reg數組對應的導出列表來注冊到lua_State*對應的Lua環境中。
3.Lua調用
那麼我們要如何調用該Dll呢?首先,把該Dll放到你的Lua能搜尋到的目錄——當前目錄、Lua安裝目錄下的clibs目錄……然後通過require函數導入。
因為Lua中如果你的函數調用參數只有一個,並且該參數為字符串的話,函數調用時的括號是可以省略的,所以:
require(“YourLibName”)和requir“YourLibName”都是合法的。我們把剛剛生成的WinFeature.dll文件拷貝到C盤下,然後在C盤啟動Lua。示例如下:
可以看到,函數調用方式都是“包名.函數名”,而包名就是你的Dll的名字。我們可以用下面的方式查看一個包中的所有函數:
for k, v in pairs(PackageName) do
print(k, v)
end
然後我們調用WinFeature.ShowMsgBox函數:
4.Lua堆棧詳解
唔,那麼lua_State結構如何管理Lua運行環境的呢?Lua又是如何將參數傳遞到C/C++函數的呢?C/C++函數又如何返回值給Lua呢?……這一切,都得從Lua堆棧講起。
Lua在和C/C++交互時,Lua運行環境維護著一份堆棧——不是傳統意義上的堆棧,而是Lua模擬出來的。Lua與C/C++的數據傳遞都通過這份堆棧來完成,這份堆棧的代表就是lua_State*所指的那個結構。
4.1.堆棧結構解析
堆棧通過lua_push系列函數向堆棧中壓入值,通過luaL_check系列從堆棧中獲取值。而用luaL_check系列函數時傳遞的參數索引,比如我們調用WinFeature.ShowMsgBox(“Hello”, “Tip”)函數時,棧結構如下:
其中,參數在棧中的索引為參數從左到右的索引(從1開始),棧頂元素索引也可以從-1記起。棧中元素個數可以用lua_gettop來獲得,如果lua_gettop返回0,表示此棧為空。(lua_gettop這個函數名取得不怎麼樣!呵呵)
4.2.提取參數
luaL_check系列函數在獲取值的同時,檢測這個值是不是符合我們所期望的類型,如果不是,則拋出異常。所有這個系列函數如下:
luaL_checkany ——檢測任何值(可以為nil)
luaL_checkint ——檢測一個值是否為number(double),並轉換成int
luaL_checkinteger ——檢測一個值是否為number(double),並轉換成lua_Integer(prtdiff_t),在我的機子上,ptrdiff_t被定義為int
luaL_checklong ——檢測一個值是否為number(double),並轉換成long
luaL_checklstring ——檢測一個值是否為string,並將字符串長度傳遞在[out]參數中返回
luaL_checknumber ——檢測一個值是否為number(double)
luaL_checkstring ——檢測一個值是否為string並返回
luaL_checkudata ——檢測自定義類型
4.3.傳遞返回值
當我們要傳遞返回值給Lua時,可以用lua_push系列函數來完成。每一個導出函數都要返回一個int型整數,這個整數是你的導出函數的返回值的個數。而返回值通過lua_push系列函數壓入棧中。比如一個Add函數:
extern “C” int Add(lua_State* luaEnv)
{
CheckParamCount(luaEnv, 2);
double left = luaL_checknumber(luaEnv, 1);
double right = luaL_checknumber(luaEnv, 2);
double result = left + right;
lua_pushnumber(luaEnv, result);
return 1;
}
可以看出,我們用lua_pushnumber把返回值壓入棧,最後返回1——1代表返回值的個數。lua_push系列函數如下:
lua_pushboolean ——壓入一個bool值
lua_pushcfunction ——壓入一個lua_CFunction類型的C函數指針
lua_pushfstring ——格式化一個string並返回,類似於sprintf
lua_pushinteger ——壓入一個int
lua_pushlightuserdata ——壓入自定義數據類型
lua_pushliteral ——壓入一個字面值字符串
lua_pushlstring ——壓入一個規定長度內的string
lua_pushnil ——壓入nil值
lua_pushnumber ——壓入lua_Number(double)值
lua_pushstring ——壓入一個string
lua_pushthread ——壓入一個所傳遞lua_State所對應的線程,如果此線程是主線程,則返回1
lua_pushvalue ——將所傳遞索引處的值復制一份壓入棧頂
lua_pushvfstring ——類似lua_pushfstring
通過這些函數,我們可以靈活的使用C/C++的高性能特性,來導出函數供Lua調用
摘自:Arnozhang的專欄