本文详细介绍了Solana程序(包括原生Solana和Anchor框架)的代码组织结构和最佳实践。它从Rust Cargo项目基础讲起,解释了程序组件、文件结构、Anchor项目工作区以及eBPF跨平台编译等关键概念,旨在帮助开发者构建可维护和可扩展的Solana程序。
Solana 程序不强制采用特定的代码库结构,因此代码组织通常取决于开发者的偏好和程序复杂性。事实上,一个 Solana 程序可以作为一个独立的 lib.rs 文件存在,正如我们在这个系列中到目前为止所看到的。
但是,随着程序复杂性增加,你会希望将逻辑和数据分离到上下文中合理的文件和清晰的文件夹结构中,以使代码更容易定位、维护和扩展。
Solana 开发生态系统遵循一种常见的模式来组织程序的各个部分。本文将教授如何按照这种模式组织 Anchor 和原生 Solana 程序。
每个 Solana 程序都是一个 Rust Cargo 库 crate。这意味着默认结构始于一个 Cargo 项目,其中包含定义依赖项和构建配置的 Cargo.toml,以及包含程序逻辑的 lib.rs。你可以通过运行命令 cargo init --lib my_program 生成此结构,生成的程序结构如下所示:
my_program/
├── Cargo.lock
├── Cargo.toml
└── src
└── lib.rs
要使其成为 Solana 程序,我们需要在 Cargo.toml 文件中添加以下代码。
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
solana-program = "2.0.0"
以上配置的含义是:
[lib] 部分中的 crate-type = ["cdylib", "lib"] 告诉 Cargo 将程序编译为动态库 (cdylib) 和标准 Rust 库 (lib)。Solana 部署时需要 cdylib 格式的 .so 文件,而 lib 格式则用于本地测试或在程序间重用逻辑。solana-program = "2.0.0" 依赖项引入了 Solana SDK crates,它们提供了访问 Solana 运行时类型、宏和链上程序开发辅助函数的功能。要正确理解 Solana 程序代码库的结构,我们首先要了解构成 Solana 程序的逻辑组件。每个程序通常包括以下部分:
borsh crate 或 Anchor 宏)。我们还需要定义将要交互的账户。我们可以使用代表上述概念的文件来构建一个简单的 Solana 程序,如下所示。在此结构中,lib.rs 作为程序的根;它暴露模块并将它们链接在一起。entrypoint.rs 文件定义了 Solana 运行时在调用程序时调用的函数。这个入口点函数,通常命名为 process_instruction,实现了一个调度器,将每个传入指令路由到其相应的处理程序(如之前的教程中所述)。
其余的每个文件都对应上述逻辑组件之一。
program/
├── src/
│ ├── entrypoint.rs // 程序入口点 (process_instruction)
│ ├── instruction.rs // 指令枚举和数据结构
│ ├── processor.rs // 每个指令的业务逻辑
│ ├── state.rs // 账户数据结构
│ ├── error.rs // 自定义错误类型
│ └── lib.rs // 模块声明和重新导出
虽然这些文件名可以是任意的,但惯例是使用与所实现概念相关的名称。
上述结构在 Anchor 和原生 Solana 程序中是相似的。主要区别在于 Anchor 使用宏自动生成入口点和处理器,而在原生 Solana 程序中,你需要手动定义。这种结构适用于简单的程序,但随着程序规模的扩大,你将拥有多个指令、处理器或状态,这意味着你可能需要将它们组织到文件夹中。
让我们深入探讨 Solana 程序应该如何组织。
Anchor 通过抽象原生 Solana 程序中所需的大部分样板代码来简化 Solana 程序开发。它还提供了一个一致的项目模板,支持从编写程序到构建、测试和部署的整个工作流程。
以下是 Anchor 项目的典型结构,你在本系列中已经多次见过。
├── Anchor.toml
├── app
├── Cargo.lock
├── Cargo.toml
├── migrations
│ └── deploy.ts
├── package.json
├── programs
│ ├── hello-program
│ │ ├── Cargo.toml
│ │ ├── src
│ │ │ └── lib.rs
│ │ └── Xargo.toml
│ ├── token_vault
│ ├── Cargo.toml
│ ├── src
│ │ └── lib.rs
│ └── Xargo.toml
├── tests
│ └──hello-program.ts
├── tsconfig.json
└── yarn.lock
此结构的每个部分在 Anchor 的工作流程中都有特定的作用:
Anchor.toml 文件 定义构建和部署设置migrations 文件夹存储部署脚本tests 目录包含在本地验证器上运行的集成测试app 目录可以包含与已部署程序交互的客户端代码请注意,Anchor 中的 programs 目录包含镜像我们之前讨论过的基本 Cargo 样板结构的子目录,这仅适用于单个程序。
Anchor 项目的结构旨在让你可以在一个项目中处理多个链上 Solana 程序。下图比较了典型的 Rust Cargo 项目与 Anchor 如何组织其项目。

