LuaJIT通过ffi接口偶现无法执行C动态库中的C函数问题追踪

1. LuaJIT介绍

1.1 什么是LuaJIT

官方介绍:https://luajit.org/luajit.html
LuaJIT目前最新的版本是2.1,其支持的最新Lua语法格式是Lua5.1(最新的Lua版本是Lua5.4)。

1.2 LuaJIT可以用来干什么

LuaJIT包括两大块儿:luajit库、luajit程序

1.2.1 luajit程序

luajit程序是基于luajit库开发的一个LuaJIT的命令行程序。通过luajit程序可以执行lua脚本,也可以编写和执行lua程序。

1.2.2 luajit库

luajit库是luajit的核心,其实现了Lua5.1的所有基础语法,同时又实现了JIT功能。此外,通过LuaJIT的FFI库,可以实现从Lua脚本直接调用C动态库中的相关C函数。

2. 出问题项目中LuaJIT应用介绍

我们有个项目为了方便快捷的进行功能拓展开发,需要将Lua和C一起联合进行编码。
主体架构是:

  1. C开发了一个进程foo;
  2. 在进程foo中通过luajit库执行一些lua脚本(lua脚本是一些快速迭代的小功能);
  3. 为了提效,luajit执行的lua脚本中有部分功能是直接通过LuaJIT的FFI库加载C的动态库libfeature.so,并通过ffi.C方法直接调用动态库libfeature.so中的C函数(C函数名称:test_luajit_c_func);

补充说明:

  1. foo进程执行lua脚本是通过lua接口lua_pcall()函数调用lua脚本中的某个Lua函数(Lua函数名称:lua_function);
  2. foo进程维护了一个lua_State对象(本lua_State对象创建一次,长期使用,除非其关联的lua脚本内容变化);
  3. foo进程执行lua脚本的方式是通过lua_pcall()接口调用lua脚本中的一个指定名称的函数(lua_function)。

3. 问题现象描述

3.1 lua脚本

local ffi = require("ffi")
ffi.load("feature", true)
ffi.cdef([[
void test_luajit_c_func(const char *str);
]])
lua_function = function()
    ffi.C.test_luajit_c_func("test luajit C function called")
end

3.2 问题现象

foo进程在运行过程中,通过lua_State对象执行上述的lua脚本时,偶尔会出现调用test_luajit_c_func()函数失败的情况。失败时LuaJIT库通过error信息提示找不到C函数test_luajit_c_func对应的符号*(undefined symbol: test_luajit_c_func)*。

4. 问题原因分析

4.1 问题分析和复现

通过问题现象分析,本问题是找不到C动态库的C函数。接下来通过分析LuaJIT的FFI源码,分析FFI库操作C库的逻辑。

4.1.1 FFI操作逻辑梳理

在分析了LuaJIT源码后发现FFI库调用C动态库中的C函数的逻辑如下:

  1. LuaJIT中通过专门的CLibrary结构体对象来和C动态库进行交互。包括:
    1.1 和C动态库的交互:dlopen()、dlsym()、dlclose()的调用,及其dlopen()返回handle的维护;
    1.2 缓存dlsym()查找到的C函数地址(CLibrary中有个GCtab对象,专门用于C函数地址缓存的),避免相同函数的重复查找。
  2. 调用ffi.load(“feature”, true)时,LuaJIT库会调用lj_clib_load()函数,再嵌套调用clib_loadlib()函数,通过clib_loadlib()函数调用了dlopen()函数加载C动态库;
  3. FFI加载的C动态库在卸载时会调用lj_clib_unload()函数,再嵌套调用clib_unloadlib()函数,通过clib_unloadlib()函数调用dlclose()函数卸载C动态库。lj_clib_unload()函数是在ffi对象被Lua虚拟机GC时调用的;
  4. 调用ffi.C.test_luajit_c_func()时,LuaJIT库会调用lj_clib_index()函数,在嵌套调用clib_getsym()函数,通过clib_getsym()函数调用dlsym()函数从C动态库中查找对应的C函数“test_luajit_c_func”。最后会还会在lj_clib_index()函数中缓存C函数地址在LuaJIT库中(缓存使用的是TValue对象);
  5. FFI库中还有一个默认的CLibrary对象,指向进程全局的动态库符号映射。该CLibrary对象的handle值是NULL。

