Giter VIP home page Giter VIP logo

macro_magic's Introduction

Macro Magic ๐Ÿช„

Crates.io docs.rs Build Status MIT License

This crate provides the #[export_tokens] macro and a number of companion macros, including the #[import_tokens_proc] and #[import_tokens_attr] macros. When used in tandem with #[export_tokens], these macros allow you to create regular and attribute proc macros in which you can import and make use of the tokens of external/foreign items marked with #[export_tokens] in other modules, files, and even in other crates merely by referring to them by name/path.

Among other things, the patterns introduced by macro_magic can be used to implement safe and efficient exportation and importation of item tokens within the same file, and even across file and crate boundaries. The only requirement is that you have control over (i.e. can add an attribute macro to) the source code of both locations.

macro_magic is designed to work with stable Rust, and is fully no_std compatible (in fact, there is a unit test to ensure everything is no_std safe).

General Syntax

You can use macro_magic to build regular and attribute proc macros that look like this:

#[my_attribute(path::to::MyItem)]
trait SomeTrait {
    // ..
}

this:

do_something!(path::to::MyItem);

or even this:

let foreign_tokens = my_macro!(path::to::MyItem);
assert_eq!(foreign_tokens.to_string(), "struct MyItem {...}");

where path::to::MyItem is the path to an item that has been marked with #[export_tokens].

All of this behavior is accomplished under the hood using proc macros that create macro_rules-based callbacks, but as a programmer this complexity is completely hidden from you via simple attribute macros you can apply to your proc macros to imbue them with the power of importing the tokens for external items based on their path.

Attribute Example

You could write an attribute macro to "inject" the fields of one struct into another as follows:

#[import_tokens_attr]
#[proc_macro_attribute]
pub fn combine_structs(attr: TokenStream, tokens: TokenStream) -> TokenStream {
    let foreign_struct = parse_macro_input!(attr as ItemStruct);
    let local_struct = parse_macro_input!(tokens as ItemStruct);
    let Fields::Named(local_fields) = local_struct.fields else {
        return Error::new(
            local_struct.fields.span(),
            "unnamed fields are not supported"
        ).to_compile_error().into()
    };
    let Fields::Named(foreign_fields) = foreign_struct.fields else {
        return Error::new(
            foreign_struct.fields.span(),
            "unnamed fields are not supported"
        ).to_compile_error().into()
    };
    let local_fields = local_fields.named.iter();
    let foreign_fields = foreign_fields.named.iter();
    let attrs = local_struct.attrs;
    let generics = local_struct.generics;
    let ident = local_struct.ident;
    let vis = local_struct.vis;
    quote! {
        #(#attrs)
        *
        #vis struct #ident<#generics> {
            #(#local_fields),
            *
            ,
            #(#foreign_fields),
            *
        }
    }
    .into()
}

And then you could use the #[combine_structs] attribute as follows:

#[export_tokens]
struct ExternalStruct {
    foo: u32,
    bar: u64,
    fizz: i64,
}

#[combine_structs(ExternalStruct)]
struct LocalStruct {
    biz: bool,
    baz: i32,
}

Which would result in the following expanded output for LocalStruct:

struct LocalStruct {
    foo: u32,
    bar: u64,
    fizz: i64,
    biz: bool,
    baz: i32,
}

Note that the attr variable on the combine_structs proc macro, thanks to the powers of #[import_tokens_attr], will receive the actual tokens for the ExternalStruct item, rather than merely receiving the tokens for the path ExternalStruct.

This gives you the ability to write attribute macros that receive tokens for two items, one specified by path via the first argument attr, as well as the tokens for the item the attribute is attached to via the 2nd argument tokens. The only requirement is that the item specified by attr has been marked with #[export_tokens].

Proc Macro Example

You could write a PHP/ruby/crystal-style verbatim import / require macro which blindly imports the tokens of the specified external module into the current context (with all the good and bad implications that would imply), like this:

#[import_tokens_proc]
#[proc_macro]
pub fn require(tokens: TokenStream) -> TokenStream {
    let external_mod = parse_macro_input!(tokens as ItemMod);
    let Some((_, stmts)) = external_mod.content else {
        return Error::new(
            external_mod.span(),
            "cannot import tokens from a file-based module since custom file-level \
            attributes are not yet supported by Rust"
        ).to_compile_error().into()
    };
    quote! {
        #(#stmts)
        *
    }
    .into()
}

You could then use the require! macro like this:

// in some external crate
#[export_tokens]
mod an_external_module {
    fn my_cool_function() -> u32 {
        567
    }

    fn my_other_function() -> u32 {
        116
    }
}
// in another crate where we will use the `require!` macro
mod my_module {
    use my_macros::require;

    fn existing_stuff() {
        println!("foo!");
    }

    require!(external_crate::an_external_module);
}

which would result in this expansion of require! within my_module:

mod my_module {
    use my_macros::require;

    fn existing_stuff() {
        println!("foo!");
    }

    fn my_cool_function() -> u32 {
        567
    }

    fn my_other_function() -> u32 {
        116
    }
}

Notice that this hypothetical require! macro is dangerous for two reasons:

  • Any types you may have brought into scope with use statements in the foreign module may or may not be available in their new context without additional use statements.
  • If existing items in the module or context where you use the require! macro conflict with something you are importing, you will get a compiler error (this is good, though).

These are just some of the capabilities of macro_magic ๐Ÿช„.

See the docs for more information.

Features

proc_support

The proc_support feature must be enabled in proc macro crates that make use of any import tokens functionality, including #[import_tokens_attr], #[import_tokens_proc] and import_tokens!. Otherwise these macros will not function correctly and will issue compiler errors complaining about items not existing under macro_magic::mm_core. The #[export_tokens] macro does not require this feature to function correctly, so you can safely use it without enabling this feature.

The reason for this feature gating is that things like syn, quote, proc_macro2, etc., are not 100% no_std compatible and should only be enabled in proc macro crates. For this reason, you should not enable this feature in crates where you are merely using #[export_tokens] and nothing else within that crate.

Limitations

One thing that macro_magic doesn't provide is the ability to build up state information across multiple macro invocations, however this problem can be tackled effectively using the outer macro pattern or in some cases using static atomics and mutexes in your proc macro crate (which we actually do in this crate to keep track of unique identifiers).

Breaking Changes

  • 0.4x removed #[use_attr] and #[use_proc] (they are no longer needed with the new self-calling macro style that has been adopted in 0.4x) and also removed the ability to access #[export_tokens] invocations in inaccessible locations like inside of functions and across module permission boundaries like in an inaccessible private module. This feature may be re-added in the future if there is interest, however removing it allowed us to consolidate naming of our macro_rules! declarations and remove the need for #[use_attr] / #[use_proc].
  • 0.2x removed and/or re-wrote a number of features that relied on a non-future-proof behavior of writing/reading files in the OUT_DIR. Versions >= 0.2.0 are completely safe and no longer contain this behavior, however features that provided the ability to enumerate all the #[export_tokens] calls in a namespace have been removed. The proper way to do this is with the outer macro pattern or with global state mutexes/atomics in your proc macro crate, as mentioned above.

More detailed historical change information can be found in releases.

macro_magic's People

Contributors

gui1117 avatar jmg-duarte avatar sam0x17 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

macro_magic's Issues

Maybe #mm_override_path expression should be isolated in some way

