Lua與ObjC的交互

1. 寫在前面

很多時候我們都需要借助一些腳本語言來為我們實現(xiàn)一些動態(tài)的配置隅忿,那么就會涉及到如何讓腳本語言跟原生語言交互的問題取试。平時在網(wǎng)上看得比較多的是使用JS(JavaScript)與iOS原生代碼ObjC交互的文章攻柠。因為JS的解析器是iOS內(nèi)部提供的(可以使用UIWebView或者JavaScriptCore.framework實現(xiàn))赃磨,所以使用JS來交互會感覺比較方便。

但是在這里轧钓,我想跟大家分享另外一種腳本語言的交互方式序厉,就是使用Lua與原生的ObjC語言進行交互。Lua是一種輕量級的腳本語言毕箍,它的腳本解析器很小弛房,編譯出來只有100多kb,因此而柑,作為一個內(nèi)嵌的腳本解析器是首選的文捶;而且Lua除了提供基本的腳本語言特性和系統(tǒng)功能外(IO讀寫),沒有多余的功能性框架(JS解析器因為要配合Web的功能實現(xiàn)帶有很多的工具庫)媒咳,這也是它輕量的表現(xiàn)粹排。同時,提供了豐富的C Api來讓其它的語言對其功能進行擴展涩澡,能夠真正做到按需定制顽耳。

那么,這里所說到的C Api就是用于與ObjC交互的重點筏养。因為ObjC本來就是C語言的超集斧抱,所以能夠很方便的調(diào)用這些C Api,下面將一步一步實現(xiàn)交互的過程渐溶。

2. 下載和編譯Lua解析器

首先辉浦,跳轉(zhuǎn)到Lua官網(wǎng)的下載頁將源碼下載下來。然后解壓下載包可以得到如下圖所示的目錄結(jié)構(gòu):

Lua源碼目錄結(jié)構(gòu)

對應(yīng)的目錄說明如下表:

名稱 說明
doc Lua相關(guān)的文檔茎辐,包括了編譯文檔宪郊、接口文檔等
Makefile 編譯Lua使用掂恕,在這里我們不使用它來進行編譯
README 關(guān)于Lua的說明文件
src Lua的源碼文件

3. 編譯Lua源碼

在這里我們只需要src目錄中的源碼文件,先打開src目錄弛槐,將Makefile懊亡、lua.c、luac.c三個文件刪除掉乎串,需要說明的是lua.c和luac.c文件是用于編譯生成lua和luac兩個命令不屬于解析器的功能店枣,如果不刪除可能會導(dǎo)致XCode無法編譯通過。

接下來打開XCode創(chuàng)建一個新的項目并把src目錄拖入項目中叹誉。如下圖所示:

導(dǎo)入Lua源碼到項目

然后Command+B進行編譯鸯两,提示編譯成功!

4. Lua C Api 與 棧

在開始實現(xiàn)Lua與OC交互之前先來了解兩個非常重要的概念长豁,一個是Lua的C Api钧唐,Lua的腳本解析器是使用C語言來編寫的(基于C語言的源碼跨平臺特性,使得Lua可以在各種系統(tǒng)下面使用)匠襟,因此它提供了豐富的C語言定義的接口來訪問和操作Lua中的所有元素钝侠,掌握這些C Api可以更加靈活和方便地擴展Lua的功能,下面的交互實現(xiàn)正是使用這些C Api進行實現(xiàn)的酸舍。

另外一個就是 的概念帅韧,在Lua和C進行交互數(shù)據(jù)的時候會用到了一個棧的結(jié)構(gòu),棧中的每個元素都能保存任何類型的Lua值父腕。要獲取Lua中的一個值時弱匪,需要調(diào)用一個C Api函數(shù),Lua就會將特定的值壓入棧中璧亮,然后再通過相應(yīng)的C Api將值取出來萧诫,如圖所示:

C/C++獲取Lua值

同樣,要將一個值傳給Lua時枝嘶,需要先調(diào)用C Api將這個值壓入棧帘饶,然后再調(diào)用C Api,Lua就會獲取該值并將其從棧中彈出群扶。 如圖所示:

C/C++給Lua傳值

