Skip to content

Commit

Permalink
Runtime API versioning
Browse files Browse the repository at this point in the history
Related to issue paritytech#11577

Add support for multiple versions of a Runtime API. The purpose is to
have one main version of the API, which is considered stable and
multiple unstable (aka staging) ones.

How it works
===========
Some methods of the API trait can be tagged with `#[api_version(N)]`
attribute where N is version number bigger than the main one. Let's call
them **staging methods** for brevity.

The implementor of the API decides which version to implement.

Example (from paritytech#11577 (comment)):

```
decl_runtime_apis! {
    #{api_version(10)]
    trait Test {
         fn something() -> Vec<u8>;
         #[api_version(11)]
         fn new_cool_function() -> u32;
    }
}
```

```
impl_runtime_apis! {
    #[api_version(11)]
    impl Test for Runtime {
         fn something() -> Vec<u8> { vec![1, 2, 3] }

         fn new_cool_function() -> u32 {
             10
         }
    }
}
```

Version safety checks (currently not implemented)
=================================================
By default in the API trait all staging methods has got default
implementation calling `unimplemented!()`. This is a problem because if
the developer wants to implement version 11 in the example above and
forgets to add `fn new_cool_function()` in `impl_runtime_apis!` the
runtime will crash when the function is executed.

Ideally a compilation error should be generated in such cases.

TODOs
=====

Things not working well at the moment:
[ ] Version safety check
[ ] Integration tests of `primitives/api` are messed up a bit. More
specifically `primitives/api/test/tests/decl_and_impl.rs`
[ ] Integration test covering the new functionality.
[ ] Some duplicated code
  • Loading branch information
tdimitrov committed Jul 5, 2022
1 parent 3ca525d commit 10ec47c
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 88 deletions.
24 changes: 24 additions & 0 deletions primitives/api/proc-macro/src/attribute_names.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// The ident used for the block generic parameter.
pub const BLOCK_GENERIC_IDENT: &str = "Block";

/// Unique identifier used to make the hidden includes unique for this macro.
pub const HIDDEN_INCLUDES_ID: &str = "DECL_RUNTIME_APIS";

/// The `core_trait` attribute.
pub const CORE_TRAIT_ATTRIBUTE: &str = "core_trait";
/// The `api_version` attribute.
///
/// Is used to set the current version of the trait.
pub const API_VERSION_ATTRIBUTE: &str = "api_version";
/// The `changed_in` attribute.
///
/// Is used when the function signature changed between different versions of a trait.
/// This attribute should be placed on the old signature of the function.
pub const CHANGED_IN_ATTRIBUTE: &str = "changed_in";
/// The `renamed` attribute.
///
/// Is used when a trait method was renamed.
pub const RENAMED_ATTRIBUTE: &str = "renamed";
/// All attributes that we support in the declaration of a runtime api trait.
pub const SUPPORTED_ATTRIBUTE_NAMES: &[&str] =
&[CORE_TRAIT_ATTRIBUTE, API_VERSION_ATTRIBUTE, CHANGED_IN_ATTRIBUTE, RENAMED_ATTRIBUTE];
152 changes: 86 additions & 66 deletions primitives/api/proc-macro/src/decl_runtime_apis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,18 @@ use crate::utils::{
extract_parameter_names_types_and_borrows, fold_fn_decl_for_client_side,
generate_call_api_at_fn_name, generate_crate_access, generate_hidden_includes,
generate_method_runtime_api_impl_name, generate_native_call_generator_fn_name,
generate_runtime_mod_name_for_trait, prefix_function_with_trait,
generate_runtime_mod_name_for_trait, parse_runtime_api_version, prefix_function_with_trait,
replace_wild_card_parameter_names, return_type_extract_type, AllowSelfRefInParameters,
};

use crate::attribute_names::{
API_VERSION_ATTRIBUTE, BLOCK_GENERIC_IDENT, CHANGED_IN_ATTRIBUTE, CORE_TRAIT_ATTRIBUTE,
HIDDEN_INCLUDES_ID, RENAMED_ATTRIBUTE, SUPPORTED_ATTRIBUTE_NAMES,
};

use proc_macro2::{Span, TokenStream};

use quote::quote;
use quote::{quote, quote_spanned};

use syn::{
fold::{self, Fold},
Expand All @@ -39,31 +44,6 @@ use syn::{

use std::collections::HashMap;

/// The ident used for the block generic parameter.
const BLOCK_GENERIC_IDENT: &str = "Block";

/// Unique identifier used to make the hidden includes unique for this macro.
const HIDDEN_INCLUDES_ID: &str = "DECL_RUNTIME_APIS";

/// The `core_trait` attribute.
const CORE_TRAIT_ATTRIBUTE: &str = "core_trait";
/// The `api_version` attribute.
///
/// Is used to set the current version of the trait.
const API_VERSION_ATTRIBUTE: &str = "api_version";
/// The `changed_in` attribute.
///
/// Is used when the function signature changed between different versions of a trait.
/// This attribute should be placed on the old signature of the function.
const CHANGED_IN_ATTRIBUTE: &str = "changed_in";
/// The `renamed` attribute.
///
/// Is used when a trait method was renamed.
const RENAMED_ATTRIBUTE: &str = "renamed";
/// All attributes that we support in the declaration of a runtime api trait.
const SUPPORTED_ATTRIBUTE_NAMES: &[&str] =
&[CORE_TRAIT_ATTRIBUTE, API_VERSION_ATTRIBUTE, CHANGED_IN_ATTRIBUTE, RENAMED_ATTRIBUTE];

/// The structure used for parsing the runtime api declarations.
struct RuntimeApiDecls {
decls: Vec<ItemTrait>,
Expand Down Expand Up @@ -452,16 +432,38 @@ fn generate_runtime_decls(decls: &[ItemTrait]) -> Result<TokenStream> {

let call_api_at_calls = generate_call_api_at_calls(&decl)?;

let trait_api_version = get_api_version(&found_attributes).unwrap_or(1);

// Remove methods that have the `changed_in` attribute as they are not required for the
// runtime anymore.
// Generate default implementations for staging methods.
decl.items = decl
.items
.iter_mut()
.filter_map(|i| match i {
TraitItem::Method(ref mut method) => {
if remove_supported_attributes(&mut method.attrs)
.contains_key(CHANGED_IN_ATTRIBUTE)
{
let method_attrs = remove_supported_attributes(&mut method.attrs);

if let Some(version_attribute) = method_attrs.get(API_VERSION_ATTRIBUTE) {
let method_api_ver =
parse_runtime_api_version(version_attribute).unwrap_or(1);
if method_api_ver < trait_api_version {
let span = method.span();
let method_ver = method_api_ver.to_string();
let trait_ver = trait_api_version.to_string();
result.push(quote_spanned! {
span => compile_error!(concat!("Method version `",
#method_ver,
"` is older than (or equal to) trait version `",
#trait_ver,
"`. Methods can't define versions older than the trait version."));
});
} else {
method.default = Some(parse_quote!({ unimplemented!() }));
}
}

if method_attrs.contains_key(CHANGED_IN_ATTRIBUTE) {
None
} else {
// Make sure we replace all the wild card parameter names.
Expand Down Expand Up @@ -558,13 +560,39 @@ impl<'a> ToClientSideDecl<'a> {
&mut self,
mut method: TraitItemMethod,
) -> Option<TraitItemMethod> {
if remove_supported_attributes(&mut method.attrs).contains_key(CHANGED_IN_ATTRIBUTE) {
let method_attrs = remove_supported_attributes(&mut method.attrs);
if method_attrs.contains_key(CHANGED_IN_ATTRIBUTE) {
return None
}

let fn_sig = &method.sig;
let ret_type = return_type_extract_type(&fn_sig.output);

// Check for staging method and validate its version - it shouldn't be older than the trait
// version
let is_staging_method = match method_attrs.get(API_VERSION_ATTRIBUTE) {
Some(version_attribute) => {
let method_api_ver = parse_runtime_api_version(version_attribute).unwrap_or(1);

let trait_api_version = get_api_version(&self.found_attributes).unwrap_or(1);

if method_api_ver < trait_api_version {
let span = method.span();
let method_ver = method_api_ver.to_string();
let trait_ver = trait_api_version.to_string();
self.errors.push(quote_spanned! {
span => compile_error!(concat!("Method version `",
#method_ver,
"` is older than (or equal to) trait version `",
#trait_ver,
"`. Methods can't define versions older than the trait version."));
});
}
true
},
None => false,
};

// Get types and if the value is borrowed from all parameters.
// If there is an error, we push it as the block to the user.
let param_types =
Expand All @@ -586,16 +614,33 @@ impl<'a> ToClientSideDecl<'a> {
let block_id = self.block_id;
let crate_ = self.crate_;

Some(parse_quote! {
#[doc(hidden)]
fn #name(
&self,
at: &#block_id,
context: #crate_::ExecutionContext,
params: Option<( #( #param_types ),* )>,
params_encoded: Vec<u8>,
) -> std::result::Result<#crate_::NativeOrEncoded<#ret_type>, #crate_::ApiError>;
})
// TODO: fix copy-pasted code here!!!
let result = match is_staging_method {
false => Some(parse_quote! {
#[doc(hidden)]
fn #name(
&self,
at: &#block_id,
context: #crate_::ExecutionContext,
params: Option<( #( #param_types ),* )>,
params_encoded: Vec<u8>,
) -> std::result::Result<#crate_::NativeOrEncoded<#ret_type>, #crate_::ApiError>;
}),
true => Some(parse_quote! {
#[doc(hidden)]
fn #name(
&self,
at: &#block_id,
context: #crate_::ExecutionContext,
params: Option<( #( #param_types ),* )>,
params_encoded: Vec<u8>,
) -> std::result::Result<#crate_::NativeOrEncoded<#ret_type>, #crate_::ApiError> {
unimplemented!()
}
}),
};

result
}

/// Takes the method declared by the user and creates the declaration we require for the runtime
Expand Down Expand Up @@ -720,31 +765,6 @@ impl<'a> Fold for ToClientSideDecl<'a> {
}
}

/// Parse the given attribute as `API_VERSION_ATTRIBUTE`.
fn parse_runtime_api_version(version: &Attribute) -> Result<u64> {
let meta = version.parse_meta()?;

let err = Err(Error::new(
meta.span(),
&format!(
"Unexpected `{api_version}` attribute. The supported format is `{api_version}(1)`",
api_version = API_VERSION_ATTRIBUTE
),
));

match meta {
Meta::List(list) =>
if list.nested.len() != 1 {
err
} else if let Some(NestedMeta::Lit(Lit::Int(i))) = list.nested.first() {
i.base10_parse()
} else {
err
},
_ => err,
}
}

/// Generates the identifier as const variable for the given `trait_name`
/// by hashing the `trait_name`.
fn generate_runtime_api_id(trait_name: &str) -> TokenStream {
Expand Down
81 changes: 62 additions & 19 deletions primitives/api/proc-macro/src/impl_runtime_apis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ use crate::utils::{
extract_parameter_names_types_and_borrows, generate_call_api_at_fn_name, generate_crate_access,
generate_hidden_includes, generate_method_runtime_api_impl_name,
generate_native_call_generator_fn_name, generate_runtime_mod_name_for_trait,
prefix_function_with_trait, return_type_extract_type, AllowSelfRefInParameters,
RequireQualifiedTraitPath,
parse_runtime_api_version, prefix_function_with_trait, return_type_extract_type,
AllowSelfRefInParameters, RequireQualifiedTraitPath,
};

use crate::attribute_names::API_VERSION_ATTRIBUTE;

use proc_macro2::{Span, TokenStream};

use quote::quote;
use quote::{quote, ToTokens};

use syn::{
fold::{self, Fold},
Expand Down Expand Up @@ -623,16 +625,55 @@ fn generate_api_impl_for_runtime_api(impls: &[ItemImpl]) -> Result<TokenStream>
Ok(quote!( #( #result )* ))
}

fn populate_runtime_api_versions<T: ToTokens>(
result: &mut Vec<TokenStream>,
sections: &mut Vec<TokenStream>,
attrs: Vec<Attribute>,
id: Path,
version: T,
crate_access: &TokenStream,
) {
result.push(quote!(
#( #attrs )*
(#id, #version)
));

sections.push(quote!(
#( #attrs )*
const _: () = {
// All sections with the same name are going to be merged by concatenation.
#[cfg(not(feature = "std"))]
#[link_section = "runtime_apis"]
static SECTION_CONTENTS: [u8; 12] = #crate_access::serialize_runtime_api_info(#id, #version);
};
));
}

/// Generates `RUNTIME_API_VERSIONS` that holds all version information about the implemented
/// runtime apis.
fn generate_runtime_api_versions(impls: &[ItemImpl]) -> Result<TokenStream> {
let mut result = Vec::with_capacity(impls.len());
let mut sections = Vec::with_capacity(impls.len());
let mut result = Vec::<TokenStream>::with_capacity(impls.len());
let mut sections = Vec::<TokenStream>::with_capacity(impls.len());
let mut processed_traits = HashSet::new();

let c = generate_crate_access(HIDDEN_INCLUDES_ID);

for impl_ in impls {
let api_ver = get_api_version(&impl_.attrs);
let api_ver = match api_ver.len() {
0 => None,
1 => Some(parse_runtime_api_version(api_ver.get(0).unwrap())? as u32),
_ =>
return Err(Error::new(
impl_.span(),
format!(
"Found multiple #[{}] attributes for an API implementation. \
Each runtime API can have only one version.",
API_VERSION_ATTRIBUTE
),
)),
};

let mut path = extend_with_runtime_decl_path(
extract_impl_trait(impl_, RequireQualifiedTraitPath::Yes)?.clone(),
);
Expand All @@ -658,20 +699,12 @@ fn generate_runtime_api_versions(impls: &[ItemImpl]) -> Result<TokenStream> {
let version: Path = parse_quote!( #path VERSION );
let attrs = filter_cfg_attrs(&impl_.attrs);

result.push(quote!(
#( #attrs )*
(#id, #version)
));

sections.push(quote!(
#( #attrs )*
const _: () = {
// All sections with the same name are going to be merged by concatenation.
#[cfg(not(feature = "std"))]
#[link_section = "runtime_apis"]
static SECTION_CONTENTS: [u8; 12] = #c::serialize_runtime_api_info(#id, #version);
};
));
if api_ver.is_none() {
populate_runtime_api_versions(&mut result, &mut sections, attrs, id, version, &c);
} else {
let version = api_ver.unwrap();
populate_runtime_api_versions(&mut result, &mut sections, attrs, id, version, &c);
}
}

Ok(quote!(
Expand Down Expand Up @@ -726,6 +759,16 @@ fn filter_cfg_attrs(attrs: &[Attribute]) -> Vec<Attribute> {
attrs.iter().filter(|a| a.path.is_ident("cfg")).cloned().collect()
}

fn get_api_version(attrs: &Vec<Attribute>) -> Vec<Attribute> {
let mut result = Vec::new();
for v in attrs {
if v.path.is_ident(API_VERSION_ATTRIBUTE) {
result.push(v.clone());
}
}
result
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
1 change: 1 addition & 0 deletions primitives/api/proc-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

use proc_macro::TokenStream;

mod attribute_names;
mod decl_runtime_apis;
mod impl_runtime_apis;
mod mock_impl_runtime_apis;
Expand Down
Loading

0 comments on commit 10ec47c

Please sign in to comment.