Solana笔记 04.认识 Rust 主程序

跟我一起从0开始学习Solana合约开发,一起实操,一起做项目。这是一个系列文章,系统地记录了我的学习笔记。

欢迎阅读我的系列文章《Solana笔记》第四讲!经过前面的学习,相信你已经完成了环境配置,并熟悉了Anchor项目。今天,我将带你学习一些关键的 Rust 基础知识,并理解src/lib.rs主程序。

请你先思考这样一个问题:用anchor init生成的主程序代码为什么是src/lib.rs而不是src/main.rs

让我们带着这个问题开始今天的内容。

库项目与二进制程序

对于刚才的问题,需要了解一些solana运行时的知识,这是一个很深入的话题,不过别担心,我们当前只需要简单了解一下,能够理解你正在开发的程序就行了。更多内容我会在以后专门写文章进行探讨。

main.rs文件编译后生成的是一个二进制程序(如.exe)。而Solana运行时只能加载.so文件并调用其中的函数,不能直接运行独立的二进制程序。因此,我们编写的Solana程序最终都会被编译为.so文件,之后再部署到链上存储在对应的账户(Account)中,每个Solana程序对应唯一的.so文件。当用户提交交易调用某个程序时,Solana节点会从账户中加载对应的.so文件到内存,执行其中的逻辑。

为了生成.so文件,需要用到rust的库项目(默认入口文件为lib.rs),而不是rust的二进制程序。另外,还需要在Cargo.toml中配置crate-type

打开programs/guide_1/Cargo.toml,可以看到如下配置:

[lib]  
crate-type = ["cdylib", "lib"]  
name = "guide_1"  

在这段配置中,name指定了库的名称,在生成的.so文件中会使用这个名称;crate-type指定了库项目的两种输出类型:

1. cdylib: 生成符合 C 动态库标准的共享对象文件(.so),适合在 Solana 上运行

2. lib: 生成 Rust 专用的静态库文件(.rlib),通常用于测试或其他 Rust 项目的依赖

通过这种配置,Rust编译器会为库项目生成两种类型的输出文件,我列个表格对比一下:

cdylib lib
输出文件 .so文件 静态库文件(.rlib)
谁使用 Solana 或其他非Rust平台加载运行 Rust自己使用
用途 部署至Solana网络 本地开发和测试时作为依赖
特点 通用的,可以被外部程序(如 Solana)加载 Rust专用

如果只配置 "cdylib",不配置 "lib" 可以吗?

仍然可以生成 .so 文件供 Solana 使用。但本地开发和测试可能会更麻烦,因为无法直接引用库中的函数,需要通过模拟或额外的设置来测试。所以,同时设置 cdylib 和 lib 是一种既方便开发又支持部署的常见做法。

Rust 的宏是一个强大而复杂的工具,为了简化学习、少走弯路,并专注于 Solana 程序开发,暂时不需要掌握 Rust 所有的宏知识。可以先简单理解为:

宏就像是代码模板,使用宏可以自动生成重复的代码,帮助简化开发工作。

以下是一些在 Solana 开发中经常用到的宏。掌握这些,就足以阅读 Solana 源码和开发 Solana 程序了。

Anchor 框架提供的宏

1. declare_id!

declare_id!宏用于声明程序的 Program ID。

2. #[program]

#[program]宏是 Anchor 框架提供的,用于声明一个模块为 Solana 程序。可以理解为程序的入口。一个程序里只能有一个 #[program] 宏。

3. msg!

在 Solana 链上的执行环境中,由于无法直接使用 Rust 的 println! 宏,Anchor 框架提供了 msg! 宏,这是专为 Solana 链上环境设计的日志工具,可用于输出日志信息以便调试。举个例子:

let user = "xxxxxxx";
let amount = 42;

// 使用 msg! 输出日志
msg!("User: {}, transferred amount: {}", user, amount);

这段代码展示了如何使用 msg! 宏记录用户和转账金额等信息,以便在链上运行时查看日志。

4. #[derive(Accounts)]

#[derive(Accounts)]宏用于处理账户验证,可以帮助你减少繁琐的账户验证代码。

先简单知道这个宏的作用即可,因为这个宏涉及到 Solana 账户模型,后面单独写一篇文章再详细讲解。

Rust 标准库的宏

这里只讲一个 vec! 宏。vec! 是 Rust 标准库提供的一个宏,用于快速创建动态数组(即 Vec<T> 类型)。Vec 是一种可增长的数组类型,你可以使用 push 方法向数组中添加元素。

你可以将 vec! 类比为 Solidity 中的动态数组,二者都允许你向数组中添加元素。然而,vec! 与 Solidity 的动态数组在运行环境、存储位置和操作成本上有显著不同。以后我会专门写文章对比这两者。

1. vec! 无默认值的用法

