构建模块 |
|
有关如何自己构建 Lua 核心代码的说明,请参阅 BuildingLua。
有关如何编写(而不是构建)C 扩展模块的说明,请参阅 BindingCodeToLua。
构建 C 扩展模块通常意味着您需要创建一个共享库,也称为动态链接库。此库在运行时由require()
(Lua 5.1 中的新模块系统)或loadlib()
(低级调用,在 Lua 5.0 中也可用)加载。
也可以将模块静态链接到核心代码,但不建议用于通用 Lua 发行版(尽管对于嵌入式系统来说是有意义的)。
请随时更正或扩展此列表。 -- MikePall
注意:通常,Lua 核心头文件(lua.h
、lauxlib.h
等)不会直接存储在标准搜索路径中,而是存储在一个子目录中。您需要通过将-I
lua_header_file_directory
添加到编译命令来向编译器指定其位置,这些示例中没有显示。如果您的平台具有pkg-config
和相关的 Lua 数据安装,您可以按如下方式获取所需的编译标志(假设 Lua 安装在包名称“lua5.1”下)
LUA_CFLAGS=`pkg-config lua5.1 --cflags`
尽管下面提供了针对多个平台的具体说明,但在大多数系统上,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 -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)。
cc -O -Kpic -c -o module.o module.c ld -G -o module.so module.o
cc +O3 +Z -c -o module.o module.c ld -b -o module.so module.o
xlc -O2 -c -o module.o module.c xlc -G -bexpall -o module.so module.o
cc -O -Kpic -c -o module.o module.c ld -Bshareable -o module.so module.o
重要说明:随着 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
编译和链接
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 退出时,症状类似于以下内容
到目前为止,我发现的唯一解决方法是从 _LOADLIB 元表中删除 __gc 方法
或者修改 loadlib.c 使其永远不会卸载 C 模块。
模块命名约定
为了与其他平台保持一致,模块也应该在 Mac OS X 上使用 .so
扩展名安装,这是包 cpath 的默认定义所假设的。
您需要一个经过修补的 Lua,它支持 oldskool CFM 库。
对于每个模块,创建一个匹配的 foo.exp 文件,其中包含一个类似的条目
luaopen_foo然后将 foo 模块编译成一个共享库“foo”(没有扩展名)
本页面上存在一个相关页面 CreatingBinaryExtensionModules,但已过时。可能应该将其合并到这里。
lua51.lib
添加到输入库(链接,输入)。
注意:确保您的主初始化函数使用 __declspec(dllexport) 导出,如下所示
int __declspec(dllexport) MyModuleName (lua_State* L) { ... }
gcc -O2 -c -o module.o module.c gcc -O -shared -o module.dll module.o -Llua_dir -lluaXX将
lua_dir
替换为 luaXX.dll
所在的目录。luaXX.dll
是 lua50.dll
或 lua51.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] 。
Rocks 构建LuaRocks 将 Lua 模块安装为自包含的包(带有依赖信息),称为“rocks”。
Lua 通常提供一定程度的 API 稳定性,但不一定提供 ABI 稳定性(二进制兼容性)。这意味着即使在小版本之间(例如从 5.0 到 5.1),升级 Lua 核心时也需要重新编译所有模块。请确保始终将编译器指向包含要编译的 Lua 核心头文件的正确包含路径(-I...)。
使用开发版本(“工作”版本)时,最好每次都重新编译,因为根本没有 API/ABI 稳定性保证。
虽然将模块链接到包含 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 的构建说明假设您以特定方式编译了 Lua 核心
lua.c
、luac.c
、print.c
之外的所有源文件,并构建 luaXX.dll
(即 lua50.dll
或 lua51.dll
)。
lua.c
编译独立可执行文件 lua.exe
并将其链接到 luaXX.dll
。
重要的是将独立的可执行文件和每个模块都编译并链接到**相同**的 lua 头文件和**相同**的luaXX.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 库来链接(例如 MSVC*.DLL
变体)
强烈建议所有在单个进程中协同工作的可执行文件和 DLL 共享相同的 C 库。这可以避免潜在的问题,例如在不同 C 库之间传递某些数据结构时可能发生的崩溃和难以诊断的行为(参见下面的 MSDN 文章)。Lua 核心小心地避免了这种传递(例如,Lua 在内部管理所有内存和文件句柄的分配/释放),尽管如果您不小心,您仍然可能会遇到陷阱(有什么例子吗?)。
如果扩展 DLL 只需要 Lua DLL、系统 DLL 和静态链接代码中的内容,那么它可能不需要链接到任何 C 库。
以下是一些背景信息
Perl 使用 msvcrt.dll 的讨论(类似问题)
默认情况下,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 是 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 扩展 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_checklstring
和 lua_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>
标志来进行疯狂的优化。 如果你的应用程序确实需要它,请这样做。 如果没有必要,就不要费心。