subxt_macro/
lib.rs

1// Copyright 2019-2025 Parity Technologies (UK) Ltd.
2// This file is dual-licensed as Apache-2.0 or GPL-3.0.
3// see LICENSE for license details.
4
5//! Subxt macro for generating Substrate runtime interfaces.
6
7use codec::Decode;
8use darling::{FromMeta, ast::NestedMeta};
9use proc_macro::TokenStream;
10use proc_macro_error2::{abort_call_site, proc_macro_error};
11use quote::ToTokens;
12use scale_typegen::typegen::{
13    settings::substitutes::path_segments,
14    validation::{registry_contains_type_path, similar_type_paths_in_registry},
15};
16use subxt_codegen::{CodegenBuilder, CodegenError, Metadata};
17use syn::{parse_macro_input, punctuated::Punctuated};
18
19#[cfg(feature = "runtime-wasm-path")]
20mod wasm_loader;
21
22#[derive(Clone, Debug)]
23struct OuterAttribute(syn::Attribute);
24
25impl syn::parse::Parse for OuterAttribute {
26    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
27        Ok(Self(input.call(syn::Attribute::parse_outer)?[0].clone()))
28    }
29}
30
31#[derive(Debug, FromMeta)]
32struct RuntimeMetadataArgs {
33    #[darling(default)]
34    runtime_metadata_path: Option<String>,
35    #[darling(default)]
36    runtime_metadata_insecure_url: Option<String>,
37    #[darling(default)]
38    derive_for_all_types: Option<Punctuated<syn::Path, syn::Token![,]>>,
39    #[darling(default)]
40    attributes_for_all_types: Option<Punctuated<OuterAttribute, syn::Token![,]>>,
41    #[darling(multiple)]
42    derive_for_type: Vec<DeriveForType>,
43    #[darling(multiple)]
44    attributes_for_type: Vec<AttributesForType>,
45    #[darling(multiple)]
46    substitute_type: Vec<SubstituteType>,
47    #[darling(default, rename = "crate")]
48    crate_path: Option<syn::Path>,
49    #[darling(default)]
50    generate_docs: darling::util::Flag,
51    #[darling(default)]
52    runtime_types_only: bool,
53    #[darling(default)]
54    no_default_derives: bool,
55    #[darling(default)]
56    no_default_substitutions: bool,
57    #[darling(default)]
58    unstable_metadata: darling::util::Flag,
59    #[cfg(feature = "runtime-wasm-path")]
60    #[darling(default)]
61    runtime_path: Option<String>,
62}
63
64#[derive(Debug, FromMeta)]
65struct DeriveForType {
66    path: syn::TypePath,
67    derive: Punctuated<syn::Path, syn::Token![,]>,
68    #[darling(default)]
69    recursive: bool,
70}
71
72#[derive(Debug, FromMeta)]
73struct AttributesForType {
74    path: syn::TypePath,
75    attributes: Punctuated<OuterAttribute, syn::Token![,]>,
76    #[darling(default)]
77    recursive: bool,
78}
79
80#[derive(Debug, FromMeta)]
81struct SubstituteType {
82    path: syn::Path,
83    with: syn::Path,
84}
85
86// Note: docs for this are in the subxt library; don't add further docs here as they will be appended.
87#[allow(missing_docs)]
88#[proc_macro_attribute]
89#[proc_macro_error]
90pub fn subxt(args: TokenStream, input: TokenStream) -> TokenStream {
91    match subxt_inner(args, parse_macro_input!(input as syn::ItemMod)) {
92        Ok(e) => e,
93        Err(e) => e,
94    }
95}
96
97// Note: just an additional function to make early returns easier.
98fn subxt_inner(args: TokenStream, item_mod: syn::ItemMod) -> Result<TokenStream, TokenStream> {
99    let attr_args = NestedMeta::parse_meta_list(args.into())
100        .map_err(|e| TokenStream::from(darling::Error::from(e).write_errors()))?;
101    let args = RuntimeMetadataArgs::from_list(&attr_args)
102        .map_err(|e| TokenStream::from(e.write_errors()))?;
103
104    // Fetch metadata first, because we need it to validate some of the chosen codegen options.
105    let metadata = {
106        let mut metadata = fetch_metadata(&args)?;
107
108        // Run this first to ensure type paths are unique (which may result in 1,2,3 suffixes being added
109        // to type paths), so that when we validate derives/substitutions below, they are allowed for such
110        // types. See <https://github.com/paritytech/subxt/issues/2011>.
111        scale_typegen::utils::ensure_unique_type_paths(metadata.types_mut())
112            .expect("ensure_unique_type_paths should not fail; please report an issue.");
113
114        metadata
115    };
116
117    let mut codegen = CodegenBuilder::new();
118
119    // Use the item module that the macro is on:
120    codegen.set_target_module(item_mod);
121
122    // Use the provided crate path:
123    if let Some(crate_path) = args.crate_path {
124        codegen.set_subxt_crate_path(crate_path)
125    }
126
127    // Respect the boolean flags:
128    if args.runtime_types_only {
129        codegen.runtime_types_only();
130    }
131    if args.no_default_derives {
132        codegen.disable_default_derives();
133    }
134    if args.no_default_substitutions {
135        codegen.disable_default_substitutes();
136    }
137    if !args.generate_docs.is_present() {
138        codegen.no_docs()
139    }
140
141    // Configure derives:
142    codegen.set_additional_global_derives(
143        args.derive_for_all_types
144            .unwrap_or_default()
145            .into_iter()
146            .collect(),
147    );
148
149    for d in args.derive_for_type {
150        validate_type_path(&d.path.path, &metadata);
151        codegen.add_derives_for_type(d.path, d.derive.into_iter(), d.recursive);
152    }
153
154    // Configure attributes:
155    codegen.set_additional_global_attributes(
156        args.attributes_for_all_types
157            .unwrap_or_default()
158            .into_iter()
159            .map(|a| a.0)
160            .collect(),
161    );
162    for d in args.attributes_for_type {
163        validate_type_path(&d.path.path, &metadata);
164        codegen.add_attributes_for_type(d.path, d.attributes.into_iter().map(|a| a.0), d.recursive)
165    }
166
167    // Insert type substitutions:
168    for sub in args.substitute_type.into_iter() {
169        validate_type_path(&sub.path, &metadata);
170        codegen.set_type_substitute(sub.path, sub.with);
171    }
172
173    let code = codegen
174        .generate(metadata)
175        .map_err(|e| e.into_compile_error())?;
176
177    Ok(code.into())
178}
179
180/// Checks that a type is present in the type registry. If it is not found, abort with a
181/// helpful error message, showing the user alternative types, that have the same name, but are at different locations in the metadata.
182fn validate_type_path(path: &syn::Path, metadata: &Metadata) {
183    let path_segments = path_segments(path);
184    let ident = &path
185        .segments
186        .last()
187        .expect("Empty path should be filtered out before already")
188        .ident;
189    if !registry_contains_type_path(metadata.types(), &path_segments) {
190        let alternatives = similar_type_paths_in_registry(metadata.types(), path);
191        let alternatives: String = if alternatives.is_empty() {
192            format!("There is no Type with name `{ident}` in the provided metadata.")
193        } else {
194            let mut s = "A type with the same name is present at: ".to_owned();
195            for p in alternatives {
196                s.push('\n');
197                s.push_str(&pretty_path(&p));
198            }
199            s
200        };
201
202        abort_call_site!(
203            "Type `{}` does not exist at path `{}`\n\n{}",
204            ident.to_string(),
205            pretty_path(path),
206            alternatives
207        );
208    }
209
210    fn pretty_path(path: &syn::Path) -> String {
211        path.to_token_stream().to_string().replace(' ', "")
212    }
213}
214
215/// Resolves a path, handling the $OUT_DIR placeholder if present.
216/// If $OUT_DIR is present in the path, it's replaced with the actual OUT_DIR environment variable.
217/// Otherwise, the path is resolved relative to CARGO_MANIFEST_DIR.
218fn resolve_path(path_str: &str) -> std::path::PathBuf {
219    if path_str.contains("$OUT_DIR") {
220        let out_dir = std::env::var("OUT_DIR").unwrap_or_else(|_| {
221            abort_call_site!("$OUT_DIR is used in path but OUT_DIR environment variable is not set")
222        });
223        std::path::Path::new(&path_str.replace("$OUT_DIR", &out_dir)).into()
224    } else {
225        let root = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
226        let root_path = std::path::Path::new(&root);
227        root_path.join(path_str)
228    }
229}
230
231/// Fetches metadata in a blocking manner, from a url or file path.
232fn fetch_metadata(args: &RuntimeMetadataArgs) -> Result<subxt_codegen::Metadata, TokenStream> {
233    // Do we want to fetch unstable metadata? This only works if fetching from a URL.
234    let unstable_metadata = args.unstable_metadata.is_present();
235
236    #[cfg(feature = "runtime-wasm-path")]
237    if let Some(path) = &args.runtime_path {
238        if args.runtime_metadata_insecure_url.is_some() || args.runtime_metadata_path.is_some() {
239            abort_call_site!(
240                "Only one of 'runtime_metadata_path', 'runtime_metadata_insecure_url' or `runtime_path` must be provided"
241            );
242        };
243        let path = resolve_path(path);
244
245        let metadata = wasm_loader::from_wasm_file(&path).map_err(|e| e.into_compile_error())?;
246        return Ok(metadata);
247    };
248
249    let metadata = match (
250        &args.runtime_metadata_path,
251        &args.runtime_metadata_insecure_url,
252    ) {
253        (Some(rest_of_path), None) => {
254            if unstable_metadata {
255                abort_call_site!(
256                    "The 'unstable_metadata' attribute requires `runtime_metadata_insecure_url`"
257                )
258            }
259
260            let path = resolve_path(rest_of_path);
261
262            subxt_utils_fetchmetadata::from_file_blocking(&path)
263                .and_then(|b| subxt_codegen::Metadata::decode(&mut &*b).map_err(Into::into))
264                .map_err(|e| CodegenError::Other(e.to_string()).into_compile_error())?
265        }
266        #[cfg(feature = "runtime-metadata-insecure-url")]
267        (None, Some(url_string)) => {
268            use subxt_utils_fetchmetadata::{MetadataVersion, Url, from_url_blocking};
269
270            let url = Url::parse(url_string).unwrap_or_else(|_| {
271                abort_call_site!("Cannot download metadata; invalid url: {}", url_string)
272            });
273
274            let version = match unstable_metadata {
275                true => MetadataVersion::Unstable,
276                false => MetadataVersion::Latest,
277            };
278
279            from_url_blocking(url, version, None)
280                .map_err(|e| CodegenError::Other(e.to_string()))
281                .and_then(|b| subxt_codegen::Metadata::decode(&mut &*b).map_err(Into::into))
282                .map_err(|e| e.into_compile_error())?
283        }
284        #[cfg(not(feature = "runtime-metadata-insecure-url"))]
285        (None, Some(_)) => {
286            abort_call_site!(
287                "'runtime_metadata_insecure_url' requires the 'runtime-metadata-insecure-url' feature to be enabled"
288            )
289        }
290        #[cfg(feature = "runtime-wasm-path")]
291        (None, None) => {
292            abort_call_site!(
293                "At least one of 'runtime_metadata_path', 'runtime_metadata_insecure_url'  or  'runtime_path` can be provided"
294            )
295        }
296        #[cfg(not(feature = "runtime-wasm-path"))]
297        (None, None) => {
298            abort_call_site!(
299                "At least one of 'runtime_metadata_path', 'runtime_metadata_insecure_url' can be provided"
300            )
301        }
302        #[cfg(feature = "runtime-wasm-path")]
303        _ => {
304            abort_call_site!(
305                "Only one of 'runtime_metadata_path', 'runtime_metadata_insecure_url'  or  'runtime_path` can be provided"
306            )
307        }
308        #[cfg(not(feature = "runtime-wasm-path"))]
309        _ => {
310            abort_call_site!(
311                "Only one of 'runtime_metadata_path' or 'runtime_metadata_insecure_url' can be provided"
312            )
313        }
314    };
315    Ok(metadata)
316}