构建模块

lua-users home
wiki

本页面列出了在各种操作系统和工具链(IDE、编译器、链接器)上构建**Lua 的 C 扩展模块**的说明。

有关如何自己构建 Lua 核心代码的说明,请参阅 BuildingLua

有关如何编写(而不是构建)C 扩展模块的说明,请参阅 BindingCodeToLua

简介

构建 C 扩展模块通常意味着您需要创建一个共享库,也称为动态链接库。此库在运行时由require()(Lua 5.1 中的新模块系统)或loadlib()(低级调用,在 Lua 5.0 中也可用)加载。

也可以将模块静态链接到核心代码,但不建议用于通用 Lua 发行版(尽管对于嵌入式系统来说是有意义的)。

请随时更正或扩展此列表。 -- MikePall

构建说明

注意:通常,Lua 核心头文件(lua.hlauxlib.h 等)不会直接存储在标准搜索路径中,而是存储在一个子目录中。您需要通过将-Ilua_header_file_directory 添加到编译命令来向编译器指定其位置,这些示例中没有显示。如果您的平台具有pkg-config 和相关的 Lua 数据安装,您可以按如下方式获取所需的编译标志(假设 Lua 安装在包名称“lua5.1”下)

LUA_CFLAGS=`pkg-config lua5.1 --cflags`

使用 libtool 的共享库

尽管下面提供了针对多个平台的具体说明,但在大多数系统上,GNU libtool 都可用,并且会执行正确的操作,同时隐藏平台特异性。

LIBTOOL="libtool --tag=CC --silent"
$LIBTOOL --mode=compile cc -c module.c
$LIBTOOL --mode=link cc -rpath /usr/local/lib/lua/5.1 -o libmodule.la module.lo
mv .libs/libmodule.so.0.0.0 module.so

请注意,cc 是大多数系统上“主编译器”的别名。根据需要插入特定的编译器和优化设置。-rpath 值并不关键,但该选项是触发共享库构建所必需的。

作为奖励,上述命令同时在.libs/libmodule.a 中生成库的静态版本。(是的,它是使用-fpic 编译的。)

使用 GCC 的 dlfcn 共享库(Linux、*BSD 等)

gcc -O2 -fpic -c -o module.o module.c
gcc -O -shared -fpic -o module.so module.o
不幸的是,-fpic 在 x86 上会带来显著的性能损失。您可以在 x86 上省略此选项(但在某些其他架构上不行),但随后共享库需要在加载时重新定位(基本上为每个进程制作一个副本)。这取决于您的使用模式,这是否真的相关(当您派生进程或使用原生线程或非抢占式方法时,这不是问题;当您执行大量进程时,这是一个问题)。

在 x86 上添加-fomit-frame-pointer 以获得显著的性能提升(不幸的是,这也阻止了 x86 上的调试)。注意:这在其他 CPU 上没有任何意义(特别是 x64,也称为 x86_64/AMD64/EM64T)。例如,gcc -O 会在所有不会干扰调试的系统上自动启用-fomit-frame-pointer。哦,x64 也没有-fpic 的问题。

避免使用-fPIC,因为它在某些 RISC 架构(特别是 Sparc)上会带来很大的性能损失。当您超过-fPIC的限制时,链接器会发出警告,然后您必须切换到-fPIC。不过,您在 Lua 模块中极不可能遇到此限制。

阅读有关构建共享库的更多信息,比您想了解的还要多 [这里](PDF,约 500K)。

使用 Sun CC(Solaris)的 dlfcn 共享库

cc -O -Kpic -c -o module.o module.c
ld -G -o module.so module.o

使用 HP CC(HP-UX)的 dlfcn 共享库

cc +O3 +Z -c -o module.o module.c
ld -b -o module.so module.o

使用 IBM XL C(AIX 4.2 或更高版本)的 dlfcn 共享库

xlc -O2 -c -o module.o module.c
xlc -G -bexpall -o module.so module.o

某些旧版 Unix 系统上的 dlfcn 共享库

cc -O -Kpic -c -o module.o module.c
ld -Bshareable -o module.so module.o

Mac OS X 共享库(Lua 5.1.3 及更高版本)

