本文深入探讨了使用 Pinocchio 构建 Solana 程序,Pinocchio 是一个高度优化的零依赖库,旨在替代 solana-program crate,通过零拷贝技术优化程序执行,从而减少计算单元(CU)的使用。
12 分钟阅读
2025年6月27日
Pinocchio 是一个高度优化、零依赖的库,可用于构建原生 Solana 程序。Pinocchio 由 Solana 的 Agave 客户端的核心开发者 Anza 创建。
Exo Tech 是一家领先的 Solana 开发公司,也是 Pinocchio 最早的采用者之一。通过我们的客户工作,我们使用 Pinocchio 开发了多个生产程序,并为 SDK 贡献了缺失的功能。
本文深入探讨了使用 Pinocchio 构建程序,探索了其优点和缺点。 我们的目标是使开发人员掌握知识,以确定 Pinocchio 是否适合他们的程序。 但是,请务必注意,Pinocchio 对初学者并不友好,因为它优先考虑优化而不是开发者体验。
Pinocchio 库 是 solana-program
crate 的替代品,它通过广泛使用 zero-copy
类型来优化程序执行。zero-copy
意味着在读取或写入时,数据不需要复制到单独的内存地址,从而节省了计算资源(或 Solana 中的 CU)。
该库具有零依赖性,并且是“no_std”。 Rust 的 std
crate 提供了访问操作系统资源的常用方法以及运行时,但鉴于 Solana 虚拟机 (SVM) 本身就是一个运行时,因此这种开销是不必要的。
每个 Solana 程序都需要一个入口点,运行时调用该入口点以执行程序。 solana-program
库公开了 entrypoint!
宏,该宏反序列化程序的输入,设置堆分配器,并创建一个 panic 处理程序。
macro_rules! entrypoint {
($process_instruction:ident) => {
/// # Safety
#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
let (program_id, accounts, instruction_data) =
unsafe { $crate::entrypoint::deserialize(input) };
match $process_instruction(&program_id, &accounts, &instruction_data) {
Ok(()) => $crate::entrypoint::SUCCESS,
Err(error) => error.into(),
}
}
$crate::custom_heap_default!();
$crate::custom_panic_default!();
};
}
Pinocchio 导出三个入口点宏。
对于那些从 solana-program
迁移过来的用户,entrypoint!
宏的功能大致相同,通过反序列化程序输入并设置分配器和处理程序。
但是,其他两个宏将入口点与堆分配器和 panic 处理程序的设置分离,从而使开发人员可以更好地控制在执行程序逻辑之前省略或优化。
program_entrypoint!
反序列化程序输入,类似于 solana-program
,而 lazy_program_entrypoint!
仅包装输入缓冲区并将处理推迟到程序,从而可以更好地控制计算。
由于这些宏未设置堆分配器或 panic 处理程序,因此 Pinocchio 库公开了默认宏供开发人员使用。
另外,如果程序知道它永远不需要堆内存,则 no_allocator!
会通过放弃设置内存分配器来节省计算单元 (CU)。
我们简要提到了 solana-program
和 Pinocchio 的入口点如何反序列化程序输入。 但是,重要的是要了解反序列化的方式有何不同,因为这是节省大量 CU 的地方。
乍一看,传递给程序指令处理程序的反序列化输入看起来相同:
/// solana-program 和 pinocchio 看起来都一样
process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult
关键区别在于 AccountInfo
的实现。
虽然 solana-program
将数据写入拥有数据的 AccountInfo
结构体,但 Pinocchio 的 AccountInfo 结构体本身只是指向表示帐户的底层输入数据的指针。 这减少了需要复制的数据量,从而节省了大量 CU。
由于指令处理器接收对指针的引用,因此使用 Pinocchio 库的开发人员会注意到,他们的逻辑很少拥有他们正在处理的数据的所有权。
在尝试访问 AccountInfo
上的值时,可以很容易地看到这一点。 使用 key()
方法读取帐户的公钥将返回对 Pubkey
的引用。 这使得在整个程序执行过程中读取帐户信息更便宜,并且修改帐户数据也更便宜。
一个很好的例子,它继续利用零拷贝进行优化是 p-token 程序。
该程序旨在替代规范的 SPL Token Program,但它使用 Pinocchio 大大减少了每个事务的计算单元数量。
你会很快注意到,所有状态都通过指针访问。
与其反序列化 token 帐户,不如检查来自 AccountInfo
的数据,然后返回一个指针。
每个属性都通过一个函数访问,所有不是原始类型的值都返回一个保持零拷贝的引用。
有关为什么这会大大减少 CU 使用率的更多信息,请查看这篇关于 CU 优化的文章。
Anchor 是一个非常流行的、有主见的 Solana 程序开发框架。 它被认为比 Pinocchio 更高级,因为它不包含公开底层结构(如 AccountInfo
)的逻辑。
相反,Anchor 依赖于前面提到的 solana-program
crate,并公开 trait 和宏来简化程序开发过程。 Anchor 提供了指令鉴别器模式和 帐户反序列化 逻辑。 反序列化逻辑依赖于 Borsh,Borsh 需要将数据复制到另一个内存地址,因为它不是零拷贝的。
虽然 Anchor 的便利性加快了 Solana 程序开发过程,但它的代价是使用了更多的 CU。
另一方面,Pinocchio 是一个旨在替代 solana-program
的库,适用于开发人员需要微调计算使用量的情况。 它完全没有主见,并允许开发人员以他们认为合适的任何方式构造程序。 每个 Pinocchio 项目的布局可能看起来完全不同,而 Anchor 项目具有明确定义的结构。
Pinocchio 库不处理任何客户端绑定或实现。 另一方面,Anchor 对 IDL 的生成提供了一流的支持,IDL 可在客户端用于与程序交互。
使用 Pinocchio 的开发人员必须编写自己的客户端或使用其他工具,如 Shank 和 Codama,我们将在下面的 “使用 Pinocchio 构建的补充工具” 部分中进行概述。
Steel 是另一个用于编写 Solana 程序的框架。 Steel 目前构建在 solana-program
之上,它公开了宏、函数和模式,使编写安全且富有表现力的程序变得容易。
Steel 的独断专行使其易于阅读,同时保持模块化。 开发人员可以选择仅使用他们需要的 Steel 组件,这与 Anchor 不同,Anchor 是一个全有或全无的框架。
Steel 的 account!
宏使用 bytemuck 来解析帐户结构,而 Pinocchio 根本不处理帐户解析。 这还包括可链接的解析器和断言,从而可以轻松添加自定义验证。 Pinocchio 没有开箱即用的这种模式,需要开发人员编写自己的验证模式。
但是,当涉及到常见的跨程序调用 (CPI)(如系统程序和 Token 程序)时,Pinocchio 和 Steel 都公开了使此类调用变得容易的模式。
Pinocchio 经过高度优化,但所有细节都由开发人员决定。 Steel 是 solana-program
库的一个很好的模块化包装器,旨在改善开发人员的体验。
为了演示用 Pinocchio 编写的程序,我们将从 Solana 开发人员示例中重写 创建 token 程序。
这是一个简单的程序,其中包含一条指令,用于创建 Token2022 token mint 并使用 Metadata token 扩展 存储有关该 token 的信息。 元数据将通过包含名称、符号和 uri 的指令数据提供。
让我们首先定义我们程序的入口点。
我们正在使用完整的入口点宏,因为我们想使用 Pinocchio 的默认分配器和 panic 处理。
entrypoint!(process_instruction);
fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
Ok(())
}
接下来,我们定义指令数据的结构,使其与其他示例程序的数据结构相匹配。 为了节省开发时间,我们将使用 Borsh 进行反序列化,并将更优化的反序列化方法留到另一篇文章中。
#[derive(BorshDeserialize, Debug)]
pub struct CreateTokenArgs {
pub name: String,
pub symbol: String,
pub uri: String,
pub decimals: u8,
}
现在编写指令处理器中的逻辑。
我们必须做的第一件事是从帐户列表中解构帐户并将指令数据反序列化到我们的 CreateTokenArgs
中。
let [mint_account, mint_authority, payer, token_program, _system_program] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
let args = CreateTokenArgs::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
解析帐户和指令数据后,我们调用 System 程序 CreateAccount
指令。
下面我们使用 ` pinocchio_system
crate 中的 CreateAccount
结构体,因为它通过设置结构体的值并调用 invoke
来非常方便地进行 CPI。
与创建普通 SPL Token mint 不同,我们必须确定我们正在使用的 token 扩展所需的额外空间。
Metadata Pointer 扩展的大小是静态的,而 Token Metadata 扩展必须根据提供的参数动态计算。
/// [4 (扩展鉴别器) + 32 (update_authority) + 32 (metadata)]
const METADATA_POINTER_SIZE: usize = 4 + 32 + 32;
/// [4 (扩展鉴别器) + 32 (update_authority) + 32 (mint) + 4 (name 大小) + 4 (symbol 大小) + 4 (uri 大小) + 4 (additional_metadata 大小)]
const METADATA_EXTENSION_BASE_SIZE: usize = 4 + 32 + 32 + 4 + 4 + 4 + 4;
/// 用于使 Mint 和 Account 扩展以相同索引开始的填充
const EXTENSIONS_PADDING_AND_OFFSET: usize = 84;
/* 在 `process_instruction` 中 */
let extension_size = METADATA_POINTER_SIZE
+ METADATA_EXTENSION_BASE_SIZE
+ args.name.len()
+ args.symbol.len()
+ args.uri.len();
let total_mint_size = Mint::LEN + EXTENSIONS_PADDING_AND_OFFSET + extension_size;
let rent = Rent::get()?;
// 创建 Mint 的帐户
CreateAccount {
from: payer,
to: mint_account,
owner: token2022_program.key(),
lamports: rent.minimum_balance(Mint::LEN),
space: Mint::LEN as u64,
}
.invoke()?;
在调用 CreateAccount
之后,SystemProgram 已将 Token2022 程序注册为 mint 帐户的所有者。
接下来,我们必须通过初始化 Metadata Pointer 扩展、使用 Token2022 程序初始化 Mint 帐户以及初始化我们的程序作为参数接收的元数据值来设置帐户数据。
以下 CPI 来自 pinocchio_token
crate 的一个正在积极开发的分支。 因此,值得注意的是,由于 Token2022 功能计划与 SPL Token crate 分离,因此该代码可能已过时。
// 初始化指向 Mint 帐户的 MetadataPointer 扩展
InitializeMetadataPointer {
mint: mint_account,
authority: Some(*payer.key()),
metadata_address: Some(*mint_account.key()),
}
.invoke()?;
// 现在将该帐户初始化为 Token2022 Mint
InitializeMint2 {
mint: mint_account,
decimals: args.decimals,
mint_authority: mint_authority.key(),
freeze_authority: None,
}
.invoke(TokenProgramVariant::Token2022)?;
// 在 Mint 帐户中设置元数据
InitializeTokenMetadata {
metadata: mint_account,
update_authority: payer,
mint: mint_account,
mint_authority: payer,
name: &args.name,
symbol: &args.symbol,
uri: &args.uri,
}
.invoke()?;
就这样!
现在我们有一个使用 Pinocchio 编写的、具有自包含元数据和 Token2022 的 token mint。
这段代码还有改进的空间,以确保最大限度的优化,但我们希望它可以让你了解如何使用 Pinocchio 编写程序。
Pinocchio 的特定工具很少,但正在增长。
帐户(反)序列化必须由 Pinocchio 程序的开发人员实现。 手动完成时,这是一个繁琐且容易出错的过程。 Bytemuck 是一个很棒的库,可以轻松地将字节数组作为结构体读取和写入。 这意味着通过限制需要复制到内存中的数据量,可以对其进行相当程度的优化。
Borsh 是处理没有固定大小的帐户时的另一种解决方案,尽管它的计算友好性较低,这也是人们选择使用 Pinocchio 而不是 Anchor 的原因之一。
由于 Pinocchio 是一个库,因此它没有像 Anchor 那样内置的 IDL 生成。 IDL(接口定义语言)是一个 JSON 文件,用于定义 Solana 程序的公共接口,包括其指令、帐户结构和错误代码,从而实现标准化交互和简化的客户端开发。
对于 IDL 生成,我们建议使用 Shank。 这个 crate 使开发人员可以非常轻松地注释其代码并使用 CLI 生成有效的 IDL。 将 ShankAccount
宏添加到结构体的派生语句中表明它是一个应该(反)序列化的帐户。 运行 shank CLI 后,此结构将最终成为 IDL 中的类型化帐户,然后用于客户端生成。
另一个重要的宏是程序指令枚举的 ShankInstruction
,它允许使用 #[account]
属性来指示该特定指令的列表中,每个帐户的索引和权限。
有关使非 Anchor 程序的 IDL 生成变得容易的有用代码注释的更多信息,请参阅 shank-macro 存储库。
一旦你有了 IDL,使用 Codama 客户端生成就变得容易了。 如果生成的代码不符合你的需求,那么你将需要手动编写客户端。
在 Exo Tech,我们整理了一个 Pinocchio 项目模板 来帮助我们快速启动 Solana 程序存储库。 随时尝试一下并打开一个 pull request 以进行任何改进!
虽然 Pinocchio 旨在替代 solana-program
,但它尚未达到功能对等。 某些 sysvar 尚未支持,非核心 crate 尚未完全支持或不存在。 例如,Pinocchio Token 程序 crate 中不支持多个签名者。 也没有对 Token2022 的支持,但它正在开发中。
使用 Pinocchio 的一个更重要的缺点是为其他 Solana 程序开发的所有 SDK 都使用 solana-program
crate。 这意味着每个 SDK 都需要拥有被传递的 AccountInfo
或数据,这使得与 Pinocchio 开发的程序进行互操作非常困难。
在与第三方程序集成时,通常需要为每个指令编写自定义 CPI 逻辑。 这最终可能会通过像 Codama 这样的代码生成器来解决,但它还没有完全实现。
重要的是要注意,Pinocchio 仍在积极开发中并且未经审计。 社区仍在努力将其余的 sysvar 添加到 SDK 中,并改进对重要的 SPL 程序(如 Token 和 Token2022)的支持。
有很多简单的贡献可以为 Pinocchio 做出贡献。
有一些 未解决的问题 和现有的 pull request 可以使用额外的支持。 加入对话或只需打开一个 pull request 供维护人员审核!
与以前的解决方案相比,Pinocchio 是一个性能更高的 Solana 程序编写库。 通过为开发人员提供对其程序入口点的更大灵活性,并使用 zero-copy
访问程序输入,可以帮助开发人员减少 CU 使用量。 但是,它仍然是一个新的库,尚未完成所有功能。 在撰写本文时,该库未经审计,因此请谨慎使用。
在评估是否使用 Pinocchio 时,重要的是要权衡其他库和框架之间的优缺点。
像 Anchor 这样有主见的框架将加快程序开发并更易于维护,使其成为上市速度很重要时的绝佳选择。
一旦你的产品稳定并收到大量事务,那么使用像 Pinocchio 这样的库来优化 Solana 程序可能更合适。
有关更多信息,请观看 Febo 在 Solana Accelerate 2025 上的演讲,并探索以下教育资源:
Solana
规模或死亡 2025:使用 Pinocchio 的无附加条件程序(Fernando Otero / Febo | Anza)
Solana
观看
- 原文链接: helius.dev/blog/pinocchi...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!