首页
/ Rust 编译器 E0136 错误代码解析:多个 main 入口点与 rustc 入口点检测机制

Rust 编译器 E0136 错误代码解析:多个 main 入口点与 rustc 入口点检测机制

2026-09-06 16:10:30作者:姚月梅Lane

E0136 是 rustc 历史上用于报告“一个二进制 crate 中定义了多个 main 函数”的错误代码,其原始文档位于 E0136.md。本文以该错误码说明文档为主体,完整还原它的语义、错误示例与修复方式,并结合当前 rustc 源码(rustc_resolverustc_passes)深入讲解编译器如今是如何定位入口点的,以及为什么 E0136 已不再由编译器发出,帮助你在编写 Rust 可执行程序时正确组织入口点、读懂相关诊断信息。

一、E0136 的原始语义:二进制只能有一个入口点

E0136.md 文档给出的原始说明是:在一个文件中定义了不止一个 main 函数。文档中的错误示例如下:

fn main() {
    // ...
}

// ...

fn main() { // error!
    // ...
}

文档的核心解释只有一句话,但信息密度很高:

一个二进制(binary)只能有一个入口点,而默认情况下这个入口点就是 main() 函数。如果存在多个该函数,请重命名其中一个。

这就是 Rust 可执行程序的基本契约:crate 链接为可执行文件(executable)时,运行时需要一个唯一的启动函数。早期编译器用 E0136 这条诊断来兜住“两个 main 同名并存”的场景,修复办法就是重命名其中一个函数(例如改名为 app_mainrun 等),让 main 在整个顶层命名空间中唯一。

文档现状:该错误码已不再发出

文档第一行明确标注:

Note: this error code is no longer emitted by the compiler.

也就是说,在当前 rustc 中,E0136 已经成为一个仅存留于文档中的历史错误码,编译器不会再输出 error[E0136]。这一状态并非随意标注,而是遵循 rustc 错误码体系的维护规范,下一节会结合源码说明。

二、E0136 在 rustc 错误码体系中的位置

所有 rustc 错误码的说明文档都集中存放在 rustc_error_codes crate 下,每个 EXXXX.md 文件对应一个错误码。该 crate 的 lib.rs 通过一个高阶宏 error_codes! 集中定义“当前仍在使用的错误码”列表,并配套了严格的维护规则,值得逐条理解:

  1. 错误码说明文档必须集中管理lib.rs 开头的注释说明,所有错误码说明统一放在 error_codes/EXXXX.md 文件中,格式需遵循 RFC 1567(Long error codes explanation normalization),且宏内容会被 tidy 工具中的 check_error_codes_docs 检查——修改宏语法时必须同步修改 tidy。
  2. 废弃的错误码不允许从列表中移除。注释原文要求:“Do *not* remove entries from this list. Instead, just add a note to the corresponding markdown file saying that this error is not emitted by the compiler any more (see E0001.md for an example), and remove all code examples that do not build any more by marking them with ignore (no longer emitted).”

这条规则正是 E0136.md 现状的直接原因:E0136 编号在历史中被分配、文档被保留下来,但触发它的代码路径后来被更靠前的检查取代,于是文档顶部加上了“no longer emitted”的说明。类似的“保留但已废弃”示例可参考同目录下的 E0001.md

三、当前编译器如何确定入口点:rustc_resolve 的 main 解析

E0136 之所以不再发出,根源在于入口点的判定被前移、并合并到了名称解析(name resolution)阶段。从源码结构看,当前逻辑分布在两个 crate 中:

1. rustc_resolve:在根模块中解析唯一的 main 绑定

rustc_resolve/src/lib.rs 中的 resolve_main 函数负责定位入口点,其关键逻辑可以归纳为三步:

fn resolve_main(&mut self) {
    let any_exe = self.tcx.crate_types().contains(&CrateType::Executable);
    // Don't try to resolve main unless it's an executable
    if !any_exe {
        return;
    }
    // 在根模块(crate 顶层)按标识符 `main` 解析一个值命名空间绑定
    let Ok(name_binding) = self.cm().maybe_resolve_ident_in_module(...) else {
        return;
    };
    let res = name_binding.res();
    let is_import = name_binding.is_import();
    let span = name_binding.span;
    // 记录(单个)入口点定义
    self.main_def = Some(MainDefinition { res, is_import, span });
}

从这段源码可以确认三个事实:

  • 只有可执行 crate 才会查找 mainany_exe 检查了 crate 类型中是否包含 Executable,库 crate(--crate-type lib)完全不参与入口点解析,这与 E0136 文档中“一个 binary 只能有一个入口点”的表述一致——约束只针对二进制。
  • 解析范围限定在根模块graph_root),即在 crate 顶层按 main 这个名字做一次常规的标识符解析。
  • main_def 记录的是单个 MainDefinition,包含该绑定的解析结果、是否为 use 导入而来(is_import)以及源位置 span。解析完成后只保留一个绑定。