這種設(shè)計方式主要是為了多種編程語言中統(tǒng)一數(shù)據(jù)交互中的存取方式及刻,并且方便Lua中的垃圾回收機制的檢測。

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

5. 初始化Lua環(huán)境

Lua環(huán)境的維護需要一個叫l(wèi)ua_State的結(jié)構(gòu)體來支持,其貫穿了整個執(zhí)行過程骆莹。因此颗搂,要使用Lua則需要先初始化一個lua_State結(jié)構(gòu)體。修改ViewController的代碼如下:

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

@interface ViewController ()

@property (nonatomic) lua_State *state;     //定義一個lua_State結(jié)構(gòu)

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.state = luaL_newstate();    //創(chuàng)建新的lua_State結(jié)構(gòu)體
    luaL_openlibs(self.state);        //加載標(biāo)準(zhǔn)庫
}

@end

6. 關(guān)于棧操作的C Api

上面說到數(shù)據(jù)“椖豢眩”的概念丢氢,C Api中提供了很多操作棧的功能接口傅联,通常可以分為四大類:入棧操作疚察、查詢操作蒸走、取值操作和其他操作。

6.1 入棧操作

表示要將本地的某個類型的值放到數(shù)據(jù)棧中貌嫡,然后提供給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類型對應(yīng)著不通的入棧接口衅枫,包括了整型(Integer)嫁艇、布爾類型(Boolean)、浮點數(shù)(Number)弦撩、字符串(String)、閉包(Closure)论皆、用戶自定義數(shù)據(jù)(Userdata)益楼、空類型(Nil)以及線程(Thread)。需要注意的是点晴,C Api沒有提供直接入棧Table類型的接口(估計是該數(shù)據(jù)類型無法與本地結(jié)構(gòu)進行對應(yīng))感凤,如果需要入棧一個Table類型,可以使用lua_createtable方法來入棧一個Table粒督,調(diào)用該方法會在棧頂放入一個Table的引用陪竿。

可見,假如我們需要在原生代碼中給Lua的一個全局變量a賦一個整型值屠橄,那么可以如下面代碼的做法:

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

其中的lua_setglobal方法為設(shè)置全局變量的值族跛,該方法會把數(shù)據(jù)棧頂?shù)脑胤湃朐摲椒ǖ诙€參數(shù)所指定的變量名對應(yīng)的變量中,同時移除棧頂元素锐墙。如圖:

lua_setglobal示意圖

6.2 查詢操作

之前說到棧中的每個元素都可以為任意類型礁哄,那么,對于如何判斷元素的類型就可以通過該類方法來實現(xiàn)溪北。該類方法的定義如下:

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)

同樣查詢操作也是提供了不同的方法來檢測不同的類型桐绒。其中第二個參數(shù)表示要檢測類型的元素處于棧中的哪個位置。

關(guān)于棧中位置在lua中有兩種形式表示之拨,第一種是正數(shù)表示法茉继,1表示棧底元素(即最先入棧的元素),然后越往上的元素蚀乔,索引值越大烁竭。另外一種是負數(shù)表示法,-1表示棧頂元素(即最后入棧的元素)乙墙,然后越往下的元素颖变,索引值越小生均。如圖所示:

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

lua_isXXX系列方主要是判斷棧中數(shù)據(jù)是否能夠被轉(zhuǎn)換為對應(yīng)數(shù)據(jù)類型時使用,如lua_isstring方法則是判斷棧中某個元素是否能夠被轉(zhuǎn)換為string類型腥刹,所以當(dāng)棧中數(shù)據(jù)為number類型時马胧,其返回值也為true。

如果要進行非轉(zhuǎn)換的強類型判斷衔峰,可以使用lua_type方法來獲取棧中元素的類型佩脊,然后根據(jù)類型來獲取值。如判斷棧頂元素的類型:

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 取值操作

棧中的所有元素的獲取都是通過該類方法來實現(xiàn)垫卤,通常該類方法跟在查詢類方法后威彰,當(dāng)知道某個數(shù)據(jù)類型后,則調(diào)用對應(yīng)數(shù)據(jù)類型的取值方法來獲取元素穴肘。其方法定義如下:

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);

