建置模組 |
|
如要說明如何自行建置 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 資料,您可以使用下列方法來取得需要的編譯旗標(假設已在套件名稱為「lua5.1」的套件下安裝 Lua)
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++ 也可能會有這個問題。
注意:不要 strip
掉 Lua 執行檔的動態符號(請使用 strip -x
)。liblua.a 沒有(且不應該,請參閱頁面底端的備註)連結到模組中,因此,當動態載入時,捆綁包會需要在執行檔中尋找 lua_ API 符號。
載入和卸載
使用 obj-c 的模組有另一個問題:它們無法卸載,Lua 會在關閉過程中嘗試執行此動作。症狀如下,Lua 在終了時會顯示類似這樣的訊息
目前為止,我找到的一個修正方法是移除 _LOADLIB metatable 中的 __gc 方法
或駭入 loadlib.c 使其永不卸載 C 模組。
模組命名慣例
為與其他平台保持一致,模組在 Mac OS X 上也應安裝為 .so
的副檔名,而這是套件 cpath 預設定義所承諾的。
您需要修正過、且支援 oldskool CFM 函式庫的 Lua。
對每個模組,建立一個搭配的 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 的問題([錯誤回報])。您可升級或始終與 -fomit-frame-pointer
使用 -maccumulate-outgoing-args
。
不要刪除 DLL 的搬移資訊(使用 strip --strip-unneeded
)。
當您有許多延伸模組時,您可能想將 -Wl,--enable-auto-image-base
加入連結命令以減少 DLL 的載入時間。
如需在 Cygwin 下使用 LuaRocks 的注意事項,請參閱 [1] [2] [3] [4] 。
Rocks 建立LuaRocks 會將 Lua 模組安裝為自給自足的套件(有依賴資訊),稱為「rocks」。
Lua 通常提供一定程度的 API 穩定性,但 ABI 穩定性(二進制相容性)則不一定。這表示當你在小版本間升級 Lua 核心(例如 5.0 到 5.1)時,你必須重新編譯所有模組。務必總是將你的編譯器指向正確的目錄路徑(-I...),其中包含你想要編譯對應的 Lua 核心標頭檔。
當你使用開發版本(「工作」發行版)時,重新編譯是一個好主意,因為沒有任何 API/ABI 穩定性保證。
雖然將模組連結到包含 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 的建置說明假設你已經以特定方式編譯 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 承認造成混亂後,包含了一個 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 函式庫會與之連結 (例如: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 等。解決這個問題很簡單,雖然有點投機取巧的意味:直接連結到 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 的兩個隱式載入,並且只載入 MSVCRT.DLL,因為 MSVCR80 有需要。如果我省略 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>
旗標。如果您的應用程式真的需要,那就這麼做。如果不用的話,就別費事。