proc-macro-workshop を通して Rust の手続き的マクロを理解する
2023-08-15 :: #Rust #macro> 目次
はじめに
Rust でプログラミングをしていると、 vec!
や println!
のような、 !
で終わる特別な関数を目にすることがあります。これらは、Rust の「マクロ」と呼ばれる機能です。
マクロは、簡単に言うと「コードを生成するコード」であり、繰り返しや特定のパターンのコードを簡単に、効率的に記述することができます。この記事では以下のような 手続き的マクロ と呼ばれる機能を深掘りしていきます。
[#derive(Debug)] // Derive macros
struct Command {
executable: String,
}
Rust のマクロについて
Rust のマクロには、宣言的マクロと手続き的マクロの 2 つの種類が存在します。
- 宣言的マクロ:
macro_rules!
構文で定義され、vec!
やprintln!
が該当します - 手続き的マクロ: このマクロは以下の 3 つの種類が存在します
- Derive macros:
#[derive]
を使用して構造体や enum に追加の処理を実装できる - Attribute macros: 構造体や enum だけではなく、関数に対しても追加の処理を実装できる
- Function-like macros: 宣言的マクロと似たような呼び出し形で、より複雑な処理が記述できる
- Derive macros:
本記事では proc-macro-workshop を通じて、手続き的マクロの各種類とその記述方法について理解度を深めていきます。
本記事で実装した内容は下記リポジトリに配置しています。
進め方
まずは本記事では #[derive]
マクロを使って Builder パターンの実装を進めていき、最終的には以下のような処理を実現できるようにしていきます。
use derive_builder::Builder;
#[derive(Builder)]
pub struct Command {
executable: String,
#[builder(each = "arg")]
args: Vec<String>,
current_dir: Option<String>,
}
fn main() {
let command = Command::builder()
.executable("cargo".to_owned())
.arg("build".to_owned())
.arg("--release".to_owned())
.build()
.unwrap();
assert_eq!(command.executable, "cargo");
}
課題を進めていく上で、以下のクレートを利用します。それぞれの細かい説明は課題を進めていく中で紹介します。
[dependencies]
proc-macro2 = "1.0.66"
quote = "1.0.32"
syn = { version = "2.0.28", features = ['extra-traits'] }
01-parse
まずは一番最初の課題である 01-parse
のテストコードでは、以下の derive
マクロを利用したときにコンパイルエラーが発生しないようにしていきます。
#[derive(Builder)]
pub struct Command {
executable: String,
args: Vec<String>,
env: Vec<String>,
current_dir: String,
}
fn main() {}
初期実装は以下のように unimplemented!()
が利用されているため、まずは関数の型シグネチャに合うように実装を追加していきます。
use proc_macro::TokenStream;
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let _ = input;
unimplemented!()
}
コンパイルを通すだけであれば、以下のように空の TokenStream
を返却すれば OK です。
use proc_macro::TokenStream;
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let _ = input;
TokenStream::new()
}
TokenStream
はマクロを適用した先の Rust コードのトークンが含まれており、 Command
構造体の場合には以下のようなトークンが入力として渡されます。
TokenStream [
Ident {
ident: "struct",
span: #0 bytes(39..45),
},
Ident {
ident: "Command",
span: #0 bytes(46..53),
},
Group {
delimiter: Brace,
stream: TokenStream [
Ident {
ident: "executable",
span: #0 bytes(60..70),
},
Punct {
ch: ':',
spacing: Alone,
span: #0 bytes(70..71),
},
Ident {
ident: "String",
span: #0 bytes(72..78),
},
# ... 残りの定義が続いていく
],
span: #0 bytes(54..151),
},
]
これはただのトークンのストリームでしかないため、Rust のソースコードを表現する構文木にパースして取り扱いしやすい形式に変換するための syn
クレートが用意されています。
今回作成しているものは derive
マクロであるため syn::DeriveInput
という struct
を前提とした構造としてパースすることが可能です。
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
// proc_macro::TokenStream -> proc_macro2::TokenStream への変換を含んでいる
// proc_macro::TokenStream は Rust のコンパイラでしか取り扱えない特殊な値
// proc_macro2::TokenStream に変換することでソースコードで取り扱える形式に変換している
let parsed = parse_macro_input!(input as DeriveInput);
TokenStream::new()
}
実際に構文木にパースした結果は以下のようになっており、Rust コードのトークンがツリー構造として変換されており、 TokenStream
よりも取り扱いしやすい形式になっていることがわかります。
DeriveInput {
attrs: [],
vis: Visibility::Inherited,
ident: Ident {
ident: "Command",
span: #0 bytes(46..53),
},
generics: Generics {
lt_token: None,
params: [],
gt_token: None,
where_clause: None,
},
data: Data::Struct {
struct_token: Struct,
fields: Fields::Named {
brace_token: Brace,
named: [
Field {
attrs: [],
vis: Visibility::Inherited,
mutability: FieldMutability::None,
ident: Some(
Ident {
ident: "executable",
span: #0 bytes(60..70),
},
),
colon_token: Some(
Colon,
),
ty: Type::Path {
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "String",
span: #0 bytes(72..78),
},
arguments: PathArguments::None,
},
],
},
},
},
# ...
],
},
semi_token: None,
},
}
これでパターンマッチなどの機能を利用して細かい制御を行うことが可能になりました。
他にもどのように構文木にパースされるのかが気になる場合は AST Explorer を実際に触って様々なパターンを見てみるとよいと思います。
02-create-builder
次の課題は Builder の derive
マクロを適用した構造体に対して、 builder
メソッドを実装し、Builder パターンを実装するための準備を行います。
#[derive(Builder)]
pub struct Command {
executable: String,
args: Vec<String>,
env: Vec<String>,
current_dir: String,
}
fn main() {
let builder = Command::builder(); // メソッドを生成するだけ
let _ = builder;
}
手続きマクロの実装に移る前に、どのようなコードを生成できればよいのかを確認します。
以下のように適用した構造体に合わせた専用の Builder 構造体と、その構造体を生成するためのメソッドを作成することを目指します。
pub struct CommandBuilder {
executable: Option<String>,
args: Option<Vec<String>>,
env: Option<Vec<String>>,
current_dir: Option<String>,
}
impl Command {
pub fn builder() -> CommandBuilder {
CommandBuilder {
executable: None,
args: None,
env: None,
current_dir: None,
}
}
}
まずは汎用性などは無視してコンパイルエラーが発生しないようにするために、いくつかのフィールドはハードコードでそのまま生成する形式で進めます。
手続きマクロの内部で Rust のコードを生成するときには quote
クレートを利用すると簡易的に生成するコードを指定することが可能です。
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let parsed = parse_macro_input!(input as DeriveInput);
// quote! 内部で生成する実装を指定します
// このマクロの内部では、型補完は有効にならないので注意が必要です
// 結果は proc_macro2::TokenStream として返却されます
let expanded = quote! {
pub struct CommandBuilder {
executable: Option<String>,
args: Option<Vec<String>>,
env: Option<Vec<String>>,
current_dir: Option<String>,
}
impl Command {
pub fn builder() -> CommandBuilder {
CommandBuilder {
executable: None,
args: None,
env: None,
current_dir: None,
}
}
}
};
// ここで proc_macro::TokenStream に型変換します
expanded.into()
}
動作確認のために cargo expand
を利用すれば、以下のようにマクロがどのように展開されているのかがわかり、今回ハードコードで指定した通りにソースコードが生成されていることがわかります。
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use demo::Builder;
struct Command {
executable: String,
args: Vec<String>,
env: Vec<String>,
current_dir: String,
}
// ↓ ここからコードが展開されている
pub struct CommandBuilder {
executable: Option<String>,
args: Option<Vec<String>>,
env: Option<Vec<String>>,
current_dir: Option<String>,
}
impl Command {
pub fn builder() -> CommandBuilder {
CommandBuilder {
executable: None,
args: None,
env: None,
current_dir: None,
}
}
}
fn main() {
let builder = Command::builder();
let _ = builder;
}
これでコンパイルエラーは発生せず、テストも PASS することができました。
しかしながら、急に出てきた quote
クレートの役割や、他の構造体でも適用できるようにするための汎用化の処理が不足しています。
quote
クレート
最初の例で見たように、 入力となる TokenStream
を実際に ログに出力してみた結果 を確認すると、Rust のコードを表すトークンの配列となっていたことがわかります。
TokenStream [
Ident {
ident: "pub",
span: #5 bytes(29..36),
},
Ident {
ident: "struct",
span: #5 bytes(29..36),
},
# ...
]
これは構文木を構成するトークンである proc_macro::TokenTree
から構成されています。
pub enum TokenTree {
Group(Group),
Ident(Ident),
Punct(Punct),
Literal(Literal),
}
例えば以下のような単純な CommandBuilder
構造体を例に考えます。
struct CommandBuilder {
executable: String,
}
この構造体は TokenTree
の各種トークンに対して以下のようにマッピングされます。
この構造体を例に関数から返却する proc_macro::TokenStream
を、 proc_macro::TokenTree
をそのまま利用して返却値を構築しようとすると以下のように定義する必要があります。
use proc_macro::{Group, Ident, Punct, Spacing, Span, TokenStream, TokenTree};
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
// IteratorTokenStream に変換するために配列で指定します
[
TokenTree::Ident(Ident::new("struct", Span::call_site())),
TokenTree::Ident(Ident::new("CommandBuilder", Span::call_site())),
TokenTree::Group(Group::new(
proc_macro::Delimiter::Brace,
[
TokenTree::Ident(Ident::new("executable", Span::call_site())),
TokenTree::Punct(Punct::new(':', Spacing::Alone)),
TokenTree::Ident(Ident::new("String", Span::call_site())),
TokenTree::Punct(Punct::new(',', Spacing::Alone)),
]
.into_iter()
.collect::<TokenStream>(),
)),
]
.into_iter()
.collect()
}
これで cargo expand
を実行すれば、以下のように設定したトークンに従って、Rust コードが生成されていることがわかります。
struct CommandBuilder {
executable: String,
}
fn main() {}
実は quote!
はこれと似たようなことをより簡単に実行できるように用意されているマクロであり、実際に Rust のコードを記述すれば、それを TokenStream
の形式に変換してくれます。
先ほどと同じことを quote!
で実現したい場合には、以下のように生成したい Rust コードをそのまま記述するだけで構いません。
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
// TokenTree を直接利用するよりも、はるかに簡易的に記述することができます
let expanded = quote! {
struct CommandBuilder {
executable: String,
}
}
// quote! が生成するのはライブラリ用に用意された proc_macto2::TokenStream なのでここで変換しています
expanded.into()
}
これが quote
クレートが提供している機能です。
Builder 構造体の名前の取得
今回の実装は Command
構造体に特化した実装になっていましたが、他の構造体やフィールドでも利用できるように汎用化させる必要があります。
具体的には Builder パターンの実装に関しては、以下のような構造体の名前とフィールドの定義のパターンが存在していることがわかります。
// 生成する構造体の名前のパターン -> [元の構造体の名前]Builder
pub struct CommandBuilder {
// フィールドの型の定義のパターン -> [フィールド名]: Option<元の型>,
executable: Option<String>,
args: Option<Vec<String>>,
env: Option<Vec<String>>,
current_dir: Option<String>,
}
つまり syn
クレートを使用して DeriveInput
にパースした後で、元の構造体の名前・構造体で定義されている各フィールドの名前と型さえ取得することができれば、汎用的な実装することが可能です。
今回は以下のように出力された DeriveInput
の内容を確認しながら、必要な情報がどこに格納されているのかを確認します。
まずは構造体の名前を抽出し [構造体の名前]Builder
という名前の Builder 用の構造体を作成していきます。
quote!
内部では識別子を単純に結合することはできないので、新しく Ident
を作成して変数として利用する必要があり、以下のように 2 つのやり方が存在しています。
// 方法① quote::format_ident! を利用する方法
let original_ident = parsed.ident;
let builder_ident = format_ident!("{}Builder", ident);
// 方法② syn::Ident::new で直接生成する方法
let original_ident = parsed.ident;
let builder_name = format!("{}Builder", ident);
let builder_ident = syn::Ident::new(&builder_name, ident.span());
// どちらの場合でも quote! 内で利用できます
quote! {
struct #builder_ident {
// ...
}
impl #original_ident {
pub fn builder() -> #builder_ident {
#builder_ident {
// ...
}
}
}
}
これでどのような構造体に対しても、対応する Builder 構造体の名前を定義することができました。
Builder 構造体のフィールドの取得
これまで quote!
を使って TokenStream
を定義する際には個別に変数を指定したり、フィールドを指定していましたが、このマクロはイテレータを展開してトークンツリーを組み立てることも可能です。
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
// Iterator の検証のために動的にフィールドを作成するための元データを用意する
let vars = vec!["a", "b", "c"];
// 内部で quote! を使用して TokenStream の Iterator を用意する
let delarations: Vec<proc_macro2::TokenStream> = vars
.into_iter()
.map(|var_name| {
let ident = format_ident!("{}", var_name);
// TokenStream を生成
quote! {
#ident: String,
}
})
.collect();
let expanded = quote! {
pub struct Sample {
// 以下のようにマクロ内で変数を展開するように指定することが可能です
#(#delarations)*
}
};
expanded.into()
}
ここで作成した内容を cargo expand
で確認すると、イテレータとして用意した変数を展開して全てのフィールドの定義を動的に展開できていることがわかります。
pub struct Sample {
a: String,
b: String,
c: String,
}
イテレータを展開する時に方法は、 quote クレートで実行しているテストを参考にするとイメージがつきやすいと思います。
CommandBuilder
の定義と builder
メソッドの実装を作成する上で、同じように各フィールドや初期値を作成するためのイテレータを用意することを目指します。
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let parsed: DeriveInput = parse_macro_input!(input as DeriveInput);
let original_ident = parsed.ident;
let builder_ident = format_ident!("{}Builder", original_ident);
// 元が構造体であることと、タプルやUnit型を想定していないため、let else で対象データを抽出する
let syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }), .. }) = parsed.data else {
panic!("This macro can only be applied to struct using named field only, not tuple or unit.");
};
// 構造体を構成する各フィールドの定義には Field からアクセスすることが可能
// Builder の定義と builder メソッドのそれぞれで必要なトークンの形に抽出する
let builder_fields = named.iter().map(|f| {
let ident = &f.ident;
let ty = &f.ty;
quote! {
#ident: Option<#ty>
}
});
// Builder の初期化
let builder_init = named.iter().map(|f| {
let ident = &f.ident;
quote! {
#ident: None
}
});
let expanded = quote! {
pub struct #builder_ident {
#(#builder_fields,)*
}
impl #original_ident {
pub fn builder() -> #builder_ident {
#builder_ident {
#(#builder_init,)*
}
}
}
};
expanded.into()
}
これを展開すれば、以下のように TokenStream
のイテレーターが展開されてそれぞれの定義が作成されていることがわかります。
pub struct CommandBuilder {
executable: Option<String>,
args: Option<Vec<String>>,
env: Option<Vec<String>>,
current_dir: Option<String>,
}
impl Command {
pub fn builder() -> CommandBuilder {
CommandBuilder {
executable: None,
args: None,
env: None,
current_dir: None,
}
}
}
これで他の構造体に対しても適用することが可能な汎用的なマクロにすることができました。
03-call-setters
次の課題では、以下のように Command
で定義されている各フィールドに対して値を設定するための setter
を準備します。
#[derive(Builder)]
pub struct Command {
executable: String,
args: Vec<String>,
env: Vec<String>,
current_dir: String,
}
fn main() {
let mut builder = Command::builder();
// フィールドと同じ名称で同じ型を引数に受け取るメソッドを用意する
builder.executable("cargo".to_owned());
builder.args(vec!["build".to_owned(), "--release".to_owned()]);
builder.env(vec![]);
builder.current_dir("..".to_owned());
}
これは構造としては以下のパターンに従うメソッドを作成することと同義です。
fn [フィールド名](&mut self, [フィールド名]: [フィールドの型]) -> &mut Self {
self.[フィールド名] = Some([フィールド名])
self
}
必要な情報は各フィールドの名前と型しかないので、前回の課題で汎用化を行なった際の実装の大部分を流用するだけで実現することができます。
let builder_setters = named.iter().map(|f| {
let ident = &f.ident;
let ty = &f.ty;
quote! {
fn #ident(&mut self, #ident: #ty) -> &mut Self {
self.#ident = Some(#ident);
self
}
}
});
let expanded = quote! {
pub struct #builder_ident {
#(#builder_fields,)*
}
// 各種 setter を追加する
impl #builder_ident {
#(#builder_setters)*
}
// ...
};
これで cargo expand
で展開すれば、以下のように指定した通りに各種メソッドが生成されていることがわかります。
impl CommandBuilder {
fn executable(&mut self, executable: String) -> &mut Self {
self.executable = Some(executable);
self
}
fn args(&mut self, args: Vec<String>) -> &mut Self {
self.args = Some(args);
self
}
fn env(&mut self, env: Vec<String>) -> &mut Self {
self.env = Some(env);
self
}
fn current_dir(&mut self, current_dir: String) -> &mut Self {
self.current_dir = Some(current_dir);
self
}
}
04-call-build
次の課題では以下のように build
メソッドを作成し、全てのフィールドに値が設定されている場合には Ok(Command)
を返却し、設定されていないフィールドがあれば Err
を返却するようにします。
fn main() {
let mut builder = Command::builder();
builder.executable("cargo".to_owned());
builder.args(vec!["build".to_owned(), "--release".to_owned()]);
builder.env(vec![]);
builder.current_dir("..".to_owned());
// build() メソッドから Result を返却する
// 設定されていないフィールドがある場合には Err を返却する
let command = builder.build().unwrap();
assert_eq!(command.executable, "cargo");
}
課題メモに従い Command
のフィールドの場合には、以下のように実装すれば良さそうです。
impl CommandBuilder {
fn build(&mut self) -> Result<Command, Box<dyn std::error::Error>> {
Ok(Command {
executable: self.executable.take().ok_or("executable is not set")?,
// ...
})
}
}
ただしこれはあくまで一例であり、以下のような実装パターンもあると思います。
&mut self
で参照として受け取り、データをtake
する- データを
Command
に移動させながら、ビルダー自体は再利用することができる - ビルダー側の値は
None
にリセットされるため、注意して利用する必要がある
- データを
&self
で参照として受け取り、データをclone
する- ビルダーの値は変更されないのでそのまま再利用できる
- データのコピーが必要であり、パフォーマンスに影響を与える可能性がある
今回は Builder の再利用などは考えないのでパターン 1 で実装します。
let build_fields = named.iter().map(|f| {
let ident = &f.ident;
quote! {
#ident: self.#ident.take().ok_or(format!("{} is not set", stringify!(#ident)))?
}
});
let expanded = quote! {
pub struct #builder_ident {
// ...
}
impl #builder_ident {
// ...
fn build(&mut self) -> Result<#original_ident, Box<dyn std::error::Error>> {
Ok(#original_ident {
#(#build_fields,)*
})
}
}
};
これでコンパイルエラーが発生することなくメソッドを追加できました。
05-method-chaining
次の課題では、メソッドチェーン形式で Builder を利用することを目指しますが、各種 setter
からの返却値を &mut Self
として定義しているため、すでにこれまでの課題が完了していれば問題なくコンパイル可能です。
fn main() {
let command = Command::builder()
.executable("cargo".to_owned())
.args(vec!["build".to_owned(), "--release".to_owned()])
.env(vec![])
.current_dir("..".to_owned())
.build()
.unwrap();
assert_eq!(command.executable, "cargo");
}
06-optional-field
次の課題では、構造体に Option
なフィールドが含まれる場合を想定しており、対象のフィールドに対しては setter
の呼び出しは必須ではなく、呼び出されていない場合には初期値として None
をそのまま代入します。
これを実現するには、構造体のフィールドの型が Option
であることを確認する必要があるため、これまでよりも複雑な処理が必要になります。
#[derive(Builder)]
pub struct Command {
executable: String,
args: Vec<String>,
env: Vec<String>,
current_dir: Option<String>, // Optionを含むフィールド定義
}
fn main() {
let command = Command::builder()
.executable("cargo".to_owned())
.args(vec!["build".to_owned(), "--release".to_owned()])
.env(vec![])
// current_dir は設定しない場合には None が設定される
.build()
.unwrap();
assert!(command.current_dir.is_none());
let command = Command::builder()
.executable("cargo".to_owned())
.args(vec!["build".to_owned(), "--release".to_owned()])
.env(vec![])
// 呼び出した場合には Some(..) が設定される
// current_dir() は引数として String の値を受け取る
.current_dir("..".to_owned())
.build()
.unwrap();
assert!(command.current_dir.is_some());
}
今の実装では、以下のように Option
が二重に付与されてしまったり setter
の引数にも Option
が付与されてしまいます。
pub struct CommandBuilder {
executable: Option<String>,
args: Option<Vec<String>>,
env: Option<Vec<String>>,
current_dir: Option<Option<String>>, // Option<Option<_>> と二重に適用されてしまう
}
impl CommandBuilder {
// 引数が String ではなく Option<String> になってしまう
fn current_dir(&mut self, current_dir: Option<String>) -> &mut Self {
self.current_dir = Some(current_dir);
self
}
// ...
}
そこでこの課題では derive
マクロが適用された構造体に対して、 Option
が定義されているフィールドの特定と、ラップされている中身の型を抽出して条件分岐的に TokenStream
を構築していくことを目指します。
方針としては Field
内の syn::Type::Path
からトップレベルの型を抽出し、その型が Option
であった場合にはさらにラップされている型も同じく syn::Type::Path
として抽出していきます。
今回は対象が Option
であった場合には内部の型を取り出して Some
として返却する関数を用意し、この関数を対象のフィールドが Option
であるかどうかの判定でも利用します。
/// Returns unwrapped Type in Option as Option<&Type>
fn unwrap_option(ty: &Type) -> Option<&Type> {
if let syn::Type::Path(syn::TypePath {
path: syn::Path { segments, .. },
..
}) = ty
{
if segments.len() == 1 {
if let Some(syn::PathSegment {
ident,
arguments:
syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
args, ..
}),
}) = segments.first()
{
if ident == "Option" && args.len() == 1 {
if let Some(syn::GenericArgument::Type(inner_ty)) = args.first() {
return Some(inner_ty);
}
}
}
}
}
None
}
型構造を順番にアンラップしているので複雑そうに見えますが、 Option
の場合には中身の型を Option<&Type>
で返却しているだけであり、最初の DeriveInput
の全体像から自然と割り出される処理なのでやっていること自体は割とシンプルです。
後はこの関数を利用して各種 Builder の型定義やメソッドのシグネチャを変更していきます。
まずは Builder の型定義を変更し、対象のフィールドが Option
の場合は追加の Option
でラップすることなく、元々の型をそのまま利用します。
let builder_fields = named.iter().map(|f| {
let ident = &f.ident;
let ty = &f.ty;
if unwrap_option(ty).is_some() {
quote! {
#ident: #ty
}
} else {
quote! {
#ident: Option<#ty>
}
}
});
これで以下のように元々の構造体のフィールドが Option
の場合にはそのまま型を利用するようになったことがわかります
struct Command {
executable: String,
args: Vec<String>,
env: Vec<String>,
current_dir: Option<String>,
}
pub struct CommandBuilder {
executable: Option<String>,
args: Option<Vec<String>>,
env: Option<Vec<String>>,
current_dir: Option<String>, // 二重に Option でラップしていない
}
同じように各メソッドや、Command の生成部分も条件分岐をさせていきます。
以下は Command を生成する時に代入先のフィールドが Option
かどうかによって内部の値をアンラップするかどうかを分岐させています。
let build_fields = named.iter().map(|f| {
let ident = &f.ident;
let ty = &f.ty;
if unwrap_option(ty).is_some() {
// 代入先も Option なので ? でアンラップする必要がない
quote! {
#ident: self.#ident.take()
}
} else {
quote! {
#ident: self.#ident.take().ok_or(format!("{} is not set", stringify!(#ident)))?
}
}
});
各 setter
メソッドの型シグネチャでは、対象のフィールドが Option
である場合には内部の型を取り出して、その型をメソッドの引数に指定するだけです。
let builder_setters = named.iter().map(|f| {
let ident = &f.ident;
let ty = &f.ty;
// Option である場合には内部の型を取り出してその型を引数に利用する
if let Some(inner_ty) = unwrap_option(ty) {
quote! {
fn #ident(&mut self, #ident: #inner_ty) -> &mut Self {
self.#ident = Some(#ident);
self
}
}
} else {
quote! {
fn #ident(&mut self, #ident: #ty) -> &mut Self {
self.#ident = Some(#ident);
self
}
}
}
});
ここまでできればコンパイルエラーなくテストを PASS させることが可能です。
07-repeated-field
次の課題では、さらにマクロの機能を深掘りしていき、特定のフィールドに対して属性を付与すると、付与した値を基準に生成されるコードを動的に変更していきます。
また、以下のように each
で指定した名称がフィールド名と重複している場合には、要素を 1 つ 1 つ登録するメソッドの生成を優先し、 Option
型ではないフィールドに対するメソッドを呼び出さなかった場合でもデフォルト値を登録するように機能を変更します。
#[derive(Builder)]
pub struct Command {
executable: String,
// フィールドに対して追加の属性を割り当てる
#[builder(each = "arg")]
args: Vec<String>,
// フィールドに対して追加の属性を割り当てる
#[builder(each = "env")]
env: Vec<String>,
current_dir: Option<String>,
}
fn main() {
let command = Command::builder()
.executable("cargo".to_owned())
// 追加した属性に基づいて、Vecを構成する1つ1つの要素を追加していくメソッドを生成する
// また、生成されるメソッドも属性で指定した名前で生成される
.arg("build".to_owned())
.arg("--release".to_owned())
.build()
.unwrap();
assert_eq!(command.executable, "cargo");
assert_eq!(command.args, vec!["build", "--release"]);
}
このような属性は、公式ドキュメント上では derive macro helper attributes
と呼ばれており、それ自体はなんらかの処理を行うようなものではなく、マクロに対して追加の情報を送ることでより複雑な処理ができるようにするものです。
実際にこの属性を付与した状態でマクロを実行すると、構造体の1 つ 1 つのフィールドを構成する syn::Field
の attrs
フィールドに以下の情報が格納されていることがわかります。
pub struct Field {
pub attrs: Vec<Attribute>, // 指定した属性が配列として格納されている
pub vis: Visibility,
pub mutability: FieldMutability,
pub ident: Option<Ident>,
pub colon_token: Option<Colon>,
pub ty: Type,
}
実装方針としては以下のように進めます
- 対象のフィールドが
Vec
であるかどうかを検証する Vec
の場合にはbuilder
属性が付与されているかどうかを検証するbuilder
属性が付与されている場合にはeach
トークンが含まれているのか確認し、適用するメソッド名を抽出する- メソッド名の抽出までできれば、各種
setter
やbuild
メソッドで生成するコードを変更する
フィールドの型の検証を行う関数を拡張する
以前の課題で Command 構造体の各フィールドの型を検証して Option
の場合には内部の型を Option<&Type>
として返却する unwrap_option
関数を用意していました。
この関数を拡張して、フィールドの型が Option
である場合には内部の型を返却し、また Vec
である場合にも内部の型を返却します。また返却する型を Option<T>
のままにしてしまうとその後で再度条件分岐させる必要があるため、 Option
や Vec
やそれ以外の場合も enum
で表現するようにします。
enum InnerType {
OptionType(Type),
VecType(Type),
PrimitiveType,
}
fn unwrap_ty(ty: &Type) -> InnerType {
// ...
}
後はこの型に合うように条件分岐をさせていきます。
/// Returns InnerType enum with unwrapped Type
fn unwrap_ty(ty: &Type) -> InnerType {
if let syn::Type::Path(syn::TypePath {
path: syn::Path { segments, .. },
..
}) = ty
{
if segments.len() == 1 {
if let Some(syn::PathSegment {
ident,
arguments:
syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
args, ..
}),
}) = segments.first()
{
if args.len() == 1 {
if let Some(syn::GenericArgument::Type(inner_ty)) = args.first() {
if ident == "Option" {
return InnerType::OptionType(inner_ty.clone());
} else if ident == "Vec" {
return InnerType::VecType(inner_ty.clone());
}
}
}
}
}
}
InnerType::PrimitiveType
}
この関数を使用して Builder の各コードを動的に制御していきます。
TokenStream のパース
Option
や Vec
のフィールドであるかどうかの検証はできるようになったため、次は Vec
であった場合には derive
マクロの属性から使用するメソッドの名称を抽出する関数を作成します。
ただし、マクロ内で指定した属性は TokenStream
として得られるため、まずは TokenStream
をどのようにパースすればいいのかを把握します。
まずは syn
クレートにおける TokenStream
のパースの仕組みを理解していきます。
syn
クレートでは proc_macro2::TokenStream
をパースするために様々な parser 関数を提供しており、 fn(input: ParseStream) -> syn::Result<Self>
というシグネチャに従って実装することで、トークンを様々な形状にパースすることが可能です。
コードで理解するために、まずは以下のように今回パースする対象と同じような TokenStream
を用意します。
fn main() {
let tokens = quote! { each = "arg" }; // proc_macro2::TokenStream
println!("{:#?}", tokens);
}
このコードを実行すれば、上記画像で示したものと同じ構造の TokenStream
が生成されていることがわかります。
後はこの構造に従ってパースできるように、それぞれのトークンに合致する型を有した構造体を定義し、 Parse
トレイトを実装していきます。
// Token! マクロは指定したトークンに合致する構造体に変換する
// 今回の場合は syn::token::Eq に内部では変換している
struct IdentEqualExpr {
ident: syn::Ident,
eq_token: syn::Token![=],
expr: syn::Expr,
}
impl syn::parse::Parse for IdentEqualExpr {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
// パースしたい順番に従って parse メソッドを呼び出す
// 内部でカーソル位置が移動するため、正しい順番で呼び出す必要がある
let ident = input.parse()?; // syn::Ident にパース
let eq_token = input.parse()?; // syn::token::Eq にパース
let expr = input.parse()?; // syn::Expr にパース
Ok(Self {
ident,
eq_token,
expr,
})
}
}
動作確認のために proc_macro2::TokenStream
をパースするための syn::parse2
を実行して結果を確認すると、ストリームから指定した型に正しくパースできていることがわかります。
fn main() {
let tokens = quote! { each = "arg" };
let ident_equal_expr = syn::parse2::<IdentEqualExpr>(tokens);
match ident_equal_expr {
// 出力結果は以下のようになり、指定した通りにパースできていることがわかる
// 後は 文字列リテラル を表している syn::LitStr から値を取り出せばよい
/**
* IdentEqualExpr {
* ident: Ident(
* each,
* ),
* eq_token: Eq,
* expr: Expr::Lit {
* attrs: [],
* lit: Lit::Str {
* token: "arg",
* },
* },
* }
*/
Ok(value) => println!("{:#?}", value),
Err(_) => panic!("unexpected token"),
}
}
syn
クレートは似たような構造をパースするための型として syn::MetaNameValue
を用意している。
pub struct MetaNameValue {
pub path: Path,
pub eq_token: Eq,
pub value: Expr,
}
最初のフィールドに関しては syn::Ident
ではなく syn::Path
として定義されているため利用することができないように思えますが、実は From
トレイトを以下のように実装しているため、入力が Ident
であってもこの型を利用して変換することが可能です。
// https://docs.rs/syn/2.0.28/src/syn/path.rs.html#13-25
impl<T> From<T> for Path
where
T: Into<PathSegment>,
{
fn from(segment: T) -> Self {
let mut path = Path {
leading_colon: None,
segments: Punctuated::new(),
};
path.segments.push_value(segment.into());
path
}
}
// https://docs.rs/syn/2.0.28/src/syn/path.rs.html#96-106
impl<T> From<T> for PathSegment
where
T: Into<Ident>,
{
fn from(ident: T) -> Self {
PathSegment {
ident: ident.into(),
arguments: PathArguments::None,
}
}
}
そのため今回のように each = "arg"
の構造をパースして、中身の値を取り出す場合には、以下のような実装にしておけばよく、 Field
には複数の Attribute
が指定されるため、最初の値のみを取り出して利用する形にします。
/// unwrap first value from #[builder(each = value)] attribute
fn unwrap_builder_attr_value(attrs: &[syn::Attribute]) -> Option<String> {
attrs.iter().find_map(|attr| {
if attr.path().is_ident("builder") {
if let Ok(syn::MetaNameValue {
value:
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(ref liststr),
..
}),
..
}) = attr.parse_args::<syn::MetaNameValue>()
{
return Some(liststr.value());
} else {
return None;
}
}
None
})
}
これで属性の値を取り出す関数は完成です。
生成する実装コードの変更
ここまでで Builder を適用した構造体に対して、各フィールドの型を Vec
や Option
としてパースする方法や、 builder(each = "arg")
のように付与された属性から "arg"
という値を取り出すことができるようになりました。
後は元々の Builder の実装から以下の箇所を変更していきます。
- build メソッド
Vec
の場合にはNone
で初期化していた箇所を、Vec::new
で初期化
- Builder の各種
setter
は、Vec
の場合には以下条件で実装するeach = expr
の指定がない場合は今まで通りに実装するeach = expr
の指定がある場合- フィールド名と重複していない場合は、メソッドを新しく追加する
- フィールド名と重複している場合は、個別に設定するメソッドを優先する
まずは build メソッドを以下のように変更します。
let build_fields = named.iter().map(|f| {
let ident = &f.ident;
let ty = &f.ty;
match unwrap_ty(ty) {
InnerType::OptionType(_) => quote! {
#ident: self.#ident.take()
},
InnerType::VecType(_) => quote! {
// None でも失敗しないように Vec::new で初期化する
#ident: self.#ident.take().unwrap_or_else(Vec::new)
},
InnerType::PrimitiveType => quote! {
#ident: self.#ident.take().ok_or(format!("{} is not set", stringify!(#ident)))?
},
}
});
これで Vec<T>
として定義されているフィールドに対して setter
が呼び出されていない場合でも初期値が代入されるようになりました。
次に setter
メソッドの定義を以下のように変更します。
// ty の場合も inner_ty の場合も同じ構造なので、依存を引数に移動させて、生成するストリームを制御する
fn generate_default_setter_with(
ident: &Option<syn::Ident>,
ty: &syn::Type,
) -> proc_macro2::TokenStream {
quote! {
fn #ident(&mut self, #ident: #ty) -> &mut Self {
self.#ident = Some(#ident);
self
}
}
}
let builder_setters = named.iter().map(|f| {
let ident = &f.ident;
let ty = &f.ty;
match unwrap_ty(ty) {
InnerType::VecType(inner_ty) => {
let default_setter = generate_default_setter_with(ident, ty);
if let Some(each) = unwrap_builder_attr_value(&f.attrs) {
let each_ident = format_ident!("{}", each);
let vec_setters = quote! {
fn #each_ident(&mut self, #each_ident: #inner_ty) -> &mut Self {
if let Some(ref mut values) = self.#ident {
values.push(#each_ident);
} else {
self.#ident = Some(vec![#each_ident]);
}
self
}
};
if ident.clone().unwrap() == each_ident {
return vec_setters;
} else {
return quote! {
#vec_setters
#default_setter
};
}
} else {
return default_setter;
}
}
InnerType::OptionType(inner_ty) => generate_default_setter_with(ident, &inner_ty),
InnerType::PrimitiveType => generate_default_setter_with(ident, ty),
}
});
これでコンパイルエラーが発生することなくテストを PASS させることができました。
08-unrecognized-attribute
次の課題では、フィールドで使用する属性に意図していない値が指定された場合にユーザーに対してわかりやすいコンパイルエラーを表示させることを目指します。
#[derive(Builder)]
pub struct Command {
executable: String,
#[builder(eac = "arg")] // 本当は each を設定しないといけない
args: Vec<String>,
env: Vec<String>,
current_dir: Option<String>,
}
テストで利用している trybuild
ではコンパイルエラー自体のテストも実行することが可能であり、以下のようにテキストとして用意したエラーメッセージを利用してアサーションを行うことが可能です。
# 08-unrecognized-attribute.stderr
error: expected `builder(each = "...")`
--> tests/08-unrecognized-attribute.rs:22:7
|
22 | #[builder(eac = "arg")]
| ^^^^^^^^^^^^^^^^^^^^
コンパイルエラーを発生させる方法の 1 つに標準ライブラリから compile_error!
マクロが用意されており、手続きマクロでも利用することで間違った指定を行なったユーザーに対してコンパイルエラーを伝えることが可能です。
実装方針としては、今まで each
という名称を気にせずに値のみを取り出していた下記の処理を変更し、 each
である syn::Ident
であるかどうかも検証するように変更します。
/// unwrap first value from #[builder(each = value)] attribute
fn unwrap_builder_attr_value(attrs: &[syn::Attribute]) -> Option<String> {
attrs.iter().find_map(|attr| {
if attr.path().is_ident("builder") {
if let Ok(syn::MetaNameValue {
value:
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(ref liststr),
..
}),
..
}) = attr.parse_args::<syn::MetaNameValue>()
// 以下では each という属性の値を気にせずに中身を取り出すようにしていた
{
return Some(liststr.value());
} else {
return None;
}
}
None
})
}
現状の返却値である Option<String>
だと細かい制御ができないため、以下のように each
が存在した場合とそうではない場合を把握できるように型シグネチャを変更します。
enum ParseBuilderAttributeResult {
Valid(String),
Invalid(syn::Meta),
}
後は該当する処理の箇所で syn::Ident
が each
であることを検証する処理を追加します。
/// unwrap first value from #[builder(each = value)] attribute
fn unwrap_builder_attr_value(attrs: &[syn::Attribute]) -> Option<ParseBuilderAttributeResult> {
attrs.iter().find_map(|attr| {
if attr.path().is_ident("builder") {
if let Ok(syn::MetaNameValue {
value:
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(ref liststr),
..
}),
path,
..
}) = attr.parse_args::<syn::MetaNameValue>()
{
// ここで検証する内容と返却値を変更する
if !path.is_ident("each") {
return Some(ParseBuilderAttributeResult::Invalid(attr.meta.clone()));
}
return Some(ParseBuilderAttributeResult::Valid(liststr.value()));
} else {
return None;
}
}
None
})
}
後は setter
を生成する際に、以下のように Invalid
なパターンの場合には to_compile_error
を利用して TokenStream
を作成するようにすることで、コンパイルエラーを伝えることができるようになります。
let builder_setters = named.iter().map(|f| {
let ident = &f.ident;
let ty = &f.ty;
match unwrap_ty(ty) {
InnerType::VecType(inner_ty) => {
let default_setter = generate_default_setter_with(ident, ty);
// ここを Invalid な場合に処理する
match unwrap_builder_attr_value(&f.attrs) {
Some(ParseBuilderAttributeResult::Valid(each)) => {
let each_ident = format_ident!("{}", each);
let vec_setters = quote! {
fn #each_ident(&mut self, #each_ident: #inner_ty) -> &mut Self {
if let Some(ref mut values) = self.#ident {
values.push(#each_ident);
} else {
self.#ident = Some(vec![#each_ident]);
}
self
}
};
if ident.clone().unwrap() == each_ident {
return vec_setters;
} else {
return quote! {
#vec_setters
#default_setter
};
}
}
// TokenStream なので返却する値の型は合うようになっている
Some(ParseBuilderAttributeResult::Invalid(meta)) => {
return syn::Error::new_spanned(meta, "expected `builder(each = \"...\")`")
.to_compile_error()
.into()
}
None => return default_setter,
};
}
InnerType::OptionType(inner_ty) => generate_default_setter_with(ident, &inner_ty),
InnerType::PrimitiveType => generate_default_setter_with(ident, ty),
}
});
これで以下のように間違った値を指定した場合にはコンパイルエラーが発生するようになりました。
09-redefined-prelude-type
最後の課題は、ユーザーが独自に定義した型と生成するコード内で使用している型が被ってしまった場合の対応になります。
type Option = ();
type Some = ();
type None = ();
type Result = ();
type Box = ();
#[derive(Builder)]
pub struct Command {
executable: String,
}
fn main() {}
今までの cargo expand
を実行した結果から分かるように、マクロはコンパイル時に展開されて Rust コードとして実行されます。
つまり quote!
内で名前空間を指定せずに Result
のように指定している場合には、テストコードのようにユーザー側で type Result = ()
と内部で使用している型と同じ名称の型を宣言してしまうと、意図していない型が適用されてしまいます。
そこで quote!
内で生成するコードに対しては、以下のように名前空間を指定します。
let expanded = quote! {
// ...
impl #builder_ident {
#(#builder_setters)*
// Result や Box の名前空間を指定する
fn build(&mut self) -> std::result::Result<#ident, std::boxed::Box<dyn std::error::Error>> {
Ok(#ident {
#(#build_fields,)*
})
}
}
// ...
};
他にも Option
/ Some
/ None
を利用している箇所で名前空間のパスを指定するようにすれば、テストは PASS できます。
これで proc-macro-workshop の Builder マクロの課題は完了です!
完成したコード
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, punctuated::Punctuated, DeriveInput, Type};
enum InnerType {
OptionType(Type),
VecType(Type),
PrimitiveType,
}
/// Returns InnerType enum with unwrapped Type
fn unwrap_ty(ty: &Type) -> InnerType {
if let syn::Type::Path(syn::TypePath {
path: syn::Path { segments, .. },
..
}) = ty
{
if segments.len() == 1 {
if let Some(syn::PathSegment {
ident,
arguments:
syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
args, ..
}),
}) = segments.first()
{
if args.len() == 1 {
if let Some(syn::GenericArgument::Type(inner_ty)) = args.first() {
if ident == "Option" {
return InnerType::OptionType(inner_ty.clone());
} else if ident == "Vec" {
return InnerType::VecType(inner_ty.clone());
}
}
}
}
}
}
InnerType::PrimitiveType
}
enum ParseBuilderAttributeResult {
Valid(String),
Invalid(syn::Meta),
}
/// unwrap first value from #[builder(each = value)] attribute
fn unwrap_builder_attr_value(attrs: &[syn::Attribute]) -> Option<ParseBuilderAttributeResult> {
attrs.iter().find_map(|attr| {
if attr.path().is_ident("builder") {
if let Ok(syn::MetaNameValue {
value:
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(ref liststr),
..
}),
path,
..
}) = attr.parse_args::<syn::MetaNameValue>()
{
if !path.is_ident("each") {
return Some(ParseBuilderAttributeResult::Invalid(attr.meta.clone()));
}
return Some(ParseBuilderAttributeResult::Valid(liststr.value()));
} else {
return None;
}
}
None
})
}
fn extract_named_fields(data: &syn::Data) -> &Punctuated<syn::Field, syn::token::Comma> {
let syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(syn::FieldsNamed{ named, .. }), .. }) = data else {
unimplemented!("This macro can only be applied to struct");
};
named
}
// ty の場合も inner_ty の場合も同じ構造なので、依存を引数に移動させて、生成するストリームを制御する
fn generate_default_setter_with(
ident: &Option<syn::Ident>,
ty: &syn::Type,
) -> proc_macro2::TokenStream {
quote! {
fn #ident(&mut self, #ident: #ty) -> &mut Self {
self.#ident = Some(#ident);
self
}
}
}
#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: TokenStream) -> TokenStream {
let parsed = parse_macro_input!(input as DeriveInput);
let original_ident = parsed.ident;
let builder_ident = format_ident!("{}Builder", original_ident);
let named = extract_named_fields(&parsed.data);
let builder_fields = named.iter().map(|f| {
let ident = &f.ident;
let ty = &f.ty;
match unwrap_ty(ty) {
InnerType::OptionType(_) => {
quote! {
#ident: #ty
}
}
_ => quote! {
#ident: std::option::Option<#ty>
},
}
});
let builder_setters = named.iter().map(|f| {
let ident = &f.ident;
let ty = &f.ty;
match unwrap_ty(ty) {
InnerType::VecType(inner_ty) => {
let default_setter = generate_default_setter_with(ident, ty);
match unwrap_builder_attr_value(&f.attrs) {
Some(ParseBuilderAttributeResult::Valid(each)) => {
let each_ident = format_ident!("{}", each);
let vec_setters = quote! {
fn #each_ident(&mut self, #each_ident: #inner_ty) -> &mut Self {
if let Some(ref mut values) = self.#ident {
values.push(#each_ident);
} else {
self.#ident = std::option::Option::Some(vec![#each_ident]);
}
self
}
};
if ident.clone().unwrap() == each_ident {
return vec_setters;
} else {
return quote! {
#vec_setters
#default_setter
};
}
}
Some(ParseBuilderAttributeResult::Invalid(meta)) => {
return syn::Error::new_spanned(meta, "expected `builder(each = \"...\")`")
.to_compile_error()
.into()
}
None => return default_setter,
};
}
InnerType::OptionType(inner_ty) => generate_default_setter_with(ident, &inner_ty),
InnerType::PrimitiveType => generate_default_setter_with(ident, ty),
}
});
let builder_init = named.iter().map(|f| {
let ident = &f.ident;
quote! {
#ident: std::option::Option::None
}
});
let build_fields = named.iter().map(|f| {
let ident = &f.ident;
let ty = &f.ty;
match unwrap_ty(ty) {
InnerType::OptionType(_) => quote! {
#ident: self.#ident.take()
},
InnerType::VecType(_) => quote! {
#ident: self.#ident.take().unwrap_or_else(Vec::new)
},
InnerType::PrimitiveType => quote! {
#ident: self.#ident.take().ok_or(format!("{} is not set", stringify!(#ident)))?
},
}
});
let expanded = quote! {
pub struct #builder_ident {
#(#builder_fields,)*
}
impl #builder_ident {
#(#builder_setters)*
fn build(&mut self) -> std::result::Result<#original_ident, std::boxed::Box<dyn std::error::Error>> {
Ok(#original_ident {
#(#build_fields,)*
})
}
}
impl #original_ident {
pub fn builder() -> #builder_ident {
#builder_ident {
#(#builder_init,)*
}
}
}
};
expanded.into()
}
追加課題: tracing-attributes
クレートのような複雑な属性のパース
Builder マクロの作成では以下のように付与された属性をパースして、動的にコードを生成していました。
#[derive(Builder)]
pub struct Command {
executable: String,
#[builder(each = "arg")] // builder という属性で name = expr 形式で指定する
args: Vec<String>,
#[builder(each = "env")] // builder という属性で name = expr 形式で指定する
env: Vec<String>,
current_dir: Option<String>,
}
サードパーティクレートの中には複雑なパースを行なっているものもあり、その 1 つが tracing-attributes クレートです。
このクレートが提供しているものは derive マクロではなく attributes マクロではありますが、分散トレーシングのために以下のような複雑な設定を行うことが可能です。
#[instrument(
name = "my_name",
level = "debug",
target = "my_target",
skip(non_debug),
fields(foo="bar", id=1, show=true),
)]
pub fn my_function(arg: usize, non_debug: NonDebug) {
// ...
}
今回は tracing-attributes
クレートが複雑な属性をどのようにパースしているのかを理解するために、以下の属性をパースすることを目標に進めていきます。なお、元々の課題とは関係ありません。
fn main() {
let tokens = quote! {
name = "sample",
skip(form, state),
fields(
username=name,
)
};
}
設計方針
07-repeated-field で行なったように、入力される TokenStream
をどのような型としてパースするのかをまずは決める必要があります。
課題の時には以下のように属性の名称と設定された値を一緒の構造体にパースしていましたが、これだとパースできてもコード側でどの属性に対応するものなのかを判定する必要が出てくるため、複数の属性をパースする必要がある場合にはコードが煩雑になってしまいます。
struct IdentEqualExpr {
ident: syn::Ident,
eq_token: syn::Token![=],
expr: syn::Expr,
}
今回は name
/ skip
/ fields
という 3 つの属性を利用することがあらかじめ決まっており、それぞれの属性に対してどのような値を設定できるのかも決まっているため、Rust の型安全性による恩恵を受けるため、パースできた時点で実装側にできるだけ条件分岐が不要になるようにしていくことを目指します。
そこでパース対象の属性名を構造体のフィールド名に設定し、対応する値にパースできた値を登録できるように、以下のように、 Parse
するための構造体とデータを保持するための構造体を設計します。
struct Args {
name: Option<syn::LitStr>,
skips: HashSet<syn::Ident>,
fields: Option<Fields>,
}
// name = expr の形式に対して Parse を実装するための構造体
// name は固定なので Key として保持する必要はない
struct NameValue(syn::LitStr);
// skip(xxx, yyy) の形式に対して Parse を実装するための構造体
struct Skips(HashSet<syn::Ident>);
// fields(key1=value1, key2=value2) の形式に対して Parse を実装するための構造体
struct Fields(Punctuated<Field, Token![,]>);
// Key と Value は自由に設定できるため、両方を保持できるようにする
struct Field {
key: syn::Ident,
value: Option<syn::Expr>,
}
この構造体に対して、それぞれ Parse
トレイトの実装を行なっていきます。
Parse トレイトの実装 - 属性値のパース
まずはそれぞれのフィールドに対して Parse
トレイトの実装を進めていきますが、今回はフィールド値が決まっているため、 syn::custom_keyword!
を利用してパース対象の値を定義していきます。
syn::custom_keyword!
マクロを利用して以下のように定義すれば、自動的に複数の機能が実装された構造体を生成することが可能です。
- Peeking —
input.peek(kw::whatever)
- Parsing —
input.parse::<kw::whatever>()?
- Printing —
quote!( ... #whatever_token ... )
- Span からのトークン生成 —
let whatever_token = kw::whatever(sp)
- 該当 Span へのアクセス —
let sp = whatever_token.span
mod kw {
syn::custom_keyword!(name);
syn::custom_keyword!(skip);
syn::custom_keyword!(fields);
}
これで TokenStream
内に指定したキーが存在する場合にはパースすることが可能になりました。
Parse トレイトの実装 - name のパース
name
属性をパースするには、 name = expr
形式のパース用に用意した以下の構造体に対して Parse
トレイトの実装が必要になります。
struct NameValue(syn::LitStr);
実装自体は非常にシンプルであり、想定通りの TokenStream
が入力されることを前提にして、構造体を構築するのに必要な部分のみを抽出していけば OK です。
impl syn::parse::Parse for NameValue {
// input の TokenSteam は「 name = expr 」 を前提としたストリームが入力される前提
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
// 以下2つは保持する必要がないので、Cursorを進めるだけにする
let _ = input.parse::<kw::name>()?;
let _ = input.parse::<Token![=]>()?;
let value = input.parse()?;
Ok(Self(value))
}
}
Parse トレイトの実装 - skip のパース
skip
属性をパースするには、 skip(xxx, yyy)
形式のパース用に用意した以下の構造体に対して Parse
トレイトの実装が必要になります。
struct Skips(HashSet<syn::Ident>);
この属性は Key-Value 形式ではなく、コンマ区切りで指定された値を保持できるようにするために、以下のように Parse
トレイトを実装していきます。
impl syn::parse::Parse for Skips {
// input の TokenSteam は「 skip(xxx, yyy, ...) 」 を前提としたストリームが入力される前提
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let _ = input.parse::<kw::skip>()?;
let content;
let _ = syn::parenthesized!(content in input);
// Punctuated<Ident, Comma> としてパースされる
let names = content.parse_terminated(syn::Ident::parse_any, Token![,])?;
let mut skips = HashSet::new();
for name in names {
if skips.contains(&name) {
continue;
}
skips.insert(name);
}
Ok(Self(skips))
}
}
(xxx, yyy)
のような形式をパースするためには、まずは始まりのかっこ( (
)と閉じかっこ( )
)が存在していることを前提に、コンマ区切りで 1 つ 1 つの値をパースする必要があります。
そのために実装で利用しているように syn::parenthesized!
マクロを利用してどのような記号で囲っているのかを指定し、 content.parse_terminated
マクロを利用してどの記号区切りでどの値としてパースするのかを指定することができます。
Parse トレイトの実装 - fields のパース
fields
属性をパースするには、 fields(key1=value1, key2=value2)
形式のパース用に用意した以下の構造体に対して Parse
トレイトの実装が必要になります。
// fields(...) 形式をパースするための構造体
struct Fields(Punctuated<Field, Token![,]>);
// key = value 形式を1つ1つパースするための構造体
struct Field {
key: syn::Ident,
value: Option<syn::Expr>,
}
まずは fields(...)
形式をパースしますが、これは 1 つ前の skip
属性とほとんど同じようにパースすることが可能です。
impl syn::parse::Parse for Fields {
// input の TokenSteam は「 fields(...) 」 を前提としたストリームが入力される前提
// 1つ1つの 「 key=value 」 は Field 型としてパースする
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let _ = input.parse::<kw::fields>()?;
let content;
let _ = syn::parenthesized!(content in input);
let fields = content.parse_terminated(Field::parse, Token![,])?;
Ok(Self(fields))
}
}
次に key = value
形式をパースしますが、現時点では =
のみをサポートするようにします。
なお tracing-attributes
クレートはログ出力用の %
やデバッグ出力用の ?
もサポートしています。
impl syn::parse::Parse for Field {
// input の TokenSteam は「 key=value 」 を前提としたストリームが入力される前提
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let key = input.parse()?;
// 「 = 」 がある場合には Cursor を進めて Expr の部分を取り出す
let value = if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
Some(input.parse()?)
} else {
None
};
Ok(Self { key, value })
}
}
Parse トレイトの実装 - Args のパース
最後にこれまでに実装を型を利用して、 TokenStream
全体をパースするための Args
型に対して Parse
トレイトの実装を進めていきます。
Builder の場合と異なり、属性の値が複数設定することが可能であるため、属性の値に応じてどの型でパースするのかを決定する必要があります。
そうした場合に利用できるものが ParseStream
が提供している以下のメソッドです。
lookahead1
:TokenStream
からの次のトークンを 1 つだけ参照するpeek
: 次のトークンを調べて、指定したトークンと一致するかどうか判定する。Cursor
は進めない
TokenStream
が空であることを確認できる is_empty
メソッドを利用すれば以下のように実装できます。
impl syn::parse::Parse for Args {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut args = Self::default();
// Cursor が全ての TokenStream を指し終わるまでループ処理を行う
while !input.is_empty() {
let lookahead = input.lookahead1();
// name = expr
if lookahead.peek(kw::name) {
let NameValue(name) = input.parse()?;
args.name = Some(name);
// skip(xxx, yyy)
} else if lookahead.peek(kw::skip) {
let Skips(skips) = input.parse()?;
args.skips = skips
// fields(key1=value1, key2=value2)
} else if lookahead.peek(kw::fields) {
args.fields = Some(input.parse()?);
// 属性間の区切り記号 skip(xxx), <- こういう時に使うコンマ記号
} else if lookahead.peek(Token![,]) {
let _ = input.parse::<Token![,]>()?;
} else {
return Err(syn::Error::new(input.span(), "unexpected token"));
}
}
Ok(args)
}
}
これで以下のようにパースすれば処理は成功し、意図通りにパースした内容が出力されていることがわかります。
fn main() {
let tokens = quote! {
name = "sample",
skip(form, state),
fields(
username=name,
)
};
match syn::parse2::<Args>(tokens) {
Ok(args) => {
println!("args - {:#?}", args.name);
println!("skips - {:#?}", args.skips);
println!("fields - {:#?}", args.fields);
}
Err(e) => eprintln!("{:?}", e),
}
}
これで複雑な属性が定義されている場合でもどのようにパースしているのか把握することができました。
事前に適用できる属性や構造体の設計を行なっていれば、そこまで複雑な処理にならずに実装できそうです。
感想
これで proc-macro-workshop の Builder derive マクロ構築課題は完了です。課題を解いていく中で tracing-attributes
や thiserror
などの実装も見ていきましたが、終盤に近づくにつれてコードに対する理解度が向上して、サードパーティのクレートの実装もかなり読めるようになっていきました。
また、課題を進めていく中で if-let
や let-else
などの構文を利用すると、パターンマッチングの要領で型をどんどんアンラップしていく処理が非常に記述しやすく、型構造が一目瞭然なので可読性も良くなるなと感じました。
ただ自由度の高い attribute
に関しては parse
していくと複雑になっていってしまうなとも感じました。 tracing-attribute
クレートの実装を参考にサンプルコードを書いていく中で、使用できる属性をしっかりと設計できている場合には、構造体にマッピングでいるためある程度複雑性を下げることもできると感じたので、こうしたマクロを実装する場合には事前の設計がかなり重要になると思います。
Builder 以外の課題や trybuild
の使い方などに興味が出てきたので、また別の記事としてまとめようかなと思います。