4.1.2 LuaJIT库加入调试日志验证

针对LuaJIT的FFI库操作C动态库的相关逻辑分析,我们在FFI库的clib_loadlib()、clib_getsym()、lj_clib_default()、lj_clib_load()、lj_clib_unload()函数中加入调试日志,用于追踪LuaJIT操作C动态库feature的过程。增加的日志代码如下(以git diff形式呈现):

diff --git a/src/lj_clib.c b/src/lj_clib.c
index 513528c..94d1a4e 100644
--- a/src/lj_clib.c
+++ b/src/lj_clib.c
@@ -54,6 +54,10 @@ LJ_NORET LJ_NOINLINE static void clib_error_(lua_State *L)
 #define CLIB_SOEXT     "%s.so"
 #endif
 
+#define LJ_LOGF(fmt, args...) printf("<%s@%d> " fmt "\n", __func__, __LINE__, ##args)
+
 static const char *clib_extname(lua_State *L, const char *name)
 {
   if (!strchr(name, '/')
@@ -128,11 +132,14 @@ static void *clib_loadlib(lua_State *L, const char *name, int global)
     if (!err) err = "dlopen failed";
     lj_err_callermsg(L, err);
   }
+  LJ_LOGF("L:%p, libname:%s, global:%s, handle:%p",
+            L, name, global ? "true" : "false", h);
   return h;
 }
 
 static void clib_unloadlib(CLibrary *cl)
 {
+    LJ_LOGF("cl:%p, cl->handle:%p", cl, cl->handle);
   if (cl->handle && cl->handle != CLIB_DEFHANDLE)
     dlclose(cl->handle);
 }
@@ -140,6 +147,8 @@ static void clib_unloadlib(CLibrary *cl)
 static void *clib_getsym(CLibrary *cl, const char *name)
 {
   void *p = dlsym(cl->handle, name);
+    LJ_LOGF("cl:%p, cl->handle:%p, funcname:%s, func:%p",
+            cl, cl->handle, name, p);
   return p;
 }
 
@@ -390,6 +399,8 @@ TValue *lj_clib_index(lua_State *L, CLibrary *cl, GCstr *name)
       lj_gc_anybarriert(L, cl->cache);
     }
   }
+  LJ_LOGF("L:%p, cl:%p, cl->handle:%p, funcname:%s, func-tv:%p",
+            L, cl, cl->handle, strdata(name), tv);
   return tv;
 }
 
@@ -415,11 +426,14 @@ void lj_clib_load(lua_State *L, GCtab *mt, GCstr *name, int global)
   void *handle = clib_loadlib(L, strdata(name), global);
   CLibrary *cl = clib_new(L, mt);
   cl->handle = handle;
+  LJ_LOGF("L:%p, libname:%s, global:%s, cl:%p, cl->handle:%p",
+            L, strdata(name), global ? "true" : "false", cl, cl->handle);
 }
 
 /* Unload a C library. */
 void lj_clib_unload(CLibrary *cl)
 {
+    LJ_LOGF("cl:%p, cl->handle:%p", cl, cl->handle);
   clib_unloadlib(cl);
   cl->handle = NULL;
 }
@@ -429,6 +443,7 @@ void lj_clib_default(lua_State *L, GCtab *mt)
 {
   CLibrary *cl = clib_new(L, mt);
   cl->handle = CLIB_DEFHANDLE;
+    LJ_LOGF("L:%p, cl:%p, cl->handle:%p", L, cl, cl->handle);
 }
 
 #endif
