Lua與ObjC的交互

1. 寫在前面

不少時候咱們都須要藉助一些腳本語言來爲咱們實現一些動態的配置,那麼就會涉及到如何讓腳本語言跟原生語言交互的問題。平時在網上看得比較多的是使用JS(JavaScript)與iOS原生代碼ObjC交互的文章。由於JS的解析器是iOS內部提供的(可使用UIWebView或者JavaScriptCore.framework實現),因此使用JS來交互會感受比較方便。git

可是在這裏,我想跟你們分享另一種腳本語言的交互方式,就是使用Lua與原生的ObjC語言進行交互。Lua是一種輕量級的腳本語言,它的腳本解析器很小,編譯出來只有100多kb,所以,做爲一個內嵌的腳本解析器是首選的;並且Lua除了提供基本的腳本語言特性和系統功能外(IO讀寫),沒有多餘的功能性框架(JS解析器由於要配合Web的功能實現帶有不少的工具庫),這也是它輕量的表現。同時,提供了豐富的C Api來讓其它的語言對其功能進行擴展,可以真正作到按需定製。github

那麼,這裏所說到的C Api就是用於與ObjC交互的重點。由於ObjC原本就是C語言的超集,因此可以很方便的調用這些C Api,下面將一步一步實現交互的過程。編程

2. 下載和編譯Lua解析器

首先,跳轉到Lua官網的下載頁將源碼下載下來。而後解壓下載包能夠獲得以下圖所示的目錄結構:vim

Lua源碼目錄結構數組

對應的目錄說明以下表:數據結構

名稱 說明
doc Lua相關的文檔,包括了編譯文檔、接口文檔等
Makefile 編譯Lua使用,在這裏咱們不使用它來進行編譯
README 關於Lua的說明文件
src Lua的源碼文件

3. 編譯Lua源碼

在這裏咱們只須要src目錄中的源碼文件,先打開src目錄,將Makefile、lua.c、luac.c三個文件刪除掉,須要說明的是lua.c和luac.c文件是用於編譯生成lua和luac兩個命令不屬於解析器的功能,若是不刪除可能會致使XCode沒法編譯經過。閉包

接下來打開XCode建立一個新的項目並把src目錄拖入項目中。以下圖所示:框架

導入Lua源碼到項目編程語言

而後Command+B進行編譯,提示編譯成功!函數

4. Lua C Api 與 棧

在開始實現Lua與OC交互以前先來了解兩個很是重要的概念,一個是Lua的C Api,Lua的腳本解析器是使用C語言來編寫的(基於C語言的源碼跨平臺特性,使得Lua能夠在各類系統下面使用),所以它提供了豐富的C語言定義的接口來訪問和操做Lua中的全部元素,掌握這些C Api能夠更加靈活和方便地擴展Lua的功能,下面的交互實現正是使用這些C Api進行實現的。

另一個就是 的概念,在Lua和C進行交互數據的時候會用到了一個棧的結構,棧中的每一個元素都能保存任何類型的Lua值。要獲取Lua中的一個值時,須要調用一個C Api函數,Lua就會將特定的值壓入棧中,而後再經過相應的C Api將值取出來,如圖所示:

C/C++獲取Lua值

一樣,要將一個值傳給Lua時,須要先調用C Api將這個值壓入棧,而後再調用C Api,Lua就會獲取該值並將其從棧中彈出。 如圖所示:

C/C++給Lua傳值

這種設計方式主要是爲了多種編程語言中統一數據交互中的存取方式,而且方便Lua中的垃圾回收機制的檢測。

有了上述所說的概念,下面正式進入主題。

5. 初始化Lua環境

Lua環境的維護須要一個叫lua_State的結構體來支持,其貫穿了整個執行過程。所以,要使用Lua則須要先初始化一個lua_State結構體。修改ViewController的代碼以下:

#import "lua.h"
#import "lauxlib.h"
#import "lualib.h"

@interface ViewController ()

@property (nonatomic) lua_State *state;     //定義一個lua_State結構

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.state = luaL_newstate();    //建立新的lua_State結構體
    luaL_openlibs(self.state);        //加載標準庫
}

