首页
/ rustc 错误码 E0591 深度解析:transmute 函数项时的零大小类型陷阱与正确修复

rustc 错误码 E0591 深度解析:transmute 函数项时的零大小类型陷阱与正确修复

2026-09-08 20:12:16作者:史锋燃Gardner

本文围绕 rustc 编译器的 E0591 错误(can't transmute zero-sized type),讲解在 Rust 中把**函数项(fn item)通过 transmute 转换为函数指针(fn pointer)**时为什么会报错、背后函数项与函数指针在类型系统与内存布局上的本质差异(源自 RFC 401 的 coercions 设计),并结合本仓库 rustc 源码中 check_transmute 的实现逻辑与 tests/ui/transmute 下的回归测试,给出可以直接照抄的正确改写方案。读完本文,你将能准确区分 typeof(foo)fn(...) 两种类型、正确判断 FFI 回调场景中何时能用 as 强制转换替代 transmute,并看懂 E0591 与 E0512 两个错误码之间的分工关系。

错误触发的典型场景

E0591 是 rustc 的编译错误码之一,其完整定义位于仓库内的 compiler/rustc_error_codes/src/error_codes/E0591.md,错误标题为 can't transmute zero-sized type

它专门用于拦截一类"从零大小类型指针大小类型进行 transmute"的写法,最常见的形态就是直接用 std::mem::transmute 把一个函数项(如 foo)转成函数指针(如 fn())。下面的代码来自该错误码文档,编译时就会触发 E0591:

extern "C" fn foo(userdata: Box<i32>) {
    /* ... */
}

fn callback(_: extern "C" fn(*mut i32)) {}

unsafe {
    let f: extern "C" fn(*mut i32) = transmute(foo);
    callback(f);
}

直觉上,很多人认为"把 foo 转成函数指针不就行了?",于是写了 transmute(foo)。编译器却认为这是一个错误——要理解为什么,必须先厘清"函数项"与"函数指针"在 Rust 类型系统里是两个完全不同的类型。

背景概念:函数项(fn item)与函数指针(fn pointer)

函数项的真实类型是 typeof(foo),而不是 fn(S)

根据 rustc 错误码文档所引用的 RFC 401(coercions),对于一个函数声明 foo

struct S;

// 下面几种不同写法的 `fn` 声明,在本文讨论的意义上是等价的:
fn foo(x: S) { /* ... */ }

extern "C" {
    fn foo(x: S);
}

impl S {
    fn foo(self) { /* ... */ }
}

foo 的类型并不是大多数人预期的 fn(S),而是一个独一无二(unique)的、零大小(zero-sized)的标记类型,文档中写作 typeof(foo)。这个类型不仅记录了 foo 的签名,还精确地标识出"这个值就是 foo 这个函数"。

为什么平时几乎感觉不到区别:隐式强转

typeof(foo) 可以被**强转(coerce)**为函数指针 fn(S),因此绝大多数普通代码根本感知不到二者的差别:

struct S;
fn foo(_: S) {}

let x: fn(S) = foo; // OK,发生一次 fn item -> fn pointer 的强转

这种隐式强转正是 RFC 401 中引入的 coercion 规则之一:凡是需要函数指针类型的地方,函数项会自动"降级"为函数指针。注意,这里发生的是一次类型层面的强转,而不是位层面的 transmute

这个区别为什么重要:静态分发 vs 指针间接调用

函数指针类型 fn(S) 并不绑定到任何特定函数——它只是一个函数指针。因此:

  • 通过 x() 调用时,实际是在做一次经由指针的间接调用(virtual call)
  • 而直接调用 foo() 时,由于 foo 的类型 typeof(foo) 已经精确告诉我们调用的是哪个函数,编译器可以做出静态分发(statically dispatched),无需经过任何指针跳转。

正是因为绝大多数场景下编译器会自动插入强转,多数代码不需要关心这个区分。但当你显式使用 transmute 这类"位级重解释"的工具时,差异就会暴露出来——因为强转语义和位级转换语义是完全不同的两回事。

为什么 transmute(foo) 是错的:零大小 vs 指针大小

transmute 的本质是按位重新解释类型,rustc 要求源类型与目标类型具有相同的布局与大小(或者经骨架计算后大小相同)。而这里恰好不满足:

  • foo 的类型是函数项 typeof(foo),它是零大小的(0 字节);
  • 目标类型 fn(...) 是函数指针,大小等于一个指针(在目标架构上非零)。

因此 transmute 直接"把一个占 0 字节的值重新解释成一个占 8(或 4)字节的指针",从大小检查上就必然失败。transmute 不是强转:它不会先帮你把函数项变成函数指针再转换,它只会照着参数给定的类型原样去重解释。

错误码文档特别指出,同样的错误也适用于向 *mut fn() 的 transmute(实践中确实出现过此类写法):

The same applies to transmutes to *mut fn(), which were observed in practice.

需要警惕的是,*mut fn() 这种类型本身通常就是使用不当的:如果意图是描述"一个函数指针",直接写 fn() 就够了,*mut fn() 表示的是"指向一个函数指针的指针"。当然,由于这类值大多只是原样传给 C 代码,实际上很少有人注意到这层差别——但这并不能改变其语义混乱的事实。

正确的改写方式

E0591 出现时,编译器提示 can't transmute zero-sized type,并给出 help: cast with as to a pointer instead。错误码文档给出了两种可用的改写思路:

方案一(首选):让原始函数声明直接匹配目标签名

foo 的形参类型改成回调真正期望的类型,把类型转换下沉到函数体内部完成。也就是说,与其在函数指针这一层做不安全的位转换,不如直接声明一个 extern "C" fn(userdata: *mut i32),在函数体内部再把原始指针转回 Box<i32>。这样顶层不需要任何 transmute

方案二:先用 as 把函数项强转为函数指针,再 transmute

如果函数项必须先于 transmute 归一化为函数指针,可以先利用强转把零大小的函数项转成指针大小的函数指针,再对两个大小一致的类型做 transmute:

extern "C" fn foo(_: Box<i32>) {}
use std::mem::transmute;

unsafe {
    // 先把 foo 强转为它的真实签名对应的函数指针,再做位级转换
    let f: extern "C" fn(*mut i32) = transmute(foo as extern "C" fn(_));
    // 也可以先强转为 usize(同样是指针大小),再转成目标指针
    let f: extern "C" fn(*mut i32) = transmute(foo as usize); // 同样可行
}

这两个写法都能通过编译:foo as extern "C" fn(_) 让源类型从零大小的函数项变成了指针大小的函数指针,随后 transmute 发生在两个大小一致的类型之间,符合 transmute 的大小约束。

编译器内部实现:E0591 从哪来

要理解 E0591 的完整语义,可以顺着 rustc 的类型检查源码一路看下去。对 std::mem::transmute 等内建函数的类型检查位于 compiler/rustc_hir_typeck/src/intrinsicck.rscheck_transmute 函数中。

其核心检查流程可以概括为三步:

  1. 先对源类型 from 与目标类型 to 做归一化(normalize),并对完全相同的类型直接放行;
  2. 通过 SizeSkeleton::compute 分别计算两侧类型的大小骨架,如果大小相同则通过;
  3. 大小不同时进入错误处理逻辑。

E0591 的触发并非默认路径,而是针对"函数项 transmute"的特判分支(见 intrinsicck.rs):

  • 先用 unpack_option_likeOption<T> 之类的"类 Option 类型"拆开,使得 Option<typeof(foo)> 也能命中同样路径;
  • 若源类型拆开后命中 ty::FnDef(..),且目标类型骨架已知、大小恰等于一个指针大小;
  • 则直接抛出 E0591 can't transmute zero-sized type,并附带两条 note(源类型、目标类型)与一条 help(cast with as to a pointer instead)。

换句话说,E0591 是一个为程序员着想的"更清晰"的专用错误:如果走通用大小不匹配的报错路径,用户看到的会是 E0512,且错误信息不会提示"目标是指针大小、源是零大小"这一层含义。测试基线里也能看到对应关系:对非指针大小的目标类型(如 u8),编译器仍然走 E0512;只有目标是 fn()*mut ()*const ()Option<fn()> 这类指针大小类型时才升级为 E0591。

错误输出的真实样例

仓库中的回归测试 tests/ui/transmute/transmute-from-fn-item-types-error.rs 与其期望输出 transmute-from-fn-item-types-error.stderr 完整记录了 E0591 的报错形态。以 transmute(foo) 为例:

error[E0591]: can't transmute zero-sized type
  --> $DIR/transmute-from-fn-item-types-error.rs:8:13
   |
LL |     let p = mem::transmute(foo);
   |             ^^^^^^^^^^^^^^
   |
   = note: source type: unsafe fn() -> (i8, *const (), Option<fn()>) {foo}
   = note: target type: *const ()
   = help: cast with `as` to a pointer instead

注意这里的 note 已经把源类型打印成了带函数名的完整函数项类型unsafe fn() -> (...) {foo}),这正是"函数项类型携带函数身份"的直观体现;而同样测试文件中把函数项转成 u8 的情形报的则是 E0512(cannot transmute between types of different sizes, or dependently-sized types),note 里源类型显示为 unsafe fn() {bar}(0 bits),目标 u8(8 bits)。

