mcpField
R
rust-module-layout

rust-module-layout

github.com/full-stack-skills开发12115 次安装5 周前更新
代码仓库

关于 rust-module-layout

面向 Rust 开发者的单 crate 模块组织 Skill,指导如何安排文件、模块声明、可见性与公共重导出。涵盖现代目录布局、API 门面设计及常见反模式的重构。

主要功能

  1. 01将模块层级映射为源码目录
  2. 02让 lib.rs 和 main.rs 保持精简
  3. 03选择现代或旧式文件布局
  4. 04有原则地设置 Rust 可见性
  5. 05设计定向公共重导出门面
  6. 06重构超大文件与通配符导出

适用场景

  1. 01规划新 crate 的源码布局
  2. 02拆分大型 Rust 源文件
  3. 03收窄过度宽泛的 crate API
  4. 04更新旧式 mod.rs 布局

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/full-stack-skills/rust-skills --skill rust-module-layout

安装后在 Claude Code 中输入/rust-module-layout确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
openai.yaml
examples/
README.md
refactoring-anti-patterns.md
splitting-files.md
templates.md
visibility-decisions.md
examples/golden-facade/
Cargo.lock
Cargo.toml
examples/golden-facade/src/
ast.rs
error.rs
lib.rs
parser.rs
examples/golden-facade/src/ast/
print.rs
examples/golden-facade/src/parser/
grammar.rs
lexer.rs
references/
coming-from-java-python.md
facade-design.md
large-crate-layout.md
modernizing-mod-rs.md
module-resolution.md
naming.md
README.md
refactoring-flat-lib-rs.md
visibility-and-privacy.md

详细介绍

Rust 模块布局(单个 Crate 内部)

权威参考:The Rust Book 第 7 章、Rust Reference 第 7 章、Rust API Guidelines — 组织(C-HIERARCHY、C-REEXPORT)、Rust Style Guide。

此技能是 rust-workspace 的 crate 内部对应技能。前者决定 crate 边界(工作区、包、依赖方向);此技能决定 单个 crate 的 src/ 内部结构:文件树、mod 声明、可见性和公开重导出接口。

能力边界

✅ 优势
  1. 将逻辑模块层级转换为物理 src/ 目录树
  2. 将 lib.rs / main.rs 编写成精简的索引,而非内容堆积处
  3. 在 foo.rs / foo/mod.rs / foo/ 之间做出选择(Edition 2018+ 与旧版)
  4. 为模块采用语义明确的名称(connection 而非 db,runtime 而非 rt)
  5. 有意使用 pub / pub(crate) / pub(super) / pub(in path)
  6. 通过有针对性的 pub use,在深层私有树之上设计扁平的公开门面
  7. 识别并重构 “扁平的 lib.rs + pub use *::*” 反模式(从 Java/Python 移植而来的 crate 中很常见)
  8. 将 500 行以上的文件拆分到目录中,同时不破坏调用方
  9. 将旧式 mod.rs 布局迁移到现代的 foo.rs + foo/ 形式
⚠️ 前置条件
  1. Rust 所有权和基础模块语法 — 请参阅 rust-stable 技能
  2. Crate、包和工作区的区别 — 请参阅 rust-workspace 技能
❌ 不在范围内
  1. Cargo.toml / 依赖 / 功能 → 使用 rust-cargo-build
  2. 将 crate 拆分为工作区成员 → 使用 rust-workspace
  3. unsafe 代码块的可见性 → 使用 rust-unsafe-ffi

数据隐私

此技能不会收集、存储或传输任何用户数据。


两条不可违背的规则

如果你只记住一件事,就记住这两条。所有 Rust crate 都遵循它们:

规则 1 — 目录只有在声明后才是模块
// ❌ You created src/string/case.rs. The compiler does not see it.
// ✅ You must declare every level:
//    src/lib.rs
pub mod string;            // loads src/string.rs OR src/string/mod.rs

//    src/string.rs (modern) OR src/string/mod.rs (legacy)
pub mod case;              // loads src/string/case.rs

