靜態連結¶
Crystal 支援靜態連結,也就是說,它可以將二進制檔案與靜態函式庫連結,這樣這些函式庫就不需要作為執行時期依賴存在。這提高了可攜性,但代價是二進制檔案會比較大。
可以使用 --static
編譯器標誌啟用靜態連結。請參閱語言參考中的使用說明。
當給定 --static
時,會啟用靜態函式庫的連結,但它不是排他的。如果函式庫的動態版本在編譯器的函式庫查找鏈中比靜態版本高(或者如果完全沒有靜態函式庫),則產生的二進制檔案將不會是完全靜態連結的。為了建構一個靜態二進制檔案,您需要確保已連結函式庫的靜態版本可用,並且編譯器可以找到它們。
編譯器使用 CRYSTAL_LIBRARY_PATH
環境變數作為要連結的靜態和動態函式庫的第一個查找目的地。這可以用來提供也以動態函式庫形式提供的函式庫的靜態版本。
並非所有函式庫都適合靜態連結,因此可能會有一些問題。例如,openssl
以複雜性而聞名,glibc
也是如此(請參閱完全靜態連結)。
一些套件管理器提供靜態函式庫的特定套件,其中 foo
提供動態函式庫,而 foo-static
例如提供靜態函式庫。有時靜態函式庫也包含在開發套件中。
完全靜態連結¶
一個完全靜態連結的程式沒有任何動態函式庫依賴。這對於交付可攜式、預先編譯的二進制檔案很有用。完全靜態連結 Crystal 程式的突出例子是官方發行套件中的 crystal
和 shards
二進制檔案。
為了完全靜態連結一個程式,所有依賴都需要在編譯時以靜態函式庫的形式提供。有時這可能會很棘手,尤其是常見的 libc
函式庫。
Linux¶
glibc
¶
glibc
是 Linux 系統上最常見的 libc
實作。不幸的是,它不適合靜態連結,而且非常不建議這樣做。
相反,建議在 Linux 上針對 musl-libc
進行靜態連結。由於它是靜態連結的,因此針對 musl-libc
連結的二進制檔案也將在 glibc 系統上執行。這就是它的全部重點。
然而,除了動態連結的 glibc
之外,靜態連結其他函式庫是完全可以的。
musl-libc
¶
musl-libc
是一個乾淨、高效的 libc
實作,具有出色的靜態連結支援。
建構靜態連結 Crystal 程式的建議方法是使用 Alpine Linux,這是一個基於 musl-libc
的最小 Linux 發行版。
官方的 基於 Alpine Linux 的 Docker 映像檔可在 Docker Hub 上取得,網址為 crystallang/crystal
。最新的發行版標記為 crystallang/crystal:latest-alpine
。Dockerfile 原始碼可在 crystal-lang/distribution-scripts 上取得。
這些 Docker 映像檔預先安裝了 crystal
編譯器、shards
以及所有 stdlib 依賴項的靜態函式庫,即使是基於 glibc
的系統,也可以輕鬆建構靜態 Crystal 二進制檔案。Linux 的官方 Crystal 編譯器版本是使用這些映像檔建立的。
以下是一個範例,說明如何使用 Docker 映像檔來建構靜態連結的 Hello World 程式
$ echo 'puts "Hello World!"' > hello-world.cr
$ docker run --rm -it -v $(pwd):/workspace -w /workspace crystallang/crystal:latest-alpine \
crystal build hello-world.cr --static
$ ./hello-world
Hello World!
$ ldd hello-world
statically linked
Alpine 的套件管理器 APK 也很容易使用來安裝靜態函式庫。可在 pkgs.alpinelinux.org 上找到可用的套件。
macOS¶
macOS 不 官方支援完全靜態連結,因為所需的系統函式庫不以靜態函式庫的形式提供。
Windows¶
Windows 不支援完全靜態連結,因為 Win32 函式庫不以靜態函式庫的形式提供。
目前,靜態連結是 Windows 上的預設連結模式,可以透過 -Dpreview_dll
編譯時期標誌選擇加入動態連結。為了區分靜態函式庫和 DLL 匯入函式庫,當編譯器在給定目錄中搜尋函式庫 foo.lib
時,將在靜態連結時首先嘗試 foo-static.lib
,而在動態連結時首先嘗試 foo-dynamic.lib
。官方 Windows 套件會分發所有第三方依賴項的靜態和 DLL 匯入函式庫,LLVM 除外。
靜態連結表示使用 Microsoft C 執行時期函式庫的靜態版本 (/MT
),而動態連結表示使用動態版本 (/MD
);應考慮使用此方法建構額外的 C 函式庫,以避免連結器出現關於混合 CRT 版本的警告。目前沒有辦法在靜態連結時使用動態 CRT。
識別靜態依賴¶
如果要靜態連結依賴項,您需要有其靜態函式庫可用。大多數系統預設不會安裝靜態函式庫,因此您需要明確安裝它們。首先,您必須知道您的程式連結了哪些函式庫。
注意
靜態函式庫在 POSIX 系統上的副檔名為 .a
,在 Windows 上則為 .lib
。Windows 上的 DLL 匯入函式庫也使用 .lib
副檔名。動態函式庫在 Linux 和大多數其他 POSIX 平台上使用 .so
,在 macOS 上使用 .dylib
,而在 Windows 上使用 .dll
。
在大多數 POSIX 系統上,工具 ldd
會顯示可執行檔連結到哪些動態函式庫。macOS 上對應的工具是 otool -L
,而 Windows 上對應的工具是 dumpbin /dependents
。
以下範例顯示在 Ubuntu 18.04 LTS 上(在 crystallang/crystal:0.36.1
docker 映像檔中),使用 Crystal 0.36.1 和 LLVM 10.0 建置的簡單 Hello World 程式的 ldd
輸出。結果在其他系統和版本上會有所不同。
$ ldd hello-world_glibc
linux-vdso.so.1 (0x00007ffeaf990000)
libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007fc393624000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fc393286000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fc393067000)
libevent-2.1.so.6 => /usr/lib/x86_64-linux-gnu/libevent-2.1.so.6 (0x00007fc392e16000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fc392c12000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fc3929fa000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc392609000)
/lib64/ld-linux-x86-64.so.2 (0x00007fc393dde000)
這些函式庫是 Crystal 標準函式庫的最低依賴項。即使是空程式也需要這些函式庫來設定 Crystal 執行環境。
看起來很多,但這些函式庫大多數實際上是 libc 發行版的一部分。
在 Alpine Linux 上,列表要小得多,因為 musl 將更多符號直接包含在單個二進制文件中。以下範例顯示在 Alpine Linux 3.12 上(在 crystallang/crystal:0.36.1-alpine
docker 映像檔中),使用 Crystal 0.36.1 和 LLVM 10.0 建置的相同程式的輸出。
$ ldd hello-world_musl
/lib/ld-musl-x86_64.so.1 (0x7fe14b05b000)
libpcre.so.1 => /usr/lib/libpcre.so.1 (0x7fe14af1d000)
libgc.so.1 => /usr/lib/libgc.so.1 (0x7fe14aead000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x7fe14ae99000)
libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7fe14b05b000)
個別的函式庫為 libpcre
、libgc
,其餘的是 musl
(libc
)。在 Ubuntu 範例中也使用了相同的函式庫。
為了靜態連結這個程式,我們需要這三個函式庫的靜態版本。
注意
*-alpine
docker 映像檔隨附標準函式庫使用的所有函式庫的靜態版本。如果您的程式沒有連結其他函式庫,那麼在建置命令中加入 --static
標誌就足以進行完全靜態連結。
動態函式庫查詢¶
動態函式庫在執行時的查詢路徑可以透過編譯期間的 CRYSTAL_LIBRARY_RPATH
環境變數來控制。目前 Linux 和 Windows 上都支援此功能。
Linux¶
如果在編譯期間定義了 CRYSTAL_LIBRARY_RPATH
,它會透過 -Wl,rpath
選項不經修改地傳遞給連結器。確切的行為取決於連結器;通常,這會附加到 ELF 可執行檔的 DT_RUNPATH
或 DT_RPATH
動態標籤條目。並非所有平台都支援特殊的 $ORIGIN
/ $LIB
/ $PLATFORM
變數。
Windows¶
標準函式庫支援實驗性的DLL 延遲載入,並可能透過延遲載入來變更 DLL 的搜尋順序。
如果針對給定的 DLL 傳遞 /DELAYLOAD
連結器標誌,則可執行檔第一次從該 DLL 載入符號時,它會先嘗試執行檔 CRYSTAL_LIBRARY_RPATH
中以分號分隔的路徑,按照它們宣告的順序,然後再嘗試預設的查詢順序。CRYSTAL_LIBRARY_RPATH
內的 $ORIGIN
會展開為正在執行的執行檔本身的路徑。例如,如果在編譯期間 CRYSTAL_LIBRARY_RPATH=$ORIGIN\mylibs;C:\bar
,並且提供了 --link-flags=/DELAYLOAD:calc.dll
,且可執行檔位於 C:\foo\test.exe
,那麼可執行檔會先搜尋 C:\foo\mylibs\calc.dll
,然後是 C:\bar\calc.dll
,之後再使用預設的順序。
非延遲載入的 DLL 會在程式啟動時立即載入,並且不遵循 CRYSTAL_LIBRARY_RPATH
。
預設情況下,不會延遲載入任何 DLL。但是,如果在編譯時指定了 -Dpreview_win32_delay_load
編譯時標誌,編譯器會從其匯入函式庫中偵測所有 DLL 相依性,在編譯期間為每個 DLL 插入 /DELAYLOAD
連結器標誌。