Rust の derive macro を書いてみよう
前回は cargo-expand を使って thiserror crate の derive macro の展開結果を見てみました 。今回は derive macro を書いて derive macro に慣れてみようと思います。
derive macro とは
derive macro とは、手続き的マクロ (procedural macro) の一種で、struct
や enum
などに derive
属性を指定したときに自動で生成されるコードを指定するマクロです。
標準で提供される derive macro の例としては Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd があります。
ユーザーが独自に custom derive macro を定義することもできます。今回はこれを扱います。
custom derive macro の定義方法
前述の通り derive macro は procedural macro です。 procedural macro は crate type を proc-macro
にしておく必要があります。 Cargo.toml
に次のような記述をすることで crate type が proc-macro
になります。
[lib]
proc-macro = true
derive macro は proc_macro_derive
属性のついた関数で定義します。関数のシグネチャは (TokenStream) -> TokenStream
です。入力として derive
属性のついたアイテムの TokenStream
を受け取り、そこに追加する TokenStream
を返します。
TokenStream
はコンパイラが提供している proc_macro
crate で定義されています。
公式のリファレンスにある例です。
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_derive(AnswerFn)]
pub fn derive_answer_fn(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
extern crate proc_macro_examples;
use proc_macro_examples::AnswerFn;
#[derive(AnswerFn)]
struct Struct;
fn main() {
assert_eq!(42, answer());
}
今回の例
今回は enum
に対して variant の名前の一覧を返す variants
関連関数を追加する derive macro を作ってみます。
こんな感じです。
#[derive(derive1::VariantsFn)]
enum E1 {
A,
B(i32),
C { b: bool },
}
#[derive(derive1::VariantsFn)]
enum E2 {}
fn main() {
assert_eq!(E1::variants(), &["A", "B", "C"]);
assert_eq!(E2::variants(), &[] as &'static [&'static str]);
}
enum
で定義されたデータ型に対して、 fn variants() -> &'static [&'static str]
な関連関数が追加され、文字列の形で返してくれます。
コード例
コード例の全体は https://github.com/bouzuya/rust-examples/tree/7c3133ce43a61204f271dbe3366cca640598f5fa/derive_macro1 です。
proc-macro 用の crate とそれを利用する crate の 2 つが必要なので、まずは cargo workspace をつくります。
Cargo.toml
Cargo.toml
を↓のように配置します。
[workspace]
members = ["crates/*"]
resolver = "3"
crates
directory を作成して、その下で cargo new derive1 --lib
と cargo new main
として、ライブラリクレートとバイナリクレートを作成します。
crates/main/Cargo.toml
crates/main/Cargo.toml
には derive1
crate を依存関係に追加します。
[package]
name = "main"
edition = "2024"
publish = false
[dependencies]
derive1 = { path = "../derive1" }
[lints.rust]
dead_code = "allow"
crates/main/src/main.rs
crates/main/src/main.rs
は先ほども挙げたとおり、こんな感じです。 #[derive(...)]
に今回定義した VariantsFn
custom derive macro が使用されています。 variants
関数も提供されていますね。
#[derive(derive1::VariantsFn)]
enum E1 {
A,
B(i32),
C { b: bool },
}
#[derive(derive1::VariantsFn)]
enum E2 {}
fn main() {
assert_eq!(E1::variants(), &["A", "B", "C"]);
assert_eq!(E2::variants(), &[] as &'static [&'static str]);
}
crates/derive1/Cargo.toml
crates/derive1/Cargo.toml
には例の lib.proc-macro = true
の記述を入れます。また proc-macro の定義に便利な crate として quote
crate と syn
crate も依存関係に追加します。
[package]
name = "derive1"
edition = "2024"
publish = false
[dependencies]
quote = "1.0.40"
syn = "2.0.106"
[lib]
proc-macro = true
crates/derive1/src/lib.rs
crates/derive1/src/lib.rs
はこんな感じです。この記事の中心的な部分です。
use proc_macro::TokenStream;
use syn::spanned::Spanned;
#[proc_macro_derive(VariantsFn)]
pub fn derive_variants_fn(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as syn::DeriveInput);
let data_enum = if let syn::Data::Enum(data_enum) = &input.data {
data_enum
} else {
return TokenStream::from(
syn::Error::new(input.span(), "VariantsFn can only be derived for enums")
.to_compile_error(),
);
};
let enum_ident = input.ident;
let enum_variant_names = data_enum
.variants
.iter()
.map(|variant| variant.ident.to_string());
let output = quote::quote! {
impl #enum_ident {
pub fn variants() -> &'static [&'static str] {
&[#(#enum_variant_names,)*]
}
}
};
TokenStream::from(output)
}
#[proc_macro_derive(VariantsFn)]
。 proc_macro_derive
属性です。これで VariantsFn
という custom derive macro の名前を決めています。
proc_macro_derive
のついている関数は説明どおりのシグネチャになっています。 pub fn derive_variants_fn(input: TokenStream) -> TokenStream
。 ファイルの先頭で use proc_macro::TokenStream
しています。
syn
crate を使って入力を解釈しています。 let input = syn::parse_macro_input!(input as syn::DeriveInput);
。単純な TokenStream
で扱うよりも parse された syntax tree で扱えるほうが便利です。 derive macro 向けの DeriveInput
という型も定義されています。
let data_enum = if let syn::Data::Enum(data_enum) = &input.data {
data_enum
} else {
return TokenStream::from(
syn::Error::new(input.span(), "VariantsFn can only be derived for enums")
.to_compile_error(),
);
};
今回は enum
以外には生成できないので、エラーを返しています。エラー時は panic してもコンパイルエラーになり、問題ないのですが、 syn
の提供する Error
を使うとエラー箇所などがわかりやすい出力になります。
let enum_ident = input.ident;
let enum_variant_names = data_enum
.variants
.iter()
.map(|variant| variant.ident.to_string());
let output = quote::quote! {
impl #enum_ident {
pub fn variants() -> &'static [&'static str] {
&[#(#enum_variant_names,)*]
}
}
};
TokenStream::from(output)
quote
crate の quote
macro を使用しています。 quote
macro はほとんど Rust コードなのですが #var
の形で変数を追加した形の proc_macro2::TokenStream
を返せます。 #(#enum_variant_names,)*
あたりは quote
macro における繰り返し表現や区切り文字などの機能です。
proc_macro2
crate は proc_macro
を機能拡張したような crate なのですが、今回はほとんど触れません。 proc_macro::TokenStream
に変換して返しておしまいです。
おそらく、思ったよりかんたんですよね。
cargo expand
してみる
前回の記事の知識を活かして cargo expand
してみましょう。
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
enum E1 {
A,
B(i32),
C { b: bool },
}
impl E1 {
pub fn variants() -> &'static [&'static str] {
&["A", "B", "C"]
}
}
enum E2 {}
impl E2 {
pub fn variants() -> &'static [&'static str] {
&[]
}
}
fn main() {
match (&E1::variants(), &&["A", "B", "C"]) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(
kind,
&*left_val,
&*right_val,
::core::option::Option::None,
);
}
}
};
match (&E2::variants(), &(&[] as &'static [&'static str])) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(
kind,
&*left_val,
&*right_val,
::core::option::Option::None,
);
}
}
};
}
assert_eq!
の展開結果でごちゃついていますが、 derive macro の部分は単純なものです。
enum E1 {
A,
B(i32),
C { b: bool },
}
impl E1 {
pub fn variants() -> &'static [&'static str] {
&["A", "B", "C"]
}
}
enum E2 {}
impl E2 {
pub fn variants() -> &'static [&'static str] {
&[]
}
}
意図した形で展開されていそうです。
余談: セキュリティ上のリスク
余談ですが、 procedural macro はコンパイルタイミングで動き、コンパイラと同様のリソースにアクセスできます。これは一定のセキュリティ上のリスクがあるということです。自身で定義したものはともかく、外部のものは十分に注意して導入する必要がありそうです。
おわりに
derive macro を試しに書いてみました。やってみると crate なども整っており、業務でも活かしていけそうです。
次回は、今回触れられなかった derive macro helper attributes や trybuild
crate などについて書きたいと思います。
参考
- Procedural Macros - The Rust Reference https://doc.rust-lang.org/reference/procedural-macros.html
- Derive - The Rust Reference https://doc.rust-lang.org/reference/attributes/derive.html
- proc_macro - Rust https://doc.rust-lang.org/proc_macro/index.html
- quote - crates.io https://crates.io/crates/quote
- syn - crates.io https://crates.io/crates/syn
Discussion