重要说明:随着 Lua 5.1.3 的发布,Mac OS X 上 Lua 的模块加载器已更改。它现在使用标准 POSIX dlfcn API。因此,只需按照上面使用 GCC 的 dlfcn 共享库下的说明操作即可。但是,与其他平台上的 GCC 不同,您仍然必须使用选项“-undefined dynamic_lookup”进行链接,以避免对 liblua 获取未定义的引用。无论如何,请不要将您的扩展与 liblua.a 链接!(请参阅下面的“不要将模块链接到 Lua 核心库”)

它对我不起作用(Mac OS X 10.4.9,powerpc-apple-darwin8-gcc-4.0.0)。我得到了无法识别的选项 -shared。下面的说明可以正常工作。 -- lhf

“要在 OSX 上正确构建模块,您需要设置一个扁平命名空间并抑制未定义的符号,或者让未定义的符号被动态查找” -- jls

Unix/Linux 等效项

gcc -bundle -flat_namespace -undefined suppress -o module.so module.o

OSX 首选

gcc -bundle -undefined dynamic_lookup -o module.so module.o

Mac OS X 包(仅适用于 Lua 5.1.0 - Lua 5.1.2)

编译和链接

MACOSX_DEPLOYMENT_TARGET="10.3"
export MACOSX_DEPLOYMENT_TARGET
gcc -O2 -fno-common -c -o module.o module.c
gcc -bundle -undefined dynamic_lookup -o module.so module.o

我花了很长时间才弄清楚这一点,但上面的链接行在(至少)OS X 10.3 上是不够的,也许 10.4 也是如此。如果您的模块出现神秘崩溃,请尝试在环境中设置 DYLD_BIND_AT_LAUNCH=1 来运行它。如果模块现在可以正常运行,则您可能在数据的“延迟”初始化方面存在问题。与其设置环境变量,不如在 -bundle 选项之后添加 -Wl,-bind_at_load。特别是,用 obj-c 实现的模块,或者调用 obj-c 的模块,存在此问题,尽管它似乎也可能是 C++ 的问题。

注意:不要从 Lua 可执行文件(使用strip -x)中strip动态符号。liblua.a 没有(也不应该,请参阅页面末尾的说明)链接到模块中,因此包在动态加载时需要在可执行文件中找到 lua_ API 符号。

加载和卸载

使用 obj-c 的模块存在另一个问题,它们无法卸载,而 lua 会在关闭期间尝试卸载。当 lua 退出时,症状类似于以下内容

objc: 无法取消映射包含 ObjC 数据的图像
程序收到信号 SIGTRAP,跟踪/断点陷阱。

到目前为止,我发现的唯一解决方法是从 _LOADLIB 元表中删除 __gc 方法

static void lua_objc_kill_dlclose(lua_State* L){
luaL_getmetatable(L, "_LOADLIB");
lua_pushnil(L);
lua_setfield(L, -2, "__gc");
}

luaopen_objc(lua_State*L) {
lua_objc_kill_dlclose(L);
... 注册我的模块....
}

或者修改 loadlib.c 使其永远不会卸载 C 模块。

模块命名约定

为了与其他平台保持一致,模块也应该在 Mac OS X 上使用 .so 扩展名安装,这是包 cpath 的默认定义所假设的。

Mac OS 9/X 共享库 (CFM)

您需要一个经过修补的 Lua,它支持 oldskool CFM 库。

对于每个模块,创建一个匹配的 foo.exp 文件,其中包含一个类似的条目

luaopen_foo
然后将 foo 模块编译成一个共享库“foo”(没有扩展名)

使用 Borland C 的 Windows DLL

本页面上存在一个相关页面 CreatingBinaryExtensionModules,但已过时。可能应该将其合并到这里。

使用 MSVC 的 Windows DLL

注意:确保您的主初始化函数使用 __declspec(dllexport) 导出,如下所示

int __declspec(dllexport) MyModuleName (lua_State* L) { ... }

使用 GCC(MinGW、Cygwin 或 MinGW 交叉编译器)的 Windows DLL

gcc -O2 -c -o module.o module.c
gcc -O -shared -o module.dll module.o -Llua_dir -lluaXX
lua_dir 替换为 luaXX.dll 所在的目录。luaXX.dlllua50.dlllua51.dll,具体取决于您使用的 Lua 版本。