let resolved_mm_override_path = match syn::parse2::<syn::Path>(String::from(#mm_override_path).parse().unwrap()) {

the expression in #mm_override_path is executed inside the block and have some import like:

macro_magic/core/src/lib.rs

Lines 844 to 846 in 7a69251

use #mm_path::__private::*;
use #mm_path::__private::quote::ToTokens;
use #mm_path::mm_core::*;

best would be that the expression doesn't have those imports in scope so user expression evaluation is less confusing.

Revamp / v0.4

(This issue make use of example from substrate, for people not familiar, please ask I will use genuine example.)

1 [x] remove need for use_attr

in import_tokens_attr_internal, instead of generating 2 proc macros orig_sig and inner_sig, we could generate only orig_sig.

here is how: if there is a special first token then orig_sig will do as inner_sig. otherwise it does as orig_sig currently except at the end if generates the forward_tokens to call himself again but with a special first token.

Having only one function allow to get rid of use_attr because there is no longer any inner_sig function to keep along.

2 allow export_tokens to export the same ident but at different path

in export_tokens instead of generating just a macro_rules we can do similar to frame::pallet:

we create a unique_name with just a global counter in the macro_magic proc crate:

thread_local! {
	/// A global counter, can be used to generate a relatively unique identifier.
	static COUNTER: RefCell<Counter> = RefCell::new(Counter(0));
}
pub fn expand_tt_default_parts(def: &mut Def) -> proc_macro2::TokenStream {
	let count = COUNTER.with(|counter| counter.borrow_mut().inc());
	let unique_name = syn::Ident::new(&format!("__macro_magic_unique_id{}", count), def.item.span());

and then we generate the macro_rule with the unique_id and then make a use statement to assign with the correct id in the scope of the definition only (instead of root scope).

		#[macro_export]
		#[doc(hidden)]
		macro_rules! #unique_name {
		}
		pub use #unique_name as ident;

This is less important but it allows for instance many pallet in the same crate to register TestDefaultConfig for instance.
Currently if 2 pallet in the same crate register the same TestDefaultConfig then there is a conflict.

3 resolution of mm_override_path done dynamically

Instead of having the path to macro_magic requested as hardcoded: e.g. in #[import_tokens_attr(frame_support::macro_magic)] we should be able to give a code that would give the path to macro_magic later when the proc_macro orig_sig is called.
It would look like this:

diff --git a/core/src/lib.rs b/core/src/lib.rs
index 8ca6158..777f200 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -689,7 +689,7 @@ pub fn import_tokens_attr_internal<T1: Into<TokenStream2>, T2: Into<TokenStream2
     attr: T1,
     tokens: T2,
 ) -> Result<TokenStream2> {
-    let mm_override_path = match parse2::<Path>(attr.into()) {
+    let mm_override_path_expr = match parse2::<Expr>(attr.into()) {
         Ok(override_path) => override_path,
         Err(_) => macro_magic_root(),
     };
@@ -753,6 +753,7 @@ pub fn import_tokens_attr_internal<T1: Into<TokenStream2>, T2: Into<TokenStream2
                 escape_extra(path.to_token_stream().to_string().as_str()),
                 escape_extra(custom_parsed.to_token_stream().to_string().as_str())
             );
+            let mm_override_path = #mm_override_path_expr
             quote::quote! {
                 #mm_override_path::forward_tokens! {
                     #pound path,

then in substrate we can write derive_impl attribute like this:

#[import_tokens_attr(generate_crate_access_2018("frame-support").or_else(generate_crate_access_2018("frame")))]

4 add a parameter to import_tokens to dynamically fetch the path to self (e.g. DeriveImpl) thus removing the need to always import the macro in scope and allow to call frame_support::DeriveImpl directly.

not very needed but we could also give into attribute the expression to resolve the path to inner_macro_ident
so the call to forward_tokens would look like this:

diff --git a/core/src/lib.rs b/core/src/lib.rs
index 8ca6158..e3dbccb 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -753,10 +753,11 @@ pub fn import_tokens_attr_internal<T1: Into<TokenStream2>, T2: Into<TokenStream2
                 escape_extra(path.to_token_stream().to_string().as_str()),
                 escape_extra(custom_parsed.to_token_stream().to_string().as_str())
             );
+            let path_to_inner_macro = #inner_macro_ident_given_expr;
             quote::quote! {
                 #mm_override_path::forward_tokens! {
                     #pound path,
-                    #inner_macro_ident,
+                    #path_to_inner_macro::#inner_macro_ident,
                     #mm_override_path,
                     #pound extra
                 }

so that derive_impl can tell that it is expected to be found in frame-support and frame and so it doesn't need to be imported in scope.
Thought the drawback is that it means DeriveImpl cannot be re-exported either because it is only expected in frame_support and frame. That said, considering macro_magic::forward_token already require to give the path to be found at (e.g. frame or frame-support), the reexport is already impossible. (what I mean is that derive_impl already need to give an expression to fetch to path to forward_tokens to if there is a reexport in crate Foo caller of derive_impl will still need to depend on crate macro_magic or frame or frame_support)

Add `#[use_export]` attribute

So right now export_tokens already can take an ident to be used as a disambiguation name (needed for Items that don't have an inherent ident, or (previously) items that would otherwise collide with another item name-wise at the crate level). Now that we don't have to deal with crate-wide collisions, I think it would be reasonable to re-purpose this so that the ident you specify becomes the real name of the item.

The problem with doing this, though, is that then from the perspective of import_tokens_attr and import_tokens_proc we have no way of knowing which kind it is, one with a fake name or one with a real ident, and currently in Rust there is no way to be like "does this symbol exist? if not do this".

Now all of that said, let's pretend for a moment that we don't have to worry about names colliding with the item we are attaching #[export_tokens] to. If that were the case, then we could have #[export_tokens] emit something like this:

#[export_tokens]
struct MyStruct {
    field: usize,
}

=>

mod MyStruct {
    macro_rules! __export_tokens_tt_my_struct_0 {
        // ...
    }
}

Then we would be able to do an import like use my::crate::MyItem and MyItem::__export_tokens_tt_my_item_0 would work without issue.

The problem of course with this approach is the name collides with the original item, so it would only work with the no_emit version of #[export_tokens].

So at the end of the day I agree that the best we can do right now until rust adds the ability to ask if a symbol is defined at the current compilation step is to write some sort of #[use_export] macro

The problem with writing such a macro, though, is we will have to know the COUNTER for that particular export_tokens ident...

So it could be that we require specifying an ident like #[export_tokens(MyIdent)] for #[export_tokens] attributes that we want to be able to use with #[use_export], since for those we won't have the COUNTER problem because they will have an explicit name.

Yeah I think that is the solution. The unfortunate thing is I don't think there is a way to get an intelligent compile-error here pushing people towards adding an ident to their #[export_tokens] other than the built in error which is just going to say "type my::crate::MyItem is a type not an expression" or something like that.

Originally posted by @sam0x17 in paritytech/substrate#14356 (comment)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.