🦀

Rust の macros はどういう時に使われているのか?

2024/02/18に公開

はじめに

Rust の特徴的な機能の1つに Macros (マクロ) があります。簡単にいうと "Rust コードを生成するコードを記述する" 機能であり、Rust におけるメタプログラミングを可能にします。今回はこのマクロを題材に、このマクロ自体がどういったケースで使われているのか、あるいは、もし自身で使うとしたらどういった時に使えばいいのか、ということを少し考えて紹介してみようと思います。

そもそも Rust のマクロとは

本題に入る前にマクロの機能について簡単に紹介しておきます。そもそもどういった時に "マクロ" というものを持ち出すのかというと、代表的な目的の一つとしてコード量を減らすためがあります。Rust の標準ライブラリにも vec! というマクロが含まれていますが、いくつかの値を含んだ Vector を宣言するためにマクロを使用しないと

let mut vec = Vec::new();
vec.push(1);
vec.push(2);
vec.push(3);
vec

のように記述しますが、このマクロを使用すると

vec![1,2,3]

と記述することができます。(マクロを使用しなくとも実際 From トレイトを使用すれば1行でかけるケースもありますがイメージしやすい例としてあげてみます。)
他にも unimplemented! というマクロもあり、これは実際裏では panic! を呼んでいてエラーメッセージを共通化するためにマクロが使用されていたりします。

macro_rules! unimplemented {
    () => {
        $crate::panicking::panic("not implemented")
    };
    ($($arg:tt)+) => {
        $crate::panic!("not implemented: {}", $crate::format_args!($($arg)+))
    };
}

Source: mod.rs - source

他にも動的に構造体や型、変数を生成したりすることもできます。

generate_struct!("Actor", {
  name: String,
  age: u8
});
// ↓
#[derive(Clone, Copy, Debug, Partial)]
struct Actor {
  name: String,
  age: u8
}

これはアプリケーションを実装する際に利用するイメージが湧かないと思いますが (実際コード量もほとんど減っていませんし、直接構造体を記述した方がわかりやすいです)、ライブラリ実装者などはこういった機能を作るケースがあります。

Macros の実装方法

先述のような用途でマクロを利用するのですが、この Rust のマクロを実装するには大きく2つの方法があります。

Declarative Macros (宣言的マクロ)

macro_rules! で記述されるマクロのことです。先ほど紹介した vec!unimplemented! などはこの方法で実装されています。

macro_rules! something { ... }

Procedural Macros (手続き型マクロ)

宣言的マクロに比べてより関数のように宣言して扱うことができるマクロです。この手続き型マクロを実装するには特別なクレートである proc_macro を利用します。そのクレートに含まれる構造体である TokenStream はトークンツリーを表現しており、マクロを実現する関数の入力と出力に使用されます。このクレートとこの構造体によって、あるトークン(群,ツリー)を入力とし、異なるトークン(群,ツリー)に変換するということを手続き型マクロで可能にしています。

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {}

さらにこの手続き型マクロには3種類の実装パターンがあります。上記の例は Derive macros の例です。

Function-like macros - custom!(...)
Derive macros - #[derive(CustomDerive)]
Attribute macros - #[CustomAttribute]

Source: Procedural Macros - The Rust Reference

Function-like Macros (関数風マクロ)

Prodecural macros の中で最も理解しやすく、先ほど紹介した宣言型マクロに最も近いのはこのマクロだと思います。proc_macroTokenStream を関数の入力と出力に使用するので、マクロのためのコードは Rust における通常のコーディングとほとんど変わりません。

#[proc_macro]
pub fn generate_hello_func(input: TokenStream) -> TokenStream {
    let name = parse_macro_input!(input as LitStr).value();
    let func_name = format!("hello_{}", name.to_lowercase());
    let func_body = format!("\"Hello {}!\".to_string()", name);

    quote! {
        fn #func_name() -> String {
            #func_body
        }
    }.into()
}

// ↓

generate_hello_func!("world", "World");
// fn hello_world() -> String { "Hello World!".to_string() }
generate_hello_func!("everyone", "everyone");
// fn hello_everyone() -> String { "Hello everyone!".to_string() }

Derive Macros

