- 文档
【免费下载链接】rfcs
RFCs for changes to Rust
本文围绕 Rust 官方 RFC 1721(text/1721-crt-static.md)展开,系统讲解 rustc 如何在不新增 target 的前提下,通过三个正交机制让同一份标准库二进制既能静态链接也能动态链接平台 C 运行时(CRT)。读完本文你将掌握crt-statictarget-feature 的命令用法、CARGO_CFG_*构建脚本环境变量的原理,以及 MSVC(/MTvs/MD)与 musl 目标下静态/动态 CRT 的完整配置方法,并理解它与后续 build-std、-Clink-self-contained等机制的演进关系。
背景与动机:各目标为何被"锁死"在单一 CRT 链接方式
在 RFC 1721 提出之前,rustc 的所有目标都在硬编码"如何链接原生 C 运行时"这一决策:
x86_64-unknown-linux-gnu动态链接 glibc;x86_64-unknown-linux-musl静态链接 musl;x86_64-pc-windows-msvc动态链接 MSVCRT。
这些默认决策对很多真实场景并不合适:例如 Alpine Linux 上的二进制往往希望动态链接 musl;而要在 Windows 上构建可移植(不依赖目标机安装 MSVC 运行库)的二进制,最省事的方式恰恰是静态链接 MSVCRT。
当时 rustc 没有任何机制可以切换这些链接方式——唯一办法是定义一个全新的 target 规格,并为此额外分发一套重新编译的标准库。由于 target 规格必须用 target triple 描述,而 target triple 已有既定的命名惯例,这套方案无法自然融入,因此社区一直拒绝这种做法。RFC 1721 正是要打破这一僵局。
整体设计:三个正交且通用的机制
RFC 的核心洞察有两点:
- std 的对象代码基本不需要随 CRT 链接方式变化。虽然 Windows 上通常需要根据链接类型正确管理 dllimport/dllexport 属性,C 代码也需要按链接类型用特定选项编译,但从技术上讲,只需正确操纵传给链接器的参数,就能用同一份 std 二进制同时产出静态或动态链接 libc 的 Rust 可执行文件与动态库。
- 存在多个尚未被满足的同类需求——配置硬件架构特性、底层平台或运行时特性,这些都需要"整个世界"(可能包含 std)以某种特定方式编译。C 运行时链接正是这种需求的又一个实例。
基于此,RFC 提出了三个可以独立使用、组合后即能切换 CRT 链接方式的机制:
- 用
-C target-feature=+crt-static/-C target-feature=-crt-static指定 CRT 链接方式。这把-C target-feature的语义从 LLVM 的"CPU 特性"扩展为"Rust 目标的特性",并复用该 flag 已有的+(添加)与-(移除)能力,以及自动降级为cfg值的既有行为。 - 把
cfg值降级为 Cargo 构建脚本环境变量,让 build script 能获知目标已启用的全部特性(如crt-static),从而在 MSVC 上正确编译 C 代码。 - 惰性链接属性(lazy link attributes),仅供 std 自带的 libc crate 副本使用,因为 std 以二进制形式分发,短期内 Cargo 尚无法自行重建 std。
需要强调的是,该 RFC不打算统一各平台的 CRT 链接方式(如一律动态或一律静态),而是把决策权留给每个目标以及未来的工作。
机制一:通过-C target-feature指定动态/静态 CRT 链接
编译器将为相关目标支持一个新的target-feature:crt-static。启用与禁用方式:
rustc -C target-feature=+crt-static ... rustc -C target-feature=-crt-static ...在设计上,此前所有target-feature都会被原样透传给 LLVM;本 RFC 扩展了其含义,使其同样承载 Rust 目标自身的特性。target 规格可以声明允许定义哪些自定义 target-feature,绝大多数既有目标将新增一个默认关闭的crt-static特性(musl 除外)。
crt-static的默认值因目标而异,这是该设计的重要细节:
x86_64-unknown-linux-musl默认开启(+crt-static);arm-unknown-linux-musleabi默认关闭(-crt-static)。
由于 target-feature 会被自动降级为cfg值,下游 crate 可以据此在编译期判断 CRT 的链接方式,这正是后续 build script 与惰性#[link]属性的基础。
机制二:cfg值降级为 Cargo 构建脚本环境变量
Cargo 将把编译器产出的cfg值转发给 build script。目前编译器已支持--print cfg来打印内部 cfg 指令,Cargo 也正借此实现平台相关依赖;本 RFC 在此基础上,为 build script 增加一族CARGO_CFG_*环境变量:对rustc --print cfg输出的每一个键,Cargo 都会设置一个环境变量供 build script 读取。
以 Linux 目标为例,本地rustc --print cfg的输出为:
target_os="linux" target_family="unix" target_arch="x86_64" target_endian="little" target_pointer_width="64" target_env="gnu" unix debug_assertions相应地,Cargo 将为该目标的 build script 设置:
export CARGO_CFG_TARGET_OS=linux export CARGO_CFG_TARGET_FAMILY=unix export CARGO_CFG_TARGET_ARCH=x86_64 export CARGO_CFG_TARGET_ENDIAN=little export CARGO_CFG_TARGET_POINTER_WIDTH=64 export CARGO_CFG_TARGET_ENV=gnu export CARGO_CFG_UNIX export CARGO_CFG_DEBUG_ASSERTIONS由于 C 标准库的链接方式被建模为 target-feature 并降级为cfg值,build script 从此能据此调整编译选项。这里有一个关键细节:Rust 的cfg值可以被定义多次(target-feature 正是如此)。当同一cfg值被多次定义时,Cargo 会合并为一个环境变量,值之间用逗号分隔。例如某目标启用了以下特性:
target_feature="sse" target_feature="crt-static"Cargo 会转换为:
export CARGO_CFG_TARGET_FEATURE=sse,crt-static通过这种方式,build script 就能获知 C 标准库当前的链接方式。这对 MSVC 目标至关重要——该目标上 C 代码必须根据 CRT 的链接方式采用不同的编译选项。
该机制还带来一个额外收益:build script 可以顺带获知所选的 CPU 特性。一旦target_feature的#[cfg]稳定,build script 就能知道其要编译的 C 代码是否启用了 SSE/AVX 等特性。
RFC 还给出了gcc-rscrate 的改造方案:解析CARGO_CFG_TARGET_FEATURE为特性列表;在 MSVC 目标上,若未启用crt-static,则以/MD(动态链接)编译 C 代码,否则以/MT(静态链接)编译。由于当时的 MSVC 目标默认就是动态链接且 gcc-rs 一直用/MD,这一改造可保证 gcc-rs 对新旧 MSVC Rust 工具链前后兼容,直到未来决定将 MSVC 工具链默认切换为+crt-static。
机制三:惰性链接属性#[link(name = "...", cfg(..))]
第三个机制是让编译器能"惰性"解释原生库的链接需求——求值依据不是声明#[link]指令所在 crate 的cfg,而是最终执行链接的下游 crate 的cfg。#[link]属性将新增一个cfg(..)参数:
#[link(name = "foo", cfg(bar))]含义是:仅当bar指令被匹配时,该#[link]注解才生效。编译器会在两个环节利用这一信息:
- 当需要应用
dllimport/dllexport时,评估最终编译单元的#[cfg]指令,判断上游#[link]是否生效; - 决定链接哪些原生库时,同样依据最终编译的
#[cfg]指令与上游#[link]指令求值。
该特性从不打算稳定,定位是链接到 std 的 libc crate(而非 crates.io 上稳定的 libc crate)的 unstable 实现细节。关于 dllimport/dllexport 与静态/动态链接的关系,可参考仓库中的 text/1717-dllimport.md:该 RFC 说明,Windows 上从动态库导入符号与从静态库导入符号时生成的代码不同,kind="static"表示静态链接(不应用 dllimport),kind缺省为dylib(应用 dllimport)。
落地实现:libc crate 中的条件链接声明
组合上述特性后,为让下游 crate 在编译期切换 CRT 链接,libccrate 将按如下模式改造:
cfg_if! { if #[cfg(target_env = "musl")] { #[link(name = "c", cfg(target_feature = "crt-static"), kind = "static")] #[link(name = "c", cfg(not(target_feature = "crt-static")))] extern {} } else if #[cfg(target_env = "msvc")] { #[link(name = "msvcrt", cfg(not(target_feature = "crt-static")))] #[link(name = "libcmt", cfg(target_feature = "crt-static"))] extern {} } else { // ... } }语义如下:
- 对 musl 目标:若 CRT 静态链接,则名为
c的库以kind = "static"静态包含进 libc.rlib;若动态链接,则c库动态链接。 - 对 MSVC 目标:静态 CRT 链接到
libcmt,动态 CRT 链接到msvcrt(与当时现状一致)。
实战:MSVC 静态链接与 musl 动态链接
组合以上机制,最终用户侧的操作非常直观。
在 MSVC 目标上静态链接 C 运行时(Windows 下生成可移植二进制):
set RUSTFLAGS=-C target-feature=+crt-static cargo build --target x86_64-pc-windows-msvc在 musl 目标上动态链接 C 运行时(Alpine 场景):
RUSTFLAGS='-C target-feature=-crt-static' cargo build --target x86_64-unknown-linux-musl注意第一个示例面向 Windows 的 cmd shell 使用set,第二个面向 Unix shell 直接以内联环境变量形式传递,这正是RUSTFLAGS跨平台传递的两种常见写法。对 musl 目标而言,-crt-static恰好覆盖了默认值+crt-static,从而在不重建 std 的情况下切换到动态链接。
演进与后续工作:从RUSTFLAGS到 build-std 时代
RFC 明确指出,这三个特性只是"支持配置 CRT 链接方式所需的最简骨架"。其主要缺点与 CPU 特性类似:选择非默认链接方式时必须借助RUSTFLAGS,较为笨拙。未来计划提供更符合人体工学的"编译代码生成选项"配置方法,统一覆盖target-cpu与 CRT 链接等场景。
RFC 还承认,从"更正确"的角度看,Rust 在 MSVC 上或许应默认静态链接 CRT(而非动态);由于 C 组件的编译方式,这在当时属于破坏性变更,但本 RFC 落地后,未来经过合理过渡期再切换默认值就不应再是破坏性变更。
该设计的隐含承诺是:已分发的 std 工件应同时可用于动态与静态链接 CRT。不过 MSVC 上代码会因是否链接动态库而编译不同,std 对 MSVCRT 的使用极少,短期内不是问题,但有掣肘未来的风险。为此 RFC 希望 Cargo 最终支持"自定义编译标准库"(即 build-std 思路),crt-static特性仅作为该逻辑的一个输入:一旦与上游工件不一致,Cargo 就自定义编译 std。
仓库中后续的 RFC 印证了这一演进方向:
- text/3874-build-std-always.md(L487-L488)指出:
unwindcrate 会继续链接系统的libunwind,其目标修饰符必须与 std 一致;同样,若使用llvm-libunwind、-Clink-self-contained=yes或-Ctarget-feature=+crt-static,则使用分发的libunwind时也必须匹配 std 的目标修饰符,否则会出现不兼容。 - text/3874-build-std-always.md 进一步说明,
linux-musl等目标需要crt1.o、crti.o、crtn.o等特殊对象文件,稳定的-Clink-self-containedflag 会在预期位置查找这些对象(通常由rust-std组件填充),其中linux-musl、windows-gnu、wasi目标默认依赖此机制。 - text/3873-build-std-context.md 也确认:启用
llvm-libunwind、-Clink-self-contained或-Ctarget-feature=+crt-static时,会静态链接 std 组件中分发的预构建libunwind(若存在)。
可见crt-static不仅单独有用,还深度参与后续-Clink-self-contained、build-std 等机制的协同工作。
权衡与备选方案
RFC 坦承了若干缺点:
- 使用
RUSTFLAGS较笨拙,但已有更优雅的 codegen 配置方案规划; - 引入一个永不稳定的特性(
#[link(.., cfg(..))])略显遗憾,但它绕开了若干棘手问题;稳定的语义是"对某些目标,--cfg crt_link=...指令影响 CRT 的链接方式"; #[link(cfg(..))]的"惰性"语义从名字上不易理解(没有任何其他cfg属性采用此语义),但该特性仅服务于单一实现目的、不打算稳定,影响较小。
设计过程中也考虑过多种替代方案,均被否决或搁置:
- 新增全新 target(如
x86_64-pc-windows-msvc-static):命名约定不佳,无法扩展到target-cpu等其他 codegen 选项,且需开始分发新工件,代价过高; - 在 target 名中内嵌元数据(如
x86_64-pc-windows-msvc+static):设计空间过大,可能与 Cargo 和 build script 配合不佳; - 编译器读取环境变量指示 CRT 链接:需为编译器与 build script 分别建立通道;
- 新增独立 flag(如
-C custom-feature):即不扩展-C target-feature语义,另起炉灶。
遗留问题
RFC 明确留下了两个未决问题:
cfg值转为环境变量时,若值本身包含逗号该如何处理?这是罕见的边界情况,build script 不应依赖此类值,但仍需妥善处理;- "惰性链接真的只有 std 的 libc 需要吗?"——如果未来分发除 std 之外的更多预编译二进制,情况又会如何?
延伸阅读(仓库内)
- RFC 1721 原文:crt-static:本文依据的完整 RFC,含 MSVCRT 静态链接、musl 动态链接等原始议题线索;
- RFC 1717:dllimport:Windows 上符号导入与静态/动态链接的代码生成差异;
- RFC 3873:build-std 上下文 与 RFC 3874:always build-std:
crt-static与-Clink-self-contained、libunwind在标准库重建时代的协同细节。
- 文档
【免费下载链接】rfcs
RFCs for changes to Rust
相关推荐
rust-bindgen `--wrap-static-fns` 完全指南:为 C 静态函数自动生成可链接的 C 包装代码
rust bindgen wrap static fns 完全指南:为 C 静态函数自动生成可链接的 C 包装代码 本篇技术指南围绕 rust bindgen
Apache OpenDAL C++ 绑定依赖全解析:Rust 静态库 + 零额外 C++ 运行时依赖
Apache OpenDAL C++ 绑定依赖全解析:Rust 静态库 + 零额外 C++ 运行时依赖 OpenDAL 的 C++ 绑定( bindings/c
数据存储后端Rust RFC 0404 深度解读:`-C prefer-dynamic` 语义重构与 dylib 静态优先链接策略
Rust RFC 0404 深度解读: C prefer dynamic 语义重构与 dylib 静态优先链接策略 导读 :本文基于本仓库中的 RFC 0404
文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考