4.1.2.1 正常时日志情况
<lj_clib_default@446> L:0x7f1299216380, cl:0x7f1299223710, cl->handle:(nil)
<clib_loadlib@135> L:0x7f1299216380, libname:feature, global:true, handle:0x7f1298eedb00
<lj_clib_load@429> L:0x7f1299216380, libname:feature, global:true, cl:0x7f1299224130, cl->handle:0x7f1298eedb00
<clib_getsym@150> cl:0x7f1299223710, cl->handle:(nil), funcname:test_luajit_c_func, func:0x7f129a0539b3
<lj_clib_index@402> L:0x7f1299216380, cl:0x7f1299223710, cl->handle:(nil), funcname:test_luajit_c_func, func-tv:0x7f1299224278
<lj_clib_index@402> L:0x7f1299216380, cl:0x7f1299223710, cl->handle:(nil), funcname:test_luajit_c_func, func-tv:0x7f1299224278
4.1.2.2 出问题时日志情况
<lj_clib_default@446> L:0x7ff033ca8380, cl:0x7ff033ca5eb8, cl->handle:(nil)
<clib_loadlib@135> L:0x7ff033ca8380, libname:feature, global:true, handle:0x7fefff5c9000
<lj_clib_load@429> L:0x7ff033ca8380, libname:feature, global:true, cl:0x7ff033ca6508, cl->handle:0x7fefff5c9000
<lj_clib_unload@436> cl:0x7ff033ca6508, cl->handle:0x7fefff5c9000
<clib_unloadlib@142> cl:0x7ff033ca6508, cl->handle:0x7fefff5c9000
<clib_getsym@150> cl:0x7ff033ca5eb8, cl->handle:(nil), funcname:test_luajit_c_func, func:(nil)
<clib_getsym@150> cl:0x7ff033ca5eb8, cl->handle:(nil), funcname:test_luajit_c_func, func:(nil)
<clib_getsym@150> cl:0x7ff033ca5eb8, cl->handle:(nil), funcname:test_luajit_c_func, func:(nil)

4.1.3 根据日志分析结果

通过对比分析正常运行时和出错时的LuaJIT日志,发现:

  1. C动态库load时的CLibrary对象和ffi.C.test_luajit_c_func()调用C函数时的CLibrary对象不同;
  2. ffi.C.test_luajit_c_func()调用C函数时的CLibrary对象恒定为LuaJIT的default-CLibrary对象(请细看CLibrary的内存地址)。因此,每次ffi.C.test_luajit_c_func()调用其实都是从进程的全局空间中查找C函数的;
  3. 发生问题时,从日志可以清晰看出在ffi.C.test_luajit_c_func()函数调用前,通过ffi.load(“feature”, true)加载的C动态库已经被成功卸载了(日志中有lj_clib_unload()和clib_unloadlib()两个函数的调用)。说明ffi.load(“feature”, true)加载的对象被GC了;

4.2 本问题偶现原因分析

进程foo加载lua脚本时,在脚本ffi.load(“feature”, true)处LuaJIT库加载了C动态库(C进程中加载lua脚本时有调用lua_pcall()函数(调用方式:lua_pcall(0,0,0,0)))。且通过分析可以发现C动态库加载后的CLibrary对象是放在LuaJIT的lua栈空间的。
但是ffi.load(“feature”, true)加载C动态库的CLibrary对象在lua_pcall()函数调用结束时,已经从Lua栈空间中弹出(处于无引用,待回收状态)。
结合日志进一步分析得出:正常运行时ffi.load(“feature”, true)加载的CLibrary对象为被释放(未被GC);异常时被释放(被GC)。
同时,ffi.load(“feature”, true)加载的C动态库是加载到了foo进程的全局空间中的。所以,即使ffi.C.test_luajit_c_func()每次调用时的CLibrary对象不是正确的ffi.load(“feature”, true)得到的(default-CLibrary对象),但由于ffi.load(“feature”, true)的CLibrary对象还未被释放,则可以通过进程全局空间找到C函数test_luajit_c_func()并执行。

