ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

fuels-rs 中 abigen! 宏解析:从 JSON ABI 到类型安全 Rust 绑定的完整原理与实战

2026/9/6 23:05:05 拓冰建站 浏览量
fuels-rs 中 abigen! 宏解析:从 JSON ABI 到类型安全 Rust 绑定的完整原理与实战 fuels-rs 中 abigen! 宏解析从 JSON ABI 到类型安全 Rust 绑定的完整原理与实战【免费下载链接】fuels-rsFuel Network Rust SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-rsabigen!是 fuels-rsFuel Network 的 Rust SDK将 Sway 智能合约、脚本、谓词的 JSON ABI 转换为类型安全 Rust 绑定的核心入口。本文基于仓库文档docs/src/abigen/the-abigen-macro.md与对应源码实现完整讲解宏的输入语法、生成代码的组织结构包括共享类型提升机制、绑定对象的实际调用方式并结合fuels-macros与fuels-code-gen两个 crate 的源码深入剖析从字符串解析到代码生成的底层调用链帮助读者既会用、又懂原理。一、abigen! 宏的输入语法abigen!是一个过程宏procedural macro它的本质是在编译期生成代码。它接受如下形式的输入ProgramType(nameMyProgramType, abimy_program-abi.json)...三个要素的含义ProgramType程序类型取值为Contract、Script或Predicate三者之一name生成绑定的命名即生成的结构体/模块将以此命名abiJSON ABI 文件的路径或者直接是 ABI 的JSON 内容字符串本身。从源码看这一解析逻辑实现在 解析入口MacroAbigenTarget::new首先将命令名转换为ProgramType通过TryFromIdent非法值会直接报编译错误随后通过UniqueNameValues提取命名参数并用validate_has_no_other_names([name, abi])严格校验只允许name与abi两个参数多传任何其他参数都会编译失败。ProgramType的合法取值在 abigen_target.rs 中定义FromStr实现只接受精确的Script、Contract、Predicate三个字符串区分大小写否则会报xxx is not a valid program type. Expected one of: Script, Contract, Predicate一次宏调用可以生成多个程序的绑定同一个abigen!调用中可以并列声明多个目标。仓库 examples/macros/src/lib.rs 给出了官方示例——一次生成两个合约、一个脚本和一个谓词的绑定use fuels::prelude::*; abigen!( Contract( name ContractA, abi e2e/sway/bindings/sharing_types/contract_a/out/release/contract_a-abi.json ), Contract( name ContractB, abi e2e/sway/bindings/sharing_types/contract_b/out/release/contract_b-abi.json ), Script( name MyScript, abi e2e/sway/scripts/arguments/out/release/arguments-abi.json ), Predicate( name MyPredicateEncoder, abi e2e/sway/predicates/basic_predicate/out/release/basic_predicate-abi.json ), );这里引用了仓库e2e/sway下由 Forc 编译出的真实 ABI 文件路径相对于 crate 根目录原因后述。abi 参数文件路径还是内联字符串如何区分这是abigen!最容易被忽视的实现细节。源码 parsing.rs 中的parse_inline_or_load_abi揭示了判定规则fn parse_inline_or_load_abi(abi_lit_str: LitStr) - ResultAbi { let abi_string abi_lit_str.value(); let abi_str abi_string.trim(); if abi_str.starts_with({) || abi_str.starts_with([) || abi_str.starts_with(\n) { abi_str.parse() // 视为内联 JSON直接解析 } else { Abi::load_from(abi_str) // 视为文件路径从磁盘加载 } // ... }即abi字符串去除首尾空白后以{、[或换行符开头则按内联 JSON 解析否则按文件路径从磁盘读取。这意味着你可以在没有编译产物文件的环境下直接把 ABI JSON 粘贴进abi r#...#中使用。对应仓库中的两种用法示例见 examples/rust_bindings/src/lib.rsuse fuels::prelude::*; // 用法一指定 JSON ABI 文件路径相对于 crate 根目录 abigen!(Contract( name MyContractName, abi examples/rust_bindings/src/abi.json )); // 用法二直接内联 JSON 字符串 abigen!(Contract( name MyContract, abi r# { programType: contract, specVersion: 1, encodingVersion: 1, concreteTypes: [ { concreteTypeId: 1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0, type: u64 } ], functions: [ { inputs: [ { name: value, concreteTypeId: 1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0 } ], name: initialize_counter, output: 1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0 }, { inputs: [ { name: value, concreteTypeId: 1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0 } ], name: increment_counter, output: 1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0 } ], metadataTypes: [] } # ));上面这份精简版 ABI完整文件见 abi.json描述了一个含initialize_counter(arg: u64) - u64与increment_counter(arg: u64) - u64两个方法的合约正是文档主线的示例合约。路径解析的细节同样值得注意Abi::load_from 会先获取当前工作目录、做规范化canonicalize再将其与传入路径拼接。由于 proc-macro 编译时的工作目录是 crate 根目录所以文档强调路径相对于 crate 根目录——例如上面的examples/rust_bindings/src/abi.json就是相对examples/rust_bindingscrate 根目录写出的路径。若路径规范化失败报错信息会同时给出工作目录、目标路径与底层 IO 错误方便定位。解析 JSON 时使用的FullProgramABI::from_json_abi来自fuel-abi-typescrate如果 JSON 不是 Forc 生成的合法 ABI会提示malformed abi. Did you use forc to create it?。二、生成代码的结构模块组织与共享类型提升文档给出了生成代码的宏观轮廓。对每个ProgramType宏会以其name为基准生成一个独立模块模块名取name转 snake_case 后加_mod后缀pub mod abigen_bindings { pub mod contract_a_mod { struct SomeCustomStruct{/*...*/}; // 该合约用到的其他自定义类型 struct ContractA {/*...*/}; impl ContractA {/*...*/}; // ... } pub mod contract_b_mod { /* ... */ } pub mod my_script_mod { /* ... */ } pub mod my_predicate_mod { /* ... */ } pub mod shared_types { /* ... */ } } pub use contract_a_mod::{/*..*/}; pub use contract_b_mod::{/*..*/}; pub use my_predicate_mod::{/*..*/}; pub use shared_types::{/*..*/};各模块内部生成该程序用到的自定义类型以及用于实际发起调用的绑定结构体。shared_types同名同构类型的去重机制文档特别指出当宏检测到多个目标程序之间存在共享类型时会额外生成一个shared_types模块把类型提升到该模块中只生成一次并由所有使用它的绑定模块共享。判定标准很严格——类型名与定义都完全一致才算共享触发场景通常是多个程序引用了同一个自定义库或std库类型或者恰好各自独立定义了完全相同的类型。为避免破坏使用习惯每个绑定模块内部会补充pub use重导出使my_contract_mod::SharedType这种仿佛该模块自己生成了类型的访问方式依然成立。pub use 与名称冲突的处理宏末尾会插入pub use语句让使用者不必写完全限定路径即可引用生成的类型。但有一条限制只有名称唯一的类型才会获得顶层pub use。如果两个程序各自生成了同名的非共享类型rustc可能报找不到类型或歧义——这时只需显式限定路径abigen_bindings::whatever_contract_mod::TheType。文档给出的强烈建议把所有绑定放在同一个abigen!调用中生成。这样做既能让宏有机会做共享类型去重也能天然避免多次abigen!在同一命名空间下产生abigen_bindings模块与顶层pub use的名称冲突。如果确实要分多次调用就要记住上面展示的生成结构把各个abigen!放到不同模块中隔离。三、生成代码的具体形态以一个计数器合约为例回到示例合约两个方法均为u64 - u64。用前面的任一abigen!调用生成绑定后会得到如下代码仓库中保留了完整格式化后的样例 rust_bindings_formatted.rspub mod abigen_bindings { pub mod my_contract_mod { #[derive(Debug, Clone)] pub struct MyContractA: ::fuels::accounts::Account { contract_id: ::fuels::types::ContractId, account: A, log_decoder: ::fuels::core::codec::LogDecoder, encoder_config: ::fuels::core::codec::EncoderConfig, } implA: ::fuels::accounts::Account MyContractA { pub fn new(contract_id: ::fuels::types::ContractId, account: A) - Self { let log_decoder ::fuels::core::codec::LogDecoder::new( ::fuels::core::codec::log_formatters_lookup(vec![], contract_id.clone().into()), ); let encoder_config ::fuels::core::codec::EncoderConfig::default(); Self { contract_id, account, log_decoder, encoder_config } } pub fn contract_id(self) - ::fuels::types::ContractId { self.contract_id } pub fn account(self) - A { self.account.clone() } pub fn with_accountU: ::fuels::accounts::Account(self, account: U) - MyContractU { /* ... */ } pub fn with_encoder_config(mut self, encoder_config: ::fuels::core::codec::EncoderConfig) - MyContractA { self.encoder_config encoder_config; self } // 读取该合约在链上的全部资产余额 pub async fn get_balances( self, ) - ::fuels::types::errors::Result ::std::collections::HashMap::fuels::types::AssetId, u64, { ::fuels::accounts::ViewOnlyAccount::try_provider(self.account)? .get_contract_balances(self.contract_id) .await .map_err(::std::convert::Into::into) } pub fn methods(self) - MyContractMethodsA { /* ... */ } } pub struct MyContractMethodsA: ::fuels::accounts::Account { /* ... */ } implA: ::fuels::accounts::Account MyContractMethodsA { #[doc This method will read the counter from storage, increment it] #[doc and write the incremented value to storage] pub fn increment_counter( self, value: u64, ) - ::fuels::programs::calls::CallHandlerA, ::fuels::programs::calls::ContractCall, u64 { ::fuels::programs::calls::CallHandler::new_contract_call( self.contract_id.clone(), self.account.clone(), ::fuels::core::codec::encode_fn_selector(increment_counter), [::fuels::core::traits::Tokenizable::into_token(value)], self.log_decoder.clone(), false, self.encoder_config.clone(), ) } pub fn initialize_counter(self, value: u64) - /* 同上selector 为 initialize_counter */ { /* ... */ } } implA: ::fuels::accounts::Account ::fuels::programs::calls::ContractDependency for MyContractA { fn id(self) - ::fuels::types::ContractId { self.contract_id.clone() } fn log_decoder(self) - ::fuels::core::codec::LogDecoder { self.log_decoder.clone() } } #[derive(Clone, Debug, Default)] pub struct MyContractConfigurables { offsets_with_data: ::std::vec::Vec(u64, ::std::vec::Vecu8), encoder: ::fuels::core::codec::ABIEncoder, } // new(encoder_config) 构造器与 FromMyContractConfigurables for Configurables 实现 } } pub use abigen_bindings::my_contract_mod::MyContract; pub use abigen_bindings::my_contract_mod::MyContractConfigurables; pub use abigen_bindings::my_contract_mod::MyContractMethods;注意以上全部是宏生成的代码开发者永远不需要手写。生成结果可能随 fuels-rs 版本变化样例仅用于展示形态。从这份真实样例可以读出几个关键设计绑定结构体是泛型的MyContractA: Account持有一个账户Wallet或ViewOnlyAccount等因此同一份绑定既能用于有签名权限的钱包调用也能用于只读查询with_account可以在两种账户之间切换。方法构建与执行分离methods()返回MyContractMethods其每个方法如increment_counter不直接发起网络请求而是把方法名经encode_fn_selector编成函数选择器、把参数经Tokenizable::into_token编码构造一个CallHandler真正的提交由链式调用.call().await完成。文档主流程对应的调用即 examples/contracts/src/lib.rs// abigen! 生成 MyContract 绑定后见上文两种用法之一 let contract_instance MyContract::new(contract_id, wallet); let response contract_instance .methods() .initialize_counter(42) // 构建 ABI 调用 .call() // 执行网络调用 .await?; assert_eq!(42, response.value);日志解码与编码器配置内建new中构造的LogDecoder决定了绑定可以解码该合约发出的日志with_encoder_config则允许运行时替换 ABI 编码配置。Configurables 支持生成的MyContractConfigurables实现FromMyContractConfigurables for Configurables用于向合约的可配置常量区写入数据其生成逻辑在 configurables.rs。ContractDependency实现使该绑定可作为依赖合约参与跨合约调用的 gas 与资产估计multicall 场景。四、底层生成流程从宏到代码生成的调用链结合源码可以还原abigen!的完整执行链路从源码结构看宏解析层fuels-macrosParse for MacroAbigenTargets用Command::parse_multiple把多个ProgramType(...)命令逐一解析为MacroAbigenTarget { name, source: Abi, program_type }其中Abi在此阶段就完成了内联 JSON 或读文件的分流。之所以在宏 crate 内单独定义MacroAbigenTarget而非复用 code-gen 的类型源码注释说明是受 Rust 孤儿规则orphan rule限制——无法为外部类型实现Parse。生成调度层abigen.rs 中的Abigen::generate接收所有目标后先按程序类型分发到script_bindings/contract_bindings/predicate_bindings见 bindings.rs再统一处理共享类型检测与提升、顶层pub use注入、以及基于 ABI 文件路径的重编译触发代码generate_macro_recompile_trigger——当 ABI 文件内容变化时使 proc-macro 重新执行保证绑定与 ABI 同步。类型与方法生成层自定义类型结构体、枚举、元组、向量等由 custom_types.rs 生成合约方法由 function_generator.rs 按 ABI 中每个函数描述生成对应CallHandler构造代码。端到端验证宏的编译期行为由packages/fuels-macros/tests/ui/abigen/下的 9 组 ui 测试守护非法程序类型、多余命名参数、重复目标等场景的编译错误快照运行时行为则由 examples/rust_bindings/src/lib.rs 的transform_json_to_bindings测试覆盖——该测试真实启动本地节点、以文件路径与内联字符串两种方式生成绑定。五、实践要点小结abi路径相对于编译期工作目录crate 根目录写错路径会得到包含完整工作目录与目标路径的规范化错误内联 ABI 与文件路径靠是否以{/[/ 换行开头自动判别粘贴 JSON 前不要加多余的前导文字多个程序尽量合并进一个abigen!调用享受共享类型去重并规避命名空间冲突冲突时以abigen_bindings::name_mod::Type显式限定生成代码中方法返回CallHandler而非直接执行这使得在提交前可以链式追加资产转移、gas 策略、模拟等操作详见docs/src/calling-contract/相关文档方法文档字符串#[doc ...]来自 ABI 中 Sway 侧的函数注释链上方法的语义在 Rust 侧可直接通过 IDE 悬停查看。以上所有事实均可在仓库中对应位置复核语法解析见 parsing.rsABI 加载与程序类型定义见 abigen_target.rs生成样例见 rust_bindings_formatted.rs实战调用见 examples/contracts/src/lib.rs。【免费下载链接】fuels-rsFuel Network Rust SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-rs创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考