@end

6. 關於棧操做的C Api

上面說到數據「棧」的概念,C Api中提供了不少操做棧的功能接口,一般能夠分爲四大類:入棧操做、查詢操做、取值操做和其餘操做。

6.1 入棧操做

表示要將本地的某個類型的值放到數據棧中,而後提供給Lua層來獲取和操做。該類操做接口有以下定義:

void lua_pushnil (lua_State *L);
void lua_pushnumber (lua_State *L, lua_Number n);
void lua_pushinteger (lua_State *L, lua_Integer n);
const char *lua_pushlstring (lua_State *L, const char *s, size_t len) ;
const char *lua_pushstring (lua_State *L, const char *s);
const char *lua_pushvfstring (lua_State *L, const char *fmt, va_list argp);
const char *lua_pushfstring (lua_State *L, const char *fmt, ...);
void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n);
void lua_pushboolean (lua_State *L, int b);
void lua_pushlightuserdata (lua_State *L, void *p);
int lua_pushthread (lua_State *L);

從上面的接口方法定義能夠看出來,不一樣的Lua類型對應着不通的入棧接口,包括了整型(Integer)、布爾類型(Boolean)、浮點數(Number)、字符串(String)、閉包(Closure)、用戶自定義數據(Userdata)、空類型(Nil)以及線程(Thread)。須要注意的是,C Api沒有提供直接入棧Table類型的接口(估計是該數據類型沒法與本地結構進行對應),若是須要入棧一個Table類型,可使用lua_createtable方法來入棧一個Table,調用該方法會在棧頂放入一個Table的引用。

可見,假如咱們須要在原生代碼中給Lua的一個全局變量a賦一個整型值,那麼能夠以下面代碼的作法:

lua_pushinteger (state, 1);
lua_setglobal (state, "a");

其中的lua_setglobal方法爲設置全局變量的值,該方法會把數據棧頂的元素放入該方法第二個參數所指定的變量名對應的變量中,同時移除棧頂元素。如圖:

lua_setglobal示意圖

6.2 查詢操做

以前說到棧中的每一個元素均可覺得任意類型,那麼,對於如何判斷元素的類型就能夠經過該類方法來實現。該類方法的定義以下:

int lua_isnil (lua_State *state, int index);
int lua_isboolean (lua_State *state, int index);
int lua_isfunction (lua_State *state, int index);
int lua_istable (lua_State *state, int index);
int lua_islightuserdata (lua_State *state, int index);
int lua_isthread (lua_State *state, int index);
int lua_isnumber (lua_State *L, int idx);
int lua_isinteger (lua_State *L, int idx);
int lua_iscfunction (lua_State *L, int idx);
int lua_isstring (lua_State *L, int idx);
int lua_isuserdata (lua_State *L, int idx)

一樣查詢操做也是提供了不一樣的方法來檢測不一樣的類型。其中第二個參數表示要檢測類型的元素處於棧中的哪一個位置。

關於棧中位置在lua中有兩種形式表示,第一種是正數表示法,1表示棧底元素(即最早入棧的元素),而後越往上的元素,索引值越大。另一種是負數表示法,-1表示棧頂元素(即最後入棧的元素),而後越往下的元素,索引值越小。如圖所示:

棧索引兩種表示方法示意圖

lua_isXXX系列方主要是判斷棧中數據是否可以被轉換爲對應數據類型時使用,如lua_isstring方法則是判斷棧中某個元素是否可以被轉換爲string類型,因此當棧中數據爲number類型時,其返回值也爲true。

若是要進行非轉換的強類型判斷,可使用lua_type方法來獲取棧中元素的類型,而後根據類型來獲取值。如判斷棧頂元素的類型:

switch(lua_type(state, -1))
 {
    case LUA_TNUMBER:
        break;
    case LUA_TSTRING:
        break;
    case LUA_TBOOLEAN:
        break;
    case LUA_TUSERDATA:
        break;
    default:
        break;
}