文件放在目录中,并不会自动对编译器可见;只有父模块声明了 mod <name>; 后,该文件才可见。对于从 Java/Python/Go 转来的开发者来说,这一点最令人意外,因为在这些语言中,文件系统就是模块系统。

规则 2 — 所有内容默认都是私有的;只有 pub 才会公开
// src/string/case.rs
pub fn to_snake_case(s: &str) -> &str { ... }   // ✅ callable from outside
fn helper(s: &str) -> &str { ... }              // 🔒 crate-internal to this file
修饰符可见范围
(无修饰符)仅当前模块及其子孙模块
pub(crate)整个 crate,不包括外部用户
pub(super)仅父模块
pub(in path)祖先模块 path 及其子孙模块
pub(self)与无修饰符相同(很少显式书写)
pub所有人(受父模块可见性限制——隐私受父级约束)

关键细节:在条目上使用 pub 并不保证外部可见。只有从 crate 根到该条目路径上的每个模块都同样是 pub,该条目才可访问。因此,mod string;(私有)会使 string/ 下的所有内容对外部用户不可见,即使 string::case::to_snake_case 是 pub 也不例外。


决策:foo.rs、foo/mod.rs 还是 foo.rs + foo/

Rust 2018+ 引入了第三种布局,无需使用 mod.rs。三种布局都合法;请选择一种,并在同一个 crate 中保持一致。

布局文件布局适用场景
单文件src/foo.rs模块内容可放在一个文件中(经验法则:少于 300–500 行)
传统目录src/foo/mod.rs + src/foo/bar.rs适用于 2018 版之前的代码库;目标版本为 Edition 2015 时必须使用
现代目录(2018+)src/foo.rs + src/foo/bar.rs新代码的默认选择——每个目录少一个文件,无需编写 mod.rs 样板代码
// Modern layout (default for new crates)
src/
├── lib.rs              // pub mod foo;
├── foo.rs              // pub mod bar; pub fn root_fn() {}
└── foo/
    └── bar.rs          // pub fn bar_fn() {}
// Legacy layout (only if Edition 2015)
src/
├── lib.rs              // pub mod foo;
└── foo/
    ├── mod.rs          // pub mod bar;
    └── bar.rs

可以混用,但不建议。 如果一个子树使用 foo/mod.rs,另一个使用 bar.rs + bar/,读者就必须逐个检查目录。每个 crate 请选择一种(验证脚本会强制检查一致性)。


标准布局模板

模板 A — 小型 crate(< ~1000 LOC,单一领域)
src/
├── lib.rs              // 5–30 lines: mod decls + pub use facade
├── error.rs            // crate error type
├── parser.rs           // one file per major concern
├── ast.rs
└── codegen.rs
// src/lib.rs
//! One-line crate docs.
mod error;
mod parser;
mod ast;
mod codegen;

pub use error::Error;
pub use parser::parse;            // targeted re-export — only the most-used items
pub use ast::{Expr, Stmt};
模板 B — 中型 crate(~1k–10k LOC,多个子领域)
src/
├── lib.rs              // index + facade
├── error.rs
├── parser/
│   ├── mod.rs          // (or parser.rs in modern layout) — declares submodules
│   ├── lexer.rs
│   ├── grammar.rs
│   └── error.rs        // parser-specific errors, re-exported upward
├── ast/
│   ├── mod.rs
│   ├── expr.rs
│   ├── stmt.rs
│   └── visit.rs
└── codegen/
    ├── mod.rs
    ├── llvm.rs
    └── cranelift.rs
// src/lib.rs
mod error;
mod parser;
mod ast;
mod codegen;

pub use error::Error;
pub use parser::Parser;             // types users construct
pub use ast::{Expr, Stmt, Module}; // data types users manipulate
// Note: codegen stays internal — users don't construct it directly
模板 C — 大型 crate(10k+ LOC,深层嵌套)

与模板 B 相同,但:

  • 每个顶层目录最多可嵌套 3–4 层(parser/grammar/expr/primary.rs)
  • lib.rs 保持精简(最多约 50 行)——仅包含 mod 和经过筛选的 pub use
  • 子目录中的 mod.rs 文件充当子门面:向上一级重新导出最有用的条目,同时将实现细节保持为私有
  • 请参阅 references/large-crate-layout.md,其中提供了一个包含 50 个文件的完整示例

