1. 写在前面
很多时候我们都需要借助一些脚本语言来为我们实现一些动态的配置,那么就会涉及到如何让脚本语言跟原生语言交互的问题。平时在网上看得比较多的是使用JS(JavaScript)与iOS原生代码ObjC交互的文章。因为JS的解析器是iOS内部提供的(可以使用UIWebView或者JavaScriptCore.framework实现),所以使用JS来交互会感觉比较方便。
但是在这里,我想跟大家分享另外一种脚本语言的交互方式,就是使用Lua与原生的ObjC语言进行交互。Lua是一种轻量级的脚本语言,它的脚本解析器很小,编译出来只有100多kb,因此,作为一个内嵌的脚本解析器是首选的;而且Lua除了提供基本的脚本语言特性和系统功能外(IO读写),没有多余的功能性框架(JS解析器因为要配合Web的功能实现带有很多的工具库),这也是它轻量的表现。同时,提供了丰富的C Api来让其它的语言对其功能进行扩展,能够真正做到按需定制。
那么,这里所说到的C Api就是用于与ObjC交互的重点。因为ObjC本来就是C语言的超集,所以能够很方便的调用这些C Api,下面将一步一步实现交互的过程。
2. 下载和编译Lua解析器
首先,跳转到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目录拖入项目中。如下图所示:
然后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将值取出来,如图所示:
同样,要将一个值传给Lua时,需要先调用C Api将这个值压入栈,然后再调用C Api,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方法为设置全局变量的值,该方法会把数据栈顶的元素放入该方法第二个参数所指定的变量名对应的变量中,同时移除栈顶元素。如图:
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