取值操作的接口也相當(dāng)簡單歇盼,分別傳入lua_State對象和棧索引即可。如果在調(diào)用時指定的類型跟棧中類型不同也不會有什么問題评抚,接口會因為類型不正確而返回0或者NULL豹缀。

要注意的是該系列接口跟lua_isXXX系列接口一樣,會對原始的類型進行轉(zhuǎn)換輸出慨代,因此在做一些跟類型相關(guān)的操作時邢笙,最好時先判斷類型再根據(jù)類型調(diào)用該方法取值,否則會導(dǎo)致一些意想不到的異常侍匙,如下面例子:

//存在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);

上面例子中的原意是要輸出下面的內(nèi)容:

targetVal to string = 1111
targetVar is Number

但是實際上卻是這樣:

targetVal to string = 1111
targetVar is String

這是由于使用了lua_tostring把棧中的targetVar的值改變了導(dǎo)致的,所以類似這樣的操作一定要謹慎想暗。

6.4 其他操作

使用上述3部分的操作可以滿足與棧中數(shù)據(jù)進行交互的大多數(shù)情況妇汗。如果需要更加靈活地對棧進行操作,例如拷貝棧中某個元素江滨,交互棧中元素位置等等的操作可以使用下面所定義的接口:

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為獲取棧頂位置铛纬,也即是棧中元素的個數(shù),其實這個方法在處理原生方法的傳入?yún)?shù)時很有用唬滑,可以確認傳入?yún)?shù)的個數(shù)告唆。有時候也可以用它來輸出各個狀態(tài)下的棧元素變化,來確認自己在操作棧時是否存在問題晶密。

lua_settop方法用于設(shè)置棧頂位置擒悬,如果新棧頂高于之前的棧頂則會push一些nil的元素來填充;如果新棧頂?shù)陀谥暗臈m攧t會丟棄新棧頂之上的所有元素稻艰。如圖所示:

lua_settop示意圖

lua_pushvalue方法表示將棧中某個元素的副本壓入棧頂懂牧。之前的棧元素不會發(fā)生變動。如圖所示:

lua_pushvalue示意圖

lua_remove方法用于移除指定索引上的元素,然后再該元素之上的所有元素會下移填補空缺(即元素的索引會發(fā)生變更)僧凤。如圖所示:

lua_remove示意圖

lua_insert會將指定索引位置之上的所有元素上移來開辟一個新的位置畜侦。然后將棧頂元素插入到該位置。如圖所示:

lua_insert示意圖

lua_replace方法會先彈出棧頂元素躯保,然后將該元素覆蓋到指定索引位置上旋膳。如圖所示:

lua_replace示意圖

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

lua_pop示意圖

了解了上面的棧操作方法后途事,下面就是要結(jié)合這些方法來實現(xiàn)交互的實際操作验懊。

7. From OC to Lua

7.1 空值傳遞

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

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

7.2 數(shù)值的傳遞

使用lua_pushinteger或者lua_pushnumber方法來將OC中的數(shù)值類型傳遞到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方法來實現(xiàn)义图,如:

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 二進制數(shù)組的傳遞

二進制數(shù)組在Lua中其實與字符串的存儲方式相同碱工,但是OC中不能直接使用lua_pushstring來進行二進制數(shù)組的傳遞,可以使用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函數(shù)指針的定義,即:

int functionName (lua_State *state);

那么桶蛔,傳入方法則需要先定義一個C語言聲明的方法,如:

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

方法里面簡單地進行了一下信息打印漫谷,其中方法的返回值是一個整數(shù)仔雷,表明了該方法需要返回多少個值到Lua中(后續(xù)章節(jié)會進行返回值的相關(guān)演示),現(xiàn)在不需要返回值則為0舔示。然后碟婆,再通過lua_pushcfunction方法將方法傳入:

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

操作完成后,在Lua中就可以直接調(diào)用了:

funcVal();

如果定義的方法是允許接受參數(shù)的惕稻,那么可以從state參數(shù)里面獲取傳入的參數(shù)竖共。拿上面的例子,例如方法接收一個名字的字符串參數(shù)俺祠,函數(shù)的代碼則可以修改為:

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

