Skip to content
This repository was archived by the owner on Nov 15, 2023. It is now read-only.

Commit 6b07b97

Browse files
tdimitrovbkchr
andauthored
Add conditional compilation support for iml_runtime_apis! (#14709)
* Handle `cfg_attr` in `decl_runtime_api` * Integration tests * UI tests * Enable staging api tests on CI * docs * Comments and minor style fixes * Fix doc comments * Apply suggestions from code review Co-authored-by: Bastian Köcher <[email protected]> * Apply suggestions from code review Co-authored-by: Bastian Köcher <[email protected]> * Fix formatting and a compilation error * Fix a doc comment * Combine the errors from `ApiImplItem` * Fix a typo in UI test * Use attribute span when reporting multiple conditional `api_version` error * Apply suggestions from code review Co-authored-by: Bastian Köcher <[email protected]> * Don't use `cfg_attr` for methods. Use simple feature gate instead * Remove a stale comment * Update primitives/api/proc-macro/src/impl_runtime_apis.rs Co-authored-by: Bastian Köcher <[email protected]> * Revert "Update primitives/api/proc-macro/src/impl_runtime_apis.rs" This reverts commit 4da20a7. * Code review feedback * Style improvements --------- Co-authored-by: Bastian Köcher <[email protected]> Co-authored-by: parity-processbot <>
1 parent bdee519 commit 6b07b97

File tree

5 files changed

+301
-18
lines changed

5 files changed

+301
-18
lines changed

primitives/api/proc-macro/src/impl_runtime_apis.rs

+165-18
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ use quote::quote;
3131

3232
use syn::{
3333
fold::{self, Fold},
34+
parenthesized,
3435
parse::{Error, Parse, ParseStream, Result},
3536
parse_macro_input, parse_quote,
3637
spanned::Spanned,
37-
Attribute, Ident, ImplItem, ItemImpl, Path, Signature, Type, TypePath,
38+
Attribute, Ident, ImplItem, ItemImpl, LitInt, LitStr, Path, Signature, Type, TypePath,
3839
};
3940

4041
use std::collections::HashSet;
@@ -67,6 +68,7 @@ fn generate_impl_call(
6768
runtime: &Type,
6869
input: &Ident,
6970
impl_trait: &Path,
71+
api_version: &ApiVersion,
7072
) -> Result<TokenStream> {
7173
let params =
7274
extract_parameter_names_types_and_borrows(signature, AllowSelfRefInParameters::No)?;
@@ -111,11 +113,40 @@ fn generate_impl_call(
111113
)
112114
};
113115

116+
let fn_calls = if let Some(feature_gated) = &api_version.feature_gated {
117+
let pnames = pnames2;
118+
let pnames2 = pnames.clone();
119+
let pborrow2 = pborrow.clone();
120+
121+
let feature_name = &feature_gated.0;
122+
let impl_trait_fg = extend_with_api_version(impl_trait.clone(), Some(feature_gated.1));
123+
let impl_trait = extend_with_api_version(impl_trait.clone(), api_version.custom);
124+
125+
quote!(
126+
#[cfg(feature = #feature_name)]
127+
#[allow(deprecated)]
128+
let r = <#runtime as #impl_trait_fg>::#fn_name(#( #pborrow #pnames ),*);
129+
130+
#[cfg(not(feature = #feature_name))]
131+
#[allow(deprecated)]
132+
let r = <#runtime as #impl_trait>::#fn_name(#( #pborrow2 #pnames2 ),*);
133+
134+
r
135+
)
136+
} else {
137+
let pnames = pnames2;
138+
let impl_trait = extend_with_api_version(impl_trait.clone(), api_version.custom);
139+
140+
quote!(
141+
#[allow(deprecated)]
142+
<#runtime as #impl_trait>::#fn_name(#( #pborrow #pnames ),*)
143+
)
144+
};
145+
114146
Ok(quote!(
115147
#decode_params
116148

117-
#[allow(deprecated)]
118-
<#runtime as #impl_trait>::#fn_name(#( #pborrow #pnames2 ),*)
149+
#fn_calls
119150
))
120151
}
121152