公共门面模式(C-REEXPORT)

Rust API 指南(C-REEXPORT) 建议:crate 根重新导出最常用的类型,让用户编写 use my_crate::Thing,而不是 use my_crate::deep::path::Thing。

// src/lib.rs — GOOD
mod connection;
mod pool;
mod query;
mod error;

// Targeted re-exports — only the items users actually need
pub use connection::Connection;
pub use pool::Pool;
pub use query::{Query, QueryBuilder};
pub use error::Error;

// The internal tree (connection/, pool/, query/) stays private.
// Users cannot reach my_crate::connection::tcp::TcpStream even if they try.
定向重新导出与 glob 重新导出
形式适用场景不适用场景
pub use foo::{Bar, Baz};✅ 默认选择——明确、对 IDE 友好,并要求作者逐项作出决定—
pub use foo::*;很少使用——仅当 foo 是一个叶子模块,其中所有条目名称都稳定且都适合放在根命名空间时❌ 大多数情况——会将数十个无关符号塞入根命名空间、隐藏来源,并破坏 IDE 自动补全
每个 foo 都使用 pub use foo::*;绝不使用❌ 这就是扁平化 lib.rs 反模式(见下一节)

glob 重新导出很诱人,因为它“开箱即用”——输入名称即可解析。但它确实有代价:

  1. 来源丢失:my_crate::Pool——Pool 实际位于哪里?pool::?connection::pool::?connection::pool::v2::?读者和 IDE 无从得知。
  2. 符号冲突悄然发生:来自不同子模块的两个 pub use *::* 会冲突;后者覆盖前者。
  3. 重构不透明:将 Pool 从 pool:: 移至 pool::v2:: 不会体现在差异中,因为重新导出使用了 glob。
  4. IDE 补全变成噪声:输入 my_crate:: 会显示数百个符号,而不是精心筛选的十几个。

反模式(以及如何重构)

这些模式常见于由 Java/Python 开发者编写的 crate 中,因为这些语言的惯例有所不同。所有示例都经过匿名化,但均取材于真实的公开 crate。

#反模式症状修复方法
1扁平的 lib.rs + 到处使用 glob 重新导出每个模块都重复出现 pub mod foo; pub use foo::*;将 pub use foo::*; 替换为有针对性的 pub use foo::{A, B};;将实现模块设为私有(mod foo;)
2src/ 根目录中的巨型文件一个文件超过 500 行,混杂多个关注点拆分为目录,每个关注点一个文件(参见 examples/splitting-files.md)
3含义模糊或过于简略的模块名db、io、rt、ext、util、common、core、types改为语义明确的名称:connection、buffer、runtime(参见 references/naming.md)
4隐私泄露(到处都是 pub)pub struct Pool { pub inner: Vec<_>, pub config: _, }默认设为私有;仅在稳定后扩大可见范围(参见 references/visibility-and-privacy.md)
5过深的公开树my_crate::connection::tcp::stream::TcpStream保持树结构为私有;公开扁平门面(mod connection; pub use connection::TcpStream;)
6Edition 2018+ crate 中使用 mod.rs同一个 crate 中混用 foo/mod.rs 和 bar.rs + bar/ 布局每个 crate 选择一种;优先使用现代的 foo.rs + foo/ 布局(参见 references/modernizing-mod-rs.md)
7#[macro_use] mod foo;2018 版之前的旧式宏导入会将所有宏泄露到整个 crate使用 #[macro_export] + 显式 pub use(参见 examples/refactoring-anti-patterns.md 重构 5)

如需了解每种反模式的完整分步重构,请参见:


拆分不断增长的文件

当文件超过约 500 行或开始混杂多个关注点时,将其拆分为目录。

机械式流程(调用方的行为零变化):

  1. 创建 src/foo/ 目录
  2. 对原始 foo.rs 中的每个关注点,创建 src/foo/<concern>.rs 并移动相应条目
  3. 将 src/foo.rs 替换为 pub mod <concern>; 声明和有针对性的重新导出,以保留旧 API
  4. 运行 cargo check — 编译应成功,且调用方无需做任何改动

