本文介绍了Solana程序如何处理指令,以及实现此功能的Rust编程概念。Solana程序通过指令处理操作,指令是发送到程序的结构化消息。文章讲解了如何使用枚举、结构体和特性管理指令,以及如何将原始字节转换为可用数据,并提供了一个清晰的文件结构和核心Rust概念,可以编写清晰、可扩展和高效的Solana程序。
Solana 程序是去中心化应用的核心,理解它们如何处理信息至关重要。本文将分解 Solana 程序如何处理指令,并介绍一些使这一切成为可能的必要 Rust 编程概念。
想象一个允许你管理笔记的程序。你可能希望创建新笔记、更新现有笔记和删除它们。这些操作中的每一个都是程序中的不同“函数”或“指令处理程序”。当你与 Solana 程序交互时,你会向其发送“指令数据”。此数据告诉程序你希望它执行的特定操作。
由于此指令数据以原始字节束的形式到达,因此如果将其转换为更结构化的格式,开发人员更容易使用它。在 Rust 中,我们经常创建一个特殊的数据类型来表示这些指令。本文将向你展示如何设置此类型,将原始数据转换为此类型(一个称为反序列化的过程),然后根据该指令告诉你的程序要执行的操作。
在我们更深入地研究 Solana 之前,让我们回顾一下你将遇到的一些核心 Rust 概念。
“struct”(结构体的缩写)是一种自定义数据类型,可让你将相关的信息片段组合在一起,放在一个名称下。可以把它想象成创建对象的蓝图。struct 中的每个信息片段都称为“字段”,并且可以有自己的类型。
这是一个 User
struct 的示例:
struct User {
active: bool,
email: String,
age: u64
}
要使用 struct,你需要通过为每个字段提供值来创建它的“实例”:
let mut user1 = User {
active: true,
email: String::from("test@test.com"),
age: 36
};
你可以使用“点表示法”访问或更改单个字段:
user1.age = 37;
“enum”(枚举的缩写)允许你通过列出其所有可能的变体来定义类型。例如,LightStatus
enum 可以有两个变体:On
或 Off
。
enum LightStatus {
On,
Off
}
Enums 也可以在其变体中保存数据,类似于 struct 中的字段:
enum LightStatus {
On {
color: String
},
Off
}
let light_status = LightStatus::On { color: String::from("red") };
这是一篇简化你提供的概念的文章,没有删除任何信息并保持人为生成:
match
语句是一种强大的方法,可以将一个值与一系列模式进行比较,然后根据匹配到的模式执行特定的代码。它们类似于其他编程语言中的 switch
语句。match
语句必须涵盖所有可能的场景;否则,你的代码将无法编译。
enum Coin {
Penny,
Nickel,
Dime,
Quarter
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25
}
}
在此示例中,match
语句检查 coin
的类型并返回其对应的美分值。
Rust 中的 impl
关键字用于定义属于特定类型(如 struct 或 enum)的函数和常量
struct Example {
number: i32
}
impl Example {
fn boo() {
println!("boo! Example::boo() was called!");
}
fn answer(&mut self) {
self.number += 42;
}
fn get_number(&self) -> i32 {
self.number
}
}
impl
块中的某些函数可以直接在类型本身上调用(Example::boo()
),而其他函数则需要该类型的实例(example.answer()
)。
你现在还不会创建自己的 traits 或 attributes,但了解它们是好的。
bark()
函数。一个常见的 attribute 是 derive
。当你向类型添加 #[derive(SomeTrait)]
时,Rust 会自动生成必要的代码来实现该类型的 SomeTrait
。这对于节省时间并减少样板代码非常有用。
现在,让我们将这些 Rust 概念带回到 Solana 程序。如前所述,大多数程序处理多个指令。对于我们的笔记应用程序,我们可能有诸如“CreateNote”、“UpdateNote”和“DeleteNote”之类的指令。
由于指令是不同类型的操作,因此它们非常适合 enum
。
enum NoteInstruction {
CreateNote {
title: String,
body: String,
id: u64
},
UpdateNote {
title: String,
body: String,
id: u64
},
DeleteNote {
id: u64
}
}
请注意,每个 NoteInstruction
变体如何携带执行相应操作所需的特定数据(如 title
、body
和 id
)。
指令数据以原始字节数组的形式到达你的程序。我们需要一种可靠的方式将此数组转换为我们结构化的 NoteInstruction
enum。此过程称为“反序列化”。
我们将使用 borsh
crate 来实现这一点,它提供了 BorshDeserialize
和 BorshSerialize
traits。通过向 struct 添加 #[derive(BorshDeserialize)]
,我们会自动获得将数据反序列化为该 struct 的方法。
为了使反序列化变得简单,我们首先创建一个“payload”struct,该 struct 镜像我们字节数组中数据的结构:
##[derive(BorshDeserialize)]
struct NoteInstructionPayload {
id: u64,
title: String,
body: String
}
接下来,我们将一个 impl
块添加到我们的 NoteInstruction
enum,特别是 unpack
函数。此函数采用原始指令数据并将其转换为正确的 NoteInstruction
变体。一种常见的做法是使用指令数据的第一个字节作为标识符,以确定要调用的指令处理程序(例如,0 代表 CreateNote,1 代表 UpdateNote,2 代表 DeleteNote)。
impl NoteInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
// 获取第一个字节作为指令类型,其余字节作为 payload
let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;
// 将剩余的字节反序列化为我们的 payload struct
let payload = NoteInstructionPayload::try_from_slice(rest)
.map_err(|_| ProgramError::InvalidInstructionData)?;
// 根据“variant”字节,创建正确的 NoteInstruction enum
match variant {
0 => Ok(Self::CreateNote {
title: payload.title,
body: payload.body,
id: payload.id,
}),
1 => Ok(Self::UpdateNote {
title: payload.title,
body: payload.body,
id: payload.id,
}),
2 => Ok(Self::DeleteNote { id: payload.id }),
_ => Err(ProgramError::InvalidInstructionData), // 处理未知的变体
}
}
}
让我们分解一下 unpack
中的一些 Rust 语法:
split_first()
:此函数将输入字节数组分为它的第一个元素(variant
)和数组的其余部分(rest
)。ok_or
:这会将 Option
类型(可以是值,也可以是“nothing”)转换为 Result
类型(可以是成功,也可以是错误)。如果它是“nothing”,它将返回指定的错误。map_err
:通过将函数应用于 Result
的错误部分来转换它。?
运算符:这是一种处理 Result
和 Option
类型的简洁方法。如果操作成功,它会展开值。如果出现错误或“nothing”,它会立即从当前函数返回该错误。有了我们的 unpack
方法,你的 Solana 程序的主入口点现在可以轻松确定已发送的指令并执行相应的代码。
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
msg!("Note program entrypoint");
// 将指令数据反序列化为我们的 NoteInstruction enum
let instruction = NoteInstruction::unpack(instruction_data)?;
// 使用 match 语句根据指令执行不同的代码路径
match instruction {
NoteInstruction::CreateNote { title, body, id } => {
msg!("Instruction: Create Note");
// 你的代码来创建笔记放在这里
},
NoteInstruction::UpdateNote { title, body, id } => {
msg!("Instruction: Update Note");
// 你的代码来更新笔记放在这里
},
NoteInstruction::DeleteNote { id } => {
msg!("Instruction: Delete Note");
// 你的代码来删除笔记放在这里
}
}
}
对于简单的程序,你可能会将指令逻辑直接放在此 match
语句内。但是,对于更复杂的程序,最佳做法是将每个指令的逻辑移动到其自己的单独函数中,并从 match
语句中调用该函数。这可以使你的代码井井有条,并且更易于管理。
随着你的 Solana 程序不断增长,将所有内容放在一个文件中会变得混乱。一个好的做法是将你的程序拆分为多个文件,每个文件处理逻辑的特定部分。例如,指令定义和反序列化代码可以转到一个文件,而程序的主入口点转到另一个文件。
一个典型的结构可能如下所示:
lib.rs
:主入口点,你可以在其中注册其他模块。instruction.rs
:包含 NoteInstruction
enum 及其 unpack
逻辑。要使文件可以相互访问,你需要使用 pub mod
在 lib.rs
中注册它们:
// 在 lib.rs 内部
pub mod instruction;
此外,你希望在其他文件中使用的任何声明(如 enums 或 structs)都必须使用 pub
关键字标记,以使其公开可用。
pub enum NoteInstruction { ... }
通过遵循这些原则,你可以构建结构良好、可维护且健壮的 Solana 程序,这些程序可以有效地处理各种指令并根据应用程序的需求进行扩展。
Solana 程序通过指令处理操作,指令只是发送到你的程序的结构化消息。Rust 帮助使用 enums、structs 和 traits 管理这些指令,而反序列化将原始字节转换为可用数据。借助清晰的文件结构和一些核心 Rust 概念,你可以编写清晰、可扩展且高效的 Solana 程序。
如果你对去中心化基础设施、链上数据系统或使用预言机网络构建真实世界的项目感兴趣,请关注:
- 原文链接: blog.blockmagnates.com/s...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!