Macro_rules resolution hell

There is this crate called async-coap-uri, which exports a macro called uri.

The macro works like this

use async_coap_uri::uri;

// verifies the uri at compile-time
let uri = uri!("https://www.example.com/");

The above code example is not working, because the uri macro is defined like this:

#[macro_export]
macro_rules! uri {
    ( unsafe $S:expr ) => {{
        // We don't do any correctness checks when $S is preceded by `unsafe`.
        $crate::_uri_const!($S, $crate::Uri)
    }};
    ( $S:expr ) => {{
        assert_uri_literal!($S);
        $crate::_uri_const!($S, $crate::Uri)
    }};
    ( ) => {
        $crate::uri!("")
    };
}

and there is this assert_uri_literal macro used that is not in scope:

error: cannot find macro `assert_uri_literal` in this scope
 --> src/main.rs:4:15
  |
4 |     let uri = uri!("https://www.example.com/");
  |               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to previous error

error: could not compile `uri`.

To learn more, run the command again with --verbose

This macro is an implementation detail that is not supposed to be imported by the crate-user, so I decided to solve this problem (or so I thought...).

I thought I could simply solve the problem by using an absolute path in the macro

#[macro_export]
macro_rules! uri {
    ( unsafe $S:expr ) => {{
        // We don't do any correctness checks when $S is preceded by `unsafe`.
        $crate::_uri_const!($S, $crate::Uri)
    }};
    ( $S:expr ) => {{
        $crate::assert_uri_literal!($S);
        $crate::_uri_const!($S, $crate::Uri)
    }};
    ( ) => {
        $crate::uri!("")
    };
}

but this causes another compiler error

error: macro-expanded `macro_export` macros from the current crate cannot be referred to by absolute paths
   --> async-coap-uri/src/macros.rs:205:9
    |
205 |         $crate::assert_uri_literal!($S);
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^
    | 
   ::: async-coap-uri/src/uri_type.rs:232:13
    |
232 |             uri!("//example.com/foo/bar").uri_type(),
    |             ----------------------------- in this macro invocation
    |
    = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
    = note: for more information, see issue #52234 <https://github.com/rust-lang/rust/issues/52234>
note: the macro is defined here
   --> async-coap-uri/src/lib.rs:237:1
    |
237 | #[proc_macro_hack]
    | ^^^^^^^^^^^^^^^^^^
    = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

the definition of assert_uri_literal in lib.rs:

// ...

use proc_macro_hack::proc_macro_hack;

/// Used by the `uri` macro to verify correctness at compile-time.
#[doc(hidden)]
#[proc_macro_hack]
pub use async_coap_uri_macros::assert_uri_literal;

// ...

Because I have no clue how to solve this I decided to check the mentioned issue (this was not an issue, but a PR and I had to scroll a bit to find the first relevant comment):

@CAD97
The error was introduced in dd0a766 to fix #53144 (and made a deprecation lint later to mitigate some fallout.

The issue is that any #[macro_export] macro m is placed into the root module, so the result of the query "is m defined in the root?" is indeterminate until the crate is completely expanded and no potential #[macro_export] s remain.

Indeterminacies like this cause import resolution to stuck, like it happened in #53144 and similar cases.
With the macro_expanded_macro_exports_accessed_by_absolute_paths error in place we can always give a determinate answer "yes" or "no" and prevent resolution from being stuck.
(More blunt, and therefore non-viable, alternative would be prohibiting macro-expanded #[macro_export] s completely.)

Perhaps the error can be relaxed a bit / made more fine-grained, but I haven't thought how to do that in detail.

I can agree with this decision, but this does not solve my problem.

After a bit of googling I found the edition guide (https://doc.rust-lang.org/edition-guide/rust-2018/macros/macro-changes.html#local-helper-macros), where an entire section explains the new macro behavior and there is also mentioned

[...] will give an error message about not finding the __impl_log! macro. This is because unlike in the 2015 edition, macros are namespaced and we must import them. We could do

use log::{__impl_log, error};

which would make our code compile, but __impl_log is meant to be an implementation detail!

It goes further on and explains that one should use

The cleanest way to handle this situation is to use the $crate:: prefix for macros, the same as you would for any other path. Versions of the compiler >= 1.30 will handle this in both editions:

So this means the solution that I am supposed to use is not allowed? What should be done instead?

One solution would be to put another crate in between async-coap-uri and async-coap-uri-macros:

// and this one re-exports it (with proc_macro_hack)
async-coap-uri v0.1.0 (/media/hdd/home/projects/rust-async-coap/async-coap-uri)
│ // this crate defines the `assert_uri_literal` macro
└── async-coap-uri-macros v0.1.0 (/media/hdd/home/projects/rust-async-coap/async-coap-uri/proc-macros)
    └── proc-macro-hack v0.5.14
// and this one would work :)
async-coap-uri v0.1.0 (/media/hdd/home/projects/rust-async-coap/async-coap-uri)
│ // and this one re-exports it (with proc_macro_hack)
└── async-coap-uri-macros-macros
    │ // this crate defines the `assert_uri_literal` macro
    └── async-coap-uri-macros v0.1.0 (/media/hdd/home/projects/rust-async-coap/async-coap-uri/proc-macros)
        └── proc-macro-hack v0.5.14

but I am hoping that there is a better solution or at least an issue/rfc that tries to solve this problem.

The issue is that your uri! macro must both be usable within the front-end crate (async-coap-uri) itself (async-coap-uri/src/uri_type.rs:232:13) and by downstream crates, as you pointed out, all while calling an internal "helper" macro that was created / generated by a macro (by #[proc_macro_hack] in this case).

AFAIK, there are only two solutions:

  • require that downstream crates use the "old" #[macro_use] extern crate ...; construct, and then have your macro assume every macro is "always in scope";

    // instead of: `use ::async_coap_uri::uri;`
    #[macro_use]
    extern crate async_coap_uri;
    
    // verifies the uri at compile-time
    let uri = uri!("https://www.example.com/");
    
    
  • avoid using uri! within your frontend crate: define an internal uri! distinct from the exported one and that is thus allowed to use non-qualified paths:

    mod exported {
        #[macro_export]
        macro_rules! uri {
            ( unsafe $S:expr ) => {{
                // We don't do any correctness checks when $S is preceded by `unsafe`.
                $crate::_uri_const!($S, $crate::Uri)
            }};
            ( $S:expr ) => {{
                $crate::assert_uri_literal!($S);
                $crate::_uri_const!($S, $crate::Uri)
            }};
            ( ) => {
                $crate::uri!("")
            };
        }
    }
    macro_rules! uri {
        ( unsafe $S:expr ) => {{
            // We don't do any correctness checks when $S is preceded by `unsafe`.
            _uri_const!($S, $crate::Uri)
        }};
        ( $S:expr ) => {{
            assert_uri_literal!($S);
            _uri_const!($S, $crate::Uri)
        }};
        ( ) => {
            uri!("")
        };
    }
    

    It does lead to code duplication, but for simple macros such as yours, it is not "that bad", and has the benefit of Just Working

3 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.