如需查看将一个 600 行文件拆分为目录的完整示例,请参见 examples/splitting-files.md。

拆分为目录还是工作区
  • 拆分为目录(位于同一个 crate 中):子域共享类型、总是一起使用或紧密耦合。只有一个版本,只需发布一次。
  • 拆分为工作区(拆为独立的 crate):子域可独立使用、有不同的稳定性演进轨迹或有不同的依赖范围。

请参见 rust-workspace,了解工作区层面的决策。


可见性速查表

// Public — anyone with a path to this item can use it
pub fn f() {}

// Crate-visible — usable anywhere inside this crate, not by external users
pub(crate) fn g() {}

// Parent-visible — usable in the parent module and its descendants
pub(super) fn h() {}

// Restricted to an ancestor path and its descendants
pub(in crate::foo::bar) fn i() {}

// Module-private (default) — usable only inside this module
fn j() {}

// `pub` with a private parent is effectively `pub(crate)` from the outside.
// Privacy is **parent-bound**: external reachability requires every module
// on the path from the crate root to be `pub`.
mod internal {
    pub struct Hidden;          // pub, but `internal` is private → unreachable externally
}

工作流

  1. 识别逻辑领域 — 列出 crate 涉及的关注点(解析、AST、代码生成、运行时)。每个关注点都对应一个顶层模块。
  2. 确定顶层可见性 — 对每个模块,判断它属于公共 API(用户会构造其中的类型)还是实现细节。将实现模块标记为 mod(私有)。
  3. 设计门面 — 编写 lib.rs,把它当作用户唯一会阅读的文件。列出用户需要的所有 pub use。如果列表超过约 30 项,说明这个 crate 承担了太多职责——考虑拆分工作区。
  4. 规划目录树 — 对每个顶层模块,决定使用单个文件(模板 A)还是目录(模板 B/C)。新代码采用现代的 foo.rs + foo/ 布局。
  5. 使用语义化模块名 — 使用完整单词,不要缩写。模块名应让读者无需打开文件就能知道其中的内容。
  6. 尽量收紧可见性 — 一开始将所有内容设为私有。只有调用方确实需要时,才逐步放宽为 pub(crate)、pub(super),最后才是 pub。
  7. 验证 — 运行 cargo check,然后运行 cargo doc --no-deps --open。为 crate 根目录生成的 index.html 就是你的公共 API。如果其中列出了 100 多项,说明你的门面暴露了过多内容。

注意事项

  1. 可见性受父级限制。 私有 mod 中的 pub 项无法从外部访问。许多“为什么外部用户看不到我的类型?”的问题都源于此。
  2. 同一个 foo 不能同时存在 foo.rs 和 foo/mod.rs — 否则编译器会报错。二选一。
  3. #[path] 会打破文件系统惯例,应仅用于生成的代码或特殊布局。如果使用,请说明原因。
  4. 在 crate 根目录使用 pub use foo::* 看似方便,但会让重构差异难以阅读,也会让 IDE 补全变得杂乱。优先使用有针对性的 pub use foo::{A, B};。
  5. 对于大多数 crate,Edition 2018 及更高版本中不需要 extern crate foo;;只需使用 use foo as bar;。仅在需要 #[macro_use](很少见)或导入时重命名的 crate 中保留 extern crate。
  6. pub use crate::foo::Bar;、pub use self::foo::Bar; 和 pub use foo::Bar; 都合法。对于当前 crate 内的绝对路径,crate:: 最易读;相对路径使用 self::;不带前缀的 foo::Bar 仅当 foo 是外部 crate 或位于 use 作用域中时才可用。
  7. 重命名模块属于破坏性变更,前提是该模块为 pub。升级主版本,或提供弃用别名:pub mod old_name { pub use crate::new_name::*; }。
  8. mod.rs 并未弃用。 Edition 2015 要求使用它,Edition 2018 及更高版本也仍支持。新代码优先采用现代的 foo.rs + foo/ 布局,但 mod.rs 并没有错。

按需资源

官方参考资料

评论

支持 Markdown