建置模組

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 資料,您可以使用下列方法來取得需要的編譯旗標(假設已在套件名稱為「lua5.1」的套件下安裝 Lua)

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++ 也可能會有這個問題。

注意:不要 strip 掉 Lua 執行檔的動態符號(請使用 strip -x)。liblua.a 沒有(且不應該,請參閱頁面底端的備註)連結到模組中,因此,當動態載入時,捆綁包會需要在執行檔中尋找 lua_ API 符號。

載入和卸載

使用 obj-c 的模組有另一個問題:它們無法卸載,Lua 會在關閉過程中嘗試執行此動作。症狀如下,Lua 在終了時會顯示類似這樣的訊息

objc: 無法解除包含 ObjC 資料的映像的對應
程式收到訊號 SIGTRAP、Trace/breakpoint 陷阱。

目前為止,我找到的一個修正方法是移除 _LOADLIB metatable 中的 __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)

您需要修正過、且支援 oldskool CFM 函式庫的 Lua。

對每個模組,建立一個搭配的 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 的問題([錯誤回報])。您可升級或始終與 -fomit-frame-pointer 使用 -maccumulate-outgoing-args

不要刪除 DLL 的搬移資訊(使用 strip --strip-unneeded)。

當您有許多延伸模組時,您可能想將 -Wl,--enable-auto-image-base 加入連結命令以減少 DLL 的載入時間。

如需在 Cygwin 下使用 LuaRocks 的注意事項,請參閱 [1] [2] [3] [4]

以 LuaRocks 建立

LuaRocks 會將 Lua 模組安裝為自給自足的套件(有依賴資訊),稱為「rocks」。

結論

升級 Lua 核心後重新編譯

Lua 通常提供一定程度的 API 穩定性,但 ABI 穩定性(二進制相容性)則不一定。這表示當你在小版本間升級 Lua 核心(例如 5.0 到 5.1)時,你必須重新編譯所有模組。務必總是將你的編譯器指向正確的目錄路徑(-I...),其中包含你想要編譯對應的 Lua 核心標頭檔。

當你使用開發版本(「工作」發行版)時,重新編譯是一個好主意,因為沒有任何 API/ABI 穩定性保證。

不要將模組連結到 Lua 核心函式庫

雖然將模組連結到包含 Lua 核心(lua*.a)的靜態函式庫可能會令人心動,但這並非好主意。

基本上,你將整個 Lua 核心(或至少大部分)的副本加入到每個模組中。除了浪費空間外,這可能會導致難以診斷的問題。核心程式碼的不同執行個體彼此不瞭解,而且不一定要同步。

萬一你建立了一個包含 Lua 核心(*)的共用函式庫,請不要將任何模組連結到它。換句話說,不要在連結器行上指定 Lua 核心函式庫的名稱(Windows DLL 為例外)。這會產生一個硬性正向相依性,而你真正需要的是一個懶惰反向相依性。使用靜態連結的 Lua 解譯器載入這個模組基本上會拖入一個第二個 Lua 核心,你會遭遇上述相同的問題。

Lua 模組預期在載入之前,Lua 核心已經存在。這可以運作,因為 Lua 核心符號會全域匯出(例如在大多數 ELF 系統上使用 GCC 執行 -Wl,-E -- 參閱 Lua Makefiles)。

相關注意事項:這就是為什麼你也不能將應用程式連結到包含 Lua 模組的動態函式庫。這通常並不會造成傷害,但無法達到你的要求,因為 luaopen_foo() 函式永遠不會呼叫。模組要透過 require() 而不是透過映像中的硬性相依性載入(套件相依性是可以的)。

(*) 在 x86/POSIX 平臺上使用 -fpic 編譯 Lua 核心所造成的效能缺點比只針對模組執行這個動作更為嚴重(如上所述)。建議的做法(顯示在 Lua 5.1 的 Makefiles 中)是將 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 承認造成混亂後,包含了一個 Proxy DLL 名為 lua51.dll,用於轉發呼叫至 lua5.1.dll (請參閱 LuaProxyDllThree),從而讓 Lua Binaries 能夠與同時使用連結的 Lua 延伸 DLL 一起運作。大多數其他預期 lua51.dll 的 Lua 發行版 (包含 LuaJit [12]) 會缺少連結回 lua5.1.dll 的 Proxy,但您可以在需要時自行加入 Proxy。因此,與 lua51.dll 連結的 Lua 延伸 DLL 可說是與所有發行版相容性最高的。正如以下連結中所示,關於正確命名的討論已經進行過很多次了。在 Lua 5.2.0-work 中,LuaBinaries 使用的是 lua52.dll,且與官方原始碼 tar 檔案相同 [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 等。解決這個問題很簡單,雖然有點投機取巧的意味:直接連結到 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 的兩個隱式載入,並且只載入 MSVCRT.DLL,因為 MSVCR80 有需要。如果我省略 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(格林威治標準時間)(diff)