静态链接¶
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
-based 系统构建静态 Crystal 二进制文件也轻而易举。官方 Crystal 编译器构建的 Linux 版本就是使用这些镜像创建的。
以下是一个关于如何使用 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 包为所有第三方依赖项(除了 LLVM)都提供了静态库和 DLL 导入库。
静态链接意味着使用 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
。
以下示例显示了使用 Crystal 0.36.1 和 LLVM 10.0 在 Ubuntu 18.04 LTS(在 crystallang/crystal:0.36.1
docker 镜像中)上构建的简单 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 将更多符号直接包含在一个二进制文件中。以下示例展示了使用 Crystal 0.36.1 和 LLVM 10.0 在 Alpine Linux 3.12(在 crystallang/crystal:0.36.1-alpine
docker 镜像中)上构建的相同程序的输出。
$ 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
链接器标志。