Struct や Enum などに付与する derive 属性を生成します。#[derive(YourAttribute)] というように宣言して使用するものです。この例は標準ライブラリの Default マクロです。

#[derive(Default)]
pub struct SomeOptions {
    pub foo: i32,
    pub bar: f32,
}

let val = SomeOptions::default();
assert_eq!(val.foo, 0);
assert_eq!(val.bar, 0.0);

Attribute Macros (属性風マクロ)

Derive Macros と同様に、この属性を付与したものに対して追加の実装をすることができます。これは関数にも適用することができ、付与したものやこの属性をアタッチした際に指定した引数をマクロで解釈することができます。

#[proc_macro_attribute]
pub fn return_as_is(_attr: TokenStream, item: TokenStream) -> TokenStream { ... }

参考

英文ではありますが、まずは公式のドキュメントを読むのが最も理解が早いと思います。今回記述した内容と照らし合わせてみてみてください。日本語の記事でもたくさん参考になるものがありますが、いくつか抽出してここに載せておきます。

どういった時に、どこで使われている?

前セクションで紹介したマクロ実装のパターンに応じて、実際に現場でどのように使われているか簡単に紹介していきたいと思います。

Declarative Macros

Declarative Macros はパターンの共通化などによりコードの繰り返しを削減することを目的として、比較的単純なコードを生成のために利用されていることが多いです。例えば、型が異なるがトレイトの実装が同じ場合やテストコードの入力/出力のみ異なる場合などです。

ex1: serde crate - Serialize の実装

https://github.com/serde-rs/serde/blob/1d54973b928bd8708a4ad2d90fca1203367ff580/serde/src/ser/impls.rs#L7

共通機能の実装について serde クレートを取り上げてみます。プリミティブな型に対しシリアライズを実装するための Serialize トレイトに対する実装を組み込むために、このマクロ実装を利用しています。

macro_rules! primitive_impl {
    ($ty:ident, $method:ident $($cast:tt)*) => {
        impl Serialize for $ty {
            #[inline]
            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
            where
                S: Serializer,
            {
                serializer.$method(*self $($cast)*)
            }
        }
    }
}

primitive_impl!(bool, serialize_bool);
primitive_impl!(isize, serialize_i64 as i64);
primitive_impl!(i8, serialize_i8);
primitive_impl!(i16, serialize_i16);
primitive_impl!(i32, serialize_i32);
primitive_impl!(i64, serialize_i64);
primitive_impl!(i128, serialize_i128);
primitive_impl!(usize, serialize_u64 as u64);
primitive_impl!(u8, serialize_u8);
primitive_impl!(u16, serialize_u16);
primitive_impl!(u32, serialize_u32);
primitive_impl!(u64, serialize_u64);
primitive_impl!(u128, serialize_u128);
primitive_impl!(f32, serialize_f32);
primitive_impl!(f64, serialize_f64);
primitive_impl!(char, serialize_char);

これは比較的読みやすいと思います。macro_rules! で宣言された primitive_impl では "型情報" (ty) と "呼び出すべき関数" (method) を引数にとり、その型に Serialize を実装して、その内部ロジックで引数で指定した関数を利用します。

ex2: num-bigint crate - Unsigned

https://github.com/rust-num/num-bigint/blob/078972dc858ea06d9638c9a4f4044b8afb0d7287/src/bigint.rs#L307-L333

Rust で数値についてビルトインでサポートされているのは 128 bit の int, unsigned int (u128,i128) までなのですが、それ以上に大きな値を扱うためのクレートに num-bigint があります。こちらでは int から unsigned int の変換を行うための impl のために Declarative Macros を利用しています。

macro_rules! impl_unsigned_abs {
    ($Signed:ty, $Unsigned:ty) => {
        impl UnsignedAbs for $Signed {
            type Unsigned = $Unsigned;

            #[inline]
            fn uabs(self) -> $Unsigned {
                self.wrapping_abs() as $Unsigned
            }

            #[inline]
            fn checked_uabs(self) -> CheckedUnsignedAbs<Self::Unsigned> {
                if self >= 0 {
                    Positive(self as $Unsigned)
                } else {
                    Negative(self.wrapping_neg() as $Unsigned)
                }
            }
        }
    };
}
impl_unsigned_abs!(i8, u8);
impl_unsigned_abs!(i16, u16);
impl_unsigned_abs!(i32, u32);
impl_unsigned_abs!(i64, u64);
impl_unsigned_abs!(i128, u128);
impl_unsigned_abs!(isize, usize);

