从 Lua 调用 C 函数
在 Verilua 中,你可以从 Lua 侧直接调用 C 函数,无需经过 SystemVerilog 中转。根据函数的来源不同,有两种主要方式:
只能调用 C ABI 函数
LuaJIT FFI 和 dlsym 都按 C 符号名查找,无法处理 C++ 的 name mangling。如果源文件是 .cpp,需要用 extern "C" 声明导出函数。
- 函数已链接进仿真器(DPI-C 导出、VPI 扩展、仿真器内部函数)→ 使用
SymbolHelper - 函数在独立的
.so中(不参与仿真器构建)→ 使用ffi.load
前置知识
本文假设你了解 LuaJIT FFI 的基本概念(ffi.cdef、ffi.load、cdata)。如果不熟悉,先阅读 LuaJIT 与标准 Lua 的常见差异。
方式一:调用链接进仿真器的 C 函数
当 C/C++ 代码通过 add_files 或链接选项编入仿真器二进制时,函数符号存在于主可执行文件的 ELF 中。此时使用 SymbolHelper 获取并调用。
C 侧
// dpic.cpp
#include <cstdint>
extern "C" int32_t my_add(int32_t a, int32_t b) {
return a + b;
}
xmake.lua 中添加源文件
add_files("dpic.cpp")
-- Verilator 下如果函数未被 SV 代码引用,需要强制保留符号
if sim == "verilator" then
add_ldflags("-u my_add")
end
Lua 侧调用
local SymbolHelper = require "verilua.utils.SymbolHelper"
local my_add = SymbolHelper.try_ffi_cast("int32_t my_add(int32_t a, int32_t b);")
print(my_add(17, 25)) --> 42
调用 SV export 的函数
SV 中 export "DPI-C" 的函数同样链接在仿真器中,但调用前需要设置 DPI scope:
sim.set_dpi_scope("tb_top.u_top") -- 必须先设置 scope
local sv_func = SymbolHelper.try_ffi_cast("int sv_square(int x);")
print(sv_func(7)) --> 49
方式二:运行时加载独立 .so
当 C 代码编译为独立的共享库、不参与仿真器构建时,使用 ffi.load 直接加载并调用。
C 侧
// mylib.c
#include <stdint.h>
int32_t fast_hash(const char* data, int32_t len) {
int32_t h = 0;
for (int32_t i = 0; i < len; i++) h = h * 31 + data[i];
return h;
}
编译:
gcc -shared -fPIC -o libmylib.so mylib.c
Lua 侧调用
local ffi = require "ffi"
-- 1. 声明函数签名(必须,否则访问会报错)
ffi.cdef [[
int32_t fast_hash(const char* data, int32_t len);
]]
-- 2. 加载 .so,得到 library handle
local mylib = ffi.load("./libmylib.so")
-- 3. 通过 handle 调用
print(mylib.fast_hash("hello", 5))
ffi.cdef 是 ffi.load 的前提
ffi.load 返回的 handle 访问任何函数前,都必须先用 ffi.cdef 声明其签名。没有声明就访问会报错,而签名写错不会报错但会产生错误结果。注意:SymbolHelper 不需要 ffi.cdef,它通过参数内联提供类型信息。
RTLD_LOCAL vs RTLD_GLOBAL
ffi.load(path) 默认以 RTLD_LOCAL 加载,符号仅通过返回的 handle 可访问:
local lib = ffi.load("./libmylib.so") -- RTLD_LOCAL(默认)
lib.fast_hash("x", 1) -- ✅ 通过 handle 调用
ffi.C.fast_hash("x", 1) -- ❌ ffi.C 找不到
如果传第二个参数 true,则以 RTLD_GLOBAL 加载,符号对 ffi.C 和 SymbolHelper.try_ffi_cast 的 fallback 路径可见:
local lib = ffi.load("./libmylib.so", true) -- RTLD_GLOBAL
ffi.C.fast_hash("x", 1) -- ✅ 全局可见
通常直接用 handle 调用就够了,不需要 RTLD_GLOBAL。
如何选择
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 调用 DPI-C 导出的函数 | SymbolHelper | 符号已在仿真器 ELF 中 |
调用仿真器内部函数(svSetScope 等) | SymbolHelper | 同上 |
调用 SV export "DPI-C" 函数 | SymbolHelper + sim.set_dpi_scope | 需要设置 scope |
调用独立编译的 .so(不改构建流程) | ffi.load + handle | 最简单,无需修改 xmake.lua |
调用系统库函数(libm、libz 等) | ffi.load("z") 或 ffi.C | 系统库通常已全局可见 |
参考
- SymbolHelper API 参考 — 完整的函数签名和符号查找机制说明
- 编写 xmake.lua — 添加 C/C++ 源文件 — 如何将 C 文件加入构建
- LuaJIT FFI Tutorial — FFI 基础教程