6.3 取值操做

棧中的全部元素的獲取都是經過該類方法來實現,一般該類方法跟在查詢類方法後,當知道某個數據類型後,則調用對應數據類型的取值方法來獲取元素。其方法定義以下:

int lua_toboolean (lua_State *L, int idx);
const char *lua_tolstring (lua_State *L, int idx, size_t *len);
lua_CFunction lua_tocfunction (lua_State *L, int idx);
void *lua_touserdata (lua_State *L, int idx);
lua_State *lua_tothread (lua_State *L, int idx);
const void *lua_topointer (lua_State *L, int idx);
lua_Integer lua_tointegerx (lua_State *L, int idx, int *pisnum);
lua_Number lua_tonumberx (lua_State *L, int idx, int *pisnum);

取值操做的接口也至關簡單,分別傳入lua_State對象和棧索引便可。若是在調用時指定的類型跟棧中類型不一樣也不會有什麼問題,接口會由於類型不正確而返回0或者NULL。

要注意的是該系列接口跟lua_isXXX系列接口同樣,會對原始的類型進行轉換輸出,所以在作一些跟類型相關的操做時,最好時先判斷類型再根據類型調用該方法取值,不然會致使一些意想不到的異常,以下面例子:

//存在targetVar = 1111;
lua_getglobal(self.state, "targetVar");

NSString *type = nil;
if (lua_isstring(self.state, -1))
{
  const char *str = lua_tostring(self.state, -1);
  NSLog(@"targetVal to string = %s", str);
}

switch (lua_type(self.state, -1))
{
  case LUA_TNIL:
    type = @"Nil";
    break;
  case LUA_TTABLE:
    type = @"Table";
    break;
  case LUA_TNUMBER:
    type = @"Number";
    break;
  case LUA_TSTRING:
    type = @"String";
    break;
  case LUA_TTHREAD:
    type = @"Thread";
    break;
  case LUA_TFUNCTION:
    type = @"Function";
    break;
  case LUA_TBOOLEAN:
    type = @"Boolean";
    break;
  case LUA_TUSERDATA:
    type = @"Userdata";
    break;
  case LUA_TLIGHTUSERDATA:
    type = @"Light Userdata";
    break;
  default:
    type = @"Unknown";
    break;
}

NSLog(@"targetVar is %@", type);

上面例子中的原意是要輸出下面的內容:

targetVal to string = 1111
targetVar is Number

可是實際上倒是這樣:

targetVal to string = 1111
targetVar is String

這是因爲使用了lua_tostring把棧中的targetVar的值改變了致使的,因此相似這樣的操做必定要謹慎。

6.4 其餘操做

使用上述3部分的操做能夠知足與棧中數據進行交互的大多數狀況。若是須要更加靈活地對棧進行操做,例如拷貝棧中某個元素,交互棧中元素位置等等的操做可使用下面所定義的接口:

int lua_gettop (lua_State *L);
void lua_settop (lua_State *L, int idx);
void lua_pushvalue (lua_State *L, int idx);
void lua_remove(lua_State *L, int idx);
void lua_insert(lua_State *L, int idx);
void lua_replace(lua_State *L, int idx);
void lua_pop(lua_State *L, int n);

其中lua_gettop爲獲取棧頂位置,也便是棧中元素的個數,其實這個方法在處理原生方法的傳入參數時頗有用,能夠確認傳入參數的個數。有時候也能夠用它來輸出各個狀態下的棧元素變化,來確認本身在操做棧時是否存在問題。

lua_settop方法用於設置棧頂位置,若是新棧頂高於以前的棧頂則會push一些nil的元素來填充;若是新棧頂低於以前的棧頂則會丟棄新棧頂之上的全部元素。如圖所示:

lua_settop示意圖

lua_pushvalue方法表示將棧中某個元素的副本壓入棧頂。以前的棧元素不會發生變更。如圖所示:

lua_pushvalue示意圖

lua_remove方法用於移除指定索引上的元素,而後再該元素之上的全部元素會下移填補空缺(即元素的索引會發生變動)。如圖所示:

lua_remove示意圖

lua_insert會將指定索引位置之上的全部元素上移來開闢一個新的位置。而後將棧頂元素插入到該位置。如圖所示:

lua_insert示意圖

lua_replace方法會先彈出棧頂元素,而後將該元素覆蓋到指定索引位置上。如圖所示:

lua_replace示意圖

lua_pop方法會從棧頂彈出指定數量的元素。如圖所示:

lua_pop示意圖

瞭解了上面的棧操做方法後,下面就是要結合這些方法來實現交互的實際操做。

7. From OC to Lua

7.1 空值傳遞

使用lua_pushnil方法能夠將任意一個Lua變量置空。如:

lua_pushnil();
lua_setglobal(self.state, "val");

7.2 數值的傳遞

使用lua_pushinteger或者lua_pushnumber方法來將OC中的數值類型傳遞到Lua中指定的某個變量。如:

//傳遞整型值
lua_pushinteger(self.state, 1024);
lua_setglobal(self.state, "intVal");

//傳遞浮點型
lua_pushnumber(self.state, 80.08);
lua_setglobal(self.state, "numVal");

7.3 布爾值的傳遞

使用lua_pushboolean方法來實現,如:

lua_pushboolean(self.state, YES);
lua_setglobal(self.state, "boolVal");

7.4 字符串的傳遞

使用lua_pushstring方法能夠傳遞字符串給Lua,要注意的是該方法接收的是一個c描述的字符串(即 char*)。如:

lua_pushstring(self.state, @"Hello World".UTF8String);
lua_setglobal(self.state, "stringVal");

7.5 二進制數組的傳遞

二進制數組在Lua中其實與字符串的存儲方式相同,可是OC中不能直接使用lua_pushstring來進行二進制數組的傳遞,可使用lua_pushlstring方法來傳遞。如:

char bytes[13] = {0xf1, 0xaa, 0x12, 0x56, 0x00, 0xb2, 0x43, '\0', '\0', 0x00, 0x90, 0x65, 0x73};
lua_pushlstring(self.state, bytes, 13);
lua_setglobal(self.state, "bytesVal");

7.6 方法的傳遞

Lua中只能接受C定義的方法傳入,而且方法的聲明必須符合lua_CFunction函數指針的定義,即:

int functionName (lua_State *state);

那麼,傳入方法則須要先定義一個C語言聲明的方法,如:

int printHelloWorld (lua_State *state)
{
    NSLog(@"Hello World!");
    return 0;
}

方法裏面簡單地進行了一下信息打印,其中方法的返回值是一個整數,代表了該方法須要返回多少個值到Lua中(後續章節會進行返回值的相關演示),如今不須要返回值則爲0。而後,再經過lua_pushcfunction方法將方法傳入:

lua_pushcfunction(self.state, printHelloWorld);
lua_setglobal(self.state, "funcVal");

操做完成後,在Lua中就能夠直接調用了:

funcVal();

若是定義的方法是容許接受參數的,那麼能夠從state參數裏面獲取傳入的參數。拿上面的例子,例如方法接收一個名字的字符串參數,函數的代碼則能夠修改成:

int printHelloWorld (lua_State *state)
{
    if (lua_gettop(state) > 0)
    {
        //表示有參數
        const char *name = lua_tostring(state, 1);
        NSLog(@"Hello %s!", name);
    }

    return 0;
}

而後在Lua中則能夠這樣調用:

funcVal ("vimfung");

若是定義的方法不是直接打印字符串,而是組合了字符串給Lua返回,那麼定義的方法裏面則須要配合'lua_pushXXXX'系列方法來進行返回值傳遞。須要注意的是:方法中return的數量要與push到棧中的值要一致,不然可能出現異常。那麼,上面定義的函數能夠作以下修改:

int printHelloWorld (lua_State *state)
{
    if (lua_gettop(state) > 0)
    {
        //表示有參數
        const char *name = lua_tostring(state, 1);

        //入棧返回值
        NSString *retVal = [NSString stringWithFormat:@"Hello %s!", name];
        lua_pushstring(state, retVal.UTF8String);

        return 1;
    }

    return 0;
}