回归测试能告诉我们的更多细节

tests/ui/transmute/transmute-from-fn-item-types-error.rs 是一份信息量很大的 UI 测试,它系统性地划清了"报 E0591 / 报 E0512 / 不报错"的边界:

  • 报 E0591:把函数项 transmute 到 *const ()*mut ()fn()Option<fn()> 等指针大小类型;
  • 报 E0591(经 Option 拆包):把 Some(foo)(即 Option<typeof(foo)>)transmute 到 *mut ()fn()Option<fn()>——这印证了 unpack_option_like 特判的真实作用;
  • 报 E0512:把函数项 bar transmute 到非指针大小的 u8(0 位 vs 8 位),此时不满足 E0591 的"目标为指针大小"前提;
  • 不报错(发生了强转)mem::transmute::<fn(), usize>(main)mem::transmute::<Option<fn()>, usize>(Some(main))。原因是这些调用点的期望类型 fn() / Option<fn()> 已经触发了函数项到函数指针的隐式强转transmute 实际作用的源类型是指针大小的 fn(),与 usize 大小一致。

最后一条最有实战启发:在期望类型为函数指针的上下文中传入函数项会先发生 coercion,这正是错误码文档反复强调的"大部分代码不必关心函数项与函数指针区别"的机制基础。上述用例中特意用 transmute::<fn(), usize>(main) 而非 transmute(main),就是为了避免源类型被推断为函数项而撞上 E0591。

实践要点小结

  • 在 Rust 中,函数名的类型是零大小的唯一标记类型 typeof(foo),仅在需要时由编译器强转为 fn(...) 函数指针;
  • transmute 只做位级重解释,不会替你完成函数项到函数指针的强转,因此 transmute(foo) 会把一个 0 字节的值解释成指针,必然不满足大小约束;
  • 遇到 E0591 时,优先考虑修改函数声明以匹配目标签名,其次用 as 先把函数项强转为函数指针或 usize,再进行 transmute,切勿让函数项直接出现在 transmute 的源类型位置;
  • 若目标是 u8 等非指针大小类型,编译器走的是 E0512;E0591 是仅针对"函数项 → 指针大小类型"这一常见误用给出的专用提示,其特殊分支位于 compiler/rustc_hir_typeck/src/intrinsicck.rs
  • 需要复现、验证这些行为时,可查看 tests/ui/transmute/transmute-from-fn-item-types-error.rs 及对应 .stderr 基线,也可以在自己的工程里用 rustc --explain E0591 查看 rustc 内置的错误解释(其原始文案即来自 E0591.md)。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
900
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
927
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.89 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
602
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
396
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
526