- 文档
【免费下载链接】rfcs
RFCs for changes to Rust
导读
cdylib是 rustc 提供的一种专用 crate-type,它让 Rust 代码可以被编译成面向 C 接口导出的动态共享库(.so / .dylib / .dll),并被 C、C++、Python、Java、.NET 等其他语言或宿主程序动态加载。本篇文章以 RFC 1510(text/1510-cdylib.md) 为核心,结合本仓库中与之相关的后续 RFC,系统梳理cdylib的诞生动机、与dylib(rdylib)的本质差异、元数据与符号可见性规则、LTO 与链接策略,以及在 Cargo 生态中的完整实战用法。读完本文,你将掌握如何在Cargo.toml、#![crate_type]与rustc --crate-type之间正确选择构建方式,并理解符号导出、FFI 导出、链接策略背后的编译器原理。
一、背景:Rust 的两种动态库形态
RFC 1510 指出,在引入cdylib之前,rustc 支持两种动态库生成模式:
- rdylib(Rust 动态库):面向"Rust 与 Rust 之间"的复用场景。这种库会暴露所有 Rust 符号,并动态链接标准库等依赖,主要用于编译器自身实现插件等场景。
- cdylib(C 动态库):面向"把 Rust 应用嵌入其他语言/应用"的场景。它与 rdylib 的主要区别是更倾向于静态链接其他 Rust 库(把它们打包进自身),最终对外导出 C API。
但当时的功能平衡过度偏向使用面极小的 rdylib。由于 Rust 默认静态链接且 ABI 不稳定,rdylib 实际使用很少,却给 cdylib 使用场景强加了许多不必要的约束:
- 元数据(Metadata):所有动态库都包含 Rust 编译元数据段,但"把 Rust 加载到别处"时根本用不到它;
- 符号可见性:动态库暴露的是"可达(reachable)"符号,而"把 Rust 加载到别处"时,和可执行文件一样,只需要导出公共的、非 Rust-ABI 的函数。过度暴露会导致库体积不必要地膨胀,也会错失"函数实际上是私有的"这一优化机会;
- LTO:无法为 dylib 运行 LTO,因为 dylib 面向最终产品,而非中间产物。
RFC 1510 的动机正是通过引入一种新的 crate-type 来解决这些缺点——即今天的cdylib。
二、核心设计:cdylib与dylib的四大差异
RFC 1510 规定,编译器新增接受cdylib这一 crate type,可通过两种方式启用:
rustc --crate-type cdylib或通过 crate 属性:
#![crate_type = "cdylib"]需要注意的是,两种 crate-type 产出的字面工件(文件、文件名等)完全相同——区别在于内部结构。RFC 明确列出四大差异:
| 维度 | dylib(rdylib) | cdylib |
|---|---|---|
| 元数据 | 库中包含 Rust 编译元数据段 | 不包含元数据段 |
| 符号可见性 | 像 rlib 一样暴露全部符号 | 像可执行文件一样暴露符号 |
| LTO | 不允许 | 允许 |
| 链接方式 | 默认动态链接其他 Rust 库(如标准库) | 默认静态链接所有 Rust 依赖 |
其中符号可见性规则尤为关键:pub fn foo() {}不会被导出,而#[no_mangle] pub extern fn foo() {}才会被导出。同时编译器还可以向链接器传递额外标志,主动隐藏来自被链接库的 Rust 导出符号。
三、符号导出规则:#[no_mangle]与extern "C"
在cdylib中,能被外部看见的只有两类符号:
#[no_mangle] pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b }这里有两个关键点:
extern "C":声明函数使用 C ABI,这是与 C/C++ 互操作的前提;#[no_mangle]:关闭 Rust 名称修饰(name mangling),使最终符号名与函数名一致,外部才能按名字查到。
普通的pub fn即使带pub,在cdylib中也不会被导出——这正是 RFC 1510 与可执行文件行为对齐的体现。符号可见性相关的后续演进,可以参看本仓库中的 RFC 3834(text/3834-export-visibility.md),它提出用#[export_visibility = "target_default"]覆盖#[no_mangle]的默认公开导出行为,并可用-Zdefault-visibility=hidden配合隐藏符号,从而减小二进制体积、加快 DSO 加载并避免符号名碰撞引发的未定义行为。该 RFC 指出,#[no_mangle]/#[export_name]符号在rustc中会被强制设置为最大可见性(对应rustc_monomorphize/src/partitioning.rs中的符号导出逻辑),这正是 cdylib 场景下控制导出面时的核心机制。
四、缺点与备选方案(RFC 1510 原文)
RFC 1510 坦诚地记录了自身局限与备选设计:
- 缺点:Rust 的链接模型本身"临时且定义不清",本 RFC 是对该模型的扩展,可能在与该输出格式的使用场景之间出现不可预见的交互。
- 备选方案:最初提议新增的 crate-type 是
rdylib,并把现有dylib重新解释为 cdylib 使用场景。但这属于破坏性变更,需要在 Cargo 中为插件准备相当复杂的过渡计划,最终认为不值得为"cdylib 可能正是你想要的"这一收益而付出代价。 - 未决问题:现有
dylib格式是否应被视为不稳定(是否要求 nightly 编译器)?由于 Rust 动态库的使用场景极其有限且易变,RFC 建议默认对其访问加以门控。
五、实战一:通过 Cargo.toml 构建 cdylib
在实际工程中,最常用的方式是修改Cargo.toml的[lib]段:
[lib] name = "myffi" crate-type = ["cdylib"]crate-type 的取值在 Cargo 文档中与本 RFC 一致,常用的有:
"lib":默认,生成 rlib(Rust 库);"cdylib":C 兼容动态库;"staticlib":C 兼容静态库(.a/.lib);"dylib":Rust 动态库(rdylib)。
关于[lib]目标配置的完整字段(如path、test、doctest、crate-type等),本仓库的 RFC 3772(text/3772-build-target-edition.md) 有完整的清单引用。构建后产物命名规则为lib<name>.so(Linux)、lib<name>.dylib(macOS)、<name>.dll(Windows)。
一个库同时产出多种格式
一个 crate 可以同时声明多个 crate-type:
[lib] crate-type = ["lib", "staticlib", "cdylib"]这样既可供 Rust 依赖方使用 rlib,又可供 C 侧分别选择静态或动态链接。RFC 1510 提到"同一仓库中 RFC 3028(text/3028-cargo-binary-dependencies.md) 的artifact依赖机制"正是围绕cdylib/staticlib/bin三类产物展开的:Cargo 允许其他 crate 以artifact = "cdylib"的形式依赖某个包产出的动态库,并在构建期通过CARGO_CDYLIB_FILE_<DEP>环境变量拿到产物完整路径。例如:
[dependencies] mypreload = { version = "1.2.3", artifact = "cdylib" }// main.rs const MY_PRELOAD_LIB: &[u8] = include_bytes!(env!("CARGO_CDYLIB_FILE_MYPRELOAD"));六、实战二:命令行方式指定 crate-type
如果不希望改动Cargo.toml,可以直接用 rustc:
rustc --crate-type cdylib src/lib.rs -o libfoo.so也可以借助 Cargo 的cargo rustc在命令行覆盖 manifest 中的设置——这正是 RFC 3180(text/3180-cargo-cli-crate-type.md) 引入的能力:
cargo rustc --crate-type cdylib --features ffi其规则为:
--crate-type取值与 manifest 中允许的值一致,可传逗号分隔的多个值;- 仅对构建
lib或example生效; - 若同时提供了命令行参数与 manifest 中的
crate-type列表,命令行参数优先(覆盖而非追加)。
该 RFC 的动机说明与cdylib场景直接相关:有些 crate(如 hyper)同时提供 Rust API 与可选 C API,多数 Rust 用户只需要rlib,强制编译cdylib属于浪费;跨平台共享库时,iOS 需要staticlib,Android 与 WASM 需要cdylib,而在 Rust 中作为依赖又需要rlib,命令行覆盖能显著缩短构建时间。
七、实战三:Windows 下导出序号与符号控制
在 Windows 上构建cdylib(即 DLL)时,符号导出行为与 ELF/Mach-O 平台不同。RFC 3641(text/3641-export-function-ordinals.md) 专门处理了"按序号导出"的需求:某些应用(如 Microsoft Detours)要求特定函数必须是固定 Ordinal(如DetourFinishHelperProcess必须为 Ordinal 1)。
在 RFC 3641 之前,只能借助.def文件与build.rs:
; lib.def LIBRARY EXPORTS DetourFinishHelperProcess @1// build.rs pub fn main() { let lib_def = "path/to/lib.def"; println!("cargo:rustc-cdylib-link-arg=/DEF:{}", lib_def); }其最大缺点是:一旦指定.def文件,就必须为 DLL 中每个要导出的函数都指定序号,否则它们不会出现在生成的.lib中。RFC 3641 提出用#[unsafe(export_ordinal(n))]属性直接在源码中标注:
#[no_mangle] #[unsafe(export_ordinal(1))] pub fn hello() {} // 也支持 extern / unsafe 函数 #[no_mangle] #[unsafe(export_ordinal(2))] pub unsafe extern "C" fn world() {}序号n必须是>= 1的正整数,且在整个程序中唯一(重复会被报错);该属性只影响 Windows 目标;未标注时编译器会在编译期分配一个未使用的序号。这与link_ordinal属性(按序号导入)形成对称。
八、与#[used]、全局分配器等其他机制的配合
cdylib 与其他编译器机制常有交集,仓库中有多处佐证:
- RFC 2386(text/2386-used.md) 在解释
#[used]时明确以cdylib作为"共享库"产物的示例:rustc 先把 crate 编译成目标文件,再调用链接器生成可执行文件或共享库(当 crate type 为"cdylib"时即.so)。#[used]只保证静态变量保留在目标文件中,并不影响链接器行为。 - RFC 1974(text/1974-global-allocators.md) 提到,在
cdylib/staticlibcrate 中选择全局分配器,应当是该产物唯一需要关注的分配器选择点——这类 C 兼容产物通常会被嵌入其他语言运行时,分配器选择直接影响跨语言内存安全。
九、常见问题与设计权衡回顾
为什么pub fn在 cdylib 中默认不被导出?
这与可执行文件的行为一致:只有显式以extern "C"+#[no_mangle]标注的符号才是 FFI 边界。这种设计让编译器能进行更多优化(把函数当作私有处理),也让库的导出面最小化。若需要导出可执行文件(而非动态库)中的符号,本仓库的 RFC 2841(text/2841-export-executable-symbols.md) 提出通过-C export-executable-symbols让可执行文件像动态库一样导出符号,服务于"宿主程序内嵌 JVM/CLR 等运行时并让它们回调 Rust"的场景。
LTO 为什么对 cdylib 开放?
因为 cdylib 是最终产物(被外部加载的成品库),可以安全地进行跨 crate 的 LTO;而 rdylib 是中间产物,还要参与后续链接,不允许 LTO。这也是两者在编译器优化管线中的根本区别之一。
如何进一步控制导出符号的可见性?
在 nightly 上,可以结合-Zdefault-visibility=hidden与#[export_visibility = "target_default"](见 RFC 3834),把默认公开导出的#[no_mangle]符号改为目标平台默认可见性,从而:
- 缩小二进制体积(避免跨语言 LTO 无法消除的重复副本与死代码);
- 加快 DSO 加载(缩小动态符号表);
- 防止外部代码依赖内部实现细节;
- 减少符号名碰撞导致的未定义行为(详见该 RFC 中对 Chromium 实际事故的案例分析)。
十、总结
RFC 1510 以一次小而精准的编译器扩展,将"Rust 动态库"与"C 接口动态库"两种完全不同的使用形态彻底区分开:cdylib无元数据、按可执行文件的规则导出符号、允许 LTO、默认静态链接 Rust 依赖,因而成为 Rust 跨语言 FFI 动态库的标准构建方式。围绕它,Cargo 生态逐步补齐了cargo rustc --crate-type(RFC 3180)、artifact 依赖(RFC 3028)、Windows 导出序号(RFC 3641)、导出可见性控制(RFC 3834)等配套能力。对于任何需要"把 Rust 编译成 .so/.dylib/.dll 供 C/C++ 或其他语言加载"的开发者,理解本文所述的符号导出规则与链接策略,是写出正确、精简、可维护 FFI 库的前提。
延伸阅读(仓库内相关文档)
- RFC 1510 原文:cdylib
- RFC 3180:cargo rustc --crate-type 命令行参数
- RFC 3028:Cargo 对 bin/cdylib/staticlib 的 artifact 依赖
- RFC 3641:Windows cdylib 导出序号属性
- RFC 3834:export_visibility 符号可见性控制
- RFC 2841:可执行文件导出符号
- RFC 2386:#[used] 与 cdylib 编译流程
- RFC 1974:全局分配器与 cdylib/staticlib
- 文档
【免费下载链接】rfcs
RFCs for changes to Rust
相关推荐
Rust 宏系统改革(RFC 0453):$crate、macro_use 与宏的导入导出机制
Rust 宏系统改革(RFC 0453):$crate、macro_use 与宏的导入导出机制 <output_article Rust 宏系统改革(RFC 0
文档Rust RFC 558 详解:禁止链式比较运算符,为何 Rust 要求为 `a == b == c` 加括号
Rust RFC 558 详解:禁止链式比较运算符,为何 Rust 要求为 a == b == c 加括号 本篇基于 Rust 语言 RFC 流程文档库(rfc
文档RT-Thread 编写 Rust 动态库(cdylib)模块实战指南
RT Thread 编写 Rust 动态库(cdylib)模块实战指南 RT Thread 不仅支持用 C 语言编写内核与应用,还在 components/ru
操作系统嵌入式物联网嵌入式OSRTOS
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考