1. 程式人生 > >快速掌握Lua 5.3 —— Lua與C之間的互動概覽

快速掌握Lua 5.3 —— Lua與C之間的互動概覽

Q:什麼是Lua的虛擬棧?

A:C與Lua之間通訊關鍵內容在於一個虛擬的棧。幾乎所有的呼叫都是對棧上的值進行操作,所有C與Lua之間的資料交換也都通過這個棧來完成。另外,你也可以使用棧來儲存臨時變數。
每一個與Lua通訊的C函式都有其獨有的虛擬棧,虛擬棧由Lua管理。
棧的使用解決了C和Lua之間兩個不協調的問題:第一,Lua會自動進行垃圾收集,而C要求顯式的分配儲存單元,兩者引起的矛盾。第二,Lua中的動態型別和C中的靜態型別不一致引起的混亂。

Q:虛擬棧索引?

A:正索引的順序為元素入棧的先後順序,棧中的第一個元素(也就是第一個被入棧的元素)索引為1,第二個元素索引為2,以此類推。我們也可以使用負索引,以棧頂作為索引的參照來存取元素,棧頂元素的索引為-1,其前面的一個元素索引為-2,以此類推。正負索引各有其適用的場合,靈活應用,簡化編碼。

Q:第一個例子?

A:一個簡單的Lua直譯器。

#include <stdio.h>
#include <string.h>
/* 定義對Lua的基本操作的函式,其中定義的所有函式都有"lua_"字首。
 * 例如呼叫Lua函式(如"lua_pcall()"),獲取以及賦值Lua的全域性變數,註冊函式供Lua呼叫,等等。
 */
#include <lua.h>
/* 定義"auxiliary library",其中定義的所有函式都有"luaL_"字首。
 * "auxiliary library"使用lua.h中提供的基本API實現了更高一層抽象方法的實現。
 * 所有的Lua標準庫都使用了auxiliary library。
 */
