use core::cmp::Reverse;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, quote_spanned, ToTokens};
use syn::{
parse_macro_input, punctuated::Punctuated, spanned::Spanned, token::Comma, Data, DeriveInput,
Fields, FnArg, Ident,
};
#[proc_macro_derive(WeightDebug)]
pub fn derive_weight_debug(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let data = if let Data::Struct(data) = &input.data {
data
} else {
return quote_spanned! {
name.span() =>
compile_error!("WeightDebug is only supported for structs.");
}
.into()
};
let fields = match &data.fields {
Fields::Named(fields) => {
let recurse = fields.named.iter().filter_map(|f| {
let name = f.ident.as_ref()?;
if name.to_string().starts_with('_') {
return None
}
let ret = quote_spanned! { f.span() =>
formatter.field(stringify!(#name), &HumanWeight(self.#name));
};
Some(ret)
});
quote! {
#( #recurse )*
}
},
Fields::Unnamed(fields) => quote_spanned! {
fields.span() =>
compile_error!("Unnamed fields are not supported")
},
Fields::Unit => quote!(),
};
let tokens = quote! {
impl #impl_generics ::core::fmt::Debug for #name #ty_generics #where_clause {
fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> core::fmt::Result {
use ::sp_runtime::{FixedPointNumber, FixedU128 as Fixed};
use ::core::{fmt, write};
struct HumanWeight(Weight);
impl fmt::Debug for HumanWeight {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0.ref_time() > 1_000_000_000 {
write!(
formatter,
"{} ms, {} bytes",
Fixed::saturating_from_rational(self.0.ref_time(), 1_000_000_000).into_inner() / Fixed::accuracy(),
self.0.proof_size()
)
} else if self.0.ref_time() > 1_000_000 {
write!(
formatter,
"{} µs, {} bytes",
Fixed::saturating_from_rational(self.0.ref_time(), 1_000_000).into_inner() / Fixed::accuracy(),
self.0.proof_size()
)
} else if self.0.ref_time() > 1_000 {
write!(
formatter,
"{} ns, {} bytes",
Fixed::saturating_from_rational(self.0.ref_time(), 1_000).into_inner() / Fixed::accuracy(),
self.0.proof_size()
)
} else {
write!(formatter, "{} ps, {} bytes", self.0.ref_time(), self.0.proof_size())
}
}
}
let mut formatter = formatter.debug_struct(stringify!(#name));
#fields
formatter.finish()
}
}
};
tokens.into()
}
struct EnvDef {
host_funcs: Vec<HostFn>,
}
struct HostFn {
item: syn::ItemFn,
version: u8,
name: String,
returns: HostFnReturn,
is_stable: bool,
alias_to: Option<String>,
not_deprecated: bool,
cfg: Option<syn::Attribute>,
}
enum HostFnReturn {
Unit,
U32,
U64,
ReturnCode,
}
impl HostFnReturn {
fn to_wasm_sig(&self) -> TokenStream2 {
let ok = match self {
Self::Unit => quote! { () },
Self::U32 | Self::ReturnCode => quote! { ::core::primitive::u32 },
Self::U64 => quote! { ::core::primitive::u64 },
};
quote! {
::core::result::Result<#ok, ::wasmi::Error>
}
}
}
impl ToTokens for HostFn {
fn to_tokens(&self, tokens: &mut TokenStream2) {
self.item.to_tokens(tokens);
}
}
impl HostFn {
pub fn try_from(mut item: syn::ItemFn) -> syn::Result<Self> {
let err = |span, msg| {
let msg = format!("Invalid host function definition.\n{}", msg);
syn::Error::new(span, msg)
};
let msg =
"Only #[version(<u8>)], #[unstable], #[prefixed_alias], #[cfg], #[mutating] and #[deprecated] attributes are allowed.";
let span = item.span();
let mut attrs = item.attrs.clone();
attrs.retain(|a| !a.path().is_ident("doc"));
let mut maybe_version = None;
let mut is_stable = true;
let mut alias_to = None;
let mut not_deprecated = true;
let mut mutating = false;
let mut cfg = None;
while let Some(attr) = attrs.pop() {
let ident = attr.path().get_ident().ok_or(err(span, msg))?.to_string();
match ident.as_str() {
"version" => {
if maybe_version.is_some() {
return Err(err(span, "#[version] can only be specified once"))
}
maybe_version =
Some(attr.parse_args::<syn::LitInt>().and_then(|lit| lit.base10_parse())?);
},
"unstable" => {
if !is_stable {
return Err(err(span, "#[unstable] can only be specified once"))
}
is_stable = false;
},
"prefixed_alias" => {
alias_to = Some(item.sig.ident.to_string());
item.sig.ident = syn::Ident::new(
&format!("seal_{}", &item.sig.ident.to_string()),
item.sig.ident.span(),
);
},
"deprecated" => {
if !not_deprecated {
return Err(err(span, "#[deprecated] can only be specified once"))
}
not_deprecated = false;
},
"mutating" => {
if mutating {
return Err(err(span, "#[mutating] can only be specified once"))
}
mutating = true;
},
"cfg" => {
if cfg.is_some() {
return Err(err(span, "#[cfg] can only be specified once"))
}
cfg = Some(attr);
},
id => return Err(err(span, &format!("Unsupported attribute \"{id}\". {msg}"))),
}
}
if mutating {
let stmt = syn::parse_quote! {
if ctx.ext().is_read_only() {
return Err(Error::<E::T>::StateChangeDenied.into());
}
};
item.block.stmts.insert(0, stmt);
}
let name = item.sig.ident.to_string();
if !(is_stable || not_deprecated) {
return Err(err(span, "#[deprecated] is mutually exclusive with #[unstable]"))
}
let msg = "Every function must start with two inferred parameters: ctx: _ and memory: _";
let special_args = item
.sig
.inputs
.iter()
.take(2)
.enumerate()
.map(|(i, arg)| is_valid_special_arg(i, arg))
.fold(0u32, |acc, valid| if valid { acc + 1 } else { acc });
if special_args != 2 {
return Err(err(span, msg))
}
let msg = r#"Should return one of the following:
- Result<(), TrapReason>,
- Result<ReturnErrorCode, TrapReason>,
- Result<u64, TrapReason>,
- Result<u32, TrapReason>"#;
let ret_ty = match item.clone().sig.output {
syn::ReturnType::Type(_, ty) => Ok(ty.clone()),
_ => Err(err(span, &msg)),
}?;
match *ret_ty {
syn::Type::Path(tp) => {
let result = &tp.path.segments.last().ok_or(err(span, &msg))?;
let (id, span) = (result.ident.to_string(), result.ident.span());
id.eq(&"Result".to_string()).then_some(()).ok_or(err(span, &msg))?;
match &result.arguments {
syn::PathArguments::AngleBracketed(group) => {
if group.args.len() != 2 {
return Err(err(span, &msg))
};
let arg2 = group.args.last().ok_or(err(span, &msg))?;
let err_ty = match arg2 {
syn::GenericArgument::Type(ty) => Ok(ty.clone()),
_ => Err(err(arg2.span(), &msg)),
}?;
match err_ty {
syn::Type::Path(tp) => Ok(tp
.path
.segments
.first()
.ok_or(err(arg2.span(), &msg))?
.ident
.to_string()),
_ => Err(err(tp.span(), &msg)),
}?
.eq("TrapReason")
.then_some(())
.ok_or(err(span, &msg))?;
let arg1 = group.args.first().ok_or(err(span, &msg))?;
let ok_ty = match arg1 {
syn::GenericArgument::Type(ty) => Ok(ty.clone()),
_ => Err(err(arg1.span(), &msg)),
}?;
let ok_ty_str = match ok_ty {
syn::Type::Path(tp) => Ok(tp
.path
.segments
.first()
.ok_or(err(arg1.span(), &msg))?
.ident
.to_string()),
syn::Type::Tuple(tt) => {
if !tt.elems.is_empty() {
return Err(err(arg1.span(), &msg))
};
Ok("()".to_string())
},
_ => Err(err(ok_ty.span(), &msg)),
}?;
let returns = match ok_ty_str.as_str() {
"()" => Ok(HostFnReturn::Unit),
"u32" => Ok(HostFnReturn::U32),
"u64" => Ok(HostFnReturn::U64),
"ReturnErrorCode" => Ok(HostFnReturn::ReturnCode),
_ => Err(err(arg1.span(), &msg)),
}?;
Ok(Self {
item,
version: maybe_version.unwrap_or_default(),
name,
returns,
is_stable,
alias_to,
not_deprecated,
cfg,
})
},
_ => Err(err(span, &msg)),
}
},
_ => Err(err(span, &msg)),
}
}
fn module(&self) -> String {
format!("seal{}", self.version)
}
}
impl EnvDef {
pub fn try_from(item: syn::ItemMod) -> syn::Result<Self> {
let span = item.span();
let err = |msg| syn::Error::new(span, msg);
let items = &item
.content
.as_ref()
.ok_or(err("Invalid environment definition, expected `mod` to be inlined."))?
.1;
let extract_fn = |i: &syn::Item| match i {
syn::Item::Fn(i_fn) => Some(i_fn.clone()),
_ => None,
};
let selector = |a: &syn::Attribute| a.path().is_ident("prefixed_alias");
let aliases = items
.iter()
.filter_map(extract_fn)
.filter(|i| i.attrs.iter().any(selector))
.map(|i| HostFn::try_from(i));
let host_funcs = items
.iter()
.filter_map(extract_fn)
.map(|mut i| {
i.attrs.retain(|i| !selector(i));
i
})
.map(|i| HostFn::try_from(i))
.chain(aliases)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { host_funcs })
}
}
fn is_valid_special_arg(idx: usize, arg: &FnArg) -> bool {
let FnArg::Typed(pat) = arg else { return false };
let ident = if let syn::Pat::Ident(ref ident) = *pat.pat { &ident.ident } else { return false };
let name_ok = match idx {
0 => ident == "ctx" || ident == "_ctx",
1 => ident == "memory" || ident == "_memory",
_ => false,
};
if !name_ok {
return false
}
matches!(*pat.ty, syn::Type::Infer(_))
}
fn expand_func_doc(func: &HostFn) -> TokenStream2 {
let func_decl = {
let mut sig = func.item.sig.clone();
sig.inputs = sig
.inputs
.iter()
.skip(2)
.map(|p| p.clone())
.collect::<Punctuated<FnArg, Comma>>();
sig.to_token_stream()
};
let func_doc = {
let func_docs = if let Some(origin_fn) = &func.alias_to {
let alias_doc = format!(
"This is just an alias function to [`{0}()`][`Self::{0}`] with backwards-compatible prefixed identifier.",
origin_fn,
);
quote! { #[doc = #alias_doc] }
} else {
let docs = func.item.attrs.iter().filter(|a| a.path().is_ident("doc")).map(|d| {
let docs = d.to_token_stream();
quote! { #docs }
});
quote! { #( #docs )* }
};
let deprecation_notice = if !func.not_deprecated {
let warning = "\n # Deprecated\n\n \
This function is deprecated and will be removed in future versions.\n \
No new code or contracts with this API can be deployed.";
quote! { #[doc = #warning] }
} else {
quote! {}
};
let import_notice = {
let info = format!(
"\n# Wasm Import Statement\n```wat\n(import \"seal{}\" \"{}\" (func ...))\n```",
func.version, func.name,
);
quote! { #[doc = #info] }
};
let unstable_notice = if !func.is_stable {
let warning = "\n # Unstable\n\n \
This function is unstable and it is a subject to change (or removal) in the future.\n \
Do not deploy a contract using it to a production chain.";
quote! { #[doc = #warning] }
} else {
quote! {}
};
quote! {
#deprecation_notice
#func_docs
#import_notice
#unstable_notice
}
};
quote! {
#func_doc
#func_decl;
}
}
fn expand_docs(def: &EnvDef) -> TokenStream2 {
let mut current_docs = std::collections::HashMap::new();
let mut funcs: Vec<_> = def.host_funcs.iter().filter(|f| f.alias_to.is_none()).collect();
funcs.sort_unstable_by_key(|func| Reverse(func.version));
for func in funcs {
if current_docs.contains_key(&func.name) {
continue
}
current_docs.insert(func.name.clone(), expand_func_doc(&func));
}
let current_docs = current_docs.values();
let mut legacy_doc = std::collections::BTreeMap::<u8, Vec<TokenStream2>>::new();
for func in def.host_funcs.iter() {
legacy_doc.entry(func.version).or_default().push(expand_func_doc(&func));
}
let legacy_doc = legacy_doc.into_iter().map(|(version, funcs)| {
let doc = format!("All functions available in the **seal{}** module", version);
let version = Ident::new(&format!("Version{version}"), Span::call_site());
quote! {
#[doc = #doc]
pub trait #version {
#( #funcs )*
}
}
});
quote! {
pub trait Current {
#( #current_docs )*
}
#( #legacy_doc )*
}
}
fn expand_env(def: &EnvDef, docs: bool) -> TokenStream2 {
let impls = expand_impls(def);
let docs = docs.then(|| expand_docs(def)).unwrap_or(TokenStream2::new());
let stable_api_count = def.host_funcs.iter().filter(|f| f.is_stable).count();
quote! {
pub struct Env;
#[cfg(test)]
pub const STABLE_API_COUNT: usize = #stable_api_count;
#impls
#[cfg(doc)]
pub mod api_doc {
use super::{TrapReason, ReturnErrorCode};
#docs
}
}
}
fn expand_impls(def: &EnvDef) -> TokenStream2 {
let impls = expand_functions(def, ExpandMode::Impl);
let dummy_impls = expand_functions(def, ExpandMode::MockImpl);
let bench_impls = expand_functions(def, ExpandMode::BenchImpl);
quote! {
impl<'a, E: Ext> crate::wasm::Environment<crate::wasm::runtime::Runtime<'a, E>> for Env
{
fn define(
store: &mut ::wasmi::Store<crate::wasm::Runtime<E>>,
linker: &mut ::wasmi::Linker<crate::wasm::Runtime<E>>,
allow_unstable: AllowUnstableInterface,
allow_deprecated: AllowDeprecatedInterface,
) -> Result<(),::wasmi::errors::LinkerError> {
#impls
Ok(())
}
}
#[cfg(feature = "runtime-benchmarks")]
pub struct BenchEnv<E>(::core::marker::PhantomData<E>);
#[cfg(feature = "runtime-benchmarks")]
impl<E: Ext> BenchEnv<E> {
#bench_impls
}
impl crate::wasm::Environment<()> for Env
{
fn define(
store: &mut ::wasmi::Store<()>,
linker: &mut ::wasmi::Linker<()>,
allow_unstable: AllowUnstableInterface,
allow_deprecated: AllowDeprecatedInterface,
) -> Result<(), ::wasmi::errors::LinkerError> {
#dummy_impls
Ok(())
}
}
}
}
enum ExpandMode {
Impl,
BenchImpl,
MockImpl,
}
impl ExpandMode {
fn expand_blocks(&self) -> bool {
match *self {
ExpandMode::Impl | ExpandMode::BenchImpl => true,
ExpandMode::MockImpl => false,
}
}
fn host_state(&self) -> TokenStream2 {
match *self {
ExpandMode::Impl | ExpandMode::BenchImpl => quote! { crate::wasm::runtime::Runtime<E> },
ExpandMode::MockImpl => quote! { () },
}
}
}
fn expand_functions(def: &EnvDef, expand_mode: ExpandMode) -> TokenStream2 {
let impls = def.host_funcs.iter().map(|f| {
let params = f.item.sig.inputs.iter().skip(2);
let module = f.module();
let cfg = &f.cfg;
let name = &f.name;
let body = &f.item.block;
let wasm_output = f.returns.to_wasm_sig();
let output = &f.item.sig.output;
let is_stable = f.is_stable;
let not_deprecated = f.not_deprecated;
let wrapped_body_with_trace = {
let trace_fmt_args = params.clone().filter_map(|arg| match arg {
syn::FnArg::Receiver(_) => None,
syn::FnArg::Typed(p) => {
match *p.pat.clone() {
syn::Pat::Ident(ref pat_ident) => Some(pat_ident.ident.clone()),
_ => None,
}
},
});
let params_fmt_str = trace_fmt_args.clone().map(|s| format!("{s}: {{:?}}")).collect::<Vec<_>>().join(", ");
let trace_fmt_str = format!("{}::{}({}) = {{:?}}\n", module, name, params_fmt_str);
quote! {
let result = #body;
if ::log::log_enabled!(target: "runtime::contracts::strace", ::log::Level::Trace) {
use core::fmt::Write;
let mut w = sp_std::Writer::default();
let _ = core::write!(&mut w, #trace_fmt_str, #( #trace_fmt_args, )* result);
let msg = core::str::from_utf8(&w.inner()).unwrap_or_default();
ctx.ext().append_debug_buffer(msg);
}
result
}
};
let expand_blocks = expand_mode.expand_blocks();
let inner = match expand_mode {
ExpandMode::Impl => {
quote! { || #output {
let (memory, ctx) = __caller__
.data()
.memory()
.expect("Memory must be set when setting up host data; qed")
.data_and_store_mut(&mut __caller__);
#wrapped_body_with_trace
} }
},
ExpandMode::BenchImpl => {
let body = &body.stmts;
quote!{
#(#body)*
}
},
ExpandMode::MockImpl => {
quote! { || -> #wasm_output {
::core::unreachable!()
} }
},
};
let into_host = if expand_blocks {
quote! {
|reason| {
::wasmi::Error::host(reason)
}
}
} else {
quote! {
|reason| { reason }
}
};
let allow_unused = if expand_blocks {
quote! { }
} else {
quote! { #[allow(unused_variables)] }
};
let sync_gas_before = if expand_blocks {
quote! {
let __gas_left_before__ = {
let fuel =
__caller__.get_fuel().expect("Fuel metering is enabled; qed");
__caller__
.data_mut()
.ext()
.gas_meter_mut()
.sync_from_executor(fuel)
.map_err(TrapReason::from)
.map_err(#into_host)?
};
__caller__.data_mut().charge_gas(crate::wasm::RuntimeCosts::HostFn)
.map_err(TrapReason::from)
.map_err(#into_host)?;
}
} else {
quote! { }
};
let sync_gas_after = if expand_blocks {
quote! {
let fuel = __caller__
.data_mut()
.ext()
.gas_meter_mut()
.sync_to_executor(__gas_left_before__)
.map_err(|err| {
let err = TrapReason::from(err);
wasmi::Error::host(err)
})?;
__caller__
.set_fuel(fuel.into())
.expect("Fuel metering is enabled; qed");
}
} else {
quote! { }
};
match expand_mode {
ExpandMode::BenchImpl => {
let name = Ident::new(&format!("{module}_{name}"), Span::call_site());
quote! {
pub fn #name(ctx: &mut crate::wasm::Runtime<E>, memory: &mut [u8], #(#params),*) #output {
#inner
}
}
},
_ => {
let host_state = expand_mode.host_state();
quote! {
#cfg
if ::core::cfg!(feature = "runtime-benchmarks") ||
((#is_stable || __allow_unstable__) && (#not_deprecated || __allow_deprecated__))
{
#allow_unused
linker.define(#module, #name, ::wasmi::Func::wrap(&mut*store, |mut __caller__: ::wasmi::Caller<#host_state>, #( #params, )*| -> #wasm_output {
#sync_gas_before
let mut func = #inner;
let result = func().map_err(#into_host).map(::core::convert::Into::into);
#sync_gas_after
result
}))?;
}
}
},
}
});
match expand_mode {
ExpandMode::BenchImpl => {
quote! {
#( #impls )*
}
},
_ => quote! {
let __allow_unstable__ = matches!(allow_unstable, AllowUnstableInterface::Yes);
let __allow_deprecated__ = matches!(allow_deprecated, AllowDeprecatedInterface::Yes);
#( #impls )*
},
}
}
#[proc_macro_attribute]
pub fn define_env(attr: TokenStream, item: TokenStream) -> TokenStream {
if !attr.is_empty() && !(attr.to_string() == "doc".to_string()) {
let msg = r#"Invalid `define_env` attribute macro: expected either no attributes or a single `doc` attribute:
- `#[define_env]`
- `#[define_env(doc)]`"#;
let span = TokenStream2::from(attr).span();
return syn::Error::new(span, msg).to_compile_error().into()
}
let item = syn::parse_macro_input!(item as syn::ItemMod);
match EnvDef::try_from(item) {
Ok(mut def) => expand_env(&mut def, !attr.is_empty()).into(),
Err(e) => e.to_compile_error().into(),
}
}