14.2 扩展 Lua


文档摘要

14.2 扩展 Lua 扩展 Lua:Lua C API 实践详解 引言 Lua 以其轻量、快速和易于嵌入的特性而闻名。尽管 Lua 本身功能强大,但在某些场景下,我们可能需要借助 C 语言来扩展 Lua 的能力。这通常出于以下几个原因: 性能优化: 对于计算密集型任务,C 语言的执行效率通常远高于 Lua。将性能瓶颈部分用 C 实现可以显著提升程序性能。 访问系统资源和 C 库: Lua 脚本本身难以直接访问操作系统底层 API 或已有的 C 语言库。通过 C 扩展,Lua 可以轻松调用这些资源,实现更丰富的功能。 代码复用: 如果已经存在大量的 C 代码库,将其封装成 Lua 扩展可以方便地在 Lua 环境中复用这些代码,避免重复开发。

14.2 扩展 Lua

扩展 Lua:Lua C API 实践详解

引言

Lua 以其轻量、快速和易于嵌入的特性而闻名。尽管 Lua 本身功能强大,但在某些场景下,我们可能需要借助 C 语言来扩展 Lua 的能力。这通常出于以下几个原因:

  • 性能优化: 对于计算密集型任务,C 语言的执行效率通常远高于 Lua。将性能瓶颈部分用 C 实现可以显著提升程序性能。

  • 访问系统资源和 C 库: Lua 脚本本身难以直接访问操作系统底层 API 或已有的 C 语言库。通过 C 扩展,Lua 可以轻松调用这些资源,实现更丰富的功能。

  • 代码复用: 如果已经存在大量的 C 代码库,将其封装成 Lua 扩展可以方便地在 Lua 环境中复用这些代码,避免重复开发。

Lua C API 交互模型:栈 (Stack)

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 扩展函数

让我们从一个最简单的例子开始,创建一个 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 库文件。

注意: 编译命令中的 -llualua54.lib 以及 Lua 库的路径可能需要根据你的 Lua 安装环境进行调整。如果编译出错,请检查 Lua 开发库是否正确安装,以及编译命令是否正确。

3. 在 Lua 脚本中使用 C 扩展:

将编译生成的动态链接库文件 (mylib.somylib.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,并输出了计算结果。

创建 Lua 模块:更规范的扩展方式

将 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 函数 addsubtract 被组织在 mylib 模块下,通过 mylib.add()mylib.subtract() 的方式调用,更加清晰和模块化。

C 函数参数和返回值处理详解

除了简单的数字类型,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 脚本中的错误处理:pcallxpcall

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 函数,并将错误信息作为参数传递给 errhandlererrhandler 函数的返回值将作为 xpcall 的错误信息返回。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U