旧版本的 GCC(4.0 之前的版本)在使用 -fomit-frame-pointer 时存在异常处理和 stdcall 的问题 ([错误报告])。您可以升级或始终使用 -maccumulate-outgoing-args-fomit-frame-pointer 一起使用。

不要从 DLL 中剥离重定位信息(使用 strip --strip-unneeded)。

您可能希望将 -Wl,--enable-auto-image-base 添加到链接命令中,以减少在您有多个扩展模块时 DLL 的加载时间。

有关在 Cygwin 下使用 LuaRocks 的说明,请参见 [1] [2] [3] [4]

使用 LuaRocks 构建

LuaRocks 将 Lua 模块安装为自包含的包(带有依赖信息),称为“rocks”。

最终说明

升级 Lua 内核后重新编译

Lua 通常提供一定程度的 API 稳定性,但不一定提供 ABI 稳定性(二进制兼容性)。这意味着即使在小版本之间(例如从 5.0 到 5.1),升级 Lua 核心时也需要重新编译所有模块。请确保始终将编译器指向包含要编译的 Lua 核心头文件的正确包含路径(-I...)。

使用开发版本(“工作”版本)时,最好每次都重新编译,因为根本没有 API/ABI 稳定性保证。

不要将模块链接到 Lua 核心库

虽然将模块链接到包含 Lua 核心(lua*.a)的静态库可能很诱人,但这**不是**一个好主意。

本质上,您是在向每个模块添加一份整个 Lua 核心(或至少是其大部分)的副本。除了浪费空间之外,这还可能导致非常难以诊断的问题。核心代码的不同实例彼此之间一无所知,并且可能不一定同步。

如果您构建了一个包含 Lua 核心(*)的共享库,请**不要**将任何模块链接到它。即,不要在链接器行上指定 Lua 核心库的名称(Windows DLL 是一个例外)。这会创建一个硬向前依赖关系,而您真正想要的是一个懒惰的向后依赖关系。使用静态链接的 Lua 解释器加载此模块本质上会拖入第二个 Lua 核心,您会遇到上面提到的相同问题。

Lua 模块期望在加载之前 Lua 核心已经存在。这是因为 Lua 核心符号是全局导出的(例如,在大多数 ELF 系统上的 GCC 中使用 -Wl,-E - 请参阅 Lua Makefile)。

相关注意事项:这也是您不应该将应用程序与包含 Lua 模块的动态库链接的原因。这通常不会造成伤害,但不会按照您的意愿执行,因为 luaopen_foo() 函数永远不会被调用。模块应该由 require() 加载,而不是通过映像中的硬依赖关系(包依赖关系是可以的)。

(*) 在 x86/POSIX 平台上使用 -fpic 编译 Lua 核心比仅对模块执行此操作(如上所述)具有更严重的性能缺陷。推荐的方法(如 Lua 5.1 Makefile 中所示)是静态链接 Lua 核心并导出所有符号(使用 -Wl,-E 启用,或在 Solaris 等系统上默认启用)。

Windows 和 luaXX.dll

上面针对 Windows 的构建说明假设您以特定方式编译了 Lua 核心

重要的是将独立的可执行文件和每个模块都编译并链接到**相同**的 lua 头文件和**相同**的luaXX.dll

我应该链接到 lua51.dll 还是 lua5.1.dll?

lua51.dll 可能是更好的选择。Lua Binaries 导致了一些混乱。原始 Lua 5.1 发行版中的 Makefile 使用名为 lua51.dll 的 DLL 构建 Lua。Lua Binaries 5.1 将此文件命名为 lua5.1.dll。为了解决由此造成的混乱,更新版本的 Lua Binaries 包含一个名为 lua51.dll 的代理 DLL,它将调用转发到 lua5.1.dll(参见 LuaProxyDllThree),从而允许 Lua Binaries 与使用任一链接的 Lua 扩展 DLL 一起使用。大多数其他期望 lua51.dll 的 Lua 发行版(包括 LuaJit [12])没有代理返回到 lua5.1.dll,但您可以在需要时自己添加代理。因此,链接到 lua51.dll 的 Lua 扩展 DLL 可以被认为与所有发行版最兼容。正如以下链接所示,关于正确的命名已经讨论过很多次。在 Lua 5.2.0-work 中,LuaBinaries 使用 lua52.dll,就像官方源代码包一样 [5]

