零、前言
在之前 "Lua 模块与包" 的文章中,介绍了 Lua 通过 require
是如何引入一个模块的,其中 C 模块并未展开分享。今天就分享如何构建和使用一个 C 模块作为 Lua 的引入模块。
Lua 通过注册 C/C++ 函数,进行感知 C/C++ 函数。一旦 C/C++ 函数用 Lua 表示和存储,Lua 就会通过对其地址的直接引用来调用,Lua 对这些 C/C++ 函数的调用不再依赖于函数名、包的位置以及可见性规则。
一、为什么要使用 C 模块
当我们使用 C/C++ 函数来扩展 Lua 程序时,将其设计为一个 C 模块是一个很不错的考虑。
他可以方便后续代码的扩展,可能一开始只是需要几个函数,但后续需求上的扩展不会让代码混乱。而且逻辑是收拢在一起的,不会分散在多个地方,方便后续查阅和维护。
二、如何编写一个 C 模块
第一步,编写需要暴露给 Lua 的 C/C++ 函数。
这里举的例子是遍历一个给定路径下的文件,会返回一个 table ,table 中存放着该路径下的文件名。
具体细节就不再赘述,可以查阅上一篇 "Lua 调用 C++ 函数"
cpp
int l_dir(lua_State *L) {
// 检测入参是否为字符串
// 如果不是,则会抛出异常到 Lua 中,can't run config. file: .../6、Lua回调C++/调用C++函数/读取目录函数.lua:11: bad argument #1 to 'dir' (string expected, got table)
const char *path = luaL_checkstring(L, 1);
// 打开相应目录
DIR *dir = opendir(path);
if (dir == nullptr) {
// 抛异常至 Lua
luaL_error(L, strerror(errno));
}
// 创建表,用于装载目录下 "索引" 和 "文件名"
// 格式为 table[index] = "文件名"
lua_newtable(L);
int i = 1;
struct dirent *entry;
while ((entry = readdir(dir)) != nullptr) {
// 压入 key ,即索引
lua_pushinteger(L, i++);
// 压入 value ,即文件名
lua_pushstring(L, entry->d_name);
// 将 key 和 value 插入 table
lua_settable(L, -3);
}
closedir(dir);
// 只有一个返回值,即 table
return 1;
}
第二步,需要声明一个数组,将模块中需要暴露的所有"函数名称"(后续给 Lua 调用)和"函数指针"以 luaL_Reg
的结构罗列在数组中。
luaL_Reg
的格式如下,可以知道需要传递 "一个函数名称的字符串" 和 "一个参数为 lua_State
指针的函数指针"。
cpp
typedef struct luaL_Reg {
const char *name;
lua_CFunction func;
} luaL_Reg;
typedef int (*lua_CFunction) (lua_State *L);
通过这种结构,将第一步编写的 l_dir
函数组织好放置到数组中,值得注意的是,数组的最后一个元素必须是一个 {NULL, NULL},用于表示数组的结束。
cpp
static const struct luaL_Reg mylib[] = {
{"dir", l_dir},
{nullptr, nullptr}
};
第三步,编写模块的主函数,用于 Lua 搜索到对应的模块,然后进行加载模块。
主函数必须遵循 luaopen_模块名
的格式,否则 Lua 在查找时,无法查找到对应的库进行加载。
为什么一定要遵循
luaopen_模块名
可以查看之前的 "Lua 调用 C++ 函数" 文章。
如果是用 C++ 编写的函数,必须要加上 extern "C" ,否则后续调用时会有问题。
cpp
extern "C" int luaopen_mylib(lua_State *L) {
luaL_newlib(L, mylib);
return 1;
}
主函数中,使用 luaL_newlib
会创建一个表,并使用第二步的数组进行填充这个表,最后返回 1 ,表示将这个表返回给 Lua 。
第四步,将模块进行编译为库。
需要编写一个 CMakeLists.txt
,内容如下:
cmake
cmake_minimum_required(VERSION 3.21)
project(MyLib)
set(CMAKE_CXX_STANDARD 17)
include_directories(lua_lib/lua-5.4.4)
add_subdirectory(lua_lib)
add_executable(mylib src/mylib.cpp)
target_link_libraries(mylib LuaLibForLib)
这一过程同样需要链接到 Lua 代码,还有需要链接我们的库代码。
最后通过以下命令,在对应的文件夹下,执行 terminal 命令。将其进行编译,得到我们可调用的库文件。
go
cmake .
make
会得到一个 mylib
的库文件,这就是我们需要的 C 模块,下一节中会进行使用。
Lua 的集成,可以查看之前的 "C++ 集成 Lua" 文章
三、如何使用 C 模块
编写 Lua 文件,需要将第二节中产生的 mylib 库文件加入到 cpath
的搜索路径中,然后就可以使用 require
加载 mylib 库,Lua 会进行模块搜索和加载。
lua
local currentPath = debug.getinfo(1, "S").source:sub(2):match("(.*/)")
print("source:", currentPath)
-- 添加一个 cpath 搜索动态库
package.cpath = package.cpath .. ";" .. currentPath .. "lib/mylib"
local mylib = require "mylib"
print("------------------ mylib ------------------")
print("mylib", mylib)
print("------------------ mylib 的所有函数 ------------------")
for i, v in pairs(mylib) do
print(i, v)
end
print("------------------ 目录 ------------------")
local dirTable
local isSuccess, msg = pcall(function()
dirTable = mylib.dir(currentPath);
end)
if isSuccess then
for key, value in pairs(dirTable) do
print(key, value)
end
else
print("打开目录失败", currentPath, msg)
end
最后进行加载运行 Lua 文件即可
cpp
lua_State *L = luaL_newstate();
luaL_openlibs(L);
printf("\n");
printf("=============== Lua调用 ===============\n");
std::string fname = PROJECT_PATH +"/7、Lua调用C++的lib/Lua加载C模块.lua";
if (luaL_loadfile(L, fname.c_str()) || lua_pcall(L, 0, 0, 0)) {
printf("can't run config. file: %s", lua_tostring(L, -1));
}
lua_close(L);
运行完之后,输出如下
bash
=============== Lua调用 ===============
source: /Users/jiangpengyong/Desktop/code/Lua/Lua_CPP_2022/7、Lua调用C++的lib/
------------------ mylib ------------------
mylib table: 0x600002178940
------------------ mylib 的所有函数 ------------------
dir function: 0x105720e20
------------------ 目录 ------------------
1 .
2 ..
3 lib
4 main.cpp
5 Lua加载C模块.lua
四、额外小技巧
如果某些情况下,不能动态链接,可以将我们编写的库主函数加到 Lua luaL_openlibs
中。
cpp
// linit.c
static const luaL_Reg loadedlibs[] = {
{LUA_GNAME, luaopen_base},
{LUA_LOADLIBNAME, luaopen_package},
{LUA_COLIBNAME, luaopen_coroutine},
{LUA_TABLIBNAME, luaopen_table},
{LUA_IOLIBNAME, luaopen_io},
{LUA_OSLIBNAME, luaopen_os},
{LUA_STRLIBNAME, luaopen_string},
{LUA_MATHLIBNAME, luaopen_math},
{LUA_UTF8LIBNAME, luaopen_utf8},
{LUA_DBLIBNAME, luaopen_debug},
{NULL, NULL}
};
LUALIB_API void luaL_openlibs (lua_State *L) {
const luaL_Reg *lib;
/* "require" functions from 'loadedlibs' and set results to global table */
for (lib = loadedlibs; lib->func; lib++) {
luaL_requiref(L, lib->name, lib->func, 1);
lua_pop(L, 1); /* remove lib */
}
}
luaL_openlibs
在 linit.c 文件中,可以将主函数添加至 loadedlibs
中即可。
五、写在最后
Lua 项目地址:Github传送门 (如果对你有所帮助或喜欢的话,赏个star吧,码字不易,请多多支持)
如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀。
公众号搜索 "江澎涌",更多优质文章会第一时间分享与你。