Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

Procedural Macros に入門していたずらしてみた

この記事は Rust Advent Calendar 2018 25日目の記事です.

Rust 2018 edition より,Procedural Macros (以下もずっと英語表記します) という機能が新たに追加されました.

Procedural Macros においては, TokenStream というストリームが概念の中心にあります.TokenStream と呼ばれる抽象的なトークンのストリームを引数として受け取り,何かしらの加工をかけて TokenStream を返すという処理を行います.AST を受け取って,別の AST に加工して返すと説明したほうがわかりやすいもしれません.中にコンパイラがいるというイメージでしょうか.

本日の記事ですが,まず動かす段階でちょっと躓いたので簡単に動かし方を書いておきます.その途中でいろいろ遊んでみたので,その結果も書いておきます.内部実装がどうなっているかの話もちょっとだけ書きました.本当に入門してみただけの記事です.

なお,時間の関係で全容がよくわからなかったので,疑問に思ったことを記しておきます.あとで疑問を解消して記事にすると思います.知見をもっている方がいらっしゃいましたら,ぜひ教えてください🙇

とりあえず使ってみる

ドキュメントにある例を試してみました.が,結構ハマりました.GitHub 等で examples を探した際に,Rust 2015 のものしかまだ出てきませんでした.ただ,次のような手順でサンプルは実行可能でした:

  1. Cargo.toml に proc-macro 用の設定を追加する
  2. lib.rs にマクロを記述する
  3. main.rs で呼び出しをしてみる

実行環境は,Rust 1.31.1 の stable 版です.

順番に説明していきます.

1. Cargo.toml に proc-macro 用の設定を追加する

こんな感じで用意してみます.シンプルに [lib] を追加するだけです.

[package]
name = "hello_procedural_macros"
version = "0.1.0"
authors = ["XXX"]
edition = "2018"

[lib]
proc-macro = true

[dependencies]

2. lib.rs にマクロを記述する

extern crate proc_macroproc_macro 用のクレートを追加しておきます.現時点 (2018-12-25) では,CLion の Rust プラグインではこのクレートを認識してくれず,見つかりませんというエラーメッセージが表示されました.ただ rustc によるコンパイルは通ります.静的解析がうまくいってないだけみたいですね.

|> lib.rs

// クレートを読み込む
extern crate proc_macro;

// マクロを作成する用に TokenStream をインポートしておく
use proc_macro::TokenStream;

#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
    "fn answer() -> u32 { 42 }".parse().unwrap()
}

TokenStreamFromStr を実装済みです"fn answer() -> u32 {42}" は &str 型で,parse() を利用して &strTokenStream への変換が可能 (parse 結果は Result 型なのでさらに unwrap が必要) になっていますね.

TokenStreamFromStr 実装を見てみると,中で __internal::token_stream_wrap() という関数が呼ばれていました.__internalTokenStream の内部用のモジュールとして定義されています.さらにざざっと中を見てみると,Procedural Macros 用の仮のソースファイルを生成して,生成したファイルを Lexer に投げているようでした.つまり,中で字句解析をして TokenStream に詰め直して返しています.その後のフェーズに関しては追いきれませんでした.評価フェーズとかどこに書いてあるんや.

解説記事を見る感じだと,Rust 本来の AST ノードとは別で,TokenStream というノードを独自に持つことによって, Rust 側の AST の内部実装に対する変更を吸収できるように設計した,みたいな感じの意図のようです.

3. main.rs で呼び出しをしてみる

Rust 2018 から,モジュール機能が改善されたため,従来であれば extern crate hello_procedural_macros という記述が main.rs の先頭に必要だったのですが,それがなくなりましたね.

それはさておき,main.rs に先ほどの make_answer マクロを呼び出していきましょう.下記のようになるかと思います.

make_answer というマクロの中にある answer() という関数を呼び出しています.

|> main.rs

use hello_procedural_macros::make_answer;

make_answer!();

fn main() {
    println!("{}", answer());
}

さて,このコードを実行してみましょう.実行してみると,

|> terminal

$ cargo run
   Compiling hello_procedural_macros v0.1.0 (/Users/xxxx/dev/rust/hello-procedural-macros)                                                                                                                                                                                                                                                                    
    Finished dev [unoptimized + debuginfo] target(s) in 1.30s                                                                                                                                                                                                                                                                                                   
     Running `target/debug/main`
42

期待通り42と出てきました.よさそうですね.

ちょっといじわるしてみる

ということで,いろいろいたずらをしてみます😈

u32 の返り値に対して,わざと String 型を返してみる

つまりこういうことです.

|> lib.rs

extern crate proc_macro;

use proc_macro::TokenStream;

#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
    // 文字列型をわざと返すようにしてみた
    "fn answer() -> u32 { String::from(\"42\") }".parse().unwrap()
}

コンパイルしてみましょう.

|> terminal

$ cargo build
   Compiling hello_procedural_macros v0.1.0 (/Users/xxx/dev/rust/hello-procedural-macros)                                                                                                                                                                                                                                                                    
error[E0308]: mismatched types                                                                                                                                                                                                                                                                                                                                  
 --> src/bin/main.rs:3:1                                                                                                                                                                                                                                                                                                                                        
  |                                                                                                                                                                                                                                                                                                                                                             