@@ -130,7 +161,6 @@ fn generate_impl_calls(
130161
let trait_api_ver = extract_api_version(&impl_.attrs, impl_.span())?;
131162
let impl_trait_path = extract_impl_trait(impl_, RequireQualifiedTraitPath::Yes)?;
132163
let impl_trait = extend_with_runtime_decl_path(impl_trait_path.clone());
133-
let impl_trait = extend_with_api_version(impl_trait, trait_api_ver);
134164
let impl_trait_ident = &impl_trait_path
135165
.segments
136166
.last()
@@ -139,14 +169,23 @@ fn generate_impl_calls(
139169

140170
for item in &impl_.items {
141171
if let ImplItem::Fn(method) = item {
142-
let impl_call =
143-
generate_impl_call(&method.sig, &impl_.self_ty, input, &impl_trait)?;
172+
let impl_call = generate_impl_call(
173+
&method.sig,
174+
&impl_.self_ty,
175+
input,
176+
&impl_trait,
177+
&trait_api_ver,
178+
)?;
179+
let mut attrs = filter_cfg_attrs(&impl_.attrs);
180+
181+
// Add any `#[cfg(feature = X)]` attributes of the method to result
182+
attrs.extend(filter_cfg_attrs(&method.attrs));
144183

145184
impl_calls.push((
146185
impl_trait_ident.clone(),
147186
method.sig.ident.clone(),
148187
impl_call,
149-
filter_cfg_attrs(&impl_.attrs),
188+
attrs,
150189
));
151190
}
152191
}
@@ -441,6 +480,18 @@ fn extend_with_api_version(mut trait_: Path, version: Option<u64>) -> Path {
441480
trait_
442481
}
443482

483+
/// Adds a feature guard to `attributes`.
484+
///
485+
/// Depending on `enable`, the feature guard either enables ('feature = "something"`) or disables
486+
/// (`not(feature = "something")`).
487+
fn add_feature_guard(attrs: &mut Vec<Attribute>, feature_name: &str, enable: bool) {
488+
let attr = match enable {
489+
true => parse_quote!(#[cfg(feature = #feature_name)]),
490+
false => parse_quote!(#[cfg(not(feature = #feature_name))]),
491+
};
492+
attrs.push(attr);
493+
}
494+
444495
/// Generates the implementations of the apis for the runtime.
445496
fn generate_api_impl_for_runtime(impls: &[ItemImpl]) -> Result<TokenStream> {
446497
let mut impls_prepared = Vec::new();
@@ -451,12 +502,29 @@ fn generate_api_impl_for_runtime(impls: &[ItemImpl]) -> Result<TokenStream> {
451502
let trait_api_ver = extract_api_version(&impl_.attrs, impl_.span())?;
452503

453504
let mut impl_ = impl_.clone();
505+
impl_.attrs = filter_cfg_attrs(&impl_.attrs);
506+
454507
let trait_ = extract_impl_trait(&impl_, RequireQualifiedTraitPath::Yes)?.clone();
455508
let trait_ = extend_with_runtime_decl_path(trait_);
456-
let trait_ = extend_with_api_version(trait_, trait_api_ver);
509+
// If the trait api contains feature gated version - there are staging methods in it. Handle
510+
// them explicitly here by adding staging implementation with `#cfg(feature = ...)` and
511+
// stable implementation with `#[cfg(not(feature = ...))]`.
512+
if let Some(feature_gated) = trait_api_ver.feature_gated {
513+
let mut feature_gated_impl = impl_.clone();
514+
add_feature_guard(&mut feature_gated_impl.attrs, &feature_gated.0, true);
515+
feature_gated_impl.trait_.as_mut().unwrap().1 =
516+
extend_with_api_version(trait_.clone(), Some(feature_gated.1));
517+
518+
impls_prepared.push(feature_gated_impl);
519+
520+
// Finally add `#[cfg(not(feature = ...))]` for the stable implementation (which is
521+
// generated outside this if).
522+
add_feature_guard(&mut impl_.attrs, &feature_gated.0, false);
523+
}
457524

525+
// Generate stable trait implementation.
526+
let trait_ = extend_with_api_version(trait_, trait_api_ver.custom);
458527
impl_.trait_.as_mut().unwrap().1 = trait_;
459-
impl_.attrs = filter_cfg_attrs(&impl_.attrs);
460528
impls_prepared.push(impl_);
461529
}
462530

@@ -663,7 +731,8 @@ fn generate_runtime_api_versions(impls: &[ItemImpl]) -> Result<TokenStream> {
663731
let c = generate_crate_access();
664732

665733
for impl_ in impls {
666-
let api_ver = extract_api_version(&impl_.attrs, impl_.span())?.map(|a| a as u32);
734+
let versions = extract_api_version(&impl_.attrs, impl_.span())?;
735+
let api_ver = versions.custom.map(|a| a as u32);
667736

668737
let mut path = extend_with_runtime_decl_path(
669738
extract_impl_trait(impl_, RequireQualifiedTraitPath::Yes)?.clone(),
@@ -687,11 +756,34 @@ fn generate_runtime_api_versions(impls: &[ItemImpl]) -> Result<TokenStream> {
687756
}
688757

689758
let id: Path = parse_quote!( #path ID );
690-
let version = quote!( #path VERSION );
691-
let attrs = filter_cfg_attrs(&impl_.attrs);
759+
let mut attrs = filter_cfg_attrs(&impl_.attrs);
760+
761+
// Handle API versioning
762+
// If feature gated version is set - handle it first
763+
if let Some(feature_gated) = versions.feature_gated {
764+
let feature_gated_version = feature_gated.1 as u32;
765+
// the attributes for the feature gated staging api
766+
let mut feature_gated_attrs = attrs.clone();
767+
add_feature_guard(&mut feature_gated_attrs, &feature_gated.0, true);
768+
populate_runtime_api_versions(
769+
&mut result,
770+
&mut sections,
771+
feature_gated_attrs,
772+
id.clone(),
773+
quote!( #feature_gated_version ),
774+
&c,
775+
);
776+
777+
// Add `#[cfg(not(feature ...))]` to the initial attributes. If the staging feature flag
778+
// is not set we want to set the stable api version
779+
add_feature_guard(&mut attrs, &feature_gated.0, false);
780+
}
692781

693-
let api_ver = api_ver.map(|a| quote!( #a )).unwrap_or_else(|| version);
694-
populate_runtime_api_versions(&mut result, &mut sections, attrs, id, api_ver, &c)
782+
// Now add the stable api version to the versions list. If the api has got staging functions
783+
// there might be a `#[cfg(not(feature ...))]` attribute attached to the stable version.
784+
let base_api_version = quote!( #path VERSION );
785+
let api_ver = api_ver.map(|a| quote!( #a )).unwrap_or_else(|| base_api_version);
786+
populate_runtime_api_versions(&mut result, &mut sections, attrs, id, api_ver, &c);
695787
}
696788

697789
Ok(quote!(
@@ -758,12 +850,64 @@ fn filter_cfg_attrs(attrs: &[Attribute]) -> Vec<Attribute> {
758850
attrs.iter().filter(|a| a.path().is_ident("cfg")).cloned().collect()
759851
}
760852

853+
/// Parse feature flagged api_version.
854+
/// E.g. `#[cfg_attr(feature = "enable-staging-api", api_version(99))]`
855+
fn extract_cfg_api_version(attrs: &Vec<Attribute>, span: Span) -> Result<Option<(String, u64)>> {
856+
let cfg_attrs = attrs.iter().filter(|a| a.path().is_ident("cfg_attr")).collect::<Vec<_>>();
857+
858+
let mut cfg_api_version_attr = Vec::new();
859+
for cfg_attr in cfg_attrs {
860+
let mut feature_name = None;
861+
let mut api_version = None;
862+
cfg_attr.parse_nested_meta(|m| {
863+
if m.path.is_ident("feature") {
864+
let a = m.value()?;
865+
let b: LitStr = a.parse()?;
866+
feature_name = Some(b.value());
867+
} else if m.path.is_ident(API_VERSION_ATTRIBUTE) {
868+
let content;
869+
parenthesized!(content in m.input);
870+
let ver: LitInt = content.parse()?;
871+
api_version = Some(ver.base10_parse::<u64>()?);
872+
}
873+
Ok(())
874+
})?;
875+
876+
// If there is a cfg attribute containing api_version - save if for processing
877+
if let (Some(feature_name), Some(api_version)) = (feature_name, api_version) {
878+
cfg_api_version_attr.push((feature_name, api_version, cfg_attr.span()));
879+
}
880+
}
881+
882+
if cfg_api_version_attr.len() > 1 {
883+
let mut err = Error::new(span, format!("Found multiple feature gated api versions (cfg attribute with nested `{}` attribute). This is not supported.", API_VERSION_ATTRIBUTE));
884+
for (_, _, attr_span) in cfg_api_version_attr {
885+
err.combine(Error::new(attr_span, format!("`{}` found here", API_VERSION_ATTRIBUTE)));
886+
}
887+
888+
return Err(err)
889+
}
890+
891+
Ok(cfg_api_version_attr
892+
.into_iter()
893+
.next()
894+
.map(|(feature, name, _)| (feature, name)))
895+
}
896+
897+
/// Represents an API version.
898+
struct ApiVersion {
899+
/// Corresponds to `#[api_version(X)]` attribute.
900+
pub custom: Option<u64>,
901+
/// Corresponds to `#[cfg_attr(feature = "enable-staging-api", api_version(99))]`
902+
/// attribute. `String` is the feature name, `u64` the staging api version.
903+
pub feature_gated: Option<(String, u64)>,
904+
}
905+
761906
// Extracts the value of `API_VERSION_ATTRIBUTE` and handles errors.
762907
// Returns:
763908
// - Err if the version is malformed
764-
// - Some(u64) if the version is set
765-
// - None if the version is not set (this is valid).
766-
fn extract_api_version(attrs: &Vec<Attribute>, span: Span) -> Result<Option<u64>> {
909+
// - `ApiVersion` on success. If a version is set or not is determined by the fields of `ApiVersion`
910+
fn extract_api_version(attrs: &Vec<Attribute>, span: Span) -> Result<ApiVersion> {
767911
// First fetch all `API_VERSION_ATTRIBUTE` values (should be only one)
768912
let api_ver = attrs
769913
.iter()
@@ -782,7 +926,10 @@ fn extract_api_version(attrs: &Vec<Attribute>, span: Span) -> Result<Option<u64>
782926
}
783927

784928
// Parse the runtime version if there exists one.
785-
api_ver.first().map(|v| parse_runtime_api_version(v)).transpose()
929+
Ok(ApiVersion {
930+
custom: api_ver.first().map(|v| parse_runtime_api_version(v)).transpose()?,
931+
feature_gated: extract_cfg_api_version(attrs, span)?,
932+
})
786933
}
787934

788935
#[cfg(test)]

primitives/api/src/lib.rs

+48
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,54 @@ pub use sp_api_proc_macro::decl_runtime_apis;
344344
/// ```
345345
/// In this case `Balance` api version 3 is being implemented for `Runtime`. The `impl` block
346346
/// must contain all methods declared in version 3 and below.
347+
///
348+
/// # Conditional version implementation
349+
///
350+
/// `impl_runtime_apis!` supports `cfg_attr` attribute for conditional compilation. For example
351+
/// let's say you want to implement a staging version of the runtime api and put it behind a
352+
/// feature flag. You can do it this way:
353+
/// ```ignore
354+
/// pub struct Runtime {}
355+
/// sp_api::decl_runtime_apis! {
356+
/// pub trait ApiWithStagingMethod {
357+
/// fn stable_one(data: u64);
358+
///
359+
/// #[api_version(99)]
360+
/// fn staging_one();
361+
/// }
362+
/// }
363+
///
364+
/// sp_api::impl_runtime_apis! {
365+
/// #[cfg_attr(feature = "enable-staging-api", api_version(99))]
366+
/// impl self::ApiWithStagingMethod<Block> for Runtime {
367+
/// fn stable_one(_: u64) {}
368+
///
369+
/// #[cfg(feature = "enable-staging-api")]
370+
/// fn staging_one() {}
371+
/// }
372+
/// }
373+
/// ```
374+
///
375+
/// [`decl_runtime_apis!`] declares two version of the api - 1 (the default one, which is
376+
/// considered stable in our example) and 99 (which is considered staging). In
377+
/// `impl_runtime_apis!` a `cfg_attr` attribute is attached to the `ApiWithStagingMethod`
378+
/// implementation. If the code is compiled with `enable-staging-api` feature a version 99 of
379+
/// the runtime api will be built which will include `staging_one`. Note that `staging_one`
380+
/// implementation is feature gated by `#[cfg(feature = ... )]` attribute.
381+
///
382+
/// If the code is compiled without `enable-staging-api` version 1 (the default one) will be
383+
/// built which doesn't include `staging_one`.
384+
///
385+
/// `cfg_attr` can also be used together with `api_version`. For the next snippet will build
386+
/// version 99 if `enable-staging-api` is enabled and version 2 otherwise because both
387+
/// `cfg_attr` and `api_version` are attached to the impl block:
388+
/// ```ignore
389+
/// #[cfg_attr(feature = "enable-staging-api", api_version(99))]
390+
/// #[api_version(2)]
391+
/// impl self::ApiWithStagingAndVersionedMethods<Block> for Runtime {
392+
/// // impl skipped
393+
/// }
394+
/// ```
347395
pub use sp_api_proc_macro::impl_runtime_apis;
348396

349397
/// Mocks given trait implementations as runtime apis.

primitives/api/test/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ static_assertions = "1.1.0"
3535
[[bench]]
3636
name = "bench"
3737
harness = false
38+
39+
[features]
40+
"enable-staging-api" = []

0 commit comments

Comments
 (0)