14.2 扩展 Lua 扩展 Lua:Lua C API 实践详解 引言 Lua 以其轻量、快速和易于嵌入的特性而闻名。尽管 Lua 本身功能强大,但在某些场景下,我们可能需要借助 C 语言来扩展 Lua 的能力。这通常出于以下几个原因: 性能优化: 对于计算密集型任务,C 语言的执行效率通常远高于 Lua。将性能瓶颈部分用 C 实现可以显著提升程序性能。 访问系统资源和 C 库: Lua 脚本本身难以直接访问操作系统底层 API 或已有的 C 语言库。通过 C 扩展,Lua 可以轻松调用这些资源,实现更丰富的功能。 代码复用: 如果已经存在大量的 C 代码库,将其封装成 Lua 扩展可以方便地在 Lua 环境中复用这些代码,避免重复开发。
Lua 以其轻量、快速和易于嵌入的特性而闻名。尽管 Lua 本身功能强大,但在某些场景下,我们可能需要借助 C 语言来扩展 Lua 的能力。这通常出于以下几个原因:
性能优化: 对于计算密集型任务,C 语言的执行效率通常远高于 Lua。将性能瓶颈部分用 C 实现可以显著提升程序性能。
访问系统资源和 C 库: Lua 脚本本身难以直接访问操作系统底层 API 或已有的 C 语言库。通过 C 扩展,Lua 可以轻松调用这些资源,实现更丰富的功能。
代码复用: 如果已经存在大量的 C 代码库,将其封装成 Lua 扩展可以方便地在 Lua 环境中复用这些代码,避免重复开发。
Lua C API 的核心概念是 栈 (Stack)。Lua 和 C 之间的数据交换、函数调用都通过这个栈来完成。可以将这个栈理解为 Lua 虚拟机和 C 代码之间共享的一块内存区域。
数据传递: 当 C 代码需要向 Lua 传递数据时,会将数据压入栈中;Lua 虚拟机则从栈顶取出数据。反之亦然,Lua 向 C 传递数据也是通过将数据压入栈,C 代码再从栈中读取。
函数调用: 当 Lua 调用 C 函数时,C 函数的参数会先被压入栈中,C 函数执行完毕后,将返回值压入栈中。Lua 虚拟机负责从栈中获取返回值。
理解栈的工作方式是掌握 Lua C API 的关键。Lua C API 提供了一系列函数来操作这个栈,包括压入数据、弹出数据、检查栈中数据类型、获取栈中数据值等等。
让我们从一个最简单的例子开始,创建一个 C 函数,该函数接受两个数字作为参数,并返回它们的和。
1. 创建 C 代码文件 (mylib.c):
#include <lua.h> #include <lauxlib.h> #include <lualib.h> // C 函数:add // 参数:Lua 状态机指针 // 返回值:返回值的数量 (压入栈中的值的数量) static int add(lua_State *L) { // 1. 检查参数数量 if (lua_gettop(L) != 2) { return luaL_error(L, "需要两个参数"); // 返回错误信息 } // 2. 检查参数类型并获取参数值 if (!lua_isnumber(L, 1) || !lua_isnumber(L, 2)) { return luaL_error(L, "参数必须是数字"); // 返回错误信息 } double a = lua_tonumber(L, 1); // 获取第一个参数 (索引为 1) double b = lua_tonumber(L, 2); // 获取第二个参数 (索引为 2) // 3. 执行计算 double sum = a + b; // 4. 将结果压入栈 lua_pushnumber(L, sum); // 5. 返回返回值数量 (这里返回一个值) return 1; } // 模块注册函数 (必须是 luaopen_模块名) // 这里模块名为 mylib,所以函数名为 luaopen_mylib int luaopen_mylib(lua_State *L) { // 1. 创建一个新的 metatable (可选,这里不使用) // 2. 注册 C 函数到模块中 lua_pushcfunction(L, add); // 将 C 函数 add 压入栈 lua_setglobal(L, "add"); // 将栈顶的值 (C 函数 add) 注册为全局变量 "add" // 另一种更规范的方式是创建模块表并注册函数 (推荐) // luaL_newlibtable(L, mylib_funcs); // 创建一个 table 用于存放函数 // luaL_setfuncs(L, mylib_funcs, 0); // 注册函数列表到 table // return 1; // 返回模块 table return 0; // 这里我们直接注册为全局函数,所以返回 0 (不返回任何值到栈顶) }
代码详解:
#include <lua.h>, #include <lauxlib.h>, #include <lualib.h>: 引入 Lua C API 相关的头文件。
lua.h: 核心 Lua API 头文件。
lauxlib.h: 辅助库 (auxiliary library) 头文件,提供了一些便捷的辅助函数,例如 luaL_error, luaL_checknumber 等。
lualib.h: 标准库头文件,如果你的 C 扩展需要使用 Lua 的标准库,需要包含此头文件。
static int add(lua_State *L): 这是 C 函数的定义,它将作为 Lua 的扩展函数被调用。
static: 表示该函数只在本文件内可见。
int: 返回值类型为 int,表示该函数返回给 Lua 的值的数量 (压入栈中的值的数量)。
lua_State *L: lua_State 结构体指针,代表当前的 Lua 虚拟机状态。所有的 Lua C API 函数都需要这个指针作为参数,用于操作当前的 Lua 状态机。
lua_gettop(L): 获取栈顶索引。栈索引从 1 开始,栈顶索引也代表栈中元素的数量。这里 lua_gettop(L) != 2 检查是否传递了两个参数。
luaL_error(L, "错误信息"): Lua 辅助库提供的错误处理函数。它会创建一个错误对象并将其压入栈顶,然后返回 LUA_ERRRUN 错误代码。Lua 虚拟机接收到错误代码后会进行相应的错误处理 (例如,抛出错误)。
lua_isnumber(L, index): 检查栈中指定索引位置的值是否为数字类型。索引 1 代表栈底第一个压入的值 (也就是 Lua 调用 C 函数时的第一个参数),索引 2 代表第二个参数,以此类推。
lua_tonumber(L, index): 将栈中指定索引位置的值转换为 double 类型。如果该位置的值不是数字类型,则返回 0。 (通常需要先使用 lua_isnumber 检查类型)。
lua_pushnumber(L, num): 将一个 double 类型的数字 num 压入栈顶。
return 1;: 表示 C 函数向 Lua 返回了一个值 (即压入栈顶的 sum)。
int luaopen_mylib(lua_State *L): 这是模块的入口函数,当 Lua 脚本使用 require("mylib") 加载该 C 扩展时,Lua 虚拟机将调用 luaopen_mylib 函数。
luaopen_模块名: 函数名必须以 luaopen_ 开头,后面跟模块名。Lua 会根据 require 的参数来查找对应的 luaopen_ 函数。
lua_pushcfunction(L, add): 将 C 函数 add 压入栈顶。
lua_setglobal(L, "add"): 将栈顶的值 (C 函数 add) 注册为全局变量,变量名为 "add"。这样在 Lua 脚本中就可以直接调用 add() 函数了。
return 0;: 这里我们选择将函数注册为全局变量,不需要返回模块表,所以返回 0。 (如果创建模块,通常会创建一个 table 并将函数注册到 table 中,然后返回 1,将模块 table 压入栈顶)。
2. 编译 C 代码生成动态链接库 (.so 或 .dll):
编译 C 代码需要将其编译成动态链接库 (Shared Library) 文件,Lua 才能加载并使用它。编译命令会根据你的操作系统和 Lua 安装方式有所不同。
Linux/macOS (假设你已经安装了 Lua 开发库):
gcc -shared -fPIC mylib.c -o mylib.so -llua
gcc: C 编译器。
-shared: 生成共享库 (动态链接库)。
-fPIC: 生成位置无关代码 (Position Independent Code),对于共享库是必需的。
mylib.c: C 代码源文件。
-o mylib.so: 输出文件名为 mylib.so (通常 Linux 和 macOS 使用 .so 后缀)。
-llua: 链接 Lua 库。确保你的系统中 Lua 开发库的路径已添加到链接器搜索路径中。
Windows (使用 MinGW 或 MSVC,假设 Lua 开发库已安装):
MinGW:
gcc -shared mylib.c -o mylib.dll -llua
MSVC (Visual Studio 开发人员命令提示符):
cl /LD mylib.c /out:mylib.dll lua54.lib (假设 Lua 库文件名为 lua54.lib,根据你的 Lua 版本可能不同)
/LD: 编译为动态链接库 (DLL)。
/out:mylib.dll: 输出文件名为 mylib.dll (Windows 使用 .dll 后缀)。
lua54.lib (或类似的 Lua 库文件): 链接 Lua 库。你需要根据你的 Lua 版本和安装方式找到对应的 Lua 库文件。
注意: 编译命令中的 -llua 或 lua54.lib 以及 Lua 库的路径可能需要根据你的 Lua 安装环境进行调整。如果编译出错,请检查 Lua 开发库是否正确安装,以及编译命令是否正确。
3. 在 Lua 脚本中使用 C 扩展:
将编译生成的动态链接库文件 (mylib.so 或 mylib.dll) 放在 Lua 脚本可以找到的路径下 (例如,与 Lua 脚本同一目录下,或者在 Lua 的 package.cpath 搜索路径中)。
创建 Lua 脚本文件 (例如 test.lua):
-- 加载 C 扩展模块 "mylib" local mylib = require("mylib") -- 调用 C 函数 add (由于我们在 C 代码中注册为全局函数,可以直接调用) local sum = add(5, 3) print("5 + 3 = " .. sum) -- 输出: 5 + 3 = 8
运行 Lua 脚本:
lua test.lua
如果一切顺利,你将看到 Lua 脚本成功调用了 C 扩展函数 add,并输出了计算结果。
将 C 函数注册为全局变量虽然简单,但当扩展功能增多时,容易造成全局命名空间污染,且不利于模块化管理。更规范的方式是将 C 函数组织成 Lua 模块。
修改 C 代码 (mylib.c):
#include <lua.h> #include <lauxlib.h> #include <lualib.h> // C 函数:add static int add(lua_State *L) { // ... (add 函数代码与之前相同) ... return 1; } // C 函数:subtract static int subtract(lua_State *L) { if (lua_gettop(L) != 2) { return luaL_error(L, "需要两个参数"); } if (!lua_isnumber(L, 1) || !lua_isnumber(L, 2)) { return luaL_error(L, "参数必须是数字"); } double a = lua_tonumber(L, 1); double b = lua_tonumber(L, 2); double diff = a - b; lua_pushnumber(L, diff); return 1; } // 模块函数列表 static const luaL_Reg mylib_funcs[] = { {"add", add}, // 函数名 "add" 对应 C 函数 add {"subtract", subtract}, // 函数名 "subtract" 对应 C 函数 subtract {NULL, NULL} // 结束标记,必须以 {NULL, NULL} 结尾 }; // 模块注册函数 (luaopen_mylib) int luaopen_mylib(lua_State *L) { // 使用 luaL_newlib 创建并注册模块 luaL_newlib(L, mylib_funcs); // 创建一个 table 并注册 mylib_funcs 中的函数 return 1; // 返回模块 table (压入栈顶) }
代码修改详解:
subtract 函数: 新增了一个 subtract 函数,实现减法功能。
luaL_Reg mylib_funcs[]: 定义一个 luaL_Reg 结构体数组,用于描述模块中的函数列表。
luaL_Reg 结构体包含两个字段: const char *name (函数在 Lua 中使用的名字) 和 lua_CFunction func (对应的 C 函数指针)。
数组的最后一个元素必须是 {NULL, NULL},作为结束标记。
luaL_newlib(L, mylib_funcs): Lua 辅助库提供的函数,用于创建并注册模块。
它会创建一个新的 table,并将 mylib_funcs 数组中定义的函数注册到这个 table 中,函数名作为 table 的 key,C 函数指针作为 value。
最后,luaL_newlib 会将这个模块 table 压入栈顶。
return 1;: luaopen_mylib 函数返回 1,表示将模块 table 作为返回值压入栈顶。
重新编译 C 代码 (编译命令与之前相同):
gcc -shared -fPIC mylib.c -o mylib.so -llua (Linux/macOS) gcc -shared mylib.c -o mylib.dll -llua (MinGW Windows) cl /LD mylib.c /out:mylib.dll lua54.lib (MSVC Windows)
修改 Lua 脚本 (test.lua):
-- 加载 C 扩展模块 "mylib" local mylib = require("mylib") -- 调用模块中的函数,使用模块名.函数名 的方式调用 local sum = mylib.add(10, 4) local diff = mylib.subtract(10, 4) print("10 + 4 = " .. sum) -- 输出: 10 + 4 = 14 print("10 - 4 = " .. diff) -- 输出: 10 - 4 = 6
运行 Lua 脚本 (命令与之前相同):
lua test.lua
现在,C 函数 add 和 subtract 被组织在 mylib 模块下,通过 mylib.add() 和 mylib.subtract() 的方式调用,更加清晰和模块化。
除了简单的数字类型,Lua 和 C 之间还可以传递其他数据类型,例如字符串、布尔值、表 (table)、userdata 等。
1. 获取参数:lua_to* 和 luaL_check* 系列函数
Lua C API 提供了一系列 lua_to* 函数用于从栈中获取不同类型的值,例如:
lua_tonumber(L, index): 获取数字 (转换为 double)。
lua_tostring(L, index): 获取字符串 (返回 const char*)。
lua_toboolean(L, index): 获取布尔值 (返回 int,1 为 true, 0 为 false)。
lua_totable(L, index): 获取 table (返回 int,成功返回 1,否则 0)。
lua_touserdata(L, index): 获取 userdata (返回 void*)。
同时,Lua 辅助库 lauxlib.h 提供了一系列 luaL_check* 函数,用于检查参数类型并获取值,如果类型不匹配则抛出错误,例如:
luaL_checknumber(L, arg): 检查参数 arg 是否为数字,如果是则返回数字值 (double),否则抛出错误。
luaL_checkstring(L, arg): 检查参数 arg 是否为字符串,如果是则返回字符串指针 (const char*),否则抛出错误。
luaL_checkinteger(L, arg): 检查参数 arg 是否为整数,如果是则返回整数值 (lua_Integer),否则抛出错误。
luaL_checktype(L, arg, type): 检查参数 arg 是否为指定类型 type (LUA_TNUMBER, LUA_TSTRING, LUA_TTABLE, 等等)。
示例:处理字符串参数和返回值
// C 函数:reverse_string // 接受一个字符串参数,返回反转后的字符串 static int reverse_string(lua_State *L) { // 1. 检查参数数量 if (lua_gettop(L) != 1) { return luaL_error(L, "需要一个字符串参数"); } // 2. 检查参数类型并获取字符串 const char *str = luaL_checkstring(L, 1); // 检查参数是否为字符串,并获取字符串指针 // 3. 反转字符串 (简单示例,实际应用中可能需要更健壮的实现) size_t len = strlen(str); char *reversed_str = (char*)malloc(len + 1); // 分配内存 if (reversed_str == NULL) { return luaL_error(L, "内存分配失败"); } for (size_t i = 0; i < len; ++i) { reversed_str[i] = str[len - 1 - i]; } reversed_str[len] = '\0'; // 添加字符串结束符 // 4. 将反转后的字符串压入栈 lua_pushstring(L, reversed_str); // 5. 释放内存 (重要!) free(reversed_str); // 6. 返回返回值数量 (返回一个字符串) return 1; } // ... (luaopen_mylib 函数中注册 reverse_string 函数) ... static const luaL_Reg mylib_funcs[] = { {"add", add}, {"subtract", subtract}, {"reverse_string", reverse_string}, // 添加 reverse_string 函数 {NULL, NULL} }; // ... (luaopen_mylib 函数保持不变) ...
Lua 脚本调用示例 (test.lua):
local mylib = require("mylib") local reversed_str = mylib.reverse_string("hello") print("反转后的字符串: " .. reversed_str) -- 输出: 反转后的字符串: olleh
2. 返回值:lua_push* 系列函数
Lua C API 提供了一系列 lua_push* 函数用于将不同类型的值压入栈顶作为返回值,例如:
lua_pushnumber(L, num): 压入数字 (double)。
lua_pushstring(L, str): 压入字符串 (const char*)。
lua_pushboolean(L, bool): 压入布尔值 (int,1 为 true, 0 为 false)。
lua_pushtable(L): 创建并压入一个新的空 table。
lua_pushnil(L): 压入 nil 值。
lua_pushcfunction(L, func): 压入 C 函数。
lua_pushlightuserdata(L, p): 压入 light userdata (轻量级用户数据)。
lua_pushuserdata(L, size): 创建并压入 userdata (用户数据)。
示例:返回 Lua 表 (table)
// C 函数:create_table // 创建并返回一个 Lua 表 static int create_table(lua_State *L) { // 1. 创建一个新的 Lua 表并压入栈顶 lua_newtable(L); // 2. 向 table 中添加键值对 lua_pushstring(L, "name"); // 键 "name" 压入栈 lua_pushstring(L, "Lua Extension"); // 值 "Lua Extension" 压入栈 lua_settable(L, -3); // 将 键值对 从栈中弹出,并设置到索引为 -3 的 table 中 (-3 表示栈顶往下数第三个,也就是新创建的 table) lua_pushstring(L, "version"); // 键 "version" 压入栈 lua_pushnumber(L, 1.0); // 值 1.0 压入栈 lua_settable(L, -3); // 设置键值对 // 3. 返回返回值数量 (返回一个 table) return 1; } // ... (luaopen_mylib 函数中注册 create_table 函数) ... static const luaL_Reg mylib_funcs[] = { {"add", add}, {"subtract", subtract}, {"reverse_string", reverse_string}, {"create_table", create_table}, // 添加 create_table 函数 {NULL, NULL} }; // ... (luaopen_mylib 函数保持不变) ...
Lua 脚本调用示例 (test.lua):
local mylib = require("mylib") local my_table = mylib.create_table() print("Table from C:") for k, v in pairs(my_table) do print(k .. ": " .. v) end -- 输出: -- Table from C: -- name: Lua Extension -- version: 1
操作 Lua 表的常用 API:
lua_newtable(L): 创建一个新的空 table 并压入栈顶。
lua_settable(L, index): 从栈中弹出 键 和 值,并将键值对设置到索引为 index 的 table 中。 栈顶的值是 值,栈顶的下一个值是 键。 table 索引可以是正数 (绝对索引) 或负数 (相对栈顶的索引,-1 表示栈顶,-2 表示栈顶的下一个,以此类推)。
lua_gettable(L, index): 从栈顶弹出一个 键,然后在索引为 index 的 table 中查找该键对应的值,并将值压入栈顶。如果 table 中没有该键,则压入 nil。
lua_rawget(L, index): 类似于 lua_gettable,但不使用 metatable 的 __index 元方法。
lua_rawset(L, index): 类似于 lua_settable,但不使用 metatable 的 __newindex 元方法。
lua_next(L, index): 用于迭代 table 中的键值对。使用前需要先将 table 压入栈顶,并压入一个 nil 作为初始键。每次调用 lua_next 会从 table 中取出一个键值对,将 键 和 值 压入栈顶,并返回 1 (如果还有键值对),如果 table 已经遍历完,则返回 0。
在 C 扩展中,错误处理至关重要。当 C 函数执行出错时,应该能够向 Lua 报告错误,并让 Lua 脚本能够捕获和处理这些错误。
1. 使用 luaL_error(L, "错误信息") 报告错误:
我们已经在之前的例子中使用了 luaL_error 函数。当 C 函数检测到错误时,调用 luaL_error(L, "错误信息") 会创建一个错误对象并压入栈顶,然后返回 LUA_ERRRUN 错误代码。Lua 虚拟机接收到错误代码后会进行错误处理,通常会终止当前 Lua 脚本的执行,并抛出错误信息。
2. Lua 脚本中的错误处理:pcall 和 xpcall
Lua 提供了 pcall (protected call) 和 xpcall (extended protected call) 函数用于在保护模式下调用函数,捕获运行时错误。
pcall(f, arg1, arg2, ...):
调用函数 f,并传递参数 arg1, arg2, ...。
如果 f 执行成功,pcall 返回 true 和 函数的返回值。
如果 f 执行出错,pcall 返回 false 和 错误信息。
xpcall(f, errhandler, arg1, arg2, ...):
类似于 pcall,但可以指定一个错误处理函数 errhandler。
当 f 执行出错时,Lua 虚拟机不会立即抛出错误,而是调用 errhandler 函数,并将错误信息作为参数传递给 errhandler。errhandler 函数的返回值将作为 xpcall 的错误信息返回。