Solana程序指令

本文介绍了Solana程序如何处理指令,以及实现此功能的Rust编程概念。Solana程序通过指令处理操作,指令是发送到程序的结构化消息。文章讲解了如何使用枚举、结构体和特性管理指令,以及如何将原始字节转换为可用数据,并提供了一个清晰的文件结构和核心Rust概念,可以编写清晰、可扩展和高效的Solana程序。

Solana 程序是去中心化应用的核心,理解它们如何处理信息至关重要。本文将分解 Solana 程序如何处理指令,并介绍一些使这一切成为可能的必要 Rust 编程概念。

处理 Solana 程序中的指令

想象一个允许你管理笔记的程序。你可能希望创建新笔记、更新现有笔记和删除它们。这些操作中的每一个都是程序中的不同“函数”或“指令处理程序”。当你与 Solana 程序交互时,你会向其发送“指令数据”。此数据告诉程序你希望它执行的特定操作。

由于此指令数据以原始字节束的形式到达,因此如果将其转换为更结构化的格式,开发人员更容易使用它。在 Rust 中,我们经常创建一个特殊的数据类型来表示这些指令。本文将向你展示如何设置此类型,将原始数据转换为此类型(一个称为反序列化的过程),然后根据该指令告诉你的程序要执行的操作。

必要的 Rust 概念

在我们更深入地研究 Solana 之前,让我们回顾一下你将遇到的一些核心 Rust 概念。

Structs

“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;

枚举 (Enums)

“enum”(枚举的缩写)允许你通过列出其所有可能的变体来定义类型。例如,LightStatus enum 可以有两个变体:OnOff

enum LightStatus {
    On,
    Off
}

Enums 也可以在其变体中保存数据,类似于 struct 中的字段:

enum LightStatus {
    On {
        color: String
    },
    Off
}

let light_status = LightStatus::On { color: String::from("red") };

这是一篇简化你提供的概念的文章,没有删除任何信息并保持人为生成:

Match 语句

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 的类型并返回其对应的美分值。

实现 (impl)

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

你现在还不会创建自己的 traits 或 attributes,但了解它们是好的。

  • Traits 定义了一组类型可以实现的加粗行为加粗。如果一个 trait 说一个类型“可以叫”,那么任何采用该 trait 的类型都必须提供一个 bark() 函数。
  • Attributes 就像你添加到代码中的标签或元数据。它们可以影响编译器处理代码的方式。

一个常见的 attribute 是 derive。当你向类型添加 #[derive(SomeTrait)] 时,Rust 会自动生成必要的代码来实现该类型的 SomeTrait。这对于节省时间并减少样板代码非常有用。

将指令表示为 Rust 数据类型

现在,让我们将这些 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 变体如何携带执行相应操作所需的特定数据(如 titlebodyid)。

反序列化指令数据

指令数据以原始字节数组的形式到达你的程序。我们需要一种可靠的方式将此数组转换为我们结构化的 NoteInstruction enum。此过程称为“反序列化”。

我们将使用 borsh crate 来实现这一点,它提供了 BorshDeserializeBorshSerialize 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 的错误部分来转换它。
  • ? 运算符:这是一种处理 ResultOption 类型的简洁方法。如果操作成功,它会展开值。如果出现错误或“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 modlib.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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
blockmagnates
blockmagnates
江湖只有他的大名,没有他的介绍。