什么是交叉编译?
在上一部分中,我们了解了如何使用 Zig 为编译器运行的同一目标生成 C/C++ 项目的构建。有了有效的交叉编译设置,你就能从 x86_64 Windows 中创建 ARM Linux 可执行文件。
当您需要发布一个可在多个平台上运行的应用程序时,交叉编译就显得尤为重要:有了 Zig,您就可以在一台机器上创建所有的发布工件!
Zig 中的交叉编译支持
对于 Zig 来说,交叉编译是一个主要的使用案例,因此从捆绑适用于所有主要平台的 libc 实现开始,Zig 投入了大量精力使其成为一种无缝体验。
如果你好奇,可以阅读安德鲁-凯利(Andrew Kelley)的这篇博文,详细了解 Zig 如何扩展 clang 以简化交叉编译。
说到 macOS,Zig 甚至有一个定制的链接器,可以为英特尔和苹果 Silicon M1 进行交叉编译,这一点甚至连 lld(LLVM 的链接器)都做不到。正因为如此,在撰写本文时,Zig 是唯一能为 Apple Silicon 进行交叉编译和交叉签名(即从另一个平台执行代码设计)的 C/C++ 编译器。
关于构建脚本的可移植性...
遗憾的是,C/C++ 项目几乎从未考虑过交叉编译。例如,一个项目可能在 macOS 和 Linux 上都能编译,因而具有可移植性,但几乎所有的编译脚本都依赖于脆弱的能力检查,需要人工干预才能迫使编译脚本配合交叉编译过程。
更重要的是,Makefile 不会以声明的方式列出需求,因此阅读所有内容是了解构建脚本依赖于哪些其他工具的唯一途径。其他构建系统在这方面可能做得更好,但总的来说,你不能指望获得与上一篇文章中相同的无缝体验。
正如我们现在看到的,Redis 也是如此。
交叉编译 Redis
如果您按照上一篇文章中的步骤,为本地目标(即编译过程实际发生的机器)编译了 Redis,那么您需要运行 make distclean,以避免因陈旧资产未被 Make 正确作废而导致的混乱问题。
下面是一些交叉编译调用的示例:
针对英特尔 macOS
make CC="zig cc -target x86_64-macos" CXX="zig c++ -target x86_64-macos" AR="zig ar" RANLIB="zig ranlib" USE_JEMALLOC=no USE_SYSTEMD=no
以 x86-64 Linux(和 musl libc)为目标
make CC="zig cc -target x86_64-linux-musl" CXX="zig c++ -target x86_64-linux-musl" AR="zig ar" RANLIB="zig ranlib" USE_JEMALLOC=no USE_SYSTEMD=no
让我们来分析一下你刚才看到的内容。第一个值得注意的变化是 -target 开关。这部分不言自明:因为我们要编译到另一个目标,所以必须指定是哪个目标。第二个显著区别是出现了一些新的重载,这是怎么回事呢?
仔细阅读构建 Redis 所涉及的 Makefile(是的,有多个),你会发现 redis-cli 和 redis-server 共享 hiredis,这是一个实现通信协议解析器的库。因此,hiredis 被编译为静态库,并包含在两个可执行文件的编译过程中。事实上,Redis 的另一个依赖库 lua 也是如此。
这种编译代码的方法依赖于两个工具:Ar 和 ranlib。事实上,在上一篇文章中也出现过这种情况,当时我们正在进行本机编译,但由于我们已经需要大量的系统工具(如 build-essential 或 Xcode 命令行工具),所以我们可以不必了解这些。但现在我们要为另一个目标构建,因此这些命令需要与交叉编译过程配合,这就要求我们使用 Zig 提供的版本。
现在让我们来谈谈另一个重写,即 USE_JEMALLOC。Redis 默认在 x86-64 Linux 上使用 jemalloc。不幸的是,jemalloc 是一个使用 CMake 的 C++ 库,这让构建过程变得非常复杂。为了简洁起见,我强制使用了 libc 中的 vanilla malloc,但如果你了解 CMake,还是有可能实现跨编译的。请读者自行解决。
编译成功了,我们就大功告成了吗?其实不然。Makefile 中还有一些细节需要我们了解。
修复 Makefile
让我们看看 Redis 主 Makefile (src/Makefile) 中最重要的两行。
uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
uname_M := $(shell sh -c 'uname -m 2>/dev/null || echo not')
这几行使用 shell 命令来获取当前机器的操作系统和架构。在交叉编译时,你可以看到这可能会造成问题。
在这种情况下,解决办法是在命令行中提供重写,但首先我们应该看看这些变量是如何使用的,以便更好地理解用什么来重写它们。
# Default allocator defaults to Jemalloc if it's not an ARM
MALLOC=libc
ifneq ($(uname_M),armv6l)
ifneq ($(uname_M),armv7l)
ifeq ($(uname_S),Linux)
MALLOC=jemalloc
endif
endif
endif
# ...
ifeq ($(USE_JEMALLOC),yes)
MALLOC=jemalloc
endif
ifeq ($(USE_JEMALLOC),no)
MALLOC=libc
endif
本例说明了 uname_M 包含 CPU 架构,而 uname_S 包含操作系统名称。我们还可以看到,该变量与一些关键字的匹配很松散,这意味着我们在覆盖时不必太精确。这项检查对我们来说并不重要,因为我们已经在命令行中禁用了 jemalloc,但它揭示了两个重要变量的使用模式。在 Makefile 中使用操作系统名称和版本的方式并不是标准化的,你需要根据具体情况了解覆盖它们的正确方式。
Makefile 还包含另一个奇怪的能力检查:
# Detect if the compiler supports C11 _Atomic
C11_ATOMIC := $(shell sh -c 'echo "\#include <stdatomic.h>" > foo.c; \
$(CC) -std=c11 -c foo.c -o foo.o > /dev/null 2>&1; \
if [ -f foo.o ]; then echo "yes"; rm foo.o; fi; rm foo.c')
ifeq ($(C11_ATOMIC),yes)
STD+=-std=c11
else
STD+=-std=c99
endif
在这里,你可以看到 Makefile 如何尝试编译一个导入了 stdatomic.h 的 C 程序,并使用退出代码来测试是否支持 C11 功能。在我们的例子中,我们可以肯定地说,Zig 支持 C11,因此可以用命令行覆盖来替代这一检查,或者我们可以不做检查,因为它总是会成功的。
让我们看看 Makefile 中的最后一段话:
# If 'USE_SYSTEMD' in the environment is neither "no" nor "yes", try to
# auto-detect libsystemd's presence and link accordingly.
ifneq ($(USE_SYSTEMD),no)
LIBSYSTEMD_PKGCONFIG := $(shell $(PKG_CONFIG) --exists libsystemd && echo $$?)
# If libsystemd cannot be detected, continue building without support for it
# (unless a later check tells us otherwise)
ifeq ($(LIBSYSTEMD_PKGCONFIG),0)
BUILD_WITH_SYSTEMD=yes
LIBSYSTEMD_LIBS=$(shell $(PKG_CONFIG) --libs libsystemd)
endif
endif
如果是为 Linux 编译,Redis 会尝试检测您是否需要 SystemD 支持,但由于我们是交叉编译,因此需要明确说明我们的意图。
为了简洁起见,我只是在编译命令中禁用了 SystemD 支持,但如果你想启用它,就必须获取正确的头文件,并将其提供给编译过程,这可能还需要重写上面代码片段中的一些逻辑。
还有更多依赖 shell 命令执行的检查示例,我把它们留给读者自己去寻找。
根据我们在 Makefile 中找到的内容,这是一组交叉编译 Linux 的重载示例: uname_S="Linux" uname_M="x86_64" C11_ATOMIC=yes USE_JEMALLOC=no USE_SYSTEMD=no。
最后的命令
make CC="zig cc -target x86_64-linux-musl" CXX="zig c++ -target x86_64-linux-musl" AR="zig ar" RANLIB="zig ranlib" uname_S="Linux" uname_M="x86_64" C11_ATOMIC=yes USE_JEMALLOC=no USE_SYSTEMD=no
无论你使用的是哪个操作系统,都可以运行相同的命令来发布 Redis。
Windows
Redis 并不针对 Windows,但如果你有通过 msys、cygwin 或 mingw 制作的软件,仍可使用上述命令在 Windows 下交叉编译到 Linux 或 Mac。
下一步是什么?
处理构建脚本很快就会失控,即使是在 Redis 这样相当严谨的项目中,你也能看到多个构建系统是如何交织在一起,使一切都变得毫无必要的笨拙。
在下一篇文章中,我们将介绍如何摆脱所有这些构建系统,只依赖 zig build。这不仅能让你更容易理解构建过程,还能让交叉编译完全无缝进行,并将你从 Make、CMake 等所需的所有系统依赖关系中解放出来。
这意味着我们甚至不需要构建必备程序、XCode 或 MSVC 就能构建 Redis。
迄今为止,Windows 并没有受到太多的青睐:Redis 无法在 Windows 上运行,虽然你可以使用 Windows 为其他操作系统交叉编译,但对 Make、CMake、shell 脚本等的依赖并没有带来真正的帮助。在下一篇文章中,这一切都将改变。
可重复性脚注
Zig 0.8.1,Redis commit be6ce8a。