Windows 和 C 运行时 DLL(msvc*.dll)

几乎每个 Windows 编译器似乎都自带自己的标准 C 库来链接(例如 MSVC*.DLL 变体)

强烈建议所有在单个进程中协同工作的可执行文件和 DLL 共享相同的 C 库。这可以避免潜在的问题,例如在不同 C 库之间传递某些数据结构时可能发生的崩溃和难以诊断的行为(参见下面的 MSDN 文章)。Lua 核心小心地避免了这种传递(例如,Lua 在内部管理所有内存和文件句柄的分配/释放),尽管如果您不小心,您仍然可能会遇到陷阱(有什么例子吗?)。

如果扩展 DLL 只需要 Lua DLL、系统 DLL 和静态链接代码中的内容,那么它可能不需要链接到任何 C 库。

以下是一些背景信息

使用 MinGW 针对 LuaBinaries 构建

默认情况下,MinGW 针对旧的运行时 (msvcrt.dll) 构建,但可以使用 -lmsvcr80 说服它链接到 msvcr80.dll。但是,您可能会遇到非平凡扩展的问题,因为 MinGW 提供的导入库中的一些导入名称错误。例如,_findfirst 实际上导出为 _findfirst32 等。解决方案很简单,虽然有点 hack:**直接**链接到 DLL 并重命名有问题的函数。

以下是一个可以使用 MinGW 与 Lua for Windows 一起使用的 makefile

以下是我用于重建 lfs 的简单 makefile;请注意,不需要导入库,只需要 Lua 头文件。

LUA_INCLUDE= d:\stuff\lua\src
LFW= c:\Program Files\Lua\5.1
LUA_LIB="$(LFW)\lua5.1.dll"
RT_LIB="$(LFW)\msvcr80.dll"

lfs.dll: lfs.c lfs.h
       gcc -shared -I$(LUA_INCLUDE) lfs.c -o lfs.dll $(LUA_LIB) $(RT_LIB)

以下是重命名的符号列表

#define _ctime _ctime32
#define _difftime _difftime32
#define _findfirst _findfirst32
#define _findnext _findnext32
#define _fstat _fstat32
#define _ftime _ftime32
#define _futime _futime32
#define _gmtime _gmtime32
#define _localtime _localtime32
#define _mkgmtime _mkgmtime32
#define _mktime _mktime32
#define _stat _stat32
#define _time _time32
#define _utime _utime32
#define _wctime _wctime32
#define _wfindfirst _wfindfirst32
#define _wfindnext _wfindnext32
#define _wstat _wstat32
#define _wutime _wutime32

(在我的项目中,我最近在编译时使用宏对上述函数列表进行别名,并使用以下链接行:

$(LUADLL) -lgcc -lmsvcr80 $(MSVCR80)

其中 LUADLL 是 lua.5.1.dll 的完全限定名称,MSVCR80 是 MSVCR80.DLL 的完全限定名称。由于我使用 LfW,因此我在其安装树中找到了这两个 DLL,它们基于它定义的 LUA_DEV 环境变量。结果是依赖树有两个隐式加载的 MSVCR80.DLL,并且由于 MSVCR80 的需要,只加载 MSVCRT.DLL。如果我省略 libgcc.a 或以其他方式将其移到对 MSVCR80 的任何引用之后,那么我将发现对 MSVCRT.DLL 的不健康的依赖关系。

您的里程可能会有所不同,因此我强烈建议在发布二进制文件之前使用 Dependency Walker 或等效工具仔细检查 DLL 依赖关系。--RossBerteig)

避免链接任何 C 运行时库(或链接到不同的库)

鉴于并非总是容易或实用地确保进程中的所有模块都使用相同的 C 运行时库,值得注意的是,如果您的 C 扩展 DLL 不调用任何 ANSI C 函数,则它不需要链接到任何 C 运行时,从而避免了其自身的问题。例如,让我们编写一个非常简单的模块,它包装 Win32 MessageBox[8] 函数