// 创建一个空的 Vec,并通过 push 添加元素
let mut data1 = vec![];  // 创建一个空的 Vec
data1.push(42);          // 向 Vec 中添加一个元素 42
data1.push(7);           // 向 Vec 中添加另一个元素 7

// 使用 msg! 打印 Vec
msg!("{:?}", data1);     // 输出: [42, 7]

这里使用了 mut 关键字。在 Rust 中,默认情况下,变量是不可变的。也就是说,如果没有 mut 修饰符,变量就不能执行如 push 这样的操作。

2. vec! 有默认值的用法

// 创建一个长度为 1024 的字节数组,初始化每个元素为 0
let data2: Vec<u8> = vec![0; 1024]; 

// 使用 msg! 打印 Vec
msg!("{:?}", data2);     // 输出: [0, 0, 0, ..., 0] (1024个0)

相关解释

vec![0; 1024]: 通过 vec![value; size] 可以创建一个带有默认值的 Vec,其中 value 是每个元素的默认值,size 是数组的长度。

{}:用于打印简单类型(如整数、浮点数等)。

{:?}:用于打印更复杂的类型(如数组、结构体等)。在这里,{:?} 格式化符号允许我们查看整个数组的内容。

关于 vec!,随着学习的深入,我会在后续补充更多具体的应用场景和实际开发中的使用示例,帮助加深对其的理解。今天先了解这些基础内容即可。

常用数据类型

这里和以太坊中 solidity 语言的数据类型对比着学习。

Pubkey

Pubkey 可以类比为 solidity 中的 address。用于标识账户、程序或智能合约的地址。

let pubkey = Pubkey::new_from_array([0; 32]);

u64

u64 可以类比为 solidity 中的 uint256。Rust 中并没有与 uint256 完全相同的类型,但 u64 能满足大部分需要处理整数的场景。Solana 程序通常使用 u64 来处理数字较大的需求。

let balance: u64 = 1000;

bool

boolsolidity 中的 bool一样。

let is_active: bool = true;

Vec<u64>

Vec&lt;u64> 可以类比为 solidity 中的 uint256[]。上面讲过了,不再赘述。

映射类型(键值对)

HashMap&lt;u64, u64>BTreeMap&lt;u64, u64> 可以类比为 solidity 中的 mapping(uint256 => uint256)。是一种键值对数据结构,类似于哈希表。

HashMap 适用于无序的键值对存储,而 BTreeMap 用于需要按顺序存储键值对的场景。

use std::collections::HashMap;
use std::collections::BTreeMap;

let mut hmap: HashMap&lt;u64, u64> = HashMap::new();
hmap.insert(1, 1001);

let mut bmap: BTreeMap&lt;u64, u64> = BTreeMap::new();
bmap.insert(2, 1002);

迫不及待?动手试试吧!

学习编程,实践才是关键!下面我们将以上讲过的所有代码在本地运行测试,看看效果。

准备工作

打开两个终端:

  • 第一个运行 solana-test-validator 启动本地节点。
  • 第二个运行 solana logs 查看程序日志。

src/lib.rs文件的完整代码如下:

use anchor_lang::prelude::*;
use std::collections::HashMap;
use std::collections::BTreeMap;

declare_id!("7pR9Lstthnphgx2bLFo6WW4gbRJmGiJDujL4wxYRFRWg");

#[program]
pub mod guide_1 {
    use super::*;

    pub fn initialize(ctx: Context&lt;Initialize>) -> Result&lt;()> {
        msg!("Greetings from: {:?}", ctx.program_id);

        // 简单类型
        let pubkey = Pubkey::new_from_array([0; 32]);
        let balance: u64 = 1000;
        let is_active: bool = true;
        msg!("pubkey: {:?}, balance: {}, is_active: {}", pubkey, balance, is_active);

        // 映射类型
        let mut hmap: HashMap&lt;u64, u64> = HashMap::new();
        hmap.insert(1, 1001);
        let mut bmap: BTreeMap&lt;u64, u64> = BTreeMap::new();
        bmap.insert(2, 1002);
        msg!("HashMap: {:?}, BTreeMap: {:?}", hmap, bmap);

        // 数组类型(无默认值)
        let mut data1 = vec![];
        data1.push(42);
        data1.push(7);
        msg!("data1: {:?}", data1);

        // 数组类型(有默认值)
        let data2: Vec&lt;u8> = vec![0; 1024]; 
        msg!("data2: {:?}", data2); 

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}

现在,执行测试命令:

anchor test --skip-local-validator

报错了吗?我估计大部分人会报错,下一篇我来讲解常见的报错问题,带你彻底理解并解决初学者很容易就遇到的各种报错。如果你想提前看到我的更新,可以关注我的公众号:认知那些事

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
认知那些事
认知那些事
0x2b62...95a0
人立于天地之间,必然有我们的出路。