5. 问题解决方案

5.1 快速解决方案

由于我们在本次项目中是选择通过FFI库将C动态库加载到进程全局空间中的。所以,我们可以通过让FFI库加载的CLibrary对象在lua接口函数lua_pcall()调用退出后仍然有被引用(ffi.load()执行的结果赋值给一个变量即可),则可以保证其不会被释放。具体修改如下:

local ffi = require("ffi")
local clib_feature = ffi.load("feature", true)
ffi.cdef([[
void test_luajit_c_func(const char *str);
]])
lua_function = function()
    ffi.C.test_luajit_c_func("test luajit C function called")
end

5.2 更好的解决方案

5.2.1 “快速解决方案”的弊端

前面的“快速解决方案”虽然解决了当前项目中遇到的问题,但是不够优雅和通用。主要原因如下:
4. 通过ffi.load(“feature”, false)加载C动态库,无法使用(因为其未加载到进程全局空间,所以用default-CLibrary对象无法找C动态库中的C函数);
5. 由于无法使用ffi.load(“feature”, false)模式加载C动态库,如果在项目中有多处通过LuaJIT潜入lua脚本,不同lua脚本中通过ffi.load()加载不同的C动态库,不同的C动态库中如果存在相同名称的接口函数。则使用ffi.load(“xxx”, true)方式加载所有的C动态库,然后通过ffi.C.xxx_func调用C函数就有可能会出问题。

5.2.2 优雅的解决方案

我个人认为更加优雅的解决方案需要具备如下特点:

  1. 通过ffi.load()加载的CLibrary对象不应该保存在lua的栈中;
  2. 可以应用ffi.load(“xxx”, false)方式将C动态库加载私有的空间中,从而实现不同lua_State对象加载的CLibrary对象在空间的隔离,最终保证其应用不会有交叉;
5.2.2.1 具体的修改方案

将所有ffi.load()加载的CLi brary对象保存在lua_State对象中。同时,在lua_State对象中维护一个全局的C函数地址的缓存,将所有ffi.C.xxx_c_func()的第一次调用得到C函数的地址缓存到这个全局的缓存中。
我clone了一份LuaJIT的最新代码,并按照上述方案思想进行了对应的修改。
本方案具体实现代码:https://github.com/xunmengzhe/LuaJIT