#include <lua.h>
#include <lauxlib.h>
#include <windows.h>
BOOL APIENTRY DllMain(HANDLE module, DWORD reason, LPVOID reserved) { return TRUE; }
static int l_messagebox(lua_State * L) {
  const char * s = luaL_checkstring(L, 1);
  MessageBox(NULL, s, "messagebox", MB_OK);
  return 0;
}
__declspec(dllexport) int luaopen_messagebox(lua_State * L) {
  lua_pushcfunction(L, l_messagebox);
  return 1;
}

由于没有 ANSI C 函数(而是只有 Win32 和 Lua 函数),我们可以从链接器选项中省略 C 运行时,如下所示,适用于各种编译器

# GCC MinGW (under Cygwin -mno-cygwin)
gcc -mno-cygwin -O2 -nostdlib -shared -I<lua_include_path>
  -o messagebox.dll messagebox.c -luser32 <lua_bin_path>/lua51.dll
  -Wl,-e,_DllMain@12 -Wl,--dll -nostartfiles

# MSVC++ 2008
cl -O2 -LD -I<lua_include_path> messagebox.c <lua_lib_path>/lua51.lib
  user32.lib -link -nodefaultlib -entry:DllMain

生成的 DLL 只有大约 3 KB。您可以使用 dumpbin 或 objdump 确认 DLL 仅链接到 user32.dll (MessageBoxA) 和 lua51.dll (luaL_checklstringlua_pushcclosure)。

现在,如果您的 C 扩展 DLL 需要执行一些 ANSI-C 式的操作,例如分配内存或写入文件怎么办?一种选择是使用 Win32 API 函数(例如 HeapAlloc [9]CreateFile [10]),这是 C 库本身所做的(您可以通过将程序静态链接到 C 库并查看导入来确认)。如果您的程序需要在非 Windows 操作系统上编译,这不太便携。另一种选择是使用纯 Win32 API 调用重写您需要的 C 运行时库(参见 [tlibc - Tiny C Runtime Library])。另一种选择是通过 Lua 间接访问这些函数。例如,调用 [lua_newuserdata] 来分配内存块,或执行 lua_getglobal(L, "io") 来获取对 Lua 的 I/O 库的引用。

第三种选择是使用 ANSI C 函数,但是,是的,动态或静态链接到可能与进程中其他模块使用的 C 运行时不同的 C 运行时。这通常不建议这样做,因为 MSDN 文章 [11] 中描述了可能的陷阱。但是,可以通过仔细注意这些问题来完成,因此这样做本身并没有错误。例如,以下操作应该是安全的

...
#include <stdio.h>
static int l_messagebox(lua_State * L) {
  char * s = malloc(1000);
  if (s) {
    sprintf(s, "%e", luaL_checknumber(L, 1));
    MessageBox(NULL, s, "messagebox", MB_OK);
  }
  free(s);
  return 0;
}
...

# GCC MinGW (under Cygwin -mno-cygwin) - links to msvcrt.dll
gcc -O2 -mno-cygwin -shared -s messagebox.c -o messagebox.dll
  -I<lua_include_path> <lua_bin_path>/lua51.dll

# MSVC++ 2008 - statically linking to the C runtime.
# Note: The resultant binary is about 66 KB when statically linking.
cl -O2 -LD messagebox.c -I<lua_include_path>
  <lua_lib_path>/lua51.lib user32.lib

这两个生成的二进制文件都可以在针对 msvcrt90.dll 编译的 Lua 版本中工作。

请注意,C 运行时通常需要初始化。通常,C 运行时定义自己的入口点(例如 DllMainCRTStartup),它初始化 C 运行时库,然后调用用户的入口点(例如 DllMain),因此这通常会为您处理。但是,如果您覆盖 DLL 入口点,C 运行时将无法正确初始化。

剥离符号信息

一般来说,我不建议剥离二进制文件。 对于现代二进制格式(如 ELF)和虚拟内存系统,所有调试信息都存储在永远不会映射到内存的数据页中。 因此,所有调试信息只占用硬盘空间(通常很充足),而不是内存空间。 不过,嵌入式系统的情况可能有所不同。 我只为需要谨慎剥离的系统添加了警告。

编译器优化级别

上面列出的设置是保守的默认值。 当然,你可以使用 -O3 -march=xyz 和各种 -f<something> 标志来进行疯狂的优化。 如果你的应用程序确实需要它,请这样做。 如果没有必要,就不要费心。


最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2014 年 4 月 24 日凌晨 5:12 GMT (差异)