Previously: $crate metavariable for procedural macros?
Summary
Procedural macros often pair with a runtime crate, and need to generate paths to that runtime crate. We add a way for procedural macros to use a $crate
-like token to refer to their runtime crate.
Motivation
Procedural macros most often refer to their runtime library crate by assuming that a user of the procedural macro will have an explicit dependency on the library crate and does not rename the crate, allowing the procedural macro to emit extern crate library
or use ::library
paths. However, this scheme breaks if the runtime crate is renamed in cargo. To combat this, a technique like proc-macro-crate can be used to lookup the crate name from the cargo manifest. However, this still leads to issues when reexporting derives, as then the library crate is not depended on by the derive user's crate at all! The best known solution to this as used by bevy and encase is to provide the procedural macro implementation in an implementation crate which takes a path to the library crate and uses that for the implementation, and anyone who wants to wrap your library providing a copy of your derive with the package name lookup customized to use their library crate as the entry point instead.
Guide-level explanation
Macro users
Basically nothing changes. If macro authors use the new functionality, it will be possible to rename crates which provide macros and reexport them from wrapper crates without running into "crate not found" style errors.
Macro writers
When writing a procedural macro that needs to refer to some types in a runtime library, use a new accepted signature for declaring procedural macros:
#[proc_macro]
pub fn my_function(
input: TokenStream,
library_path: TokenStream,
) -> TokenStream {
/* implementation */
}
#[proc_macro_attribute]
pub fn my_attribute(
input: TokenStream,
annotated_item: TokenStream,
library_path: TokenStream,
) -> TokenStream {
/* implementation */
}
#[proc_macro_derive(MyDerive)]
pub fn my_derive(
annotated_item: TokenStream,
library_path: TokenStream,
) -> TokenStream {
/* implementation */
}
This provides a new library_path: TokenStream
argument to your procedural macro entry point. library_path
contains a sequence of tokens usable as a module path provided by your library crate, typically to a module containing any symbols which the macro expansion needs to refer to.
The tokens provided to library_path
make up a path accepted by the declarative macros pattern $(::)? $($path_segment:ident)::+
. Splitting library_path
into individual tokens and trying to use them in any way except printing them as the library_path
stream is not guaranteed to have any particular behavior. (For example, it would be valid for library_path
to be a single source-invalid identifier which the compiler recognizes as referring to the chosen library path.) Additionally, the token hygiene/spans must be preserved for the library_path
to function.
In your library crate, you reexport your procedural macros as such:
#[macro_library_path(crate::__macro_support)]
pub use library_macros::{my_function, my_attribute, MyDerive};
The path provided to #[macro_library_path]
is the path used by library_path
. The provided path is required to be an absolute path (that is, start with either crate
or a name in the extern prelude), and the path must be externally visible from the crate root. When used in the expansion of the procedural macro, library_path
will refer to the provided path and can use any pub
item in it, no matter what crate uses the macro, even if the user crate does not have visibility of your library crate.
If a procedural macro is use
d from the procedural macro crate without specifying #[macro_library_path]
, it is treated as if they wrote #[macro_library_path(::crate)]
. When an item is used from a reexport not from the procedural macro crate, it inherits the #[macro_library_path]
unless the use is also a use
and provides a new #[macro_library_path]
. In particular, when a procedural macro is invoked, it calls the procedural macro server using the #[macro_library_path]
provided when the macro name was use
d, or if the used name does not have #[macro_library_path]
, where that name was use
d from, continuing until a #[macro_library_path]
is found (or a procedural macro crate is found, in which case the crate
of the first non-proc-macro
-crate the item is used in is use
d in instead).
Example
In the procedural macro crate library_macros
:
#[proc_macro_derive(Trait)]
pub fn my_derive(
annotated_item: TokenStream,
library_path: TokenStream,
) -> TokenStream {
let DeriveInput { attrs, vis, ident, generics, data } =
syn::parse_macro_input!(input as syn::DeriveInput);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let expanded = quote! {
impl #impl_generics #library_path::Trait for #ident #ty_generics #where_clause {
fn consume(&self, food: #library_path::Food) {
// just throw it away, they won't know the difference
::std::mem::drop(food);
}
}
};
expanded.into()
}
In the runtime library crate library
:
#[macro_library_path(crate)] // inferred if omitted
pub use library_macros::Trait;
pub trait Trait {
fn consume(&self, food: Food);
}
pub struct Food {
calorie_count: usize,
}
Reference-level explanation
TODO: explain how this functions in more detail.
Implementation notes:
- The
library_macros
crate is still only compiled a single time for the compiler host platform. Thelibrary_path
is purely a runtime concept tolibrary_macros
. - All procedural macros are always treated as taking a
library_path
, and alibrary_path
is passed over the procedural macro bridge. (It is for this reason the default#[macro_library_path]
is provided, rather than requiring the presence of the attribute; to support old-style macros which don't takelibrary_path
.) If the procedural macro is not declared to take thelibrary_path
argument, it simply is discarded by the bridge and not provided to the function.
Drawbacks
- Additional surface area complicating the procedural macro bridge.
- Makes the procedural macro entry points more magic by both
- allowing them to be declared with different airities, and
- providing yet another argument not distinguished by type.
Rationale and alternatives
This fills an obvious need in the ecosystem; people are building workaround which cover most use cases but which require significant manual intervention to set up and still can break in edge cases. Additionally, reading the macro caller's Cargo.toml
is not a thing that procedural macros are necessarily guaranteed to be able to do, such as if they were to be sandboxed into wasm without adhoc filesystem access.
While this can be almost completely polyfilled, it requires significant manual work (e.g. providing a new proc macro crate for each new facade) and the extra crates involved negatively impact compile time compared to the build and module system supporting this use case.
Alternatively to providing the runtime library path in an entry argument, we could support the procedural macro outputting a special compound token like $crate
which is resolved to refer to the path provided to #[macro_library_path]
. However, using literally $crate
is likely a bad idea, as procedural macros which emit macro_rules!
definitions would like to emit literal $crate
for the macro_rules!
implementation.
More likely is instead providing an API like Ident::macro_library_path()
which returns a compound identifier which resolves to the configured #[macro_library_path]
, but which cannot be constructed directly. However, this is a pure library addition on top of the bridge support, which can be polyfilled by 3rd party crates and/or added to the standard proc_macro
distribution at a later date.
Prior art
In the crates ecosystem:
-
proc-macro-crate, which offers a reusable way to read
Cargo.toml
to determine how your library crate can be named - bevy_encase_derive (link to PR), which wraps encase_derive_impl to provide a version of encase_derive which uses bevy_macro_utils to set the path to encase through bevy's facade
Note also that this functionality can and sometimes already is emulated for functionlike procedural macros by exporting a declarative macro wrapper instead, e.g.
#[macro_export]
macro_rules! functionlike {
( $($tt:tt)* ) => {
$crate::__proc_macros::functionlike! {
#![crate = $crate]
$($tt)*
}
}
}
In other ecosystems:
- None known yet.
Unresolved questions
- Unknown unknowns.
Future possibilities
- Cargo packages that provide both a proc-macro crate and a runtime crate versioned together will naturally be a primary user of this technique. In that case, the default macro library path should likely be the associated library crate, since it is known by the build system.
- A global
Ident::macro_library_path()
(see rationale-and-alternatives). - Supporting more than one path for the library to pass to the procedural macro implementation.