Substrate pallet 的编写方式从 declarative macro 变成了 procedural macro. substrate 整体的迁移进度可以参考这里. 本文属于现学现卖,简单介绍这个变化。在此之前,需要了解 declarative
macro 和 procedural
macro 是如何实现的,可以参考这里。
可以打开一个 pr, 比如 Migrate assets pallet to new macros , 来快速看下具体的变化内容。比较明显的几点变化:
少了这几个 declarative
macro
decl_storage! {}
decl_error! {}
decl_module! {}
decl_event! {}
这些内容被包裹到了, 名为 pallet 的 attribute-like
macro 里,
#[frame_support::pallet]
pub mod pallet {
//
}
pallet attribute-like
macro 的实现在 frame/support/procedural/src/lib.rs
文件里。
/// Macro to define a pallet. Docs are at `frame_support::pallet`.
#[proc_macro_attribute]
pub fn pallet(attr: TokenStream, item: TokenStream) -> TokenStream {
pallet::pallet(attr, item)
}
调用的 pallet 函数在 frame/support/procedural/src/mod.rs
文件里
pub fn pallet(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream
) -> proc_macro::TokenStream {
if !attr.is_empty() {
let msg = "Invalid pallet macro call: expected no attributes, e.g. macro call must be just \
`#[frame_support::pallet]` or `#[pallet]`";
let span = proc_macro2::TokenStream::from(attr).span();
return syn::Error::new(span, msg).to_compile_error().into();
}
let item = syn::parse_macro_input!(item as syn::ItemMod);
match parse::Def::try_from(item) {
Ok(def) => expand::expand(def).into(),
Err(e) => e.to_compile_error().into(),
}
}
此函数会判断使用 pallet 宏的时候有没有属性,然后调用 expand 函数
/// Expand definition, in particular:
/// * add some bounds and variants to type defined,
/// * create some new types,
/// * impl stuff on them.
pub fn expand(mut def: Def) -> proc_macro2::TokenStream {
let constants = constants::expand_constants(&mut def);
`let pallet_struct = pallet_struct::expand_pallet_struct(&mut def);`
let call = call::expand_call(&mut def);
let error = error::expand_error(&mut def);
let event = event::expand_event(&mut def);
let storages = storage::expand_storages(&mut def);
let instances = instances::expand_instances(&mut def);
let store_trait = store_trait::expand_store_trait(&mut def);
let hooks = hooks::expand_hooks(&mut def);
let genesis_build = genesis_build::expand_genesis_build(&mut def);
let genesis_config = genesis_config::expand_genesis_config(&mut def);
let type_values = type_value::expand_type_values(&mut def);
let new_items = quote::quote!(
#constants
#pallet_struct
#call
#error
#event
#storages
#instances
#store_trait
#hooks
#genesis_build
#genesis_config
#type_values
);
def.item.content.as_mut().expect("This is checked by parsing").1
.push(syn::Item::Verbatim(new_items));
def.item.into_token_stream()
}
主要的代码生成逻辑在各个 module 的 expand_xxx 函数里,逻辑分的比较开。相较于 declarative
macro(如下)
#[macro_export]
macro_rules! decl_error {
(
$(#[$attr:meta])*
pub enum $error:ident
for $module:ident<
$generic:ident: $trait:path
$(, $inst_generic:ident: $instance:path)?
>
$( where $( $where_ty:ty: $where_bound:path ),* $(,)? )?
{
$(
$( #[doc = $doc_attr:tt] )*
$name:ident
),*
$(,)?
}
) => {
// ...
基于 attribute-like
macro 的生成的逻辑的编写是非常简单的,而 declarative
犹如天书, 可读性很差。虽然底层逻辑我们不用太关心,但作为底层逻辑的调用者,使用 attribute-like
macro 也可以让我们遇到更少的底层 bug
在开始具体的升级之前,先简单看下基于 procedural
macro 的实现细节。
// syn::custom_keyword declarative macro 会将该这些词作为像 rust 关键字一样解析
mod keyword {
syn::custom_keyword!(Block);
syn::custom_keyword!(NodeBlock);
syn::custom_keyword!(UncheckedExtrinsic);
syn::custom_keyword!(Module);
syn::custom_keyword!(Call);
syn::custom_keyword!(Storage);
syn::custom_keyword!(Event);
syn::custom_keyword!(Config);
syn::custom_keyword!(Origin);
syn::custom_keyword!(Inherent);
syn::custom_keyword!(ValidateUnsigned);
}
// 这是一个 function-like macro
#[proc_macro]
pub fn construct_runtime(input: TokenStream) -> TokenStream {
construct_runtime::construct_runtime(input)
}
// storage 看起来比较特殊,单独定义了一个 function-like macro
#[proc_macro]
pub fn decl_storage(input: TokenStream) -> TokenStream {
storage::decl_storage_impl(input)
}
以 error 的生成为例 frame/support/procedural/src/pallet/expand/error.rs
expand_error 函数为 pub enum ErrorDebug
trait, as_u8
, as_str
, from
, metadata
等函数。
再比如 event
event_item.attrs.push(syn::parse_quote!(
#[derive(
#frame_support::CloneNoBound,
#frame_support::EqNoBound,
#frame_support::PartialEqNoBound,
#frame_support::RuntimeDebugNoBound,
#frame_support::codec::Encode,
#frame_support::codec::Decode,
)]
));
expand_event 为我们自动添加了很多 derive
宏
其实在整个过程里,pallet 函数里的这句比较关键
let item = syn::parse_macro_input!(item as syn::ItemMod);
在 parse 目录下有各种经过 parsing 后的结构,这些结构实现了 syn:parse
pallet::Def
-> event::EventDef
-> PalletEventAttr
是这样一层层 parse 出来的,然后存在结构体里,再经过各种 exapnd 函数生成最终的代码。
在迁移之前要想好怎么做迁移后的验证工作,保证迁移后的正确性。
#[pallet]
pub mod pallet {
//! This inner attribute will make span fail
..
}
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
use super::*;
}
首先,添加一个 pallet module. pub use pallet::*
引用的是此 module
之前在 pallet module 外面定义的类型可以保留
mod types {
// --- darwinia ---
use crate::*;
pub type MappedRing = u128;
pub type AccountId<T> = <T as frame_system::Config>::AccountId;
pub type RingBalance<T> = <RingCurrency<T> as Currency<AccountId<T>>>::Balance;
type RingCurrency<T> = <T as Config>::RingCurrency;
}
使用 use super::*
引入
decl_module!
的内容移入 pallet
并放在 #[pallet::config]
下面
#[pallet::config]
pub trait Config: frame_system::Config {
#[pallet::constant]
type ModuleId: Get<ModuleId>;
type RingCurrency: Currency<AccountId<Self>>;
type WeightInfo: WeightInfo;
type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;
}
以前在 Config 里的 const
需要加上 #[pallet::constant]
, Event 的定义是固定的
module 被迁移成了两部分
#[pallet::hooks]
impl<T: Config> Hooks for Pallet<T> {
}
#[pallet::call]
impl<T: Config> Pallet<T> {
}
这两部分都是必要的,空的也要写上去。 其中,以前定义的函数放在 call 下面, 以 substrate node-template 为例:
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
#[pallet::call]
impl<T:Config> Pallet<T> {
/// An example dispatchable that takes a singles value as a parameter, writes the value to
/// storage and emits an event. This function must be dispatched by a signed extrinsic.
#[pallet::weight(10_000 + T::DbWeight::get().writes(1))]
pub fn do_something(origin: OriginFor<T>, something: u32) -> DispatchResultWithPostInfo {
// Check that the extrinsic was signed and get the signer.
// This function will return an error if the extrinsic is not signed.
// https://substrate.dev/docs/en/knowledgebase/runtime/origin
let who = ensure_signed(origin)?;
// Update storage.
<Something<T>>::put(something);
// Emit an event.
Self::deposit_event(Event::SomethingStored(something, who));
// Return a successful DispatchResultWithPostInfo
Ok(().into())
}
}
hook 里可以实现一些函数 on_initialize/on_finalize/on_runtime_upgrade/offchain_worker/integrity_test
origin: OriginFor<T>
DispatchResultWithPostInfo
#[compact]
现在写为 #[pallet::compact]
#[weight = ..]
现在写为 #[pallet::weight(..)]
#[pallet::event]
pub enum Event<T: Config> {
/// Dummy Event. [who, swapped *CRING*, burned Mapped *RING*]
DummyEvent(AccountId<T>, RingBalance<T>, MappedRing),
}
#[pallet::event]
#[pallet::metadata(T::AccountId = "AccountId")]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Event documentation should end with an array that provides descriptive names for event
/// parameters. [something, who]
SomethingStored(u32, T::AccountId),
}
// Errors inform users that something went wrong.
#[pallet::error]
pub enum Error<T> {
/// Error names should be descriptive.
NoneValue,
/// Errors should have helpful documentation associated with them.
StorageOverflow,
}
#[pallet::genesis_config]
/// 这里可以根据需要加 T 或者不加 T, mock.rs 里要对应
pub struct GenesisConfig<T> {
pub total_mapped_ring: MappedRing,
pub phantom: std::marker::PhantomData<T>,
}
/// 这里要加 std
#[cfg(feature = "std")]
// 和之前的定义要匹配, Default 是强制的
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self {
total_mapped_ring: Default::default(),
phantom: Default::default(),
}
}
}
#[pallet::genesis_build]
/// 之前的 build 要放在这里
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
fn build(&self) {
let _ = T::RingCurrency::make_free_balance_be(
&T::ModuleId::get().into_account(),
T::RingCurrency::minimum_balance(),
);
<TotalMappedRing<T>>::put(self.total_mapped_ring);
}
}
#[pallet::pallet]
/// 注意 storage prefix 发生了变化,以前这里是用 as 来改指定的前缀,现在应该没有办法指定了
/// 所以在 runtime 里引入的时候要改下 pallet 的名称来保持 storage prefix 的一致
#[pallet::generate_store(pub(super) trait Store)]
pub struct Pallet<T>(_);
// The pallet's runtime storage items.
// https://substrate.dev/docs/en/knowledgebase/runtime/storage
#[pallet::storage]
#[pallet::getter(fn something)]
// Learn more about declaring storage items:
// https://substrate.dev/docs/en/knowledgebase/runtime/storage#declaring-storage-items
/// 第一个参数 _ 是固定的, Storage 的类型还是那几个
pub type Something<T> = StorageValue<_, u32>;
如果有 instance, 在做上述迁移的时候要加上