他の利用例: slog crate - Implementation of common trait
https://github.com/slog-rs/slog/blob/586519aadc72d2480fde01d1ac8018d9d7843ce7/src/lib.rs#L3022-L3054


次にテストで利用している箇所について触れてみます。

ex3: num-bigint crate - assertion の共通化

https://github.com/rust-num/num-bigint/blob/dc9a828d78a7f0dc2ba83a31590893981962411c/tests/bigint.rs#L586

先ほど紹介した num-bigint にあるコードです。プリミティブな数値型と独自の構造体 BigInt の変換をテストするコードですが、この複数の assertion を共通化するために利用されています。実際にこういう共通化は関数化でもできることが多いですが、マクロにおいては動的にコード生成をするので、それぞれのコードがコンパイル可能であれば問題ないです。関数化した場合には、この check の引数は使用したいすべての型を網羅できるように関数宣言しなくてはいけないので、その困難さを回避できることにマクロ化のメリットがあります。

#[test]
fn test_convert_from_uint() {
    macro_rules! check {
        ($ty:ident, $max:expr) => {
            assert_eq!(BigInt::from($ty::zero()), BigInt::zero());
            assert_eq!(BigInt::from($ty::one()), BigInt::one());
            assert_eq!(BigInt::from($ty::MAX - $ty::one()), $max - BigInt::one());
            assert_eq!(BigInt::from($ty::MAX), $max);
        };
    }

    check!(u8, BigInt::from_slice(Plus, &[u8::MAX as u32]));
    check!(u16, BigInt::from_slice(Plus, &[u16::MAX as u32]));
    check!(u32, BigInt::from_slice(Plus, &[u32::MAX]));
    check!(u64, BigInt::from_slice(Plus, &[u32::MAX, u32::MAX]));
    check!(
        u128,
        BigInt::from_slice(Plus, &[u32::MAX, u32::MAX, u32::MAX, u32::MAX])
    );
    check!(usize, BigInt::from(usize::MAX as u64));
}

他の利用例: uint (in parity-common) crate - Tests to convert from primitive types to proprietary structures
https://github.com/paritytech/parity-common/blob/master/uint/tests/uint_tests.rs#L224

Procedural Macros: Function-like

Function-like Macros は用途としては Declarative Macros に近いのですが、多くはこのマクロ自体をライブラリクレートの公開関数として扱う場合に使用されます。動的な (型や関数などの) コード生成に使用されるのですが、このマクロのコード自体が Rust のコードで書くことができるので、静的解析やコード最適化とともに複雑なロジックも表現可能であることが大きなメリットです。

ex1: json_typegen crate - Generate Struct by JSON

https://github.com/evestera/json_typegen/blob/1cf1883b6cac4dccec2b164d1ce22ac2f454be8b/json_typegen/src/lib.rs#L68-L79

json_typegen という入力した json フォーマットのデータから構造体を生成する機能を持つクレートのマクロを紹介します。引数に構造体名とそのフィールドの情報源となる String 型の JSON データをとることで構造体を自動生成するマクロが Function-like macros で提供されています。

