Solana C 语言 BPF 程序开发指南:从 makefile 构建到单元测试的完整实战
【免费下载链接】solanaWeb-Scale Blockchain for fast, secure, scalable, decentralized apps and marketplaces.项目地址: https://gitcode.com/GitHub_Trending/so/solana
本指南以 Solana 官方 C SDK(sdk/bpf/c/README.md)为核心,系统讲解如何使用 C 语言编写 Solana 链上 BPF 程序:包括项目骨架搭建、bpf.mk构建系统用法、Criterion 单元测试框架集成,以及 C 语言开发的关键限制与规避方案。读完本文后,你将能独立完成一个 C 语言 Solana 程序的编写、编译、测试与部署准备工作。
一、背景:为什么用 C 写 Solana 程序
Solana 链上程序(Program)运行在 BPF(Berkeley Packet Filter)虚拟机之上,官方主推 Rust 生态,但同时也提供了完整的 C/C++ SDK。C 语言程序编译为bpfel目标 ELF 文件后,同样可通过 BPF Loader 部署上链。
在 Solana 源码仓库中,C 语言的完整开发工具链集中在 sdk/bpf/c 目录:包含头文件目录inc/、构建脚本bpf.mk、链接脚本bpf.ld,以及配套的 sdk/bpf/scripts(工具链下载脚本)与 sdk/bpf/env.sh(环境配置脚本)。仓库自带的 programs/sbf/c/src 下大量 C 示例程序(如noop、alloc、move_funds等)均采用本指南介绍的这套构建体系。
二、快速开始:搭建你的第一个 C 程序
根据 sdk/bpf/c/README.md 的 Quick start 章节,只需两个文件即可起步。
1. 编写 makefile
在项目根目录创建makefile,内容仅需一行:
include path/to/bpf.mkpath/to/bpf.mk指向仓库中的 sdk/bpf/c/bpf.mk。实际使用中通常写为相对路径,例如../../sdk/bpf/c/bpf.mk。该 makefile 接管了后续全部构建逻辑。
2. 编写程序源码
创建src/program.c,内容为:
#include <solana_sdk.h> extern uint64_t entrypoint(const uint8_t *input) { SolAccountInfo ka[1]; SolParameters params = (SolParameters) { .ka = ka }; if (!sol_deserialize(input, ¶ms, SOL_ARRAY_SIZE(ka))) { return ERROR_INVALID_ARGUMENT; } return SUCCESS; }这个最小程序完成了三件核心工作:
entrypoint是程序入口:每个 Solana 程序必须导出uint64_t entrypoint(const uint8_t *input)函数,接收一个序列化后的输入缓冲区(见 entrypoint.h);- 准备
SolParameters接收反序列化结果:SolParameters结构体包含账户数组指针ka、账户数量ka_num、指令数据指针data/data_len以及当前程序 IDprogram_id; - 调用
sol_deserialize解析输入:成功返回true,失败返回ERROR_INVALID_ARGUMENT(该错误码定义于 types.h,值为内置错误的高 32 位编码)。
关于
sol_deserialize的实现细节:deserialize.h 中的sol_deserialize采用零拷贝反序列化——它不复制数据,而是直接在原始缓冲区上填充SolAccountInfo的指针与长度字段,因此程序对lamports或账户数据的修改会直接作用到原缓冲区,结束时也无需再序列化写回。同时它支持账户去重(dup_info字段,重复账户直接复制首份账户的指针),这是 BPF Loader 输入格式的重要特征。
3. 构建与产物
执行make即可构建,默认输出目录为./out,产物为out/program.o。若你的源码目录、测试前缀等需要定制,可通过命令行覆盖bpf.mk中的可配置变量(详见下文「构建系统深度解析」)。
三、构建系统深度解析:bpf.mk 的工作原理
bpf.mk是整个 C 开发体验的基石。理解它的约定与目标规则,是高效开发的前提。
1. 目录与命名约定
make help输出的说明明确了默认约定(见 bpf.mk):
| 约定项 | 默认值 | 说明 |
|---|---|---|
| 源码目录 | SRC_DIR ?= ./src | 程序位于$(SRC_DIR)/<program name> |
| 程序命名 | 按目录名 | 目录src/foo/即程序名foo |
| 测试前缀 | TEST_PREFIX ?= test_ | 测试文件须以test_开头,位于程序目录内 |
| 输出目录 | OUT_DIR ?= ./out | 所有产物输出到该目录 |
| 详细模式 | V | V=1时回显构建命令,默认静默 |
2. 可用的 make 目标
| 目标 | 作用 |
|---|---|
make help | 显示帮助信息与可用程序/测试列表 |
make all | 构建全部程序与测试并运行测试 |
make programs | 构建所有程序 |
make tests | 构建并运行所有测试 |
make <program name> | 按名称构建单个程序 |
make <test name> | 构建并运行单个测试 |
make dump_<program name> | 用llvm-objdump反汇编输出程序(含源码标注) |
make readelf_<program name> | 用llvm-readelf显示 ELF 二进制信息 |
make clean | 删除$(OUT_DIR) |
例如,仓库示例程序 programs/sbf/c/src/noop/noop.c 所在目录名为noop,那么make noop会编译出out/noop.so,make dump_noop可查看其反汇编。
3. 编译与链接参数
bpf.mk中实际使用的工具链与参数(bpf.mk):
- 工具链:
clang/clang++/ld.lld/llvm-objdump/llvm-readelf,全部来自bpf-tools(由 install.sh 自动下载); - C 编译标志:
-Werror -O2 -fno-builtin -std=c17,标准为C17; - BPF 目标标志:
-target bpf -fPIC -march=bpfel+solana(bpfel即 little-endian BPF); - 链接标志:
-z notext -shared --Bdynamic,使用 bpf.ld 链接脚本,入口点固定为entrypoint; - SBFv2 支持:设置环境变量
SOL_SBFV2=1时追加-DSOL_SBFV2=1编译宏并启用--pack-dyn-relocs=relr动态重定位打包。
链接脚本 bpf.ld 将 ELF 组织为text、rodata、data、dynamic四个段,并丢弃.eh_frame、.gnu.hash、.hash等无关段,以最小化链上程序体积。
依赖自动安装:install.sh 会在首次构建时自动下载 Criterion 测试框架与 Rust-BPF 平台工具(
platform-tools),并缓存到~/.cache/solana;env.sh 则负责导出CC、AR、OBJDUMP、OBJCOPY等环境变量指向 bpf-tools 中的 LLVM 工具。
4. 构建产物与部署提示
每次成功链接出.so后,bpf.mk还会自动生成对应的<program>-keypair.json密钥对文件(若不存在),并提示部署命令:
solana program deploy /absolute/path/to/out/program.so四、单元测试:基于 Criterion 的测试体系
内置的单元测试支持来自Criterion测试框架(详见 sdk/bpf/c/README.md 的 Unit tests 章节,以及 install.sh 中自动下载 Criterion 的逻辑)。
1. 编写测试
按约定,测试文件放在test/目录(命名须以test_开头)。创建test/example.c:
#include <criterion/criterion.h> #include "../src/program.c" Test(test_suite_name, test_case_name) { cr_assert(true); }关键点:
- 直接
#include程序源码(而非链接编译产物),使测试能访问程序内部的静态函数与符号; - 使用 Criterion 的
Test(suite, case)宏声明用例,cr_assert进行断言; - 测试编译时会追加
-DSOL_TEST宏与 Criterion 的头文件/库路径(见 bpf.mk),macOS 下还会通过install_name_tool修正libcriterion的动态库路径。
2. 运行测试
make testmake test会依次构建并执行所有测试;make <test name>只运行单个测试。测试可执行文件输出在$(OUT_DIR)/<program>/<test_name>,通过LD_LIBRARY_PATH指向 Criterion 库目录后运行(见 bpf.mk)。
3. 测试示例
仓库中 programs/sbf/c/src/sanity/sanity.c 等示例程序均包含test_sanity.c之类的 Criterion 测试,可作为编写更复杂测试用例(如构造SolAccountInfo数组、验证指令处理逻辑)的直接参考。
五、必须知道的限制与应对
sdk/bpf/c/README.md 明确列出了 C 开发的两条核心限制,理解它们能避免大量踩坑:
限制 1:程序必须完整包含在单个 .c 文件中
- 构建脚本只扫描
$(SRC_DIR)/<program>/下的*.c/*.cc文件并逐一编译、链接(见 bpf.mk),不支持跨目录多文件项目组织; - 应对方案:将业务逻辑拆分为多个头文件(
.h),在唯一的.c文件中#include引入。头文件内容会被编译器内联进单一翻译单元,等效于单文件。
限制 2:没有 libc,但solana_sdk.h提供最小原语集
- BPF 目标下标准 C 库不可用,
printf、malloc等 libc 函数无法直接使用; - 替代方案:
solana_sdk.h(见 solana_sdk.h)聚合了全部可用头文件,提供包括:
| 头文件 | 提供的原语 |
|---|---|
| sol/types.h | 定长整数类型(uint8_t~uint64_t)、错误码、SOL_ARRAY_SIZE |
| sol/entrypoint.h | SolAccountInfo、SolParameters结构与entrypoint声明 |
| sol/deserialize.h | sol_deserialize输入反序列化 |
| sol/log.h | sol_log等链上日志输出原语 |
| sol/cpi.h | 跨程序调用(CPI)支持 |
| sol/sha.h、sol/keccak.h、sol/blake3.h、sol/secp256k1.h、sol/alt_bn128.h、sol/big_mod_exp.h | 密码学原语 |
| sol/pubkey.h、sol/assert.h、sol/return_data.h | 公钥、断言、return data 等辅助能力 |
- 内存分配方面:链上程序拥有固定虚拟地址的堆区(
HEAP_START_ADDRESS为0x300000000,长度 32KB,见 constants.h),SDK 通过sol_calloc/sol_free内置函数管理该区域,也允许程序自行在该区域实现自定义堆。
六、完整实战:把示例串起来
下面是一个贴合仓库真实结构的完整开发闭环示例:
my-program/ ├── makefile # 内容:include ../../sdk/bpf/c/bpf.mk ├── src/ │ └── counter/ │ ├── counter.c # 程序本体(含 entrypoint) │ └── test_counter.c # Criterion 测试(以 test_ 开头)# makefile include ../../sdk/bpf/c/bpf.mk// src/counter/counter.c #include <solana_sdk.h> extern uint64_t entrypoint(const uint8_t *input) { SolAccountInfo ka[1]; SolParameters params = (SolParameters) { .ka = ka }; if (!sol_deserialize(input, ¶ms, SOL_ARRAY_SIZE(ka))) { return ERROR_INVALID_ARGUMENT; } // ... 业务逻辑:读写 ka[0] 的 data 与 lamports ... sol_log("counter program executed"); return SUCCESS; }// src/counter/test_counter.c #include <criterion/criterion.h> #include "counter.c" Test(counter_suite, returns_success) { // 构造输入、调用 entrypoint、断言返回值 cr_assert(true); }对应的构建与验证命令:
make # 构建全部:生成 out/counter.so make counter # 只构建 counter 程序 make test_counter # 构建并运行单个测试 make dump_counter # 查看反汇编,检查 BPF 指令 make readelf_counter # 查看 ELF 段布局 make clean # 清理 out 目录构建成功后,make会打印solana program deploy <abs path>/out/counter.so部署命令,配合生成的counter-keypair.json即可上链。
七、小结
Solana 的 C SDK 提供了一套开箱即用的 BPF 开发体验:通过 bpf.mk 一行 include 即可获得完整的编译、链接、反汇编、单元测试流水线;solana_sdk.h头文件族覆盖了反序列化、日志、CPI、密码学等链上开发必需原语。遵循「单 .c 文件 + 头文件拆分」「无 libc、用 SDK 原语」两条铁律,你就能用 C 语言写出可测试、可部署的 Solana 链上程序。如需更深入的原语用法,可直接查阅 sdk/bpf/c/inc/sol 下的各头文件源码,以及 programs/sbf/c/src 中的全部示例实现。
【免费下载链接】solanaWeb-Scale Blockchain for fast, secure, scalable, decentralized apps and marketplaces.项目地址: https://gitcode.com/GitHub_Trending/so/solana
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考