传统的交叉构建由载荷承担。一份工具链为一个目标而构建,它的驱动只有一个答案, 到达第二个目标意味着获取第二份工具链。因此一个发行方必须发布的载荷数, 等于它支持的宿主-目标对数。
openkal 改变了「被交叉的是什么」。目标侧 —— 平台接口、C 库、编译器运行时与 C++ 运行时 —— 成为一组由依赖图解析、并由当前运行的编译器从源码构建的包。 留给编译器的是代码生成,而一个 Clang 二进制发出它被构建时所包含的每一种对象格式。
本文陈述该模型、工程需要书写的内容、生态所供给的内容,以及已被实测的界限。
一个由 N 个平台与 M 个架构构成的生态,需要的是一个接口的 N 份实现, 而不是 N×M 份工具链。这个计数来自目标侧所在的位置:一个从源码构建的包, 是为编译器被要求发出的那个目标而构建的,因此一份平台实现只写一次, 就到达编译器支持的每一个架构。
该论断由一个三宿主 × 三目标的矩阵验证,每一格构建同一份源码并运行其结果。
[dependencies]
openkal-llvm-runtime = "0.1.1"
[toolchain]
default = "llvm@22.1.8"两行。第一行选定目标侧的三个层;第二行命名一个编译器, 并且对其余一切来自何处只字未提。
目标在命令行上给出:
mcpp build --target x86_64-linux
mcpp build --target aarch64-macos
mcpp build --target x86_64-windows-gnu宿主目标不需要 [target.<三元组>] 段,源码中也不需要任何预处理指令。
可运行的示例见 examples/06-openkal-cross。
| 包 | 层 | 内容 |
|---|---|---|
openkal |
— | 规范,以及声明它的那些 C++ 模块 |
openkal-linux |
kernel-abi |
参考实现,建立在 Linux 系统调用之上 |
openkal-macos |
kernel-abi |
建立在 macOS 的系统调用面之上 |
openkal-windows |
kernel-abi |
建立在 Win32 与对象管理器之上,不使用任何 C 运行时符号 |
openkal-opensbi |
kernel-abi |
建立在 RISC-V 的 SBI 之上,无操作系统 |
openkal-uefi |
kernel-abi |
建立在 UEFI Boot Services 之上,在操作系统存在之前 |
openkal-musl |
c-abi |
被重定向到 openkal 的 musl,只移植一次 |
openkal-llvm-runtime |
compiler-runtime、c++-abi |
compiler-rt builtins、libunwind、libc++abi 与 libc++,为 openkal-musl 配置 |
一个工程命名其中最后一个。其余由它的依赖推出。
openkal-llvm-runtime 把这项要求声明出来,而不是留待被发现:
requires = ["mcpp:compiler=llvm"]它的源码是 libc++ 的,其中 std 模块源尤其由 Clang 编译。
把那份源码交给 GCC,会在 libc++ 自己的头文件深处失败,
其消息命名一个读者从未打开过的文件:
fatal error: __config: No such file or directory
有了这项声明,构建在编译任何东西之前拒绝该组合, 并指出选择一个满足它的编译器的那条命令。
mcpp 自身词表的目标行可以携带一条工具链约定。该约定命名的是 供给该目标 C 库的那份载荷,并且仅在两个条件同时成立时生效: 清单对该目标未作陈述,且依赖图中没有任何东西供给该目标的系统。
第二个条件只有在图被解析之后才可知。因此一个 C 库来自 openkal-musl 的工程
保留它所要求的编译器,而一个没有依赖的工程仍然收到该行所命名的载荷。
两种行为均经实测;提前按任一方向决定,对另一方向都是错的。
在 Linux 上,目标三元组的第三段命名 C 库。在 openkal 之下 C 库来自图, 因此一个命名了 C 库的三元组陈述的是一项图未必兑现的请求:
mcpp build --target x86_64-linux-gnu # 请求 glibc
c-abi musl (openkal-musl@0.3.3, graph)
以图为准。省略该段即不作请求,并产出相同的产物:
mcpp build --target x86_64-linux
该段存在且不一致时,构建报出这一点。它是报出而非拒绝,
因为该段是被忽略而非被违反。在一台宿主上实测 x86_64-linux 与 x86_64-linux-musl:两个可执行文件不同,strip 之后逐字节相同。差异在调试信息里,它记录了输出目录,而目录以三元组命名。代码是同一份代码。
在 Windows 上,同一段命名的是对象 ABI —— gnu 是 PE 加 GNU ABI,
msvc 是 PE 加微软的 —— 而两者都与不止一种 C 库相容。
因此该项报出被限定在该段命名 C 库的那些平台上。
一个没有操作系统的目标,是同一个模型,只是平台层由固件而非内核供给。
riscv64-none-elf 之上的 OpenSBI 运行与宿主目标相同的源码,含 import std,
因为它所使用的标准库是图供给的那份,而不是编译器自带的那份。
有两样东西必须声明,两者都是板子的性质而非默认值:
[target.riscv64-none-elf]
sysroot = ""
runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic",
"-no-reboot", "-bios", "default", "-kernel"]sysroot = "" 选定零 libc 档。使用哪个机器模型与哪种固件模式是板子的事实,
而一个去猜测它的引擎,是另一块板子必须与之搏斗的引擎。
「同一份源码」是关于工具链与标准库的断言,而它成立:import std 可用,
C++ 运行时是图供给的那份,没有任何 #if 区分目标。它不是「任何程序都能
为任何目标构建」的断言,而规范把原因写得很清楚。
一个裸机后端提供一部分接口而不提供另一部分。openkal-opensbi 提供
abort、stream、memory、env 与 time;它不提供文件系统也不提供任务,
因为这台机器没有。6.1 条把这种缺席定为链接期的事实:
实现不提供的接口,作为链接期定义是缺席的,使用它的消费者链接失败。
因此能力字回答的问题比它初看上去更窄。它说的是一个实现在它提供的接口内 如何表现 —— 名字是否区分大小写、一个时钟的粒度是多少。接口是否存在, 由更早的东西回答:依赖图,以及退而求其次的链接器。
kal_fs_props 的地址,于是在整份源码没有任何
文件系统调用的情况下链接失败。在后端把那个字定义为零可以消掉这个错误,
而它恰是该条禁止的唯一补法 —— 程序随后越过了链接器存在的意义。
这条路走过,发布为 openkal-opensbi@0.1.3,并已撤回。
一台没有操作系统的 x86_64 机器有两种到达方式,区别在于谁加载这个程序。
| 路线 | 目标 | 平台层 | 入口 |
|---|---|---|---|
| UEFI 应用 | x86_64-windows-gnu |
openkal-uefi |
固件,Boot Services 可用 |
| 内核,或裸机 | x86_64-none-elf |
无,或 openarch |
复位向量,其下一无所有 |
UEFI 应用是 PE/COFF,经微软 x64 调用约定进入。两者都是 LLVM 工具链已有的性质,
因此它的目标与一个 Windows 程序是同一个三元组,而固件函数指针被直接调用。
把它与一次 Windows 构建区分开的,是图解析出的平台接口实现,
以及三个选定 IMAGE_SUBSYSTEM_EFI_APPLICATION 的链接 flag。
一个内核没有固件服务可以调用。它的目标是 x86_64-none-elf,即零 libc 档:
编译行上没有 C 库,链接上没有库目录,#include <stdio.h> 不解析。
程序在它自己的 _start 被进入,并直接触达硬件。
openarch 是这类程序所建立于其上的层。它不是平台接口,也不应答
mcpp:kernel-abi;它是架构机制 —— 执行上下文、陷阱、每 CPU 状态与地址空间 ——
作为一个跨若干指令集的接口呈现,每个指令集一个后端包。
一个内核依赖它,并供给自己的平台层,或者不供给。
riscv64-none-elf 与 aarch64-none-elf 是表中的行,除此之外别无他物:
Clang 对两者都有 BareMetal 工具链,自行驱动它们的链接并到达 ld.lld。
它对 x86_64 没有,于是那个三元组落到通用的 GCC 工具链,而后者的链接器是宿主的 g++:
g++: error: unrecognized command-line option '-fuse-ld=…/ld.lld'
对裸 x86_64 三元组的每一种拼写都实测过,且无法由任何 flag 纠正。
因此该行携带一个链接器 emulation,由 mcpp 自己调用 ld.lld ——
这也是为什么必须证明宿主工具链没有参与这样一次链接。
三条,记录在此是因为每一条都由构建而非由阅读发现。
一个后端必须定义每一个能力字。 规范的查询是属性对象上的 inline 函数,
因此一个仅仅询问是否存在文件系统的程序,会取 kal_fs_props 的地址。
一个省略了自身所缺层的能力字的后端,使该问题恰恰在这个问题为之存在的那类机器上
链接失败。
同一层的两个供给者是错误而非选择。 C 库、平台接口与 C++ 运行时互斥。 选错不会使链接失败,它产出一个能够运行且间歇性不能运行的程序。
载荷的 C++ 运行时不能位于外来的 C 库之上。 它的 __config_site 记录了
它被构建时的配置。解析器的结构在默认路径上阻止该组合,
而一条诊断覆盖工程显式覆写该契约的那些路径。
docs/14 — 目标侧 给出五个层、四种来源与规则。 SPEC-002 给出能力语法的规范性陈述。