#[proc_macro]
pub fn json_typegen(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    match codegen_from_macro_input(&input.to_string()) {
        Ok(code) => code,
        Err(e) => {
            let message = display_error_with_causes(&e);
            format!(r##"compile_error!(r#"{}"#);"##, message)
        }
    }
    .parse()
    .unwrap()
}

// ↓

json_typegen!("Point", r#"{ "x": 1, "y": 2 }"#);

このように Function-like macros を利用することで、マクロコードの世界からすぐに Rust コードの世界に戻して実装をすることができます。このマクロ内で呼ばれている codegen_from_macro_input は String を引数にとる関数なので通常の Rust コードの実装と同様に扱うことができます。

pub fn codegen_from_macro_input(input: &str) -> Result<String, JTError> { ... }

ex2: solidity-bindgen-macros crate - Generate Struct to call Contract by ABI

https://github.com/graphprotocol/solidity-bindgen/blob/master/solidity-bindgen-macros/src/lib.rs

次はブロックチェーンの開発に関連したクレートです。ブロックチェーンの Ethereum おいて、アプリケーションモジュールのようなものをスマートコントラクト (またはコントラクト) と言います (色々端折ってすごく端的に言うと、です)。このコントラクトのインタフェースを表現するために ABI (Application Binary Interface) というものがあります。solidity-bindgen クレートは、この ABI からそのコントラクトを呼び出すための関数を持った構造体を自動生成する機能を提供します。

#[proc_macro]
pub fn contract_abi(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let s = parse_macro_input!(input as LitStr);
    let path = Path::new(&current_dir().unwrap()).join(s.value());
    let metadata = metadata(&path).unwrap();

    let tokens = if metadata.is_file() {
        abi_from_file(path)
    } else {
        panic!("Expected a file. To generate abis for an entire directory, use contract_abis");
    };

    tokens.into()
}

// ↓

contract_abi!("../abi/ERC20.json")

このように Function-like Macros では Procedural Macros を定義するための TokenStream を入力/出力とした関数を宣言し、その関数内で実際に計算/変換を行うためのロジックをほぼすべて別関数に移譲しているような実装パターンが多いです。

NOTE: Procedural Macros でも外部公開はできる

Function-like Macros で外部公開されるマクロを提供していることが多いのですが、先述の Declarative Macros でも外部公開しているケースはあります。例えば Procedural Macros を記述するために必須となる quote クレートでは #[macro_export] という属性を付与して Procedural Macros で記述したマクロを外部公開しています。

https://github.com/dtolnay/quote/blob/master/src/lib.rs

#[cfg(not(doc))]
#[macro_export]
macro_rules! quote {
    () => {
        $crate::__private::TokenStream::new()
    };

    // Special case rule for a single tt, for performance.
    ($tt:tt) => {{
        let mut _s = $crate::__private::TokenStream::new();
        $crate::quote_token!{$tt _s}
        _s
    }};

    // Special case rules for two tts, for performance.
    (# $var:ident) => {{
        let mut _s = $crate::__private::TokenStream::new();
        $crate::ToTokens::to_tokens(&$var, &mut _s);
        _s
    }};
    ($tt1:tt $tt2:tt) => {{
        let mut _s = $crate::__private::TokenStream::new();
        $crate::quote_token!{$tt1 _s}
        $crate::quote_token!{$tt2 _s}
        _s
    }};

    // Rule for any other number of tokens.
    ($($tt:tt)*) => {{
        let mut _s = $crate::__private::TokenStream::new();
        $crate::quote_each_token!{_s $($tt)*}
        _s
    }};
}

// ↓

let token = quote! {
    impl<'a, T: ToTokens> ToTokens for &'a T {
        fn to_tokens(&self, tokens: &mut TokenStream) {
            (**self).to_tokens(tokens)
        }
    }
}
token.to_string()
// impl<'a, T: ToTokens> ToTokens for &'a T {
//     fn to_tokens(&self, tokens: &mut TokenStream) {
//         (**self).to_tokens(tokens)
//     }
// }

他の利用例: serde_json crate - json! macro for defining JSON type from String
https://github.com/serde-rs/json/blob/master/src/macros.rs#L54

Procedural Macros: Derive Macros

Derive Macros は特定のトレイトや機能を型に自動的に実装するために使用されるマクロです。Derive Macros も Function-like Macros と同様に、マクロ自体を宣言するための関数を宣言し、その中で使用するロジックは別関数に移譲するという実装パターンが多いです。

ex1: serde crate - Implements Serialize/Deserialize

https://github.com/serde-rs/serde/blob/1d54973b928bd8708a4ad2d90fca1203367ff580/serde_derive/src/lib.rs

既に紹介済みですが Rust における Serialize/Deserialize で最もよく見かけるクレートである serde です。この Derive Macros で提供される Derive を使用することで、その構造体が Serialize/Deserialize 可能になります。

#[proc_macro_derive(Serialize, attributes(serde))]
pub fn derive_serialize(input: TokenStream) -> TokenStream {
    let mut input = parse_macro_input!(input as DeriveInput);
    ser::expand_derive_serialize(&mut input)
        .unwrap_or_else(syn::Error::into_compile_error)
        .into()
}

#[proc_macro_derive(Deserialize, attributes(serde))]
pub fn derive_deserialize(input: TokenStream) -> TokenStream {
    let mut input = parse_macro_input!(input as DeriveInput);
    de::expand_derive_deserialize(&mut input)
        .unwrap_or_else(syn::Error::into_compile_error)
        .into()
}

// ↓

#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
struct Player {
  name: String,
  age: u8
}

ex2: clap crate - Derives for CLI application

https://github.com/clap-rs/clap/blob/e0304268197af97194327d895ea8c54d09ff69db/clap_derive/src/lib.rs

Rust で CLI アプリケーションを実装する際によく利用するクレートの一つである clap クレートです。このクレートは開発者が Derive ベースで構築するための機能と、メソッドチェーンのように Builder ベースで実装するための機能の2種類が搭載されています。

#[proc_macro_derive(ValueEnum, attributes(clap, value))]
pub fn value_enum(input: TokenStream) -> TokenStream {
    let input: DeriveInput = parse_macro_input!(input);
    derives::derive_value_enum(&input)
        .unwrap_or_else(|err| {
            let dummy = dummies::value_enum(&input.ident);
            to_compile_error(err, dummy)
        })
        .into()
}

#[proc_macro_derive(Parser, attributes(clap, structopt, command, arg, group))]
pub fn parser(input: TokenStream) -> TokenStream { ... }

#[proc_macro_derive(Subcommand, attributes(clap, command, arg, group))]
pub fn subcommand(input: TokenStream) -> TokenStream {
    let input: DeriveInput = parse_macro_input!(input);
    derives::derive_subcommand(&input)
        .unwrap_or_else(|err| {
            let dummy = dummies::subcommand(&input.ident);
            to_compile_error(err, dummy)
        })
        .into()
}

#[proc_macro_derive(Args, attributes(clap, command, arg, group))]
pub fn args(input: TokenStream) -> TokenStream {
    let input: DeriveInput = parse_macro_input!(input);
    derives::derive_args(&input)
        .unwrap_or_else(|err| {
            let dummy = dummies::args(&input.ident);
            to_compile_error(err, dummy)
        })
        .into()
}

他の利用例: darling crate - Derives for Macro Developers
https://github.com/TedDriggs/darling/blob/6d5b86417744197d3e23b563f2073c6e2d183c69/macro/src/lib.rs#L5

終わりに

今回は Rust のマクロについて、マクロ機能自体の概要と合わせて利用ユースケースにフォーカスして紹介してみました。僕自身が最初に踏み込む際に、作り方の紹介はいくつもあったのですが、参考になるものをどう見つけて意図をどう読み取ればいいのか理解するまで時間がかかったのでこういった記事を作ってみました。あくまで僕自身の観測範囲であり、主観によるものなので、是非フィードバックください!知っている方はむしろ色々教えてください m(_ _)m

最後に前述の参考リンクで挙げたサイトから一文を紹介します。
マクロクラブ Rust支部 | κeenのHappy Hacκing Blog の最初に記載されていることが印象的でずっと頭に残っているので個別取り上げたいと思っていました。

マクロ・クラブのルール

  1. マクロを書くな
  2. それがパターンをカプセル化する唯一の方法ならば、マクロを書け
  3. 例外: 同等の関数に比べて、 呼び出し側が楽になるならば、マクロを書いても構わない

今自身の業務でメタプログラミングのためにマクロの利用が必要となり、これから紹介しますがいろいろ読んでみて、書いてみて、本当にこの通りだと感じています。(本当に適切な設計ができていればその通りでないかもしれませんが) 書かないで済むなら書かずに済ませる方が良いと思います。普通のコードに比べ、可読性は低くなりやすいし、テストも書きづらいことが多いです。ですが使うのが適切なケースはありますし、実際多くのライブラリで使用されていて、書くことは少なくとも読むことはあります。この記事がそういった開発者向けになると幸いです。

Discussion