第一步:准备 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
总结流程
- C/C++ 层:编写功能,编译为 .so(C++ 注意 extern “C”)。
- Lua 定义层:在模块顶层用 ffi.cdef 声明 API。
- Lua 加载层:用 ffi.load 加载库。
- 业务层:使用 ffi.new 管理内存,像调用普通函数一样调用 C 函数。