惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
N
News and Events Feed by Topic
S
Schneier on Security
G
GRAHAM CLULEY
Scott Helme
Scott Helme
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
GbyAI
GbyAI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
C
CERT Recently Published Vulnerability Notes
T
The Exploit Database - CXSecurity.com
C
Cisco Blogs
T
The Blog of Author Tim Ferriss
Cisco Talos Blog
Cisco Talos Blog
P
Privacy & Cybersecurity Law Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
博客园 - 司徒正美
Blog — PlanetScale
Blog — PlanetScale
Project Zero
Project Zero
MyScale Blog
MyScale Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Apple Machine Learning Research
Apple Machine Learning Research
小众软件
小众软件
The Last Watchdog
The Last Watchdog
Vercel News
Vercel News
The Cloudflare Blog
C
Check Point Blog
Help Net Security
Help Net Security
Microsoft Security Blog
Microsoft Security Blog
AI
AI
Simon Willison's Weblog
Simon Willison's Weblog
云风的 BLOG
云风的 BLOG
M
MIT News - Artificial intelligence
Stack Overflow Blog
Stack Overflow Blog
腾讯CDC
NISL@THU
NISL@THU
S
Security @ Cisco Blogs
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
S
SegmentFault 最新的问题
MongoDB | Blog
MongoDB | Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
Threatpost
AWS News Blog
AWS News Blog
Cloudbric
Cloudbric
N
News and Events Feed by Topic
PCI Perspectives
PCI Perspectives
S
Securelist
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
Vulnerabilities – Threatpost
S
Secure Thoughts

云风的 BLOG

云风的 BLOG: 高尿酸血症确诊 云风的 BLOG: 小孩的英语辅导 云风的 BLOG: 从猎户座之王到群星 云风的 BLOG: 对基本有序的序列排序算法 云风的 BLOG: 缺氧和异星工厂的比较 云风的 BLOG: 一个简单的缺氧地热模块 云风的 BLOG: 我对《缺氧》的游戏理解 云风的 BLOG: 除法的意义 Star Trek : Captain's Chair 初体验 云风的 BLOG: Star Trek : Captain's Chair 初体验 用 AI 辅助读书 云风的 BLOG: 用 AI 辅助读书 日常锻炼的一些记录 云风的 BLOG: 日常锻炼的一些记录 介绍几款单人桌游 云风的 BLOG: 介绍几款单人桌游 和 AI 聊游戏设计 云风的 BLOG: 和 AI 聊游戏设计 Skynet 升级到 Lua 5.5.0 云风的 BLOG: Skynet 升级到 Lua 5.5.0 带可可学数学 云风的 BLOG: 带可可学数学 最近玩的几款卡牌构筑类电子游戏 云风的 BLOG: 最近玩的几款卡牌构筑类电子游戏 欧陆风云5的游玩笔记 云风的 BLOG: 欧陆风云5的游玩笔记 嵌入主线程消息循环的任务调度器 云风的 BLOG: 嵌入主线程消息循环的任务调度器 关于桌游设计大赛的介绍 云风的 BLOG: 关于桌游设计大赛的介绍 有惊无险的一次网站系统升级
云风的 BLOG: soluna 外挂 C 模块
云风 · 2026-03-11 · via 云风的 BLOG

soluna 集成了 lua 虚拟机,但默认构建方式是将 lua 库静态链接到唯一的执行文件中。这将导致无法以动态库的形式外挂 Lua 的 C 扩展。

这是因为,如果独立编译 Lua 的 C 扩展库,通常需要链接 Lua 的 C API 。标准的方法是动态链接 lua 实现,如果静态链接 liblua.a ,会导致进程中有多份 lua 的实现。在 Lua 的历史版本中,这将导致运行期错误。

这是因为,Lua 的实现中有一个静态的“空”对象,所有的 nil 都指向这个对象。如果进程空间中有多份 Lua 实现,就会出现多个空对象。运行时的数据结构中会引用这个空对象,而不同副本的实现将“空”和自身保留的“空”对象引用做比较时,就会出现错误的判断。

在更早期的版本,出现这种链接出现的项目,bug 会隐藏得很深。所以后来 Lua 增加了 luaL_checkversion() ,倡议在外部库初始化时调用,除了检查版本号,还会检查当前执行的 lua 实现是否和虚拟机创建时用的实现是同一个副本。

但不知道怎样正确链接 lua 的项目(保证进程中只有一份 Lua 实现)还是太多,从 Lua 5.4 以后,这个“空”对象就被移入了 lua_State 这个运行期结构。以牺牲一点运行时的代价,挽救那些似乎永远也搞不懂“加载和链接”的程序员。终于,错误的链接 Lua 也能不出错了。

