[Rust] Procedural Macroの仕組みと実装方法
最初に
この記事ではRustのproc-macroの仕組みとその実装方法について解説します。
マクロとはコードからコードを生成する仕組みのことです。
組み込みの#[derive(Clone)]
,#[derive(Debug)]
などもproc-macroの一種です。
proc-macroとはその名前の通り、手続き的(procedural)にマクロを作ることができるもので、Rustのコードでコード生成の手順を書くことで実装します。
例として、structのfieldごとにgetter関数を定義したい場合を考えます。
普通にコードを書くと以下のようになります。
struct Sample {
field1: String,
field2: String
}
impl Sample {
fn get_field1(&self) -> String {
self.field1.clone()
}
fn get_field2(&self) -> String {
self.field2.clone()
}
}
fn main() {
let sample = Sample {
field1: "field 1".to_string(),
field2: "field 2".to_string(),
};
println!("{}", sample.get_field1()); // field 1
println!("{}", sample.get_field2()); // field 2
}
fieldが増えるたびにgetter関数をその都度追加する必要があります。
そこで、Getter
というマクロを定義します。
#[derive(Getter)]
struct Sample {
field1: String,
field2: String
}
fn main() {
let sample = Sample {
field1: "field 1".to_string(),
field2: "field 2".to_string(),
};
println!("{}", sample.get_field1()); // field 1
println!("{}", sample.get_field2()); // field 2
}
Getter
マクロは以下のコードで実装しています。
use proc_macro::TokenStream;
use quote::quote;
use syn::{ext::IdentExt, parse_macro_input, DeriveInput};
#[proc_macro_derive(Getter)]
pub fn getter_derive(input: TokenStream) -> TokenStream {
let input = &parse_macro_input!(input as DeriveInput);
match generate_getter(input) {
Ok(generated) => generated,
Err(err) => err.to_compile_error().into(),
}
}
fn generate_getter(derive_input: &DeriveInput) -> Result<TokenStream, syn::Error> {
let struct_data = match &derive_input.data {
syn::Data::Struct(v) => v,
_ => {
return Err(syn::Error::new_spanned(
&derive_input.ident,
"Must be struct type",
));
}
};
let mut get_fields = Vec::new();
for field in &struct_data.fields {
let ident = field.ident.as_ref().unwrap();
let ty = &field.ty;
let method_name: proc_macro2::TokenStream = format!("get_{}", ident.unraw().to_string())
.parse()
.unwrap();
get_fields.push(quote! {
pub fn #method_name(&self) -> #ty {
self.#ident.clone()
}
});
}
let struct_name = &derive_input.ident;
let (impl_generics, _, where_clause) = &derive_input.generics.split_for_impl();
let expanded = quote! {
impl #impl_generics #struct_name #where_clause {
#(#get_fields)*
}
};
Ok(expanded.into())
}
deriveマクロを使用することで、structのfieldを元に動的にgetterメソッドを生成できました。
Getter
マクロをどのように実装しているかは後ほど説明します。
proc-macroの仕組み
proc-macroは次の手順で実装します。
- 1: RustのソースコードをsynクレートでparseしてASTに変換する。
- 2: ASTからコードの情報を取り出して、生成したいコードを組み立てる。
原理は非常にシンプルです。babelのpluginを作ったり、TypeScriptのCompiler APIを使ったことがある方であれば、ほぼそれらと同じことをしていると考えていただいて問題ありません。
また、proc-macroは以下の2種類があるので、必要に応じて使い分けてください。
Derive Macro
struct, enum, unionといったデータ構造のソースコードからコードを生成したい場合に使用する。
Attribute Macro
impl, fn, traitといった処理のソースコードからコードを生成したい場合に使用する。
proc-macroの用途
一般的なWebサービスや業務システムをRustで実装する場合にproc-macroを使用することはほとんどないと思います。
proc-macroはcrateの実装でよく使用されます。
例えば、tokioの#[tokio::main]
やactix webのroutingの#[get("/stream")]
などです。
使用者がcrateの実装の詳細を意識する必要がなく、crateの作成者が柔軟に処理を実装できます。
proc-macroの作り方
では、先ほど作成したGetter
マクロの実装を説明していきます。
packageを作成する
proc-macroを作る場合は単体でpackageを作る必要があります。 cargo new getter_macro --lib
を実行し、Cargo.tomlにproc-macro = true
を追加します。
また以下の3種類のcrateも追加します。
- proc-macro2
- quote
- syn
[package]
name = "getter_macro"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0.36"
quote = "1.0.15"
syn = {version = "1.0.86", features = ["full", "extra-traits", "visit-mut", "visit"]}
synでRustをASTに変換する
まずはsynでRustのソースコードをparseします。
Getter
はstructに対してのマクロなので、AST Explorerを使用してRustのstructがどのようなASTに変換されるのか確認してみます。
すると、ItemStruct
のfields
配下にstructのfieldのコードが木構造に変換されて格納されています。
このようにAST Explorerで取得したいソースコードの情報がASTのどこに格納されているのか確認しながら、実装します。
parseしているコードは以下の通りです。
TokenStreamがRustのソースコードの文字列でsynのparse_macro_input!を使用して、ASTに変換しています。
use proc_macro::TokenStream;
use quote::quote;
use syn::{ext::IdentExt, parse_macro_input, DeriveInput};
// structに対してのマクロを作るので、proc_macro_deriveでマクロ名を定義する
#[proc_macro_derive(Getter)]
pub fn getter_derive(input: TokenStream) -> TokenStream {
// synのparse_macro_input!でRustのソースコードをparseする
let input = &parse_macro_input!(input as DeriveInput);
match generate_getter(input) {
Ok(generated) => generated,
Err(err) => err.to_compile_error().into(),
}
}
コラム: synの活用方法
synはproc-macroとセットでよく使用されていますが、それ自体はRustのparserなのでマクロ以外でも有用です。
例えば、私が作っているRustのGraphQLライブラリのrusty-gqlでは、GraphQLからRustのコードを自動生成しています。
コード生成の際に、すでに生成しているファイルとGraphQLスキーマに差分があった場合、既存ファイルを更新する必要があります。
このときにsynを使用して既存ファイルのソースコードをparseしてコードを差分更新しています。
proc-macro以外でもsynの活用方法は色々あります。rusty-gqlもぜひ使ってみて、気軽にstarしてもらえると筆者が喜びます。
ASTから情報を取り出して、生成するコードを組み立てる。
では、実際にコード生成の処理を見てみましょう。
generate_getter
の関数に処理を書いています。
DeriveInputのdataにソースコードをparseしたASTが入っています。
このdataはsyn::Data::Struct
, syn::Data::Enum
, syn::Data::Union
の3種類の可能性があります。
今回のGetter
マクロはstructに対してのみ有効なので、struct以外はErrを返しています。
fn generate_getter(derive_input: &DeriveInput) -> Result<TokenStream, syn::Error> {
let struct_data = match &derive_input.data {
syn::Data::Struct(v) => v,
_ => {
return Err(syn::Error::new_spanned(
&derive_input.ident,
"Must be struct type",
));
}
};
...
}
次にstructのfieldごとにgetterメソッドを定義したいので、struct_data.fieldsに対してfor文で回してコードを生成します。
具体的に何をやっているかはコードのコメントを参照してください。
fn generate_getter(derive_input: &DeriveInput) -> Result<TokenStream, syn::Error> {
...
// 生成するメソッドのTokenStreamを格納するVec
let mut get_fields = Vec::new();
for field in &struct_data.fields {
// field名の情報, field1やfield2
let ident = field.ident.as_ref().unwrap();
// fieldの型情報
let ty = &field.ty;
// メソッド名をformat!で組み立てて、proc_macro2::TokenStreamに変換
let method_name: proc_macro2::TokenStream = format!("get_{}", ident.unraw().to_string())
.parse()
.unwrap();
// quote!でソースコードの文字列を組み立てる。出来上がるコードは以下のようになる。
// fn get_field1(&self) -> String {
// self.field1().clone()
// }
get_fields.push(quote! {
pub fn #method_name(&self) -> #ty {
self.#ident.clone()
}
});
}
...
}
これでgetterメソッドの文字列を作ることができたので、最後に生成するコードを組み立てて完成です。
fn generate_getter(derive_input: &DeriveInput) -> Result<TokenStream, syn::Error> {
...
// struct名の情報
let struct_name = &derive_input.ident;
// generics, where句の情報
let (impl_generics, _, where_clause) = &derive_input.generics.split_for_impl();
// 最終的に生成するコード
// impl Sample {
// fn get_field1(&self) -> String {
// self.field1.clone()
// }
// fn get_field2(&self) -> String {
// self.field2.clone()
// }
// }
let expanded = quote! {
impl #impl_generics #struct_name #where_clause {
#(#get_fields)*
}
};
Ok(expanded.into())
}
#(#get_fields)*
はTokenStreamのVecを展開しており、以下のコードと対応しています。
fn get_field1(&self) -> String {
self.field1.clone()
}
fn get_field2(&self) -> String {
self.field2.clone()
}
もし、引数などでカンマ区切りのコードを生成したい場合は、#(#args),*
といったように*
の前に,
を足すとカンマ区切りでTokenStreamを生成できます。
デバッグ
実際にproc-macroを作っていると、デバッグしたくなります。
自分はよく紹介されているcargo-expandを使用する、もしくはdbg!()やprintln!()などでprintデバッグしていています。
fn generate_getter(derive_input: &DeriveInput) -> Result<TokenStream, syn::Error> {
dbg!(&derive_input.data);
...
}
// [getter_macro/src/lib.rs:24] &derive_input.data = Struct(
// DataStruct {
// struct_token: Struct,
// fields: Named(
// FieldsNamed {
// brace_token: Brace,
// named: [
// Field {
// attrs: [],
// vis: Inherited,
// ident: Some(
// Ident {
// ident: "field1",
// span: #0 bytes(65..71),
// },
// ),
// colon_token: Some(
// Colon,
// ),
// ty: Path(
// TypePath {
// qself: None,
// path: Path {
// leading_colon: None,
// segments: [
// PathSegment {
// ident: Ident {
// ident: "String",
// span: #0 bytes(73..79),
// },
// arguments: None,
// },
// ],
// },
// },
// ),
// },
// ...
最後に
proc-macroの原理、実装方法について説明させていただきました。
この記事のコードはこちらで公開しています。
proc-macroのコードは初見では何をやっているのかわかりにくいと思いますが、やっていることは非常にシンプルなのでぜひトライしてみてください。
今回扱ったのは非常にシンプルなマクロなので、実際に作る際はproc-macroを実装しているcrateのコードを読んで参考にされることをオススメします。
Discussion