而後在Lua中則能夠這樣調用:

local retVal = funcVal("vimfung");
print(retVal);

7.7 數組和字典的傳遞

在Lua中,數組(Array)和字典(Dictionary)都由一個Table類型所表示(在Lua看來數組其實也屬於一種字典,只是它的key是有序而且爲整數)。如:

-- 定義數組
local arrayVal = {1,2,3,4,5,6};
-- 定義字典
local dictVal = {a=1, b=3, c=4, d=5};

上面的例子分別用了不帶key的聲明和帶key的聲明兩種方式來建立Table類型。其中不帶key的聲明方式,解析器會默認爲其建立一個key,該key是從1開始,由小到大進行分配,其等效於:

local arrayVal = {1=1, 2=2, 3=3, 4=4, 5=5, 6=6};

固然,兩種方式是能夠混合使用,如:

local tbl = {1, 2, a=1, b=2, 3};

Table屬於比較複雜的數據結構,所以提供操做它的C Api也比較複雜,下面將根據數組和字典分別講述它們的傳遞方式。

7.7.1 數組傳遞

首先,須要將一個Table類型入棧,這樣才能對其進行進一步的操做。因爲沒有pushtable這樣的方法,可是可使用lua_newtable來建立一個Table對象,而且該對象會自動放入棧頂位置。如:

lua_newtable(self.state);

而後對要傳遞的數組進行遍歷,並經過lua_rawseti方法將元素值設置到Table中。如:

NSArray *array = @[@1, @2, @3, @4, @5, @6];
 [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

    NSInteger value = [obj integerValue];
    lua_pushinteger(self.state, value);
    lua_rawseti(self.state, -2, idx + 1);

}];

lua_getglobal(self.state, "arrayVal");

經過上面的代碼就能夠把一個數組傳遞給arrayVal變量。值得注意的是:lua_rawseti方法表示要棧頂的元素設置給指定的Table對象的指定索引。其中的第二個參數是指Table對象在棧中的位置,第三個參數是表示在Table中的索引,通常索引是從1開始算起,所以上面代碼中的idx須要加1。通過這樣的操做後,棧頂的元素會被移除。以下圖所示:

lua_rawseti示意圖

7.7.2 字典傳遞

字典的傳遞一樣須要先入棧一個Table:

lua_newtable(self.state);

而後對要傳遞的字典進行遍歷,並經過lua_setfield方法將元素設置到Table中。如:

[dict enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {

    NSInteger value = [obj integerValue];
    lua_pushinteger(self.state, value);
    lua_setfield(self.state, -2, key.UTF8String);

}];

lua_setglobal(self.state, "dictVal");

lua_setfieldlua_rawseti功能類型,都是把一個元素放入Table中,只是一個用於指定整數索引,一個是指定字符串索引。經過上面的方式就能夠把字典傳遞給Lua了。

7.8 自定義數據傳遞

Lua中一個比較強大的地方是它能夠將任意的類型(包括類對象)進行傳遞。特別是在提供原生處理方法時,須要用到一些特定的數據類型做爲參數時,Lua就能夠幫咱們實現這一塊的傳遞。

要想傳遞自定義的數據則必需要使用Lua提供的Userdata類型。該類型有兩種引用方式,一種是強引用Userdata,由Lua的GC來負責該類型變量的生命週期。另一種是弱引用Userdata,又稱Light Userdata,該類型不被GC所管理,其生命週期由原生層來決定。下面來看一下兩種方式是如何實現的。

首先咱們來定義一個OC的User類:

@interface User : NSObject

@property (nonatomic, copy) NSString *name;

@end

@implementation User

@end

而後,利用lua_newuserdata方法來建立一個強引用Userdata,並建立一個User對象賦值給新建的Userdata。如:

void *instanceRef = lua_newuserdata(self.state, sizeof(User *));
instanceRef = (__bridge_retained void *)[[User alloc] init];
lua_setglobal(self.state, "userdataVal");