然后在Lua中則可以這樣調(diào)用:

funcVal ("vimfung");

如果定義的方法不是直接打印字符串公给,而是組合了字符串給Lua返回,那么定義的方法里面則需要配合'lua_pushXXXX'系列方法來進行返回值傳遞蜘渣。需要注意的是:方法中return的數(shù)量要與push到棧中的值要一致淌铐,否則可能出現(xiàn)異常。那么蔫缸,上面定義的函數(shù)可以做如下修改:

int printHelloWorld (lua_State *state)
{
    if (lua_gettop(state) > 0)
    {
        //表示有參數(shù)
        const char *name = lua_tostring(state, 1);
        
        //入棧返回值
        NSString *retVal = [NSString stringWithFormat:@"Hello %s!", name];
        lua_pushstring(state, retVal.UTF8String);
        
        return 1;
    }
    
    return 0;
}

然后在Lua中則可以這樣調(diào)用:

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

7.7 數(shù)組和字典的傳遞

在Lua中腿准,數(shù)組(Array)和字典(Dictionary)都由一個Table類型所表示(在Lua看來數(shù)組其實也屬于一種字典,只是它的key是有序并且為整數(shù))拾碌。如:

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

上面的例子分別用了不帶key的聲明和帶key的聲明兩種方式來創(chuàng)建Table類型吐葱。其中不帶key的聲明方式街望,解析器會默認為其創(chuàng)建一個key,該key是從1開始弟跑,由小到大進行分配灾前,其等效于:

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

當(dāng)然,兩種方式是可以混合使用窖认,如:

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

Table屬于比較復(fù)雜的數(shù)據(jù)結(jié)構(gòu)豫柬,因此提供操作它的C Api也比較復(fù)雜,下面將根據(jù)數(shù)組和字典分別講述它們的傳遞方式扑浸。

7.7.1 數(shù)組傳遞

首先烧给,需要將一個Table類型入棧,這樣才能對其進行進一步的操作喝噪。由于沒有pushtable這樣的方法础嫡,但是可以使用lua_newtable來創(chuàng)建一個Table對象,并且該對象會自動放入棧頂位置酝惧。如:

lua_newtable(self.state);

然后對要傳遞的數(shù)組進行遍歷榴鼎,并通過lua_rawseti方法將元素值設(shè)置到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");

通過上面的代碼就可以把一個數(shù)組傳遞給arrayVal變量晚唇。值得注意的是:lua_rawseti方法表示要棧頂?shù)脑卦O(shè)置給指定的Table對象的指定索引巫财。其中的第二個參數(shù)是指Table對象在棧中的位置,第三個參數(shù)是表示在Table中的索引哩陕,一般索引是從1開始算起平项,因此上面代碼中的idx需要加1。經(jīng)過這樣的操作后悍及,棧頂?shù)脑貢灰瞥?/strong>闽瓢。如下圖所示:

lua_rawseti示意圖

7.7.2 字典傳遞

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

lua_newtable(self.state);

然后對要傳遞的字典進行遍歷,并通過lua_setfield方法將元素設(shè)置到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中,只是一個用于指定整數(shù)索引缨叫,一個是指定字符串索引椭符。通過上面的方式就可以把字典傳遞給Lua了。

7.8 自定義數(shù)據(jù)傳遞

Lua中一個比較強大的地方是它可以將任意的類型(包括類對象)進行傳遞弯汰。特別是在提供原生處理方法時艰山,需要用到一些特定的數(shù)據(jù)類型作為參數(shù)時,Lua就可以幫我們實現(xiàn)這一塊的傳遞咏闪。

要想傳遞自定義的數(shù)據(jù)則必須要使用Lua提供的Userdata類型曙搬。該類型有兩種引用方式,一種是強引用Userdata,由Lua的GC來負責(zé)該類型變量的生命周期纵装。另外一種是弱引用Userdata征讲,又稱Light Userdata,該類型不被GC所管理橡娄,其生命周期由原生層來決定诗箍。下面來看一下兩種方式是如何實現(xiàn)的。

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

@interface User : NSObject

