🤖
外部クレートの derive macro を自分の derive macro 内で使う
やりたいこと
外部クレートの #[derive(Sample)]
を自分の #[derive(MyDerive)]
の実装内で使いたい。
問題
単に
lib.rs
mod internals;
use proc_macro::TokenStream;
#[proc_macro_derive(MyDerive)]
pub fn derive_myderive(stream: TokenStream) -> TokenStream {
internals::derive_myderive(stream.into())
.unwrap_or_else(|err| err.into_compile_error())
.into()
}
internals.rs
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Error, ItemStruct};
pub(super) fn derive_myderive(input: TokenStream) -> Result<TokenStream, Error> {
let ItemStruct { ident .. }
= syn::parse2(input.clone())?;
Ok(quote!{
impl my_crate::MyDerive for #ident {}
#[derive(external_crate::Sample)]
#input
})
}
みたいにすると、
user.rs
#[derive(MyDerive)]
struct User {
id: u64,
name: String,
}
の展開結果が
expanded.rs
struct User {
id: u64,
name: String,
}
impl my_crate::MyDerive for User {}
struct User {
id: u64,
name: String,
}
impl external_crate::Sample for User {
// ...
}
となりアウト
準備
今から自分は行儀が悪いコードを書こうとしているのだということを心に留めつつ、attribute 系のマクロ ( derive, attribute ) は上から ( 外側から ) 展開される という仕様を突いて展開後の struct を1つ消す。
具体的には、まず以下のような attribute macro を用意する:
lib.rs
#[proc_macro_attribute]
pub fn consume_struct(_: TokenStream, derived_struct: TokenStream) -> TokenStream {
internals::consume_struct(derived_struct.into())
.unwrap_or_else(|err| err.into_compile_error())
.into()
}
internals.rs
pub(super) fn consume_struct(input: TokenStream) -> Result<TokenStream, Error> {
let _: ItemStruct = syn::parse2(input)?;
Ok(TokenStream::new())
}
これは struct を1つ受け取って空の TokenStream
を返すので、例えば
user.rs
#[consume_struct]
struct User {
id: u64,
name: String,
}
は
expanded.rs
となり、#[consume_struct]
された struct が消去されている。
次がポイントで、attribute 系のマクロは上から順に処理、展開される。例えば
Taro.rs
#[consume_struct]
#[derive(MyDerive)]
struct Taro;
Jiro.rs
#[derive(MyDerive)]
#[consume_struct]
struct Jiro;
というコードを考えると、前者はまず #[consume_struct]
が処理され、このときに #[derive(MyDerive)] struct Taro;
の部分が消費されて空の TokenStream
が返されるので
Taro.expanded.rs
となる。一方、後者はまず #[derive(MyDerive)]
が処理されて
Jiro.inter_expanding.rs
#[consume_struct]
struct Jiro;
impl my_crate::MyDerive for Jiro {}
となって、次に #[consume_struct]
が処理されるので、最終的に
Jiro.expanded.rs
impl my_crate::MyDerive for Jiro {}
となる ( よってコンパイルエラー ) 。
解決
internals.rs
pub(super) fn derive_myderive(input: TokenStream) -> Result<TokenStream, Error> {
let ItemStruct { ident .. }
= syn::parse2(input.clone())?;
Ok(quote!{
impl my_crate::MyDerive for #ident {}
#[derive(external_crate::Sample)]
+ #[consume_struct]
#input
})
}
とすればよい。
これによって
user.rs
#[derive(MyDerive)]
struct User {
id: u64,
name: String,
}
の展開結果は
expanded.rs
struct User {
id: u64,
name: String,
}
impl my_crate::MyDerive for User {}
- struct User {
- id: u64,
- name: String,
- }
impl external_crate::Sample for User {
// ...
}
となって、ちょうどやりたかったことが実現された。
補足
この展開は #[derive(MyDerive)]
だけで行われるので、他の derive があったりしても影響はない。例えば
user.rs
#[derive(A, MyDerive, B)]
struct User {
id: u64,
name: String,
}
は、まず A
で
user.inter_expanding_a.rs
#[derive(MyDerive, B)]
struct User {
id: u64,
name: String,
}
impl A for User {}
となり、次に MyDerive
で
user.inter_expanding_myderive.rs
#[derive(B)]
struct User {
id: u64,
name: String,
}
impl my_crate::MyDerive for User {}
impl external_crate::Sample for User {
// ...
}
impl A for User {}
となり、最後に B
で
user.expanded.rs
struct User {
id: u64,
name: String,
}
impl B for User {}
impl my_crate::MyDerive for User {}
impl external_crate::Sample for User {
// ...
}
impl A for User {}
となる。
Discussion