經過上面的代碼就能夠把User類實例封裝成Userdata再傳遞給Lua。若是你要傳遞的對象並不須要Lua來管理生命週期,那麼就能夠建立一個弱引用的Userdata,如:

User *user = [[User alloc] init];
lua_pushlightuserdata(self.state, (__bridge void *)(user));
lua_setglobal(self.state, "userdataVal");

下面來看一個比較實際的例子,假設有一個提供給Lua調用的原生接口printUser,該接口會打印傳入進來的用戶信息,代碼以下:

static int printUser (lua_State *state)
{
    if (lua_gettop(state) > 0)
    {
        //表示有參數傳入
        User *user = (__bridge User *)(lua_topointer(state, 1));
        NSLog(@"user.name = %@", user.name);
    }

    return 0;
}

該方法經過lua_topointer方法來獲取了一個Userdata數據類型並轉換爲User類實例對象而後打印其名稱。接下來將其導出給Lua:

lua_pushcfunction(self.state, printUser);
lua_setglobal(self.state, "printUser");

而後生成一個User對象,並調用該方法傳入該用戶對象。如:

//建立User對象
User *user = [[User alloc] init];
user.name = @"vimfung";

lua_getglobal(self.state, "printUser");
//傳入User參數
lua_pushlightuserdata(self.state, (__bridge void *)(user));
lua_pcall(self.state, 1, 0, 0);

上面的代碼就是使用C Api來調用Lua的方法(下面的章節會詳細講述這塊內容),經過OC代碼建立了一個User對象並將其做爲了參數傳給了Lua的printUser方法。最終的輸出信息以下:

user.name = vimfung

從OC到Lua的全部類型的轉換和交互基本上都涉及到了,下面章節將會詳細描述從Lua到OC上的一些交互和數據交換。

8. From Lua to OC

8.1 獲取數值

經過lua_tonumber方法能夠獲取某個數值變量的值。如:

lua_getglobal(self.state, "aa");
double value = lua_tonumber(self.state, -1);
NSLog(@"aa = %f", value);
lua_pop(self.state, 1);

上述代碼中的lua_getglobal方法是用於獲取全局變量的值,調用它會把一個值放入棧頂。而後再經過lua_tonumber把棧頂的值讀取出來並打印。須要注意的是經過lua_getglobal獲取獲得的值,在棧中是不會自動清除,所以,在用完某個變量時記得把它從棧中清除掉,代碼中是經過lua_pop把值彈出棧的。

8.2 獲取布爾值

與獲取數值相同,經過lua_toboolean方法來獲取某個布爾變量的值。如:

lua_getglobal(self.state, "aa");
BOOL value = lua_toboolean(self.state, -1);
if (value)
{
  NSLog(@"aa = YES");
}
else
{
  NSLog(@"aa = NO");
}
lua_pop(self.state, 1);

8.3 獲取字符串

lua_tostring方法來獲取字符串變量的值。如:

lua_getglobal(self.state, "aa");
const char *value = lua_tostring(self.state, -1);
NSLog(@"aa = %s", value);
lua_pop(self.state, 1);

8.4 獲取二進制數組

lua_tolstring方法來獲二進制數組變量的值。如:

lua_getglobal(self.state, "aa");
size_t len = 0;
const char *bytes = lua_tolstring(self.state, -1, &len);
NSData *data = [NSData dataWithBytes:bytes length:len];
NSLog(@"aa = %@", data);
lua_pop(self.state, 1);

8.5 方法的獲取和調用

通常狀況下,要獲取Lua中的某個Function主要是用於對其進行調用。假設有一個Lua方法定義以下:

function printHelloWorld ()
  print("Hello World");
end

那麼,對應OC中須要下面的代碼來獲取和調用它:

lua_getglobal(self.state, "printHelloWorld");
lua_pcall(self.state, 0, 0, 0);

上述代碼中的lua_pcall方法表示將棧中的元素視做Function來進行調用。其中第二個參數爲傳入參數的數量,必須與壓棧的參數數量一致;第三個參數爲返回值的數量,表示調用後其放入棧中的返回值有多少個。第四個參數是用於發生錯誤處理時的代碼返回。其運行原理以下圖所示:

lua_pcall原理示意圖

對於帶參數和返回值的方法,在得到方法對象後,須要調用lua_pushXXX系列方法來設置傳入參數。能夠參考下面例子:

假設有一個加法的Lua方法,其定義以下:

function add (a, b)
  return a + b;
end

那麼,OC中則能夠進行下面操做來調用方法並傳遞參數,最終取得返回值而後打印到控制檯:

lua_getglobal(self.state, "add");
lua_pushinteger(self.state, 1000);
lua_pushinteger(self.state, 24);

lua_pcall(self.state, 2, 1, 0);

NSInteger retVal = lua_tonumber(self.state, -1);
NSLog(@"retVal = %ld", retVal);

8.6 Table的獲取和遍歷

Table的獲取跟其餘變量同樣,一旦放入棧後能夠根據須要經過調用lua_getfield方法來指定的key的值。如:

//假設Lua中有一個Table變量aa = {key1=1000, key2=24};
lua_getglobal(self.state, "aa");
lua_getfield(self.state, -1, "key2");
NSInteger value = lua_tonumber(self.state, -1);
NSLog(@"value = %ld", value);
lua_pop(self.state, 1);

若是Table是聲明時沒有指定key,那麼則須要調用lua_rawgeti來獲取Table的值。如:

//假設Lua中有一個Table變量aa = {1000, 24};
lua_getglobal(self.state, "aa");
lua_rawgeti(self.state, -1, 2);
NSInteger value = lua_tonumber(self.state, -1);
NSLog(@"value = %ld", value);
lua_pop(self.state, 1);

有時候,Table存儲的信息會在函數體外被訪問,那麼咱們須要對Table進行遍歷而後把它放入一個字典中,而後提供給程序使用。代碼以下:

//假設Lua中有一個Table變量aa = {1000, 24};
lua_getglobal(self.state, "aa");
lua_pushnil(self.state);
while (lua_next(self.state, -2))
{
  NSInteger value = lua_tonumber(self.state, -1);

  if (lua_type(self.state, -2) == LUA_TSTRING)
  {
    const char *key = lua_tostring(self.state, -2);
    NSLog(@"key = %s, value = %ld", key, value);
  }
  else if (lua_type(self.state, -2) == LUA_TNUMBER)
  {
    NSInteger key = lua_tonumber(self.state, -2);
    NSLog(@"key = %ld, value = %ld", key, value);
  }

  lua_pop(self.state, 1);
}

上述代碼利用lua_next方法來遍歷Table的全部元素,該方法從棧頂彈出一個元素做爲遍歷Table的起始Key,而後把每一個元素的Key和Value放入棧中。爲了遍歷全部元素因此起始的Key設置了一個nil值,證實要從Table最開始的Key進行遍歷。如圖:

lua_next原理示意圖

值得注意的是,在獲取Key值時,最好先判斷Key的類型,而後再根據其對應類型調用相應的lua_toXXX方法。不然,由於lua_toXXX系列方法會對元素值進行類型轉換,如整型的Key被lua_tostring轉換爲String後再給到lua_next進行遍歷就會報找不到指定Key的錯誤。

8.7 獲取自定義數據

利用lua_topointer方法來獲取自定義數據,如:

lua_getglobal(self.state, "ud");
NSObject *obj = (__bridge User *)(lua_topointer(state, 1));

9. 結語

Lua雖然小巧,但五臟俱全。最主要的是它提供了豐富又強大的C Api接口容許咱們進行高度的定製和擴展。可利用這些擴展特性開發適合本身的一套腳本引擎。基於上面所講的,鄙人開發了一個LuaScriptCore的開源橋接框架,簡化Lua與各類平臺的交互,有興趣的同窗能夠了解一下。

最後,但願經過本篇文章可以讓你們對OC與Lua之間的交互有更近一步的瞭解,創造更多的可能性。

相關文章
相關標籤/搜索