@property (nonatomic, copy) NSString *name;

@end

@implementation User

@end

然后挽唉,利用lua_newuserdata方法來創(chuàng)建一個強引用Userdata滤祖,并創(chuàng)建一個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來管理生命周期匠童,那么就可以創(chuàng)建一個弱引用的Userdata,如:

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

下面來看一個比較實際的例子塑顺,假設(shè)有一個提供給Lua調(diào)用的原生接口printUser汤求,該接口會打印傳入進來的用戶信息,代碼如下:

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

該方法通過lua_topointer方法來獲取了一個Userdata數(shù)據(jù)類型并轉(zhuǎn)換為User類實例對象然后打印其名稱严拒。接下來將其導(dǎo)出給Lua:

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

然后生成一個User對象扬绪,并調(diào)用該方法傳入該用戶對象。如:

//創(chuàng)建User對象
User *user = [[User alloc] init];
user.name = @"vimfung";

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

上面的代碼就是使用C Api來調(diào)用Lua的方法(下面的章節(jié)會詳細講述這塊內(nèi)容)裤唠,通過OC代碼創(chuàng)建了一個User對象并將其作為了參數(shù)傳給了Lua的printUser方法挤牛。最終的輸出信息如下:

user.name = vimfung

從OC到Lua的所有類型的轉(zhuǎn)換和交互基本上都涉及到了,下面章節(jié)將會詳細描述從Lua到OC上的一些交互和數(shù)據(jù)交換种蘸。

8. From Lua to OC

8.1 獲取數(shù)值

通過lua_tonumber方法可以獲取某個數(shù)值變量的值赊颠。如:

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

上述代碼中的lua_getglobal方法是用于獲取全局變量的值,調(diào)用它會把一個值放入棧頂劈彪。然后再通過lua_tonumber把棧頂?shù)闹底x取出來并打印。需要注意的是通過lua_getglobal獲取得到的值顶猜,在棧中是不會自動清除沧奴,因此,在用完某個變量時記得把它從棧中清除掉长窄,代碼中是通過lua_pop把值彈出棧的滔吠。

8.2 獲取布爾值

與獲取數(shù)值相同,通過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 獲取二進制數(shù)組

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

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 方法的獲取和調(diào)用

一般情況下嚣潜,要獲取Lua中的某個Function主要是用于對其進行調(diào)用冬骚。假設(shè)有一個Lua方法定義如下:

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

那么,對應(yīng)OC中需要下面的代碼來獲取和調(diào)用它:

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

上述代碼中的lua_pcall方法表示將棧中的元素視作Function來進行調(diào)用。其中第二個參數(shù)為傳入?yún)?shù)的數(shù)量只冻,必須與壓棧的參數(shù)數(shù)量一致庇麦;第三個參數(shù)為返回值的數(shù)量,表示調(diào)用后其放入棧中的返回值有多少個喜德。第四個參數(shù)是用于發(fā)生錯誤處理時的代碼返回山橄。其運行原理如下圖所示:

lua_pcall原理示意圖

對于帶參數(shù)和返回值的方法,在獲得方法對象后舍悯,需要調(diào)用lua_pushXXX系列方法來設(shè)置傳入?yún)?shù)航棱。可以參考下面例子:

假設(shè)有一個加法的Lua方法萌衬,其定義如下:

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

那么饮醇,OC中則可以進行下面操作來調(diào)用方法并傳遞參數(shù),最終取得返回值然后打印到控制臺:

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的獲取跟其他變量一樣奄薇,一旦放入棧后可以根據(jù)需要通過調(diào)用lua_getfield方法來指定的key的值驳阎。如:

//假設(shè)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,那么則需要調(diào)用lua_rawgeti來獲取Table的值馁蒂。如:

//假設(shè)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存儲的信息會在函數(shù)體外被訪問,那么我們需要對Table進行遍歷然后把它放入一個字典中沫屡,然后提供給程序使用饵隙。代碼如下:

//假設(shè)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設(shè)置了一個nil值,證明要從Table最開始的Key進行遍歷勺届。如圖:

lua_next原理示意圖