关键推论:如果顶层模块中写了两个 fn main,那么 main 这个名字在同一模块内被定义了两次,会在名称解析阶段先以“同名重复定义”的通用诊断报出来(这是 Rust 对同一模块内重复定义名字的常规处理)。从源码结构看,正因为这类情况在 resolve_main 之前就会被名字重复定义的检查拦截,E0136 这条专门针对“多个 main”的独立错误码才失去了存在必要,文档中标注的“no longer emitted”与这一机制是吻合的。

2. rustc_passes:entry 通道对 main 的补充校验

名字解析完成之后,rustc_passes/src/entry.rs 中的入口点检查(configure_main)继续负责“main 存在但不对/缺失”等边角情况。该文件中的 EntryContext 结构维护两类信息:

  • rustc_main_fn:带有 #[rustc_main] 属性、显式指定为入口的函数(源码注释原文:“The function has the #[rustc_main] attribute”);
  • non_main_fns:注释说明是“functions that one might think are main but aren't, e.g. main functions not defined at the top level. For diagnostics”,即定义在顶层之外、可能被误认为 main 的函数,仅用于后续诊断。

从源码结构看,entry.rs 中的检查顺序为:

  1. 若用户显式声明了不需要 main,则直接停止(源码注释:“If the user wants no main function at all, then stop here”);
  2. 若存在 #[rustc_main] 标注的函数,优先采用它作为入口;
  3. 否则读取 resolver 记录的 main_def(即上一节 resolve_main 的产物),本地定义的 main 直接确认入口,非本地的 import 形式的 main 走额外的跨 crate 处理分支;
  4. 如果最终既没有 #[rustc_main] 也没有可用的 main,则调用 no_main_err 报告“找不到 main”的诊断,并把 non_main_fns 中记录的可疑函数位置附在诊断信息中,提示用户“你可能在子模块里定义了 main”。

这一结构与 E0136 文档的原始诉求形成完整闭环:入口点唯一性由名称解析阶段保证(同名 main 不允许重复定义),入口点缺失与非顶层 main 由 entry 通道兜底诊断。原来需要 E0136 专门覆盖的“两个 main 并存”场景,已经被更基础的同名重复检查吸收。

四、实战指南:如何避免“多个 main”类问题

结合 E0136 文档与当前源码行为,编写可执行程序时可以遵循以下做法:

  1. 顶层 main 必须唯一。修复方式与 E0136 原文档建议一致:重命名其中一个函数。业务逻辑函数可以命名为 app_mainrun 等,由唯一的 fn main() 转调。
  2. main 必须定义在 crate 顶层resolve_main 只在 graph_root 中查找,non_main_fns 的存在说明编译器会专门记录“定义在非顶层位置、可能被误当 main”的函数用于提示。把 main 写进某个子模块不会成为入口,反而会让编译器报出找不到 main 的诊断并指向这些可疑函数。
  3. 库与二进制的区分。只有 crate 类型包含 Executable 时才会解析 main;纯库 crate 定义 fn main() 也不会产生入口点(它只是一个普通函数,但仍需注意与同模块其他同名定义冲突)。
  4. 同目录多入口需求应拆分为多个 bin target。若一个 crate 目录下确实需要多个“程序入口”,正确做法是用 Cargo 的 [[bin]] 配置或 src/bin/ 目录声明多个二进制 target,让每个 main 各属于一个 crate,而不是塞进同一个编译单元——因为一个编译单元内的入口点契约就是“恰好一个”。
  5. 读懂诊断时对照错误码文档。当遇到入口点相关报错时,可先到 compiler/rustc_error_codes/src/error_codes/ 目录查对应 EXXXX.md;注意文档顶部的 “no longer emitted” 标注,说明该编号是历史遗留(如本节的 E0136),实际报错会以当前机制对应的其他错误码呈现。

五、小结

  • E0136 的原始语义:一个二进制 crate 中出现多个 main 函数,修复方式是重命名使 main 唯一;该错误码现已不再由编译器发出,文档按 rustc 错误码维护规范(rustc_error_codes/src/lib.rs 中的注释)保留编号与说明。
  • 当前实现中,入口点解析在 rustc_resolve/src/lib.rsresolve_main 中完成,仅对可执行 crate 生效,且只在 crate 根模块查找 main
  • 后续的入口点校验逻辑(#[rustc_main] 优先、main 缺失诊断 no_main_err、非顶层 main 的记录)位于 rustc_passes/src/entry.rs
  • 对开发者而言,核心约束始终是:一个可执行编译单元只能有一个顶层 main,多入口需求应通过多个 bin target 实现。
登录后查看全文
热门项目推荐
相关项目推荐