3 | make_answer!();                                                                                                                                                                                                                                                                                                                                             
  | ^^^^^^^^^^^^^^^                                                                                                                                                                                                                                                                                                                                             
  | |                                                                                                                                                                                                                                                                                                                                                           
  | expected u32, found struct `std::string::String`                                                                                                                                                                                                                                                                                                            
  | expected `u32` because of return type                                                                                                                                                                                                                                                                                                                       
  |                                                                                                                                                                                                                                                                                                                                                             
  = note: expected type `u32`                                                                                                                                                                                                                                                                                                                                   
             found type `std::string::String`                                                                                                                                                                                                                                                                                                                   
                                                                                                                                                                                                                                                                                                                                                                
error: aborting due to previous error                                                                                                                                                                                                                                                                                                                           
                                                                                                                                                                                                                                                                                                                                                                
For more information about this error, try `rustc --explain E0308`.                                                                                                                                                                                                                                                                                             
error: Could not compile `hello_procedural_macros`.                                                                                                                                                                                                                                                                                                             

To learn more, run the command again with --verbose.

なるほど,型が違うというのは教えてくれました.しかし,マクロの中のどこの関数のどの箇所で違う,というところまではさすがに教えてはくれない感じですね.エラー解析が大変なんですかね.

関数名を変えてみる

関数名を answer から別のものに変えてみましょう.関数が見つからないと言われることを期待しました.

|> lib.rs

extern crate proc_macro;

use proc_macro::TokenStream;

#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
    // 関数名を変えてみた
    "fn result() -> u32 { 42 }".parse().unwrap()
}

すると,

|> terminal

$ cargo build
   Compiling hello_procedural_macros v0.1.0 (/Users/xxx/dev/rust/hello-procedural-macros)                                                                                                                                                                                                                                                                    
error[E0425]: cannot find function `answer` in this scope                                                                                                                                                                                                                                                                                                       
 --> src/bin/main.rs:6:20                                                                                                                                                                                                                                                                                                                                       
  |                                                                                                                                                                                                                                                                                                                                                             
6 |     println!("{}", answer());                                                                                                                                                                                                                                                                                                                               
  |                    ^^^^^^ not found in this scope                                                                                                                                                                                                                                                                                                           
                                                                                                                                                                                                                                                                                                                                                                
error: aborting due to previous error                                                                                                                                                                                                                                                                                                                           
                                                                                                                                                                                                                                                                                                                                                                
For more information about this error, try `rustc --explain E0425`.                                                                                                                                                                                                                                                                                             
error: Could not compile `hello_procedural_macros`.                                                                                                                                                                                                                                                                                                             

To learn more, run the command again with --verbose.

main 関数の中で呼び出している answer() が見つかりませんでした.期待通りの結果でしたね.マクロ内の関数名の一致不一致もコンパイル時に静的に解決できているという点でいいですね.

全然違う言語の文法を入れてみる

Scala っぽい文法を持った関数をわざと記述してみましょう.

|> lib.rs

extern crate proc_macro;

use proc_macro::TokenStream;

#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
    // Scala 風の関数を記述してみる
    "def answer(): Int = 42".parse().unwrap()
}

多分 parse に失敗するはず.コンパイルしてみましょう.

|> terminal

$ cargo build
   Compiling hello_procedural_macros v0.1.0 (/Users/xxx/dev/rust/hello-procedural-macros)                                                                                                                                                                                                                                                                    
error: expected one of `!` or `::`, found `answer`                                                                                                                                                                                                                                                                                                              
 --> src/bin/main.rs:3:1                                                                                                                                                                                                                                                                                                                                        
  |                                                                                                                                                                                                                                                                                                                                                             
3 | make_answer!();                                                                                                                                                                                                                                                                                                                                             
  | ^^^^^^^^^^^^^^^                                                                                                                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                                                                                                                                                
error: aborting due to previous error                                                                                                                                                                                                                                                                                                                           
                                                                                                                                                                                                                                                                                                                                                                
error: Could not compile `hello_procedural_macros`.                                                                                                                                                                                                                                                                                                             

To learn more, run the command again with --verbose.

期待通りでした!

TokenStream の中身とは?

気になりました.調べました.

TokenStreamTokenTreeイテレータとなっています.TokenTree は enum になっていて,4つのノードを持っています

  • Group: TokenStream をカッコで囲ってグループとみなせる何かっぽい.
  • Ident: 変数名を表現している.
  • Punct: +. などで始まるオペレータと,複合記号のオペレータ (>> とか => とか) がここに含まれる.
  • Literal: char, string, あるいは数字を表現している.

ということは,中でイテレータを回しながら字句解析結果を見て,中身を定義して返せるという感じなんですかね.

まとめ

  • とりあえず挙動を確認した感じ,中で Rust の文法らしいものの字句解析と,中でちょっとした関数の紐づけ付等を行う感じの処理が走っているみたいです.が,全容がよくわからなかったです.
  • TokenStreamの 字句解析結果をさらに自分で定義できる感じですかね?たとえば,Rust の for 文を回す際に,イテレータの変数名を渡すと自動でそのイテレータに対する任意の処理を行って返すみたいな実装が考えられそう.
  • まだまだ機能がありそうですし,実際 Rust の関数以外の文法をもった何かを突っ込むこともできそうな雰囲気がありますが,使い方がまだイマイチわかっておらずよくわからないですね.

よくわからないばかりですみません.良いお年を!