### 回答1: 可以使用luajit ffi库的相关函数来读取文件路径下的文件名,以下是一个简单的示例代码: ``` local ffi = require("ffi") -- 定义readdir系统调用函数 ffi.cdef[[ typedef struct dirent { long d_ino; off_t d_off; unsigned short d_reclen; unsigned char d_type; char d_name[256]; } dirent; typedef struct DIR { int fd; dirent ent; } DIR; DIR *opendir(const char *name); dirent *readdir(DIR *dirp); int closedir(DIR *dirp); ]] -- 读取文件路径下的文件名 function list_files(path) local dir = ffi.C.opendir(path) local files = {} if dir ~= nil then repeat local entry = ffi.C.readdir(dir) if entry ~= nil then local name = ffi.string(entry.d_name) if name ~= "." and name ~= ".." then table.insert(files, name) end end until entry == nil ffi.C.closedir(dir) end return files end -- 测试 local files = list_files(".") for i, file in ipairs(files) do print(i, file) end ``` 在这个示例,我们使用ffi.cdef()函数定义了readdir、opendir和closedir系统调用函数。接下来我们定义了一个list_files()函数,该函数使用opendir()函数打开指定路径下的目录,使用readdir()函数读取目录下的文件列表,并使用closedir()函数关闭目录。最后,我们遍历文件列表,将每个文件的文件名添加到一个数组,并返回该数组。 要使用这个函数,只需将要读取的文件路径传递给list_files()函数,它将返回一个包含该目录下所有文件名的数组。在本例,我们传递了"."作为路径,它将返回当前目录下的所有文件名。 ### 回答2: 在LuaJIT使用ffi库读取文件路径下的文件名相对简单,只需要用到ffi库的C语言接口Lua的文件操作函数就可以完成。以下是一个可以实现该功能的示例代码: ```lua local ffi = require("ffi") -- 定义C函数readdir实现读取文件路径下的文件名 ffi.cdef[[ typedef struct dirent { char d_name[256]; } dirent; typedef struct DIR DIR; DIR* opendir(const char* path); dirent* readdir(DIR* dirp); int closedir(DIR* dirp); ]] -- 定义读取文件路径下文件名的函数 function getFilenames(path) local dir = ffi.C.opendir(path) -- 打开目录 if dir == nil then return nil end local filenames = {} -- 存储文件名的数组 local entry = ffi.C.readdir(dir) -- 读取第一个文件 while entry ~= nil do local filename = ffi.string(entry.d_name) -- 转换为Lua字符串 if filename ~= "." and filename ~= ".." then table.insert(filenames, filename) -- 加入数组 end entry = ffi.C.readdir(dir) -- 读取下一个文件 end ffi.C.closedir(dir) -- 关闭目录 return filenames end -- 示例使用: local path = "./folder" -- 文件路径 local filenames = getFilenames(path) -- 获取文件名数组 if filenames == nil then print("无法打开目录") else for i, filename in ipairs(filenames) do print(filename) end end ``` 以上示例代码使用LuaJITffi库调用了C语言函数opendir、readdir和closedir来实现读取文件路径下的文件名。首先,通过调用opendir打开指定路径的目录;然后使用readdir循环读取目录的所有文件,并将文件名加入到一个Lua数组;最后,调用closedir关闭目录并返回获取到的文件名数组。示例也处理了当前目录(".")和上层目录(".. ")两个特殊文件名的情况。 你可以将示例代码保存在一个Lua文件,并根据自己的实际需求修改文件路径,然后运行该Lua文件,即可看到打印出的文件名列表。 ### 回答3: 使用LuaJITffi库可以很方便地调用C函数,进而实现读取文件路径下的文件名的功能。下面是一个示例代码: ```lua -- 引入ffi库 local ffi = require("ffi") -- 定义C函数 ffi.cdef[[ typedef struct { void *d; } DIR; DIR *opendir(const char *filename); void closedir(DIR *dirp); const char *readdir(DIR *dirp); ]] -- 遍历文件路径下的文件名 function getFilenames(filepath) local dir = ffi.C.opendir(filepath) -- 打开目录 if dir ~= nil then local filenames = {} local dirent = ffi.C.readdir(dir) while dirent ~= nil do local name = ffi.string(dirent) -- 排除当前目录和上级目录 if name ~= '.' and name ~= '..' then table.insert(filenames, name) end dirent = ffi.C.readdir(dir) end ffi.C.closedir(dir) -- 关闭目录 return filenames end return nil end -- 测试 local path = "/path/to/directory" local filenames = getFilenames(path) if filenames ~= nil then for i, filename in ipairs(filenames) do print(filename) end else print("Failed to open directory") end ``` 上述代码首先使用ffi.cdef定义了C的DIR结构和相关函数,其opendir用于打开目录,readdir用于读取目录的文件名,closedir用于关闭目录。 然后,在getFilenames函数,我们首先使用ffi.C.opendir打开指定的文件路径,然后循环使用ffi.C.readdir读取目录的文件名,将有效的文件名加入到filenames表,最后使用ffi.C.closedir关闭目录。 最后,在测试的部分,我们指定一个文件路径并调用getFilenames函数获取文件名列表,然后遍历打印每个文件名。如果无法打开目录,会打印"Failed to open directory"。 以上便是使用LuaJITffi库实现读取文件路径下的文件名的代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值