Press "Enter" to skip to content

OpenResty 调用外部 .so 库(动态链接库)

第一步:准备 C 代码并编译为 .so

假设你有一个简单的 C 项目,功能是计算两个数的和以及处理字符串。

1. 编写 C 代码 (my_lib.c):

// my_lib.c
#include <stdio.h>
#include <string.h>

// 计算加法
int add(int a, int b) {
    return a + b;
}

// 字符串处理:将输入字符串转为大写(简单演示)
// 注意:实际开发中要注意内存安全
void upper_case(const char *input, char *output) {
    int i = 0;
    while (input[i]) {
        if (input[i] >= 'a' && input[i] <= 'z') {
            output[i] = input[i] - 32;
        } else {
            output[i] = input[i];
        }
        i++;
    }
    output[i] = '\0';
}

2. 编译为动态链接库:
使用 gcc 编译。注意 -fPIC 和 -shared 参数。

gcc -O2 -fPIC -shared my_lib.c -o libmylib.so

第二步:在 OpenResty 中使用 FFI 调用

在你的 Lua 代码中,按照以下结构编写逻辑。

1. 编写 Lua 模块 (my_module.lua):

local ffi = require("ffi")

-- 1. 声明 C 函数的原型 (必须与 C 代码中的声明完全一致)
ffi.cdef[[
    int add(int a, int b);
    void upper_case(const char *input, char *output);
]]

-- 2. 加载 .so 文件
-- 建议使用绝对路径,或者确保 .so 在系统的库搜索路径中(如 /usr/local/lib)
local mylib = ffi.load("/path/to/your/project/libmylib.so")

local _M = {}

function _M.test_add(a, b)
    -- 直接调用
    return mylib.add(a, b)
end

function _M.test_upper(str)
    local len = #str
    -- 3. 分配 C 内存空间用于接收输出(FFI 会自动管理这块内存的 GC)
    local buf = ffi.new("char[?]", len + 1)
    
    -- 调用 C 函数
    mylib.upper_case(str, buf)
    
    -- 4. 将 C 字符串转回 Lua 字符串
    return ffi.string(buf)
end

return _M

第三步:在 Nginx 配置中调用

1. Nginx 配置 (nginx.conf):

http {
    # 如果你的 .so 不在标准路径,可以设置 LD_LIBRARY_PATH 环境变量
    # 或者在 ffi.load 中写死绝对路径

    server {
        listen 80;
        location /test {
            content_by_lua_block {
                local my_mod = require("my_module")
                local sum = my_mod.test_add(10, 20)
                local upper = my_mod.test_upper("hello openresty")
                
                ngx.say("Sum: ", sum)
                ngx.say("Upper: ", upper)
            }
        }
    }
}

关键细节与进阶指南

1. 关于路径问题

  • 绝对路径: ffi.load(“/var/www/libs/libmylib.so”) 最简单,不容易出错。
  • 相对路径: ffi.load(“mylib”) 会在系统的标准路径(如 /usr/lib, /usr/local/lib)下搜索。
  • 环境变量: 如果你想通过 ffi.load(“mylib”) 加载,启动 Nginx 前需执行 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/your/path。

2. 如果是 C++ 项目

如果你的 .so 是由 C++ 编写的,必须在 C++ 函数定义时加上 extern “C”,否则 LuaJIT 会因为“符号修饰(Name Mangling)”找不到函数名。

// C++ 代码中
extern "C" {
    int add(int a, int b) { return a + b; }
}

3. 性能优化:避免重复定义

不要在请求处理函数(如 content_by_lua_block)中写 ffi.cdef 或 ffi.load。

  • 错误做法: 每次请求都 load 一次,性能极差且可能导致内存泄漏。
  • 正确做法: 将它们放在 Lua 模块的顶层(如上文示例),这样它们只会在 Nginx Worker 启动、模块第一次被 require 时执行一次。

4. 处理复杂类型(结构体)

如果 C 函数需要结构体:

ffi.cdef[[
    typedef struct {
        int id;
        double score;
    } user_t;
    void process_user(user_t *u);
]]

local user = ffi.new("user_t")
user.id = 1
user.score = 99.5
mylib.process_user(user)

5. 错误处理

可以使用 pcall 包装 ffi.load,防止由于 .so 文件缺失导致 Nginx 启动失败或请求报错:codeLua

local status, mylib = pcall(ffi.load, "mylib")
if not status then
    ngx.log(ngx.ERR, "failed to load libmylib: ", mylib)
    return
end

总结流程

  1. C/C++ 层:编写功能,编译为 .so(C++ 注意 extern “C”)。
  2. Lua 定义层:在模块顶层用 ffi.cdef 声明 API。
  3. Lua 加载层:用 ffi.load 加载库。
  4. 业务层:使用 ffi.new 管理内存,像调用普通函数一样调用 C 函数。
发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注