iOS中调用Lua

作者: iOS_zy | 来源:发表于2018-04-09 16:40 被阅读762次

    最近项目中提到需要调用Lua,所以简单的研究了一下,也看了不少网上大佬的文章,在这就简单的写一下收获吧。
    下载和编译Lua解析器
    首先,跳转到Lua官网的下载页将源码下载下来。然后解压下载包可以得到如下图所示的目录结构:

    Lua源码目录结构

    对应的目录说明如下表:

    名称 说明
    doc Lua相关的文档,包括了编译文档、接口文档等
    Makefile 编译Lua使用,在这里我们不使用它来进行编译
    README 关于Lua的说明文件
    src Lua的源码文件

    编译Lua源码
    在这里我们只需要src目录中的源码文件,先打开src目录,将Makefile、lua.c、luac.c三个文件删除掉,需要说明的是lua.c和luac.c文件是用于编译生成lua和luac两个命令不属于解析器的功能,如果不删除可能会导致XCode无法编译通过。

    接下来打开XCode创建一个新的项目并把src目录拖入项目中。如下图所示:

    导入Lua源码到项目

    然后Command+B进行编译,提示编译成功!

    接下来就是实际操作:

    From OC to Lua

    1 空值传递

    使用lua_pushnil方法可以将任意一个Lua变量置空。如:

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

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

    3 布尔值的传递

    使用lua_pushboolean方法来实现,如:

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

    4 字符串的传递

    使用lua_pushstring方法可以传递字符串给Lua,要注意的是该方法接收的是一个c描述的字符串(即 char*)。如:

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

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

    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 数组和字典的传递

    在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.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.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了。

    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上的一些交互和数据交换。

    From Lua to OC

    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把值弹出栈的。

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

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

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

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

    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的错误。

    7 获取自定义数据

    利用lua_topointer方法来获取自定义数据,如:

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

    下面是实际操作:

    需要一个lua文件来写我们的测试方法。直接新建一个txt记事本,将后缀名改为.lua即可,so easy。这个时候就需要在demo.lua中来写我们的测试方法了,简单实现一个加法运算。

    function addFunc (add1, add2)
    return add1 + add2;
    end
    

    创建lua_State对象L

    通过luaL_dofile调用demo.lua文件

    将方法名和参数入栈

    通过调用lua_pcall方法调用lua中的addFun

    返回值通过lua_tostring得到


    image.png

    参考:
    https://blog.csdn.net/vimfung/article/details/53788784
    https://www.aliyun.com/jiaocheng/360469.html

    相关文章

      网友评论

        本文标题:iOS中调用Lua

        本文链接:https://www.haomeiwen.com/subject/upvihftx.html