Lua与C/C++交互实战:从动态库编译到性能优化全解析

发布时间:2026/7/23 7:02:35
Lua与C/C++交互实战:从动态库编译到性能优化全解析 1. 项目概述为什么需要打通Lua与C/C的桥梁如果你用过OpenResty或者玩过一些游戏的Mod那你大概率已经接触过Lua了。Lua这门小巧的脚本语言凭借其简洁的语法和极低的嵌入成本在配置、插件、游戏逻辑等领域遍地开花。但脚本语言有个天然的短板性能。当遇到密集计算、底层硬件操作或者复用现有庞大的C/C代码库时纯Lua就显得力不从心了。这时候动态链接库在Windows上是.dll在Linux/macOS上是.so就成了关键的“性能加速器”和“功能扩展包”。这个项目要解决的就是如何让Lua脚本这只“灵巧的手”去调用C/C这只“强壮的手”写成的功能。这不仅仅是简单地把两个东西连起来更涉及到数据类型在两种语言间的转换、内存管理的权责划分、以及如何设计出既高效又安全的接口。网上搜“Lua C API”例子不少但很多要么过于简略只给个“Hello World”要么一上来就是复杂的对象生命周期管理让初学者看得云里雾里。更别提实际开发中那些坑了比如我常看到有人问“传参类型不对怎么办”、“内存泄漏如何排查”、“在OpenResty里怎么加载.so”等等。所以我打算用一个完整的、贴近实际需求的例子把从编写C函数、编译成动态库、到Lua加载调用的全流程掰开揉碎讲清楚。我们不止步于让代码跑起来更要弄明白每一步背后的“为什么”以及我踩过哪些坑、总结出哪些好用的技巧。无论你是想给Nginx写高性能Lua扩展还是为你的C程序提供一个灵活的脚本配置界面这篇文章都能给你一套可直接复用的“脚手架”。2. 核心交互原理与Lua C API精要在动手写代码之前我们必须先理解Lua和C/C是如何“对话”的。Lua本身是一个独立的解释器但它设计之初就预留了与宿主程序通常是C/C程序通信的接口这就是Lua C API。这套API的核心是一个虚拟的“栈”Stack所有的数据交换都通过这个栈来完成。2.1 理解Lua栈数据交换的枢纽你可以把Lua栈想象成一个临时托盘。当Lua需要调用C函数时它会把这个C函数需要的参数按顺序从左到右压入这个栈。然后C函数被唤醒它的工作就是从栈顶你可以想象为托盘最上面把这些参数一个个取出来处理完业务逻辑后再把需要返回给Lua的结果压回栈里。最后C函数告诉Lua“我返回了几个值”Lua再从栈里把这些结果取走。这个设计非常巧妙。因为Lua和C/C是两套完全不同的语言体系Lua有table、function这种动态类型而C/C是静态类型。栈作为一个中间层通过一系列API函数如luaL_checknumber,lua_pushstring负责进行类型转换和搬运从而屏蔽了底层的差异。这里有个关键点栈索引。Lua提供了两种索引方式正数索引和负数索引。正数索引从栈底1开始负数索引从栈顶-1开始。在编写C函数时使用负数索引来获取参数通常更直观因为你知道-1就是最后一个参数-2是倒数第二个以此类推。2.2 编写可供Lua调用的C函数所有能被Lua直接调用的C函数都必须遵循一个统一的函数签名typedef int (*lua_CFunction) (lua_State *L);也就是说它接收一个指向lua_State代表一个Lua线程状态的指针并返回一个整数。这个整数的含义是该函数向Lua栈中压入了多少个返回值。让我们来看一个最简单的例子实现一个两数相加的函数#include lua.h #include lauxlib.h #include lualib.h static int l_add(lua_State *L) { // 1. 从栈中获取两个参数 // luaL_checknumber 会检查指定索引处的值是否为数字不是则抛出错误 double a luaL_checknumber(L, 1); // 第一个参数索引1 double b luaL_checknumber(L, 2); // 第二个参数索引2 // 2. 执行计算 double sum a b; // 3. 将结果压入栈中 lua_pushnumber(L, sum); // 4. 返回结果的数量 return 1; }这个函数清晰地展示了“取参-计算-压入结果-返回数量”的标准流程。luaL_checknumber这类函数是“安全”的它会进行类型检查。如果你确定类型一定正确可以使用更快的lua_tonumber但它不会进行检查。2.3 模块注册将C函数暴露给Lua单个C函数写好了怎么让Lua知道它呢这就需要“注册”。通常我们会把一组相关的C函数打包成一个模块。Lua C API提供了两种主要方式方式一使用luaL_Reg结构体数组这是最常用、最清晰的方式。我们定义一个数组列出所有要导出的函数名和对应的C函数指针。static const luaL_Reg mylib[] { {add, l_add}, {sub, l_sub}, // 假设我们还有另一个函数 {NULL, NULL} // 哨兵表示数组结束 };然后我们需要一个“模块入口函数”。当Lua加载这个动态库时会寻找并调用这个函数。// 这个函数名有讲究在Linux/macOS下必须是 luaopen_xxx其中xxx是模块名。 // 在Windows下通常是 __declspec(dllexport) int __cdecl luaopen_xxx(...) LUAMOD_API int luaopen_mymath(lua_State *L) { // 创建一个新的table并将mylib数组中的所有函数注册进去 luaL_newlib(L, mylib); // 现在栈顶就是这个包含所有函数的table它会被返回给Lua return 1; }luaL_newlib这个宏帮我们做了创建table并填充函数的工作非常方便。注意模块入口函数名的约定是跨平台兼容性的关键点之一。在类Unix系统Linux, macOS上动态加载器如dlopen会直接查找名为luaopen_模块名的函数。在Windows上情况稍微复杂但通常通过预处理器宏如LUAMOD_API在luaconf.h中定义来处理导出声明。为了确保可移植性最好始终使用luaopen_前缀并依赖Lua头文件提供的宏。3. 实战构建一个数学工具动态库理论讲得差不多了我们动手建一个实际的项目。这个项目将创建一个名为mymath的模块提供一些数学函数并演示如何传递复杂的数据结构。3.1 项目结构与环境准备首先确保你的系统安装了Lua开发库。在Ubuntu上可以sudo apt-get install liblua5.3-devmacOS用brew install luaWindows则需要下载Lua的二进制发行版或源码编译。我们创建如下目录结构lua_c_integration/ ├── src/ │ ├── mymath.c # C源码 │ └── mymath.h # 头文件可选 ├── build/ # 编译输出目录 └── test.lua # Lua测试脚本3.2 C源码实现 (mymath.c)我们将实现三个函数add加法、sum计算table内所有数字的和、stats计算一个数字数组的平均值和方差返回多个值。// mymath.c #include lua.h #include lauxlib.h #include lualib.h #include math.h // 用于sqrt static int l_add(lua_State *L) { lua_Number a luaL_checknumber(L, 1); lua_Number b luaL_checknumber(L, 2); lua_pushnumber(L, a b); return 1; } static int l_sum(lua_State *L) { // 检查第一个参数是否为table luaL_checktype(L, 1, LUA_TTABLE); lua_Number total 0.0; int len (int)lua_rawlen(L, 1); // 获取table的数组部分长度 for (int i 1; i len; i) { lua_rawgeti(L, 1, i); // 将 t[i] 压入栈顶 if (lua_isnumber(L, -1)) { total lua_tonumber(L, -1); } lua_pop(L, 1); // 弹出刚取出的值保持栈平衡 } lua_pushnumber(L, total); return 1; } static int l_stats(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); int len (int)lua_rawlen(L, 1); if (len 0) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; // 返回两个0 } lua_Number sum 0.0, sum_sq 0.0; for (int i 1; i len; i) { lua_rawgeti(L, 1, i); if (lua_isnumber(L, -1)) { lua_Number val lua_tonumber(L, -1); sum val; sum_sq val * val; } lua_pop(L, 1); } lua_Number mean sum / len; lua_Number variance (sum_sq / len) - (mean * mean); lua_pushnumber(L, mean); lua_pushnumber(L, sqrt(variance 0 ? variance : 0)); // 返回标准差 return 2; // 返回两个值平均值和标准差 } // 模块函数注册表 static const luaL_Reg mymath_lib[] { {add, l_add}, {sum, l_sum}, {stats, l_stats}, {NULL, NULL} }; // 模块入口函数 LUAMOD_API int luaopen_mymath(lua_State *L) { luaL_newlib(L, mymath_lib); return 1; }3.3 编译为动态链接库编译命令因平台而异。关键点是链接Lua库并生成位置无关代码-fPIC。在Linux/macOS上# 进入项目根目录 mkdir -p build gcc -stdc11 -fPIC -shared -o build/mymath.so src/mymath.c -I/usr/include/lua5.3 -llua5.3-I指定Lua头文件路径-llua5.3指定链接的Lua库名版本号可能不同如-llua、-llua-5.4。在Windows上使用MinGWgcc -stdc11 -shared -o build/mymath.dll src/mymath.c -IC:\Path\To\Lua\include -LC:\Path\To\Lua\lib -llua53注意输出文件是.dll。如果遇到“无法定位程序输入点于动态链接库”这类错误通常是因为编译时链接的Lua库版本如lua53.dll和运行时Lua解释器使用的版本不匹配。确保使用同一份Lua发行版。实操心得编译时的路径问题。这是新手最常见的坑。如果你在VSCode等IDE中配置任务tasks.json来编译务必检查-I和-L参数是否正确指向了你的Lua安装位置。一个可靠的方法是使用pkg-config如果Lua安装支持的话gcc $(pkg-config --cflags --libs lua5.3) -fPIC -shared -o mymath.so mymath.c。在Windows上可能需要手动将Lua的bin目录包含lua53.dll添加到系统的PATH环境变量中或者将lua53.dll复制到你的可执行文件lua.exe同级目录下。4. Lua脚本加载与调用动态库编译成功后我们会在build目录下得到mymath.so或mymath.dll。现在让我们用Lua脚本来调用它。4.1 基础加载与函数调用创建一个test.lua文件-- test.lua -- 使用package.loadlib或require加载动态库 -- require是更高级、更常用的方式它会搜索package.cpath中定义的路径 local mymath require(mymath) -- 注意require的参数是模块名mymath它会自动查找mymath.so、mymath.dll或mymath.dylib等文件。 -- 默认情况下它会从Lua的C模块路径package.cpath以及当前目录中查找。 print(Testing mymath module...) -- 测试 add 函数 local result mymath.add(10, 20.5) print(10 20.5 .. result) -- 测试 sum 函数 local tbl {1, 2, 3, 4, 5} local total mymath.sum(tbl) print(Sum of {1,2,3,4,5} .. total) -- 测试 stats 函数接收多个返回值 local data {2, 4, 4, 4, 5, 5, 7, 9} local mean, stddev mymath.stats(data) print(string.format(Data: {2,4,4,4,5,5,7,9})) print(string.format(Mean %.2f, Standard Deviation %.2f, mean, stddev))运行这个脚本lua test.lua如果一切顺利你将看到计算结果输出。require(mymath)这一行是关键。Lua的require机制会检查package.loaded.mymath是否已加载避免重复加载。在package.cpath指定的路径列表例如./?.so;/usr/local/lib/lua/5.3/?.so中查找名为mymath的动态库文件。找到后加载该库并调用其导出的luaopen_mymath函数。将该函数的返回值就是我们模块的table存储到package.loaded.mymath并返回。4.2 处理加载路径问题如果你遇到module mymath not found的错误说明Lua在它的搜索路径里找不到你的动态库。有几种解决方法将动态库放到Lua的C模块路径下打印出package.cpath看看路径是什么然后把.so或.dll文件复制过去。修改Lua脚本的搜索路径在require之前动态地添加当前目录。-- 在脚本开头添加 package.cpath package.cpath .. ;./?.so;./?.dll local mymath require(mymath)设置环境变量在运行Lua前设置LUA_CPATH环境变量。LUA_CPATH./?.so; lua test.lua注意事项OpenResty环境的特殊性。在OpenResty中你通常使用lua_package_cpath指令在Nginx配置中指定C模块的搜索路径。例如lua_package_cpath /usr/local/openresty/lualib/?.so;;;。此外OpenResty使用的LuaJIT与标准Lua 5.1在C API兼容性上高度一致但如果你使用了某些新版本Lua的特性可能需要调整。编译动态库时应链接OpenResty自带的LuaJIT头文件和库。5. 进阶在C/C中操作Lua复杂类型简单的数字和字符串传递不难但实际项目中我们经常需要在C和Lua之间传递更复杂的结构比如数组、哈希表table甚至函数。5.1 从Lua Table中读取复杂数据前面的l_sum和l_stats函数已经演示了如何遍历Lua table的数组部分。对于哈希表部分键值对我们需要使用lua_next函数。假设我们想实现一个l_update_config函数接收一个配置table并打印所有键值对static int l_update_config(lua_State *L) { luaL_checktype(L, 1, LUA_TTABLE); // 将第一个参数table压入栈顶作为lua_next遍历的起始位置 lua_pushnil(L); // 首次调用需要压入一个nil key while (lua_next(L, 1) ! 0) { // 此时栈顶是value-2位置是key // 打印key和value这里简单处理实际可能根据类型做不同操作 const char *key lua_tostring(L, -2); // key在-2位置 if (lua_isstring(L, -1)) { printf(Config[%s] %s\n, key, lua_tostring(L, -1)); } else if (lua_isnumber(L, -1)) { printf(Config[%s] %g\n, key, lua_tonumber(L, -1)); } else if (lua_isboolean(L, -1)) { printf(Config[%s] %s\n, key, lua_toboolean(L, -1) ? true : false); } // 弹出value保留key供下一次迭代 lua_pop(L, 1); } return 0; // 没有返回值 }lua_next是遍历table的核心函数使用时需要小心维护栈的状态。5.2 在C中创建并返回Lua Table反过来我们也需要在C函数中构造一个Lua table并返回。例如实现一个l_point函数返回一个表示二维坐标的table。static int l_point(lua_State *L) { lua_Number x luaL_checknumber(L, 1); lua_Number y luaL_checknumber(L, 2); // 创建一个新的table并预分配数组部分和哈希表部分的空间性能优化 lua_createtable(L, 0, 2); // 0个数组元素2个哈希表槽位 // 设置 table.x x lua_pushstring(L, x); lua_pushnumber(L, x); lua_settable(L, -3); // 操作栈顶的table // 设置 table.y y (更高效的写法lua_setfield) lua_pushnumber(L, y); lua_setfield(L, -2, y); // 等价于上面三行但更简洁 // 现在栈顶就是新创建的table return 1; // 返回这个table }在Lua中就可以这样调用local pt mymath.point(3, 4) print(pt.x, pt.y) -- 输出 3 45.3 错误处理与资源管理在C函数中如果遇到错误比如参数类型不对、内存分配失败我们不能简单地返回错误码而应该使用Lua的错误处理机制。使用luaL_error抛出错误static int l_safe_divide(lua_State *L) { lua_Number a luaL_checknumber(L, 1); lua_Number b luaL_checknumber(L, 2); if (b 0.0) { // 抛出一个错误会中断当前C函数的执行并将错误信息传递回Lua return luaL_error(L, division by zero); } lua_pushnumber(L, a / b); return 1; }资源管理如果在C函数中分配了内存如用malloc必须确保在函数返回前释放或者在Lua中注册__gc元方法进行垃圾回收。对于简单的扩展尽量避免在C侧进行复杂的内存管理优先使用Lua来管理数据生命周期。6. 常见问题排查与调试技巧实录即使按照步骤来在实际集成过程中也难免会遇到各种问题。下面是我总结的一些高频问题和排查思路。6.1 编译与链接阶段问题问题1undefined reference to luaL_checknumber等链接错误。原因编译器找到了头文件但链接器找不到Lua库的实现。解决检查链接参数-llua是否正确版本号是否匹配如-llua5.3vs-llua。检查库文件路径-L参数是否正确。在Windows上确保链接的是导入库.lib或.dll.a而运行时需要对应的.dll文件。问题2module xxx not found(Lua运行时)。原因require在package.cpath中找不到对应的动态库文件。解决打印print(package.cpath)检查路径。确认动态库文件名是否正确mymath.sovsmymath.dll。确认动态库是否真的编译成功可以用file命令或依赖查看器检查。使用绝对路径加载测试require(/full/path/to/mymath)。6.2 运行时崩溃与错误问题3Lua脚本调用C函数时崩溃Segmentation fault。原因这是最棘手的问题通常源于C函数内部的错误。排查思路栈索引越界检查luaL_checknumber(L, 3)但函数只传了2个参数。使用负数索引更安全。类型错误使用了lua_tostring去读取一个非字符串或数字的值。在不确定类型时先用lua_isxxx系列函数判断。栈不平衡C函数压入栈的数据量与其声明的返回值数量return的值不匹配。确保每次函数调用后栈都恢复到调用前的状态除了要返回的值。内存错误在C侧访问了已经失效的Lua对象比如从栈上弹出后还去使用它的指针。记住Lua对象如字符串指针的生命周期只在它位于栈上或被锚定如全局变量时才有效。问题4bad argument #1 to xxx (number expected, got nil)原因Lua调用C函数时传递的参数类型或数量不对而C函数中使用了luaL_checkxxx进行严格检查。解决检查Lua调用代码确保参数传递正确。如果希望参数可选可以在C函数中使用lua_isnoneornil判断并提供默认值。6.3 调试技巧使用gdb/lldb调试C扩展# 编译时加上 -g 选项生成调试信息 gcc -g -fPIC -shared -o mymath.so mymath.c ... # 用调试器运行lua解释器 gdb --args lua test.lua # 在gdb中可以在C函数入口处设置断点 (gdb) break l_add (gdb) run在C代码中打印调试信息简单粗暴但有效。使用printf或fprintf(stderr, ...)输出栈信息、参数值等。使用Lua的debug.traceback在C函数中发生错误时可以获取Lua调用栈。#include stdio.h // 在可能出错的地方之后 if (error_condition) { lua_pushstring(L, Something went wrong); luaL_traceback(L, L, NULL, 1); // 获取traceback并压栈 fprintf(stderr, Error: %s\n%s\n, lua_tostring(L, -2), lua_tostring(L, -1)); lua_pop(L, 2); // 清理栈 return luaL_error(L, custom error); }7. 性能优化与最佳实践当你的C扩展开始处理大量数据或高频调用时性能就变得至关重要。7.1 减少Lua与C之间的数据搬运Lua和C之间的每次数据传递压栈、弹栈、类型检查都有开销。一个重要的优化原则是尽量减少跨语言边界的调用次数和数据交换量。批处理与其让Lua循环调用一个C函数每次调用都有开销不如在C函数内部实现循环。就像我们的l_sum函数一次性接收整个table在C内部完成遍历和计算。使用轻量级用户数据Light Userdata对于只需要在Lua和C之间传递一个不透明指针如一个C结构体的地址的情况可以使用lua_pushlightuserdata。它不管理内存只是传递指针开销极小。但需要非常小心指针的生命周期管理。复用字符串频繁从C向Lua推送相同的字符串时可以考虑使用Lua的字符串驻留lua_pushlstring配合缓存或者直接传递整数ID。7.2 管理复杂对象使用完全用户数据Full Userdata对于需要在Lua中表示一个复杂C结构体如一个图像对象、一个网络连接的情况应该使用“完全用户数据”Full Userdata。Lua会为它分配一块内存并允许你为其绑定元表Metatable从而在Lua中实现面向对象式的操作如obj:method()。// 假设我们有一个简单的结构体 typedef struct { int id; double value; } MyObject; // 创建用户数据 static int l_new_object(lua_State *L) { // 分配内存lua_newuserdata会返回指向这块内存的指针 MyObject *obj (MyObject *)lua_newuserdata(L, sizeof(MyObject)); obj-id (int)luaL_checkinteger(L, 1); obj-value luaL_checknumber(L, 2); // 为这个用户数据设置元表以便支持面向对象语法和垃圾回收 luaL_getmetatable(L, MyObjectMT); lua_setmetatable(L, -2); return 1; } // 定义一个元方法用于获取属性 static int l_object_get_value(lua_State *L) { // 检查第一个参数是否是类型为MyObjectMT的用户数据 MyObject *obj (MyObject *)luaL_checkudata(L, 1, MyObjectMT); lua_pushnumber(L, obj-value); return 1; } // 垃圾回收元方法 static int l_object_gc(lua_State *L) { MyObject *obj (MyObject *)luaL_checkudata(L, 1, MyObjectMT); // 如果MyObject内部有需要手动释放的资源如malloc的内存、文件句柄在这里释放 printf(GC called for object id%d\n, obj-id); return 0; } // 注册元表 static const luaL_Reg myobject_methods[] { {getValue, l_object_get_value}, {NULL, NULL} }; static const luaL_Reg myobject_metamethods[] { {__gc, l_object_gc}, {NULL, NULL} }; // 在模块入口函数中注册这个元表 LUAMOD_API int luaopen_mymath(lua_State *L) { // ... 注册普通函数 ... // 创建并注册MyObject的元表 luaL_newmetatable(L, MyObjectMT); luaL_setfuncs(L, myobject_methods, 0); // 设置方法 luaL_setfuncs(L, myobject_metamethods, 0); // 设置元方法 lua_pop(L, 1); // 弹出元表 return 1; }在Lua中就可以这样使用local obj mymath.new_object(100, 3.14) print(obj:getValue()) -- 通过冒号语法调用obj作为第一个参数传入 -- 当obj不再被引用时其__gc元方法会被调用7.3 确保代码安全与健壮始终检查参数使用luaL_checkxxx或lua_isxxx进行防御性编程。保持栈平衡这是Lua C编程的铁律。确保函数在返回时栈的高度比进入时高出n返回值数量。复杂的逻辑分支下要仔细检查。注意字符串生命周期lua_tostring返回的指针在对应的Lua字符串被垃圾回收或从栈中弹出后可能失效。如果需要长期持有使用lua_pushstring复制一份或者用luaL_checklstring获取长度并自行拷贝。避免阻塞操作如果你的C函数会执行长时间的操作如网络IO、复杂计算要考虑是否会阻塞整个Lua状态在类似OpenResty的协程环境中尤其重要。可能需要将操作异步化或者使用Lua的调试钩子debug hook来让出执行权。8. 与现代开发工具链的集成最后聊聊如何把这件事做得更“工程化”让它融入现代的开发和调试流程。8.1 使用CMake管理跨平台编译手写gcc命令对于小项目还行项目复杂后用CMake这样的构建工具会更方便。创建一个CMakeLists.txtcmake_minimum_required(VERSION 3.10) project(mymath LANGUAGES C) # 查找Lua头文件和库 find_package(Lua REQUIRED) # 打印找到的路径便于调试 message(STATUS Lua include dir: ${LUA_INCLUDE_DIR}) message(STATUS Lua library: ${LUA_LIBRARIES}) # 添加共享库目标 add_library(mymath SHARED src/mymath.c) target_include_directories(mymath PRIVATE ${LUA_INCLUDE_DIR}) target_link_libraries(mymath ${LUA_LIBRARIES}) # 设置输出目录 set_target_properties(mymath PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/build) # 在Windows上需要定义正确的导出符号 if(WIN32) target_compile_definitions(mymath PRIVATE LUA_BUILD_AS_DLL) endif()然后使用cmake -B build和cmake --build build来编译它可以自动处理不同平台的编译差异。8.2 在VSCode中配置开发环境如果你用VSCode可以配置tasks.json和launch.json来实现一键编译和调试。.vscode/tasks.json(编译任务){ version: 2.0.0, tasks: [ { label: build mymath, type: shell, command: gcc, args: [ -stdc11, -g, -fPIC, -shared, -o, ${workspaceFolder}/build/mymath.so, ${workspaceFolder}/src/mymath.c, -I/usr/include/lua5.3, -llua5.3 ], group: { kind: build, isDefault: true }, problemMatcher: [$gcc] } ] }.vscode/launch.json(调试配置){ version: 0.2.0, configurations: [ { name: (gdb) Launch Lua with mymath, type: cppdbg, request: launch, program: /usr/bin/lua5.3, args: [${workspaceFolder}/test.lua], stopAtEntry: false, cwd: ${workspaceFolder}, environment: [], externalConsole: false, MIMode: gdb, setupCommands: [ { description: Enable pretty-printing for gdb, text: -enable-pretty-printing, ignoreFailures: true } ], preLaunchTask: build mymath // 启动前先执行编译任务 } ] }这样你就可以在VSCode里按F5直接编译C扩展并启动Lua脚本进行调试在C代码中设置的断点也会生效。8.3 单元测试为C扩展写Lua单元测试是个好习惯。可以使用Lua自带的assert或者更专业的测试框架如busted。-- test_mymath.lua local mymath require(mymath) assert(mymath.add(1, 2) 3) assert(mymath.add(1.5, 2.5) 4.0) local sum mymath.sum({1, 2, 3}) assert(sum 6, string.format(expected 6, got %f, sum)) -- 测试错误处理 local ok, err pcall(mymath.safe_divide, 10, 0) assert(not ok and string.find(err, division by zero))通过自动化测试可以确保你的C扩展在修改后依然行为正确。