#include <lauxlib.h> /* 定義開啟各種庫的函式。 */ #include <lualib.h> int main(void) { char buff[256] = {0}; int error = 0; lua_State *L = luaL_newstate(); // 建立Lua狀態機。 luaL_openlibs(L); // 開啟Lua狀態機"L"中的所有Lua標準庫。 while (fgets(buff, sizeof(buff), stdin) != NULL) { // 獲取使用者輸入。 /* "luaL_loadbuffer()",編譯使用者輸入為Lua的"chunk"並將其入棧。 * "line"是"chunk"的名字,用於除錯資訊和錯誤訊息。 * "lua_pcall()",以保護模式執行"chunk"並將其從棧頂彈出。 * 兩個函式均是在成功時返回"LUA_OK"(實際的值是0), * 失敗時返回錯誤碼,並將錯誤資訊入棧。 */
error = luaL_loadbuffer(L, buff, strlen(buff), "line") || lua_pcall(L, 0, 0, 0); if (error) { fprintf(stderr, "%s", lua_tostring(L, -1)); // 從棧頂取出錯誤資訊列印。 lua_pop(L, 1); // 彈出棧頂的錯誤資訊。 } } lua_close(L); // 關閉Lua狀態機。 return 0; }
prompt> gcc main.c -llua -ldl -lm
prompt> ./a.out
print("Hello World!")
Hello World!

Q:如何將元素入棧與出棧?

A:Lua中的資料型別與C中的並不一一對應,對於不同的資料型別,需要不同的入棧函式。

// 將"b"作為一個布林值入棧。
void lua_pushboolean(lua_State *L, int b)

/* 將C函式"fn"以及其在虛擬棧上關聯的"n"個值作為"Closure"入棧。
 * "n"最大為255,第一個被關聯的值首先入棧,棧頂是最後一個被關聯的值,
 * 這些值會在函式呼叫成功後被出棧。
 */
void lua_pushcclosure(lua_State *L, lua_CFunction fn, int n)

// 將C函式"f"作為函式入棧。內部實際呼叫"lua_pushcclosure(L, f, 0)"。
void lua_pushcfunction(lua_State *L, lua_CFunction f)

/* 將一個被格式化後的字串入棧。函式返回這個字串的指標。
 * 與C語言中的"sprintf()"類似,其區別在於:
 * 1、不需要為結果分配空間。
 *    其結果是一個Lua字串,由Lua來關心其記憶體分配(同時通過垃圾收集來釋放記憶體)。
 * 2"fmt"不支援符號、寬度、精度。只支援:
 *    "%%": 字元'%'。
 *    "%s": 帶零終止符的字串,沒有長度限制。
 *    "%f": "lua_Number"(Lua中的浮點數型別)。
 *    "%L": "lua_Integer"(Lua中的整數型別)。
 *    "%p": 指標或是一個十六進位制數。
 *    "%d": "int"。
 *    "%c": "char"。
 *    "%U": 用"long int"表示的UTF-8字。
 */
const char *lua_pushfstring(lua_State *L, const char *fmt, ...)

/* 將長度為"len",非字面形式的字串"s"入棧。
 * Lua對這個字串做一個內部副本(或是複用一個副本),
 * 因此"s"處的記憶體在函式返回後,可以釋放掉或是立刻重用於其它用途。
 * 字串內可以是任意二進位制資料,包括'\0'。函式返回內部副本的指標。
 */
const char *lua_pushlstring(lua_State *L, const char *s, size_t len)

// 將字面形式的字串"s"入棧,函式自動給出字串的長度。返回內部副本的指標。
const char *lua_pushliteral(lua_State *L, const char *s)

/* 將以'\0'結尾的字串"s"入棧。
 * Lua對這個字串做一個內部副本(或是複用一個副本),
 * 因此"s"處的記憶體在函式返回後,可以釋放掉或是立刻重用於其它用途。
 * 函式返回內部副本的指標。如果"s""NULL",將"nil"入棧並返回"NULL"*/
const char *lua_pushstring(lua_State *L, const char *s)

// 等價於"lua_pushfstring()"。不過是用"va_list"接收引數,而不是用可變數量的實際引數。
const char *lua_pushvfstring(lua_State *L, const char *fmt, va_list argp)

// 將全域性環境入棧。
void lua_pushglobaltable(lua_State *L)

// 將值為"n"的整數入棧。
void lua_pushinteger(lua_State *L, lua_Integer n)

/* 將一個輕量使用者資料"p"入棧。
 * 使用者資料是保留在Lua中的C值。輕量使用者資料表示一個指標"void*"。
 * 它像一個數值,你不需要專門建立它,它也沒有獨立的元表,
 * 而且也不會被收集(因為從來不需要建立)。只要表示的C地址相同,兩個輕量使用者資料就相等。
 */
void lua_pushlightuserdata(lua_State *L, void *p)

// 將空值入棧。
void lua_pushnil(lua_State *L)

// 將一個值為"n"的浮點數入棧。
void lua_pushnumber(lua_State *L, lua_Number n)

// "L"表示的執行緒入棧。如果這個執行緒是當前狀態機的主執行緒的話,返回1。
int lua_pushthread(lua_State *L)

// 將虛擬棧上索引"index"處的元素的副本入棧。
void lua_pushvalue(lua_State *L, int index)

對應的出棧函式就很簡單了,只有一個lua_pop()

// 從虛擬棧中彈出"n"個元素。
void lua_pop(lua_State *L, int n)

Q:如何檢查虛擬棧中元素的型別?

A:

// 如果棧中索引"index"處的元素為"bool"型別,則返回1,否則返回0int lua_isboolean(lua_State *L, int index)

// 如果棧中索引"index"處的元素是一個C函式,則返回1,否則返回0int lua_iscfunction(lua_State *L, int index)

// 如果棧中索引"index"處的元素是一個C函式或是一個Lua函式,則返回1,否則返回0int lua_isfunction(lua_State *L, int index)

// 如果棧中索引"index"處的元素是一個整數,則返回1,否則返回0int lua_isinteger(lua_State *L, int index)

// 如果棧中索引"index"處的元素是一個輕量級的"userdata",則返回1,否則返回0int lua_islightuserdata(lua_State *L, int index)

// 如果棧中索引"index"處的元素是一個"nil",則返回1,否則返回0int lua_isnil(lua_State *L, int index

// 如果"index"是一個無效索引時,返回1,否則返回0int lua_isnone(lua_State *L, int index)

// 如果"index"是一個無效索引或者"index"處的元素是一個"nil",則返回1,否則返回0int lua_isnoneornil(lua_State *L, int index)

/* 如果棧中索引"index"處的元素是一個數值或者是一個可以轉換為數值的字串,
 * 則返回1,否則返回0*/
int lua_isnumber(lua_State *L, int index)

/* 如果棧中索引"index"處的元素是一個字串或者是一個可以轉換為字串的數值,
 * 則返回1,否則返回0*/
int lua_isstring(lua_State *L, int index)

// 如果棧中索引"index"處的元素是一個"table",則返回1,否則返回0int lua_istable(lua_State *L, int index)

// 如果棧中索引"index"處的元素是一個執行緒,則返回1,否則返回0int lua_isthread(lua_State *L, int index)

// 如果棧中索引"index"處的元素是一個"userdata",則返回1,否則返回0int lua_isuserdata (lua_State *L, int index)

// 如果棧中的"coroutine"可以被掛起,則返回1,否則返回0int lua_isyieldable(lua_State *L)

/* 返回棧中索引"index"處元素的型別。這些型別在"lua.h"中定義,如下:
 * #define LUA_TNONE       (-1)    // 無效
 * #define LUA_TNIL        0    // "nil"
 * #define LUA_TBOOLEAN        1    // "bool"
 * #define LUA_TLIGHTUSERDATA  2    // 輕量級"userdata"
 * #define LUA_TNUMBER     3    // 數值
 * #define LUA_TSTRING     4    // 字串
 * #define LUA_TTABLE      5    // "table"
 * #define LUA_TFUNCTION       6    // 函式
 * #define LUA_TUSERDATA       7    // "userdata"
 * #define LUA_TTHREAD     8    // 執行緒
 */
int lua_type(lua_State *L, int index)

// 返回"tp"表示的型別的名字。"tp""lua_type()"的返回值之一。
const char *lua_typename(lua_State *L, int tp)

Q:如何轉換虛擬棧中元素的型別?

A:

// 將棧中"index"處的元素轉換為C中的"bool"值返回。
int lua_toboolean(lua_State *L, int index)

// 將棧中"index"處的元素轉換為一個C函式返回。指定的元素必須是一個C函式,否則返回"NULL"。
lua_CFunction lua_tocfunction(lua_State *L, int index)

/* 將棧中"index"處的元素轉換為一個整數返回。
 * 指定的元素必須是一個整數或是一個可以被轉換為整數的數字或字串,否則返回0。
 * 如果"isnum""NULL""*isnum"會被賦值為操作是否成功的"bool"值。
 */
lua_Integer lua_tointegerx(lua_State *L, int index, int *isnum)

// 內部呼叫"lua_tointegerx(L, index, NULL)"。
lua_Integer lua_tointeger(lua_State *L, int index)

/* 將棧中"index"處的元素轉換為一個C字串並將其指標返回。
 * 如果"len""NULL""*len"將獲得字串的長度。
 * 指定元素必須是一個字串或是一個數字,否則返回返回"NULL"。
 * 如果指定元素是一個數字,函式會將元素的型別轉換為字串。
 * 返回的字串結尾包含'\0',而在字串中允許包含多個'\0'。
 * 函式返回的字串應立即轉存,否則有可能被Lua垃圾回收器回收。
 */
const char *lua_tolstring(lua_State *L, int index, size_t *len)

/* 將棧中"index"處的元素轉換為一個浮點數返回。
 * 指定的元素必須是一個數字或是一個可被轉換為數字的字串,否則返回0。
 * 如果"isnum""NULL""*isnum"會被賦值為操作是否成功的"bool"值。
 */
lua_Number lua_tonumberx(lua_State *L, int index, int *isnum)

// 內部呼叫"lua_tonumberx(L, index, NULL)"。
lua_Number lua_tonumber(lua_State *L, int index)

/* 將棧中"index"處的元素轉換為一個C指標返回。
 * 指定的元素必須是一個"userdata""table",執行緒或是一個函式,否則返回"NULL"*/
const void *lua_topointer(lua_State *L, int index)

// 內部呼叫"lua_tolstring(L, index, NULL)"。
const char *lua_tostring(lua_State *L, int index)

/* 將棧中"index"處的元素轉換為一個Lua執行緒返回。
 * 指定的元素必須是一個執行緒,否則返回"NULL"*/
lua_State *lua_tothread(lua_State *L, int index)

/* 棧中"index"處的元素如果是一個完全"userdata",則返回其記憶體地址的指標;
 * 如果是一個輕量級"userdata",則返回其儲存的指標。
 */
void *lua_touserdata(lua_State *L, int index)

Q:一些維護虛擬棧的方法?

A:

/* int lua_gettop(lua_State *L)
 * 返回棧頂元素的索引。
 * 因為棧中元素的索引是從1開始編號的,所以函式的返回值相當於棧中元素的個數。
 * 返回值為0表示棧為空。
 */
lua_gettop(L);    // 返回棧中元素的個數。

/* void lua_settop(lua_State *L, int index)
 * 設定棧頂為索引"index"指向處。
 * 如果"index""lua_gettop()"的值大,那麼多出的新元素將被賦值為"nil"*/
lua_settop(L, 0);    // 清空棧。

/* void lua_remove(lua_State *L, int index)
 * 移除棧中索引"index"處的元素,該元素之上的所有元素下移。
 */

/* void lua_insert(lua_State *L, int index)
 * 將棧頂元素移動到索引"index"處,索引"index"(含)之上的所有元素上移。
 */

/* void lua_replace(lua_State *L, int index)
 * 將棧頂元素移動到索引"index"處。(相當於覆蓋了索引"index"處的元素)
 */

一個”dump”整個堆疊內容的例子,

#include <stdio.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>

static void stackDump(lua_State *L)
{
    int i = 0;
    int top = lua_gettop(L);    // 獲取棧中元素個數。
    for (i = 1; i <= top; ++i)    // 遍歷棧中每一個元素。
    {
        int t = lua_type(L, i);    // 獲取元素的型別。
        switch(t)
        {
            case LUA_TSTRING:    // strings
                printf("'%s'", lua_tostring(L, i));
                break;

            case LUA_TBOOLEAN:    // bool
                printf(lua_toboolean(L, i) ? "true" : "false");
                break;

            case LUA_TNUMBER:    // number
                printf("%g", lua_tonumber(L, i));    // %g,自動選擇%e或%f表示數值。
                break;

            default:    // other values
                printf("%s", lua_typename(L, t));    // 將巨集定義的型別碼轉換為型別名稱。
                break;
        }
        printf("  ");    // put a separator
    }
    printf("\n");
}

int main(void)
{
    lua_State *L = luaL_newstate();    // 建立Lua狀態機。
    // 向虛擬棧中壓入值。
    lua_pushboolean(L, 1);    // true
    lua_pushnumber(L, 10);    // 10
    lua_pushnil(L);    // nil
    lua_pushstring(L, "hello");    // "hello"
    stackDump(L);    // true  10  nil  'hello'

    lua_pushvalue(L, -4);    // 將索引-4處的值的副本入棧。
    stackDump(L);    // true  10  nil  'hello'  true

    lua_replace(L, 3);    // 將棧頂元素移動到索引3處,並覆蓋原先的元素。
    stackDump(L);    // true  10  true  'hello'

    lua_settop(L, 6);    // 將棧頂設定為索引6處,多出來的新元素被賦值為"nil"。
    stackDump(L);    // true  10  true  'hello'  nil  nil

    lua_remove(L, -3);    // 移除索引-3處的元素,其上所有元素下移。
    stackDump(L);    // true  10  true  nil  nil

    lua_settop(L, -5);    // 將棧頂設定為索引-5處。
    stackDump(L);    // true

    lua_close(L);    // 關閉Lua狀態機。

    return 0;
}

附加:

1、Lua庫沒有定義任何全域性變數,它所有的狀態都儲存在動態結構lua_State中,而且指向這個結構的指標作為所有Lua函式的一個引數。這樣的實現方式使得Lua能夠重入(reentrant),並且為在多執行緒中的使用中作好準備。
2、如果你是在C++的程式碼中使用Lua,別忘了,

extern "C" {
    #include <lua.h>
}

3、Lua中的字串可以不以'\0'作為結束符。這樣,字串中可以包含任意的二進位制(甚至是'\0'),字串的長度由明確的長度指定。
4、在lua_pushlstring()lua_pushliteral()以及lua_pushstring()中,Lua不儲存字串(變數)指標。因此當這些函式返回時,你就可以修改你的字串了。
5、對於入棧是否有棧空間的情況,你需要自己判斷,別忘了現在你是一個C程式設計師。當Lua啟動或者任何Lua呼叫C的時候,虛擬棧中至少有20個空間(在”lua.h”中LUA_MINSTACK定義),這對於一般情況下夠用了,所以一般不用考慮。但有時候確實需要更多的棧空間(比如呼叫一個不定引數的函式),此時你需要使用lua_checkstack檢查棧空間的情況。

/* 確保堆疊上至少有"n"個額外空位。如果不能把堆疊擴充套件到相應的尺寸,函式返回"false"。
 * 失敗的原因包括將把棧擴充套件到比固定最大尺寸還大(至少是幾千個元素)或分配記憶體失敗。
 * 這個函式永遠不會縮小堆疊,如果堆疊已經比需要的大了,那麼就保持原樣。
 */
int lua_checkstack(lua_State *L, int sz)

6、遍歷一個”table”時,不要將lua_tolstring()作用在”key”上,這樣會導致lua_next()無法正常執行。
7、在平常的編碼中,對於執行失敗時會返回0lua_to*()類別的函式,我們最好先使用lua_is*()類別的函式判斷引數的型別,之後再使用lua_to*()類別的函式對引數進行轉換;而對於執行失敗時會返回NULLlua_to*()類別的函式,我們可以直接使用lua_to*()類別的函式直接對引數進行轉換,判斷函式的返回值非NULL與否,就能判斷轉換是否成功。
8、lua_pop()就是通過lua_settop()實現的(在”lua.h”中定義),
#define lua_pop(L,n) lua_settop(L, -(n)-1)
9、以下操作對於虛擬棧沒有任何影響,

lua_settop(L, -1);    /* set top to its current value */
lua_insert(L, -1);    /* move top element to the top */
lua_replace(L, -1);    /* replace top element by the top element */