上一篇文章主要讲解如何在 C 代码中调用 Lua 代码,即将 C 代码作为应用程序代码,Lua 代码作为库代码。这篇文章将介绍如何在 Lua 代码中调用 C 语言,即将 Lua 代码作为应用代码,将 C 代码作为库代码。
在 Lua 中调用 C 语言
Lua 可以调用 C 语言函数,但是并不意味着 Lua 可以调用所有的 C 函数。当 Lua 调用 C 函数时,也使用了一个与 C 调用 Lua 函数时相同类型的栈,C 函数从栈中获取参数,并将结果压入栈中。每个函数都有其私有的局部栈,当 Lua 调用一个 C 函数时,第一个参数总是位于这个局部栈中索引为 1 的位置,依次类推。
另外,当 Lua 调用 C 函数时,必须注册该函数,即必须以一种恰当的方式来为 Lua 提供该 C 函数的地址。
C 函数
如下是一个可供 Lua 调用的 C 函数:
1 | static int l_sin(lua_State *L) { |
所有在 Lua 中注册的函数都必须使用一个相同的原型,该原型就是定义在 lua.h
中的 lua_CFunciton
:
1 | typedef int (*lua_CFunction)(lua_State *L); |
该函数的返回值表示压入栈中的返回值的个数。该函数在压入结果前无需清空栈。在该函数返回后,Lua 会自动保存值并清空整个栈。
在 Lua 中,要调用这个函数之前,还需要通过 lua_pushcfunction
注册该函数。lua_pushcfunction
会获取一个指向 C 函数的指针,然后在 Lua 中创建一个 function
类型,表示待注册的函数。注册完成后,C 函数就可以像其他 Lua 函数一样被 Lua 调用了。
如下代码将 l_sin
添加到 lua 解释器中:首先将函数类型的值压入栈中,之后将该值赋值给全局变量 mysin。
1 | lua_pushcfunction(L, l_sin); |
要编写一个更专业的正弦函数,首先必须检查其参数的类型,Lua 辅助库可以帮我们完成这个任务。luaL_checknumber
可以检查指定的参数是否为一个数字。
如下实现了读取目录的函数:
1 |
|
注意,由于 l_dir
是在 lua 中被调用的,因此上述代码中所以一 lua_
开头的函数都是在向 Lua 返回值。
延续
由于 Lua 的 CAPI 提供了 lua_pcall
和 lua_call
,因此一个被 Lua 调用的 C 函数也可以通过这些 API 回调 Lua 函数。通常 Lua 是可以处理这种调用顺序的,毕竟与 C 的集成是一大特点。但是有一种情况这种调用会有问题,就是协程。此时调用顺序为:宿主程序(C)-> 脚本代码(Lua) -> C 库接口(C)-> Lua 函数(Lua)。
一般 Lua 是可以处理这种调用顺序的。Lua 中每个协程都有自己的栈,其中保存了该协程所挂起调用的信息,具体来说就是栈中存储了每一个调用的返回地址、参数和局部变量。对于 Lua 的调用,解释器只需要这个栈即可,我们称其为软栈。然而对于 C 的调用,解释器必须使用 C 语言栈,因为 C 函数的返回地址和局部变量都位于 C 语言栈中。
对于解释器来说,拥有多个软栈并不难,但是 ISO C 的运行时环境却只能拥有一个内部栈。因此 Lua 中的协程并不能挂起 C 函数的执行,如果一个 C 函数位于从 resume 到对应 yield 的调用路径中,那么 Lua 无法保存 C 函数的状态以便于在下次 resume 时恢复状态。
Lua 5.2 及后续版本中,用延续(continuation)改善了对这个问题的处理。Lua 5.2 使用长跳转实现了 yield,并用相同的方式实现了错误处理。长跳转简单丢弃 C 语言栈中关于 C 函数的所有信息,因而无法 resume 这些函数。但是一个 C 函数 foo 可以指定一个延续函数 foo_k。当解释器发现它应该恢复 foo 执行时,如果长跳转(long jump)已经丢弃了 C 语言栈中有关 foo 信息,则调用 foo_k 来替代。
lua_pcalllk
和 lua_pcall
类似,但是支持被调用的函数中途 yield。lua_pcalllk
支持设置一个延续函数,当解释器发现需要 resume 时,它会调用延续函数。
延续体系是一种为了支持 yield 而设计的精巧机制,但它也不是万能的,某些 C 函数可能会需要给他们的延续传递相当多的上下文。
C 模块
Lua 模块就是一个代码段,其中定义了一些函数并将其存储在恰当的地方(通常是表中的元素)。为 Lua 编写的 C 语言模块可以模仿这种行为。除了 C 函数定义外,C 模块还必须定义一个特殊的函数,该特殊函数相当于 Lua 库中的主代码段,用于注册模块中的所有 C 函数,并将其存储在恰当的地方(通常也是表中)。其他初始化工作也应该在函数中完成。
Lua 通过注册过程感知 C 函数,一旦一个 C 函数用 Lua 表示和存储,Lua 就会通过对其地址的直接引用来调用它。通常一个 C 模块中只有一个用于打开库的公共函数,其他函数都是私有的,在 C 中被声明为 static。
宏 luaL_newlib
接收一个由 C 函数及其对应函数名组成的数组,并将这些函数注册一个新的表中。如下是一个示例:
1 | static int l_dir(lua_State *L) { |
编写完这个库之后,还必须将其链接到解释器。如果 Lua 解释器支持动态连接的话,那么最简单的方法就是使用动态链接机制。此时必须将代码编译成一个动态链接库(即 Linux 下的 .so
文件),并将这个库放到 C 语言路径中的某个地方。完成了以上步骤之后,就可以使用 require 直接在 Lua 中加载这个模块了:
1 | local mylib = require "mylib" |
此时动态库 mylib 链接到 Lua 中,并查找函数 luaopen_mylib
,将其注册为一个 C 语言函数,然后调用它以打开模块。动态链接器总是以 luaopen_ + 模块名
这样的函数。
如果解释器不支持动态链接器,就必须连同新库一起重新编译 Lua 语言。而且需要告诉独立解释器,它应该在打开一个新的 lua_State 时打开这个库,一种简单的做法是把 luaopen_mylib
添加到由 luaL_openlibs
打开的标准库列表(该列表位于 linit.c
中)。
编写 C 函数的技巧
官方 API 和辅助库都提供了一些机制用来帮助用户编写 C 函数。
数组操作
Lua 中的数组就是以特殊方式使用的表。lua_settable
、lua_gettable
这种用来操作表的通用函数,也可以用来操作数组。但是 CAPI 也为使用整数索引的表访问/更新提供了专门的函数:
1 | void lua_geti(lua_State *L, int index, int key); |
如下实现了一个 map 函数,该函数对数组中的所有元素调用一个指定的函数,然后用该函数返回的结果替换掉对应的数组元素。
1 | int l_map(lua_State *L) { |
luaL_checktype
用于确保指定的参数具有指定的类型lua_call
做的是不受保护的调用,该函数类似于lua_pcall
,但是发生错误时lua_call
会传播错误而不是返回错误码。在应用中编写代码主函数时,一般不用lua_call
因为我们需要捕获所有的错误。但是编写函数时使用lua_call
是合适的。如果发生错误,由关心错误的人去处理
字符串操作
当 C 函数接受到一个 Lua 字符串作为参数时,必须遵守规则:在使用字符串期间不能从栈中将其弹出,而且不应该修改字符串。当 C 函数需要创建一个返回给 Lua 的字符串时,由 C 语言负责缓冲区分配、释放等任务。
LUA API 提供了一些函数来帮助完成这些任务:
lua_pushlstring
将字符串中指定长度的子串压入栈中lua_concat
用于连接字符串,它可以将数字转换为字符串,lua_concat(L, n)
会连接栈最顶端的 n 个值,并将结果压入栈lua_pushfstring
类似于 C 语言的sprintf
,它会根据格式字符串和额外的参数来创建字符串。不管字符串多大,Lua 都会动态为我们创建,lua_pushfstring
会将结果字符串压入栈中并返回一个指向它的指针
如下函数根据指定的分隔符(单个字符)来分割字符串,并返回一张包含子串的表。
1 | static int l_split(lua_State *L) { |
当连接多个字符串时,逐个连接效率较低。此时我们可以使用辅助库提供的缓冲机制。缓冲机制的简单用法只包含两个函数:
- 一个用于在组装字符串时提供任意大小的缓冲区
- 另一个用于将缓冲区的内容转换为一个 Lua 字符串
如下是一个示例:
1 | static int str_upper(lua_State *L) { |
- 首先声明一个 luaL_Buffer 类型的变量,然后调用
luaL_buffinitsize
获取一个指向指定大小缓冲区的指针 luaL_pushresultsize
将缓冲区的内容转换为一个新的 Lua 字符串,并将该字符串压栈
如果不知道返回结果大小的上限值,可以通过逐步增加内容的方式来使用辅助缓冲区。辅助库提供了一些用于向缓冲区中增加内容的函数,这里函数包括 luaL_addvalue
、luaL_addlstring
等。
如下示例对 table 中的元素进行连接:
1 | static int tconcat(lua_State *L) { |
需要注意,在初始化一个缓冲区后,Lua 栈中可能会保留某些内部数据,我们不能假设在使用缓冲区之前栈顶仍然停留在最初的位置。
在 C 函数中保存状态
如果想在 C 函数中保存一些非局部数据,即生存时间超过 C 函数执行时间的数据,我们通常使用全局变量或者静态变量来实现。但是当为 Lua 编写库函数时,这并不是好办法。首先我们无法在一个 C 变量中保存普通的 Lua 值,另外,使用这类变量的库也无法用于多个 Lua 状态。
Lua 函数有两个地方地方用于存储非局部数据,即全局变量和非局部变量,而 CAPI 也提供了两个类似的地方来存储非局部数据:注册表和 upvalue。
注册表是一张能够被 C 代码访问的全局表。通常使用注册表来存储多个模块间共享的数据。注册表总是位于伪索引 LUA_REGISTRYINDEX 中,例如要获取注册表中键为 key
的值,可以使用 lua_getfield(L, LUA_REGISTRYINDEX, "key")
。
在注册表中不能使用数值类型的键,因为 Lua 将其用作引用系统的保留字。引用系统由辅助库中的一对函数组成,有了这两个函数,我们可以在表中存储值时不用担心如何创建唯一键。
1 | int ref = luaL_ref(L, LUA_REGISTRYINDEX); |
该调用会从栈中弹出一个 value,然后分配一个新的整型键,使用这个键将从栈中弹出的值保存到注册表中,最后返回该整型键,而这个键就被称为引用。要将与引用 ref 关联的值压入栈中,只需要:
1 | lua_rawgeti(L, LUA_REGISTRYINDEX, ref); |
要释放值和引用,则可以调用 luaL_unref(L, LUA_REGISTRYINDEX, ref)
。
引用系统将 nil 视为一种特殊情况:
- 为一个 nil 值调用
luaL_ref
不会创建新的引用,而是返回一个常量引用 LUA_REFNIL lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_REFNIL)
向栈中压入一个 nil
常量 LUA_NOREF 可以用于表示无效引用。创建 Lua 状态时,注册表有两个预定义的引用:
LUA_RIDX_MAINTHREAD
:指向 Lua 状态本身,即主线程LUA_RIDX_GLOBALS
:指向全局变量
另一种在注册表中创建唯一键的方法是,使用代码中的静态变量的地址。C 的链接器会确保键在所有已加载的库中的唯一性。要使用这种方法,需要用到 lua_pushuserdata
,该函数会在栈中压入一个表示 C 指针的值。如下是一个示例:
1 | static char key = 'k'; |
为了简化将变量地址用作唯一键的用法,Lua5.2 提供了 lua_rawgetp
和 lua_rawsetp
,它们使用 C 语言指针(转换为轻量级用户数据)作为 key,上述代码可以重写为:
1 | static char key = 'k'; |
注册表提供了全局变量,而 upvalue 则实现了一种类似于 C 语言静态变量(只在特定函数中可见)的机制。每一次在 Lua 中创建新的 C 函数时,都可以将任意数量的 upvalue 与这个函数相关联,每一个 upvalue 都可以保存一个 Lua 值。后面在调用该函数时,可以通过伪索引来自由地访问这些 upvalue。我们可以将这种 C 函数与其 upvalue 的关联称为闭包。C 语言的闭包类似于 Lua 闭包。可以使用相同的函数代码来创建不同的闭包,每个闭包可以拥有不同的 upvalue。
1 | static int counter(lua_State *L); |
lua_pushcclosure
会创建一个新的闭包,它的第二个参数是一个基础函数,第三个参数是 upvalue 的数量。在创建新的闭包前,需要将 upvalue 的初始值压入栈中。lua_pushcclosure
会将一个新的闭包留在栈中,并将其作为 newCounter
的返回值。
counter
的定义如下,其中 lua_upvalueindex
用于生成 upvalue 的伪索引:
1 | static int counter(lua_State *L) { |
如下使用 upvalue 实现了元组:
1 | int t_tuple(lua_State *L) { |
这段代码要点如下:
luaL_optinteger
可以获取可选参数,当参数不存在时,返回指定的默认值lua_argcheck
可以检查参数的有效性,它可以检查给定的条件,如果条件不符合,则会引发错误并返回一条错误信息lua_isnone
用于测试指定的 upvalue 是否存在
我们经常需要在同一个库的所有函数之间共享某些值或变量,虽然可以使用注册表来完成该任务,但也可以使用 upvalue。但是与 Lua 的闭包不同,C 的闭包不能共享 upvalue,每个闭包都有独立的 upvalue。但是可以设置不同函数的 upvalue 指向同一张表,这张表就称为一个共同的环境。
当我们使用 luaL_newlib
打开 C 语言库时,它会调用 luaL_newlibtable
:
1 |
宏 luaL_newlibtable
只是为库创建一个新表(该表预先分配的大小等同于指定库中函数的数量),然后 luaL_setfuncs
将列表 lib 中的函数添加到位于栈顶的新表中,该函数的第三个参数指定了库中新函数共享的 upvalue 值个数。
因此如果要创建一个库,这个库中的所有函数共享一张表作为它的 upvalue,可以使用如下代码:
1 | luaL_newlibtable(L, lib); |