值得注意的是驶俊,在獲取Key值時,最好先判斷Key的類型免姿,然后再根據(jù)其對應(yīng)類型調(diào)用相應(yīng)的lua_toXXX方法饼酿。否則,因為lua_toXXX系列方法會對元素值進行類型轉(zhuǎn)換胚膊,如整型的Key被lua_tostring轉(zhuǎn)換為String后再給到lua_next進行遍歷就會報找不到指定Key的錯誤故俐。

8.7 獲取自定義數(shù)據(jù)

利用lua_topointer方法來獲取自定義數(shù)據(jù),如:

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

9. 結(jié)語

Lua雖然小巧紊婉,但五臟俱全药版。最主要的是它提供了豐富又強大的C Api接口允許我們進行高度的定制和擴展∮骼纾可利用這些擴展特性開發(fā)適合自己的一套腳本引擎槽片『位海基于上面所講的绅项,鄙人開發(fā)了一個LuaScriptCore的開源橋接框架绘雁,簡化Lua與各種平臺的交互,有興趣的同學(xué)可以了解一下辞嗡。

最后蝙云,希望通過本篇文章能夠讓大家對OC與Lua之間的交互有更近一步的了解氓皱,創(chuàng)造更多的可能性。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末勃刨,一起剝皮案震驚了整個濱河市波材,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌身隐,老刑警劉巖廷区,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異贾铝,居然都是意外死亡隙轻,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進店門垢揩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來玖绿,“玉大人,你說我怎么就攤上這事叁巨“叻耍” “怎么了?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵锋勺,是天一觀的道長蚀瘸。 經(jīng)常有香客問我,道長庶橱,這世上最難降的妖魔是什么贮勃? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮苏章,結(jié)果婚禮上衙猪,老公的妹妹穿的比我還像新娘。我一直安慰自己布近,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布丝格。 她就那樣靜靜地躺著撑瞧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪显蝌。 梳的紋絲不亂的頭發(fā)上预伺,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天订咸,我揣著相機與錄音,去河邊找鬼酬诀。 笑死脏嚷,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瞒御。 我是一名探鬼主播父叙,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼肴裙!你這毒婦竟也來了趾唱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤蜻懦,失蹤者是張志新(化名)和其女友劉穎甜癞,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宛乃,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡悠咱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了征炼。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片析既。...
    茶點故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖柒室,靈堂內(nèi)的尸體忽然破棺而出渡贾,到底是詐尸還是另有隱情,我是刑警寧澤雄右,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布空骚,位于F島的核電站,受9級特大地震影響擂仍,放射性物質(zhì)發(fā)生泄漏囤屹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一逢渔、第九天 我趴在偏房一處隱蔽的房頂上張望肋坚。 院中可真熱鬧,春花似錦肃廓、人聲如沸智厌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽铣鹏。三九已至,卻和暖如春哀蘑,著一層夾襖步出監(jiān)牢的瞬間诚卸,已是汗流浹背葵第。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留合溺,地道東北人卒密。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像棠赛,于是被迫代替她去往敵國和親哮奇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,747評論 2 361

推薦閱讀更多精彩內(nèi)容

  • Nginx API for Lua Introduction ngx.arg ngx.var.VARIABLE C...
    吃瓜的東閱讀 5,807評論 0 5
  • ¥開啟¥ 【iAPP實現(xiàn)進入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程恭朗,因...
    小菜c閱讀 6,444評論 0 17
  • 第一篇 語言 第0章 序言 Lua僅讓你用少量的代碼解決關(guān)鍵問題屏镊。 Lua所提供的機制是C不擅長的:高級語言,動態(tài)...
    testfor閱讀 2,681評論 1 7
  • 當(dāng)在Lua和C之間交換數(shù)據(jù)時主要的問題是自動回收與手動回收內(nèi)存管理的不一致痰腮。因此而芥,Lua 用一個抽象的棧在Lua與...
    luffier閱讀 2,664評論 0 3
  • 說說自己打個比方吧,我170斤這就是事實膀值,有的說我很胖棍丐,有的說男孩子就要這么大的個子,這個就是觀點吧沧踏。胖是事實還...
    A0FineYoga易君閱讀 219評論 0 0