但我还是认为,在同一进程中置入多份 Lua 实现是不好的。

注:这也是 Windows 动态库的一个独有问题。因为 Windows 的 DLL 不允许有未完成的符号,必须在编译链接时指定所有符号(Lua C API)的来源;如果是 Linux ,可以不链接 Lua C API 的库,在运行时加载动态库,加载器就能把进程内的对应符号装载起来。


回到题头的问题:soluna 静态链接了 Lua ,并未导出 C API ,要用 C 写额外的库怎么办?

曾经在 Ant Engine 中,我采用了一个方法:提供一个假的代理动态库,提供所有 Lua C API 的符号。外部库可以动态链接它,而它将所有 Lua C API 调用转发到 engine 内部链接的 Lua 实现中。

这样做的好处是,即使是预编译好的 Lua C 库,只要它正确的以动态链接形式链接了 Lua ,就能直接被 Ant Engine 加载。如果不需要外部库,这个代理库也可以不发布。

今天,我想给 soluna 加上类似的特性,但尝试了新的方案:外部库在构建时额外实现一个简单的入口函数,它不依赖真的 Lua 实现,而是链接 soluna 项目中的 extlua/extlua.c 这个 Lua API 代理实现。再由 soluna 的定制加载器来加载这个外部库。

比如,我有一个叫做 foobar 的外部库,原本的实现是这样的:

static int
lhello(lua_State *L) {
    lua_pushstring(L, "Hello World");
    return 1;
}

extern int
luaopen_foobar(lua_State *L) {
    luaL_Reg l[] = {
        { "hello", lhello },
        { NULL, NULL },
    };
    luaL_newlib(L, l);
    return 1;
}

当我们编译成动态库时,导出的 luaopen_foobar() 是库的入口。lua 的 require 可以正确的导入它。但这个实现依赖若干 lua C APIs ,例如 lua_pushstring() 等。

如何在 soluna 里正确加载它呢?我们需要在调用 luaopen_foobar() 这个入口函数前,将进程中的 Lua C APIs 注入这个动态库。

在这个方案中,只需要链接 soluna 项目中的 extlua/extlua.c 单个文件,然后导出一个额外的库入口函数:

extern int
extlua_init(lua_State *L) {
    luaapi_init(L);
    luaL_Reg l[] = {
        { "ext.foobar", luaopen_foobar },
        { NULL, NULL },
    };
    luaL_newlib(L, l);
    return 1;
}

这个函数的第一行需要调用 luaapi_init(L) ,它的实现在 extlua.c 中。然后用 luaL_newlib() 注入原有的模块入口函数即可。

luaapi_init(L) 并不依赖任何 Lua 的内部实现,只依赖 Lua 的一个官方宏 lua_getextraspace() 完成了注入 Lua C APIs 的魔法。

这是个有趣的技巧:

lua_getextraspace(L) 的官方定义是这样的:

#define lua_getextraspace(L)    ((void *)((char *)(L) - LUA_EXTRASPACE))

每个 Lua_State 结构前都保留有一个指针的空间,可以用来传递数据。soluna.external.load 会构建一个空的 Lua 虚拟机,并把所有 Lua C APIs 的引用放在它的 extraspace 。因为上面的 extlua_init() 是一个标准的 lua_CFunction ,所以可以用标准函数 package.loadlib 读出。传入这个带 C APIs 的空 Lua 虚拟机,luaapi_init() 就能正确的导入所有 API 了。随后的 luaL_newlib() 会把所有真正的入口函数放在这个空虚拟机中。当然,只是一些字符串(入口名)和 C 函数指针。

接下来,soluna.external.load 再从这个虚拟机中把整个入口函数表复制到当前虚拟机,并销毁掉这个临时虚拟机,就完成了整个外部模块的动态加载。


soluna.extlib(name) 的实现是这样的:

function soluna.extlib(name)
    local extlua = require "soluna.extlua"
    local filename = assert(package.searchpath(name, package.cpath))
    settings = settings and soluna.settings()
    local entry = assert(package.loadlib(filename, settings.extlua_entry))
    return extlua.load(entry)
end

要使用上面例子中的放在 sample.dll 中的库 ext.foobar 只需要这样:

local libs = soluna.extlib "sample"
local foobar = require "ext.foobar"
assert(libs["ext.foobar"] == foobar)

即使要静态链接 sample 模块(iOS 不支持动态库,可能必须静态链接),只需要采用以下编译方案即可正确工作:

  1. 静态链接 sample 模块
  2. 不要链接 extlua/extlua.c
  3. luaapi_init() 定义为一个空函数
  4. extlua_init() 这个入口函数导入
  5. 用 soluna.extlua.load(入口函数) 加载