Lua 调用 C 模块中的函数

零、前言

在之前 "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吧,码字不易,请多多支持)

如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀

公众号搜索 "江澎涌",更多优质文章会第一时间分享与你。

相关推荐
ZZZ_O^O18 分钟前
二分查找算法——寻找旋转排序数组中的最小值&点名
数据结构·c++·学习·算法·二叉树
代码雕刻家1 小时前
数据结构-3.9.栈在递归中的应用
c语言·数据结构·算法
小飞猪Jay2 小时前
C++面试速通宝典——13
jvm·c++·面试
Kalika0-03 小时前
猴子吃桃-C语言
c语言·开发语言·数据结构·算法
代码雕刻家3 小时前
课设实验-数据结构-单链表-文教文化用品品牌
c语言·开发语言·数据结构
龙图:会赢的3 小时前
[C语言]--编译和链接
c语言·开发语言
rjszcb3 小时前
一文说完c++全部基础知识,IO流(二)
c++
小字节,大梦想4 小时前
【C++】二叉搜索树
数据结构·c++
吾名招财4 小时前
yolov5-7.0模型DNN加载函数及参数详解(重要)
c++·人工智能·yolo·dnn
Cons.W5 小时前
Codeforces Round 975 (Div. 1) C. Tree Pruning
c语言·开发语言·剪枝