Lua 是一种嵌入式语言,这就意味着 Lua 并不是一个独立的应用,而是一个库,它可以链接到其他应用程序,将 Lua 功能融入这些应用。这就涉及到 Lua 提供的 C API,通过这些 API,实现在自己的应用程序中通过 Lua 来扩展应用。
C 语言 API 总览
之前我们编写的 Lua 程序都是通过 Lua 解释器运行的,Lua 解释器是一个小应用,它是用 Lua 标准库实现的独立解释器。这个解释器负责与用户的交互,将用户的文件和字符串传递给 Lua 标准库,由标准库完成主要工作。
由于能够被当做库来扩展应用程序,所以 Lua 是一种嵌入式语言。同时,使用了 Lua 的程序可以在 Lua 环境中注册新的函数,例如用 C 实现的函数,从而增加一些无法直接用 Lua 编写的功能。因此 Lua 是一种可扩展的语言。这两种对 Lua 的定位分别对应 C 和 Lua 之间两种形式:
- 第一种形式中(嵌入式语言),C 拥有控制权,而 Lua 被用作库,这种交互形式中的 C 代码被称为应用代码
- 第二种形式中(可扩展语言),Lua 拥有控制权,而 C 被用作库,此时 C 代码被称为库代码
应用代码和库代码都使用相同的 API 和 Lua 通信,这些 API 被称为 CAPI。CAPI 是一个由函数、常量和类型组成的集合,通过它,C 代码就可以和 Lua 语言交互。CAPI 遵循 C 的操作模式。
第一个示例
如下实现了一个简单的 Lua 独立解释器:
1 |
|
使用如下方式编译该程序,需要链接 lua 库和数学库(顺序很重要):
1 | gcc -g -o luac luac.c -llua -lm |
程序运行如下:
1 | # ./luac |
接下来对该程序进行说明:
lua.h
头文件声明了 Lua 提供的基础函数,包括创建新 Lua 环境
、调用 Lua 函数
、读写环境中的全局变量
、注册供 Lua 语言调用的新函数
。该头文件中声明的内容均以lua_
作为前缀lauxlib.h
头文件声明了辅助库(auxlib)所提供的函数,所有声明都以luaL_
开头。辅助库使用lua.h
提供的基础 API 来提供更高层次的抽象,特别是对标准库用到的相关机制进行抽象。基础 API 追求经济性和正交性,而辅助库则追求对常见任务的实用性
Lua 标准库没有定义任何 C 全局变量,所有的状态均保存在动态的结构体 lua_State
中。Lua 所有函数都接收一个该结构指针作为参数,这种设计使得 Lua 是可以重入的,可以直接用于编写多线程代码。
- 当
luaL_newstate
创建新的Lua 状态
时,新环境中没有包含预定义的函数。头文件lualib.h
中声明了用于打开这些库的函数,luaL_openlibs
用于打开所有标准库 luaL_loadstring
用于编译用户输入的每一行内容,如果没有错误,则返回 0,并向栈中压入编译后得到的函数lua_pcall
从栈中弹出编译后的函数,并以保护模式运行。如果没有发生错误,该函数返回 0- 以上两个函数在发生错误时,都会向栈中压入一条错误信息,可以通过
lua_tostring
获取错误信息。在打印之后,使用lua_pop
将其从栈中删除
如果将 C 作为 C 代码编译出来后,又要在 C++ 中使用,可以引入 lua.hpp
来替代 lua.h
。
栈
Lua 和 C 之间的通信主要组件是无处不在的虚拟栈。几乎所有 API 调用都是在操作这个栈中的值,Lua 与 C 之间所有的数据交换都是通过这个栈来完成的,还可以通过这个栈来保存中间结果。
如果想在 Lua 和 C 之间交换数据时,会面对两个问题:
- 问题 1:动态类型和静态类型体系之间不匹配
- 问题 2:自动内存管理和手动内存管理之间的不匹配问题
通过栈在 Lua 和 C 之间交换数据,栈中的每个元素都能保存 Lua 中任意类型的值:
- 当我们想从 Lua 中获取一个值,只需要调用 Lua,Lua 就会将指定值压入栈中,之后从栈中取出值即可
- 当想要将一个值传给 Lua 时,则首先要将这个值压入栈中,然后调用 Lua,Lua 会将其从栈中弹出
尽管仍然需要一个不同的函数将每种 C 类型的值压入栈中,还需要一个不同的函数从栈中弹出每种 C 语言类型的值,但是避免了过多的组合。另外,由于栈是 Lua 状态的的一部分,因此垃圾收集器知道 C 正在使用哪些值。
针对每一种能用 C 直接表示的 Lua 数据类型,CAPI 都有一个对应的压栈函数:
- lua_pushnil
- lua_pushboolean
- lua_pushnumber
- lua_pushinteger
- lua_pushlstring
- lua_pushstring
无论何时向栈中压入一个元素,都应该确保栈中有足够的空间,lua_checkstack
可以用来检查栈中是否有足够的空间。辅助库也提供了高层函数来检查栈空间 luaL_checkstack
。如果有可能,这两个函数会尝试增加栈的大小,以容纳所需的额外空间。
CAPI 使用索引来引用栈中的元素,第一个被压入栈中的元素索引为 1,第二个被压入的元素索引为 2,以此类推。还可以以栈顶为参照,使用负数索引来访问栈中的元素,此时 -1 表示栈顶元素(即最后被压入的元素)。
要检查栈中的一个元素是否为特定类型,CAPI 提供了一系列名为 lua_is*
的函数,其中 * 可以是任意一种任意 Lua 数据类型,这些函数包括 lua_isnill
、lua_isnumber
、lua_isstring
、lua_istable
。
lua_type
用于返回栈中元素的类型,每一种类型都由一个对应的常量表示。lua_to*
用于从栈中获取一个值。
除了上述在 C 语言和栈之间交互数据的函数之外,CAPI 还提供了下列通用栈操作的函数:
- lua_gettop:返回栈中元素的个数,即栈顶元素的索引
- lua_settop:将栈顶设置为一个指定的值,即修改栈中元素的数量。
lua_settop(L, 0)
用于清空栈 - lua_pushvalue:用于将指定索引上的元素的副本压入栈
- lua_rotate:将指定索引到栈顶位置之间的元素旋转 n 个位置。n 为正数,表示将元素向栈顶方向移动,n 为负数则表示向相反方向移动
- lua_remove:删除指定索引的元素,并将该位置之上的所有元素向下移动以填补空缺
- lua_insert:用于将栈顶元素移动到指定位置,并上移指定位置之上的所有元素以开辟出一个元素的空间
- lua_replace:将栈顶元素移动到指定索引上(并且将这个栈顶元素弹出),不移动任何元素
- lua_copy:将一个索引上的值复制到另一个索引上,原值不受影响
如下程序展示了上述 API 的使用:
1 |
|
使用 CAPI 进行错误处理
LUA API 中的绝大多数函数都可能抛出异常。Lua 使用异常来提示错误,而没有在 API 的每个操作中使用错误码。而 C 语言没有提供异常处理机制。为了解决这个问题,Lua 使用了 C 中的 setjmp 机制,提供一个类似异常处理的机制,因此大多数 API 函数都可以抛出异常(即调用 longjmp)而不是直接返回。当编写库函数时(被 Lua 调用的 C 函数),由于 Lua 会捕获所有异常,使用 longjmp 并不用进行额外操作。但是编写应用代码(调用 Lua 的 C 代码时),则必须提供一种捕获异常的方式。
如果应用调用了 Lua API 中的函数,就可能发生错误。Lua 通过长跳转来提示错误,但是如果没有对应的 setjmp,解释器就无法长跳转。此时 API 中的任何错误都会导致 Lua 调用紧急函数,当函数返回之后,应用就会退出。可以通过 lua_atpanic
来设置自己的紧急函数。
要正确处理应用代码的错误,需要通过 Lua 来调用我们的代码,这样 Lua 才能设置合适的上下文来捕获异常,即在 setjmp 的上下文中运行代码。可以把 C 代码封装到一个函数 F 中,然后使用 lua_pcall
调用这个函数。通过这种方式,C 代码会在保护模式中运行,即使发生内存分配失败,lua_pcall 也会返回一个对应的错误码,使得解释器能够保持一致的状态。
在为 Lua 编写库函数时,通常无需进行错误处理。库函数抛出的错误要么被 Lua 中的 pcall 捕获,要么被应用代码中的 lua_pcall 捕获。因此 C 库函数检查到错误时,只需要调用 lua_error(或者 luaL_error),它会收拾 Lua 系统的残局,然后跳转会保护模式调用处,并传递错误信息。
内存分配
Lua 语言核心对内存分配不进行任何假设,它只会通过一个分配函数来分配和释放内存,当用户创建 Lua 状态时必须提供该函数。luaL_newstate
是一个用默认分配函数来创建 Lua 状态的辅助函数。该默认分配函数使用了 C 标准库中的 malloc-realloc-free
。如果要完整控制 Lua 的内存分配,则需要使用原始的 lua_newstate
来创建 Lua 状态。
Lua 内部不会为了重用而缓存空闲内存,它假定分配函数会完成这种缓存工作,Lua 也不会试图压缩内存碎片。
扩展应用
Lua 的重要用途之一就是用作配置语言,接下来会介绍如何使用 Lua 来配置一个程序。
基础知识
假设 C 程序有一个窗口,并希望用户能够指定窗口的初始大小。我们使用一个 Lua 配置文件(也是一个普通的文本文件,只不过它是一个 Lua 程序),例如内容如下:
1 | width = 200 |
我们需要使用 Lua API 来指挥 Lua 语言解析该文件,并获取全局变量 width 和 height 的值。
1 | int getglobalint(lua_State *L, const char *var) { |
这段代码的关键是调用 lua_getglobal
让 Lua 将相应的全局变量的值压入栈中,之后调用 lua_tointegerx
将这个值转换为整型以保证类型正确。这段代码还用到了之前封装的一些库函数,这里不再详述。
使用 Lua 的好处是:
- Lua 为我们处理了所有语法细节,甚至配置文件都可以有注释
- 可以 Lua 实现更复杂的配置
操作表
在配置中,使用 Lua 的表结构可以让脚本变得更加架构化。例如对于如下配置文本:
1 | BLUE = {red = 0, green = 0, blue = 1.0} |
可以通过代码实现对 table 的操作:
1 |
|
lua_gettable
以表在栈中的位置为参数,获取以栈顶元素为 key 的对应 value,之后从栈中弹出 key、再压入相应的 value。
可以继续扩展该示例,为用户引入颜色名字。用户除了可以使用颜色表,还可以直接使用常用颜色的预定义名称。要实现该功能,需要在 C 程序中就要有一张颜色表,然后用这个颜色表在 Lua 中定义对应的全局变量。
1 | struct ColorTable { |
函数 lua_newtable
用于创建一个空表,并将其压入栈中,之后的 setcolorfield
设置表的各个字段,最后函数 lua_setglobal
弹出表,并将其设置为具有指定名称全局变量的值,这样就为 Lua 脚本注册了指定的颜色。
除了给 Lua 脚本注册颜色名称,还有一种解决该问题的思路。即在应用程序中(C 代码中)根据 Lua 脚本的配置来在 C 程序中搜索对应的颜色配置。
尽管 Lua 的 CAPI 追求简洁性,但是 Lua 也没有做得过于激进。因此 CAPI 为一些常用操作提供了一些简便方法:
- lua_getfield 用于通过字符串类型的键来检索表,该函数会返回结果的类型
- lua_setfield 用于为字符串类型的键来设置表中的 value
- lua_createtable 可以创建表并为元素预分配空间
调用 Lua 函数
调用 Lua 函数的 API 规范很简单,首先将待调用的函数压栈,然后压入函数的参数,接着调用 lua_pcall 进行实际的调用,最后从栈中取出结果。假设 Lua 配置脚本中有如下函数:
1 | function f(x, y) |
如下展示了从 C 语言中调用 Lua 函数:
1 | double f(lua_State *L, double x, double y) { |
在调用函数 lua_pcall
时,第二个参数表示传递的参数数量,第三个参数是期望的结果数量,第四个参数代表错误处理函数。lua_pcall
会根据所要求的数量来调整返回值的个数,即压入 nil 或者丢弃多余的结果。在压入结果前,lua_pcall 会把函数和其参数从栈中移除。当返回多个结果时,第一个结果最先被压入。
如果 lua_pcall
在运行过程中出现错误,它会返回一个错误码,并在栈中压入一条错误信息(仍会弹出函数及其参数)。如果有错误处理函数,在压入错误信息之前,lua_pcall
会先调用错误处理函数。lua_pcall
的最后一个参数即表示错误处理函数在栈中的索引(0 表示没有错误处理函数)。
如下说明 lua_pcall
返回的错误码:
- 对于普通的错误,返回 LUA_ERRRUN
- 对于内存分配失败,返回 LUA_ERRMEM
- 对于错误处理函数本身出错,返回 LUA_ERRGCMM
一个通用的调用函数
如下实现了一个调用 Lua 函数的包装程序:
1 |
|