anchor build 并获得一个可工作的程序二进制文件。Cargo.toml(如我们前面所述)来手动配置 Cargo 样板作为 Solana 程序。如果你想在一个项目中运行多个程序,你需要一个 programs 目录,就像 Anchor 的 programs 目录一样。惯例是使用 programs 这个名称来存放程序目录,尽管你可以使用任何名称,并且你通过每个程序自己的 Cargo.toml 文件来配置它。请注意,Xargo.toml 文件位于 Anchor 生成的项目结构中。

它们是 Anchor 的默认配置文件,用于处理 Anchor 程序如何编译为 Extended Berkeley Packet Filter (eBPF) 字节码。我们将在下一节中了解更多信息。
每个程序内的 Xargo.toml 文件告诉 Rust 如何为 Solana 的区块链环境编译你的代码。你的 Solana 程序在验证器上的 Solana Virtual Machine (SVM) 中运行,该虚拟机执行 Extended Berkeley Packet Filter (eBPF) 字节码。
当你在自己的机器上编写 Rust 代码并编译它时,Rust 编译器通常会为你的计算机处理器(例如 x86 或 ARM)生成指令。但 Solana 验证器无法执行这些指令。它们只理解 eBPF 字节码。
这种为不同架构进行编译的过程称为交叉编译。Xargo.toml 文件指定了 Rust 编译器应该如何为 eBPF 而不是你的本地机器构建代码。该文件控制 Rust 标准库的哪些部分被包含在最终编译的程序中。
Solana 程序有严格的大小限制(10 MB),因此 Xargo.toml 配置确保编译后的程序只包含你的程序所需的内容。当你运行 anchor init {project-name} 或 anchor new {program-name} 时,Anchor 会自动生成 Xargo.toml 文件。你无需修改它。Xargo.toml 文件的内容如下所示:
[target.bpfel-unknown-unknown.dependencies.std]
features = []
目标名称 bpfel-unknown-unknown 是一个 Rust 编译目标三元组。它告诉 Rust 编译器它正在为哪种机器和环境构建。它有 3 个部分,用连字符分隔。每个部分的含义如下:
bpfel - 为 BPF 架构编译,采用 little-endian 字节序unknown - 第一个 unknown 意味着,不为任何特定操作系统编译unknown - 最后一个 unknown 意味着,不为任何特定 ABI (Application Binary Interface) 编译空的 features (features = []) 数组意味着你正在使用一个针对区块链部署优化的最小标准库版本。
原生 Solana 程序以不同的方式处理交叉编译。你使用 cargo-build-sbf 构建你的程序,它将你的 Rust 代码编译为 eBPF 字节码,而不需要单独的 Xargo.toml 文件。
Anchor 使用 Cargo 工作区来管理一个项目中的多个程序。工作区允许你处理共享依赖项和构建配置的多个相关程序。
Anchor 使用两种不同用途的 Cargo.toml 文件。根目录下的 Cargo.toml 定义了工作区结构和共享构建配置。每个程序目录都包含其自己的 Cargo.toml,声明该特定程序的依赖项。
即使两个 Solana 程序使用相同的 Rust crate,每个程序也必须在其程序目录内的 Cargo.toml 文件中单独声明它。你不能在工作区级别声明一次依赖项,然后让所有程序都继承它。
如果同一个 Anchor 工作区中的两个程序,例如 A 和 B,依赖于同一个 crate,它们可能会以不同的方式使用它。程序 A 可能使用一个库函数,而程序 B 使用五个。这会影响在编译期间可以修剪多少库代码。
但是程序共享构建配置。 工作区依赖项和构建配置定义在你的 Anchor 项目根目录中的 Cargo.toml 文件中,该文件还包含一些默认设置,如下图所示。
根目录中 Cargo.toml 文件的 [workspace] 部分定义了一个 members 数组,该数组定义了属于工作区的特定目录。请注意,它包含 [”programs/”],这是你的默认 Solana 程序目录的名称。下面的 Cargo 文件是根 Cargo.toml 文件:

以下是根 Cargo.toml 文件中每个部分的含义:
[workspace] 部分定义了哪些程序属于工作区,并控制 Cargo 如何处理多个相关包之间的依赖关系。
members = ["programs/*"] - programs 目录内的每个子目录都属于此工作区resolver = "2" - 此设置告诉 Cargo 使用其较新的依赖项解析算法(版本 2)[profile.release] 部分控制在发布模式下构建时的编译设置。它允许你配置优化级别、调试信息和代码生成行为
overflow-checks = true - 在发布版本中保持算术溢出检查启用,防止可能损坏程序状态的整数溢出错误lto = "fat" - 此设置启用链接时优化,它在编译器链接期间(编译器将所有编译代码组合成最终可执行文件的过程)分析整个程序,以移除未使用的代码和内联函数,从而减小最终二进制文件的大小。如果我们不想要更快的 LTO 但优化程度较低,我们可以将 lto 参数设置为 thincodegen-units = 1 - 一次性编译整个程序,而不是将编译工作分成多个并行块,值越高,编译将分为的块越多。将其设置为 1 允许编译器一次性在整个代码库中执行优化,生成更小、更优化的二进制文件,但代价是编译时间更长。默认的 Rust 设置是 codegen-units = 16,这会加快构建速度,但可能导致二进制文件略大。[profile.release.build-override] 部分专门为构建脚本指定编译设置,构建脚本是在主代码编译之抢跑的程序,用于生成代码或配置构建。
opt-level = 3 - 对构建脚本应用最大优化incremental = false - 禁用增量编译。每次编译都从头到尾完成。这会减慢编译时间,但会降低遗留文件干扰编译过程的风险codegen-units = 1 - 对构建脚本应用相同的单单元优化整个 Anchor 项目及其中的各个程序不包含显式的 entrypoint.rs 和 processor.rs 文件,而这在 Solana 项目中是关键要求。如我们之前所讨论的,lib.rs 文件中的 #[program] 属性会自动生成入口点源代码以及处理指令解码、调度和账户反序列化的逻辑(这在原生 Solana 程序中通常会存在于 processor.rs 文件中)。
下面的代码展示了 #[program] 属性在 Anchor 程序中的使用方式:
#[program]
pub mod hello_program {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
msg!("Program initialized!");
Ok(())
}
}
Anchor 1.0 引入了一种项目布局,鼓励在每个程序内部更好地组织文件。
在 Anchor 1.0 中运行 anchor init 将生成所有标准项目组件,例如指令、状态、错误和常量,并将它们放置在单独的模块中。
这一改变有助于新开发者学习组织大型 Solana 程序的更清晰模式。你仍然可以使用 --template single 标志 (anchor init --template single) 生成旧的单文件布局。
以下是新默认结构的示例:
programs
└── vote
├── Cargo.toml
└── src
├── instructions
│ ├── initialize.rs
│ └── mod.rs
├── state
│ └── mod.rs
├── constants.rs
├── error.rs
└── lib.rs
有两个新的注意事项:每个目录内的 mod.rs 文件和 initialize.rs 指令模块。我们将在稍后部分讨论它们。
有了这个新结构,你不再需要从一个 lib.rs 文件开始程序,其中混合了账户和指令。相反,你的状态存在于自己的目录中,并且每个指令都位于自己的模块中。
下面的示例展示了 Anchor 默认使用的旧的单文件模式,其中程序逻辑和账户类型一起位于 lib.rs 内部。
use anchor_lang::prelude::*;
declare_id!("6uAEFiYjmgJhCCqw8JPH8chZRWJPzHFBJYuZFMWaML3w");
#[program]
pub mod program_structure {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
msg!("Greetings from: {:?}", ctx.program_id);
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize {}
至此,你已经了解了 Solana 程序的每个部分的作用以及 Anchor 如何自动组织它们。但即使没有 Anchor,你也可以遵循一致的模块化布局,确保你的程序易于维护。
我们研究了一些顶级 Solana 项目如何构建其程序,并注意到一种一致的模式,该模式促进了协作、可维护性和可扩展性。一个典型的 Solana 程序遵循以下结构,其中 entrypoint 和 processor 目录仅在原生 Solana 程序中明确定义。
program/
├── Cargo.toml
└── src/
├── entrypoint.rs
├── instructions/
│ ├── mod.rs
│ ├── initialize.rs
│ └── transfer.rs
├── processor/
│ ├── mod.rs
│ ├── initialize.rs
│ └── transfer.rs
├── state/
│ ├── mod.rs
│ ├── account.rs
│ └── config.rs
├── error.rs
├── utils/
│ ├── mod.rs
│ ├── pda.rs
│ ├── math.rs
│ └── validation.rs
└── lib.rs
我们引入了新文件:每个目录的 mod.rs 和上一节及上述结构中的 initialize.rs。下面我们来解释它们:
mod.rs 文件mod.rs 文件在 Rust 中充当目录的模块声明点。当你将相关代码组织到 instructions/ 这样的文件夹中时,Rust 不会自动将其中的文件识别为程序的一部分。你需要明确告诉 Rust 哪些文件应该被编译并可供代码库的其他部分访问,这就是 mod.rs 的作用(这并非任意,Rust 在通过目录定义模块时会期望这个确切的文件名)。
以下是 instructions/mod.rs 的样子:
pub mod initialize;
pub mod transfer;
每行都将目录中的一个文件声明为一个模块。pub 关键字使这些模块可以在 instructions 目录之外公开访问。如果没有这些声明,Rust 将不会编译 initialize.rs 或 transfer.rs,并且程序的其他部分也无法导入它们的内容。
在你的 lib.rs 文件中,你会暴露 instructions 模块,使其在整个程序中可用:
pub mod instructions;
pub use instructions::*;
这种模式在你程序的每个目录中重复。
processor/mod.rs 文件声明处理器模块state/mod.rs 声明状态模块utils/mod.rs 声明工具模块。instructions/initialize.rs 文件Anchor lib.rs 文件中的 initialize 函数是 Anchor 设置程序初始状态以便指令使用的约定。我们可以将状态初始化重定位到不同的文件中,以保持 lib.rs 文件的整洁。
这是默认生成的 initialize 函数在 lib.rs 文件中的样子:

当你将 initialize 函数移至专用的 initialize.rs 文件并将其作为 instructions 目录中的一个模块时,在 Anchor 项目的 lib.rs 文件中将按如下方式使用它:

processors 目录在原生 Solana 中,processor/ 实现了指令处理程序。每个处理器模块都对应 instructions/ 中的一个指令模块。
一个标准的 Anchor 风格程序结构,针对单个程序而言,将如下所示,没有入口点或处理器文件。此结构镜像了 Anchor 1.0 的项目结构:
programs/
└── my_program/
├── Cargo.toml
└── src/
├── lib.rs
├── instructions/
│ ├── transfer.rs
│ └── mod.rs
├── state/
│ ├── config.rs
│ ├── account.rs
│ └── mod.rs
├── error.rs
├── utils/
│ ├── mod.rs
│ ├── pda.rs
│ ├── math.rs
│ └── validation.rs
有了这种结构,开发者可以更容易地定位相关逻辑,理解程序流程,并在不破坏现有行为的情况下扩展功能,无论是原生 Solana 程序还是 Anchor 程序。
本文是 Solana 开发 系列教程的一部分
- 原文链接: rareskills.io/post/organ...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!