🪥

【Rust】健全なmacro_rules!にはパスに曖昧さが無い

2023/06/11に公開

https://ja.wikipedia.org/wiki/健全なマクロ

Rustの宣言的マクロ(macro_rules!)は、Cのマクロに比べて健全なマクロ(hygienic macros 衛生的なマクロとも訳される)が書きやすいようにできています。macro_rules!では、Rustの文法に違反するようなマクロは記述できないようになっています。しかし、コンパイルできたマクロ=健全なマクロかというと、そのような保証はありません。そこで、「健全なマクロ」にするためにどのような点に気をつければよいか?という記事です。ちなみに、この記事では宣言的マクロに限定して書いていますが、手続き的マクロ(proc_macro)でも同様です。

※この記事のRustのバージョンは1.70.0です。

要約

  • クレート内の識別子のパスは$crate::(Crate Rootを指す)から始める。
  • クレート外の識別子のパスは、マクロを公開しない場合は::(Extern Preludeを指す)から始める。公開する場合は識別子を再エクスポートして$crate::から始める。
  • トレイトメソッドはメソッド形式で書かない。

macro_rules!内の名前解決

以下のように、my_print!マクロを作成しました。このマクロは、同じmymacroモジュール内のprint関数を呼び出すことを期待しています。しかし、下記のようなマクロの使い方では、コンパイルエラーが発生します。なぜでしょうか?

fn main() {
    my_print!("macro!!!");
}

mod mymacro {
    use std::fmt::Display;
    pub fn print<T: Display>(val: T) {
        println!("{val}");
    }
    #[macro_export]
    macro_rules! my_print {
        ($val:expr) => {
            print($val);
        };
    }
}

マクロは、呼び出した場所に展開されますが、この時、マクロ内で参照されている識別子は「そのままのパスで」展開され、展開先のスコープで参照されます。上記の例のmy_print!マクロではprint関数を参照していますが、main関数内では以下のように展開されます。

fn main() {
    print("macro!!!");
}

このprint関数は、先頭に修飾子がないパスで指定されているため、カレントスコープやPreludeを探しに行き、そこに無ければコンパイルエラーとなります。my_print!マクロは、mymacro::print関数が呼ばれることを想定していますが、マクロの使用場所によっては違うprint関数を参照しに行ってしまうのです。

参照するには、mymacroモジュールを指定しなければなりません。

fn main() {
    mymacro::print("macro!!!");
}

では、my_print!マクロは以下のように定義すれば問題ないでしょうか。

#[macro_export]
macro_rules! my_print {
    ($val:expr) => {
        mymacro::print($val);
    };
}

確かに、main関数からmy_print!マクロを呼び出した場合は、これで問題ありません。しかし、このマクロはどこから呼び出されるかわかりません。以下のように、他のモジュールからの呼び出した場合にコンパイルエラーとなってしまいます。

fn main() {
    my_print!("macro!!!");
    mymod::myfunc();
}

mod mymod {
    use crate::my_print;
    pub fn myfunc() {
        my_print!("myfunc!!!");
        // 展開後
        // mymacro::print("myfunc!!!"); ←mymacroは直接見えない!
    }
}

mod mymacro {
    use std::fmt::Display;
    pub fn print<T: Display>(val: T) {
        println!("{val}");
    }
    #[macro_export]
    macro_rules! my_print {
        ($val:expr) => {
            mymacro::print($val);
        };
    }
}

このエラーの原因は、マクロ内で使用している識別子の参照先に曖昧さがあるため、マクロの使用場所によって参照先が変わってしまうことにあります。このようなエラーを防ぐため、マクロ内の識別子は、どこでマクロを使用しても同じ参照先となるようなパスで指定する必要があります。厄介なことに、このマクロのコンパイルエラーになるのは、実際にマクロが使用されて展開された時にエラーになる場合のみなので、マクロ作成段階ではエラーにはならないのです。なので、問題があることに気が付きにくいのです。

絶対パス(正規パス)

どこでマクロを使用しても同じ参照先となるような形で指定するためには、パスがそのアイテムのルートから始まるような絶対パス(正規パス)である必要があります。

https://doc.rust-lang.org/reference/paths.html

絶対パスになるパス修飾子には以下のようなものがあります。

::

::で始まるパスの意味は、Rustのバージョンによって異なります。

  • 2018 Edition以降のバージョンでは、Extern Preludeから識別子を探します。
  • 2015 Editionでは、クレートルート(Crate Root)から識別子を探します(crate::と同様)。

crate::

crate::から始まるパスは、常にクレートルートから識別子を探します。クレートルートは、このクレートの全ての識別子のルートであり、このクレートで定義された識別子は全てここから辿ることが出来ます。

$crate::

$crateは、macro_rules!内で使用できるメタ変数で、常にマクロが定義されているクレートルートを指します。

クレート外に公開しないマクロ

クレート内の識別子

さて、先程の問題のあるマクロ

mod mymacro {
    use std::fmt::Display;
    pub fn print<T: Display>(val: T) {
        println!("{val}");
    }
    #[macro_export]
    macro_rules! my_print {
        ($val:expr) => {
            mymacro::print($val);
        };
    }
}

を、修正しましょう。

クレート内の識別子なので、先述の絶対パスのうち、crate::または$crate:: を使用できます。どちらを使えばいいでしょうか。クレート外に公開しない場合は、どちらでも問題ないです。但し、クレート外にマクロを公開する場合にcrate::だとマクロを使用したクレートを指すことになってしまいます。$crate::の場合は常にマクロが定義されているクレートを指すので、常に$crate::を使うようにすると良いでしょう。

つまり、マクロの修正結果は以下のようになります。

mod mymacro {
    use std::fmt::Display;
    pub fn print<T: Display>(val: T) {
        println!("{val}");
    }
    #[macro_export]
    macro_rules! my_print {
        ($val:expr) => {
            $crate::mymacro::print($val);
        };
    }
}

クレート外の識別子

クレート外の識別子は、Extern preludeから辿れるので、先述の絶対パスのうち、::を使用するのが適切です。外部クレートの代表stdを例にすると、以下のようなパスで指定すると健全です。

fn main() {
    def_struct!(MyStruct);
    println!("{}", MyStruct);
}
mod mymacro {
    #[macro_export]
    macro_rules! def_struct {
        ($name:ident) => {
            struct $name;
            impl ::std::fmt::Display for $name {
                fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
                    ::std::write!(f, ::std::stringify!($name))
                }
            }
        };
    }
}

(補足) 先頭に修飾子のないパスの解決順

ちなみに、このパスを::std::から始めず、std::から始めると、どういう場合にまずいのでしょうか。例えば、以下のコードはコンパイルエラーとなります。

fn main() {
    def_struct!(MyStruct);
    println!("{}", MyStruct);
}

mod std {} // std::はこのモジュールを指してしまう

mod mymacro {
    #[macro_export]
    macro_rules! def_struct {
        ($name:ident) => {
            struct $name;
            impl std::fmt::Display for $name {
                fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                    std::write!(f, std::stringify!($name))
                }
            }
        };
    }
}

先頭に修飾子の無いパスの場合、概ね以下のような優先順で解決を試みます。

  • カレントスコープから、該当する識別子を探す。(ローカル定義があればそちらを優先)
  • 無ければ、Extern preludeから探す。
  • 無ければ、他のPreludeから探す。

上記のコードの場合は、クレートルートにstdモジュールが存在するため、Extern Preludeにある標準ライブラリ::stdよりも、crate::stdが優先されてしまいます。

もう少しはっきりした例で示すと、以下のi32::BITS

// i32クレートのアイテム
pub const BITS: u32 = 3; // 優先順3
mod inner {
    mod i32 {
        pub const BITS: i32 = 2; // 優先順2
    }
    pub fn f() {
        struct i32;
        impl i32 {
            pub const BITS: u32 = 1; // 優先順1
        }
        println!("{}", i32::BITS); // このi32::BITSの優先順は?
    }
}
  1. ローカル定義のi32::BITS
  2. カレントモジュールcrate::innterに定義されている、crate::inner::i32::BITS
  3. Extern Preludeの::i32::BITS
  4. プリミティブ型(Language Prelude)の::core::primitive::i32::BITS

の優先順で名前解決を試みます。

(補足) Prelude

今更ですが、Preludeとは何なのでしょうか?その辺に関しては、以下のReferenceのページに載っています。

https://doc.rust-lang.org/reference/names/preludes.html

A prelude is a collection of names that are automatically brought into scope of every module in a crate.

「プレリュードは、クレート内のすべてのモジュールのスコープに自動的に組み込まれる名前のコレクションです。」とあります。これはつまり、先述の「先頭に修飾子のないパス」の解決に使われていることを言っています。カレントスコープには無いけど、パスの先頭にできる名前は、全てPreludeのコレクション内に存在しています。

先述のように、この仕組みをマクロ内部で使うと、パスに「曖昧さ」ができてしまうため、マクロ内では$crate(Crate Root)か::(Extern Prelude)からパスを始めましょう。

トレイトメソッド

トレイトメソッドは、そのトレイトがカレントスコープに存在しないと(インポートされていないと)、メソッド形式で使えません。つまり、マクロ内でトレイトメソッドをメソッド形式で使うと、コンパイルエラーが発生する可能性があります。例えば、以下のコードは、call_my_trait_method!を使用したスコープにMyTraitトレイトが存在しないため、コンパイルエラーとなります。

fn main() {
    struct MyStruct;
    impl mymacro::MyTrait for MyStruct {}
    let my_struct = MyStruct;
    call_my_trait_method!(&my_struct);
}

mod mymacro {
    pub trait MyTrait {
        fn my_trait_method(&self) {
            println!("MyTrait::my_trait_method()");
        }
    }
    #[macro_export]
    macro_rules! call_my_trait_method {
        ($val:expr) => {
            $val.my_trait_method();
        };
    }
}

ちなみに、マクロの中でもuseキーワードは使えるので、以下のようにすれば一見問題ないように見えます。

fn main() {
    struct MyStruct;
    impl mymacro::MyTrait for MyStruct {}
    let my_struct = MyStruct;
    call_my_trait_method!(&my_struct);
}

mod mymacro {
    pub trait MyTrait {
        fn my_trait_method(&self) {
            println!("MyTrait::my_trait_method()");
        }
    }
    #[macro_export]
    macro_rules! call_my_trait_method {
        ($val:expr) => {
            use $crate::mymacro::MyTrait; // スコープにトレイトを持ち込む
            $val.my_trait_method();
        };
    }
}

しかし、このMyStruct構造体にmy_trait_methodメソッドが実装されていたらどうなるでしょうか。

fn main() {
    struct MyStruct;
    impl mymacro::MyTrait for MyStruct {}
    impl MyStruct {
        // 同じ名前のメソッドがあるとこっちが優先される
        fn my_trait_method(&self) {
            println!("ERROR!!");
        }
    }
    let my_struct = MyStruct;
    call_my_trait_method!(&my_struct);
}

mod mymacro {
    pub trait MyTrait {
        fn my_trait_method(&self) {
            println!("MyTrait::my_trait_method()");
        }
    }
    #[macro_export]
    macro_rules! call_my_trait_method {
        ($val:expr) => {
            use $crate::mymacro::MyTrait; // スコープにトレイトを持ち込む
            $val.my_trait_method();
        };
    }
}

この時、どちらのメソッドが呼ばれるかというと、構造体に実装されているMyStruct::my_trait_methodが呼ばれます。このように、useを使っても健全とは言えません。

以下のような完全な形で記述すれば、使用場所に依存しないマクロにできます。

#[macro_export]
macro_rules! call_my_trait_method {
    ($val:expr) => {
        $crate::mymacro::MyTrait::my_trait_method($val);
    };
}

クレート外に公開するマクロ

さて、ここまではマクロを定義したクレート内のみで使用する場合についての話でした。では、マクロをクレート外に公開する場合は、どうなるでしょうか。

クレート内の識別子

先述の通り、クレート内の識別子は常に$crate::で始まるパスで指定しましょう。

クレート外の識別子

クレート外の識別子も先述の通り、::で始めるとどうでしょう。例えば、以下のマクロをmymacroクレートに定義して、クレート外から使用した場合を考えます。

mod mymacro {
    #[macro_export]
    macro_rules! def_struct {
        ($name:ident) => {
            struct $name;
            impl ::std::fmt::Display for $name {
                fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
                    ::std::write!(f, ::std::stringify!($name))
                }
            }
        };
    }
}
fn main() {
    mymacro::def_struct!(MyStruct);
    println!("{}", MyStruct);
}

一見問題なく動くのですが、では、こうしてみるとどうでしょう。

extern crate std as _std;
extern crate mymacro as std;

fn main() {
    std::def_struct!(MyStruct);
    _std::println!("{}", MyStruct);
}

こうすると、クレート名が上書きされてしまい、::stdmymacroクレートを指すことになります。こうなると、マクロ内で使用している::stdで始まるパスは機能しなくなってしまいます。

このように、クレートの名前は変更・上書きされる可能性があるため、::で始まるパスも信用できないのです。唯一信用できるパスは$crateで始まるパスのみということになります。つまり、マクロ内で使用する識別子はすべて再エクスポートし、$crate::から指定しなければなりません。

つまり、このマクロは以下のように修正します。

pub mod __export {
    pub use ::std::fmt::{Display, Formatter, Result};
    pub use ::std::{write, stringify};
}

pub mod mymacro {
    #[macro_export]
    macro_rules! def_struct {
        ($name:ident) => {
            struct $name;
            impl $crate::__export::Display for $name {
                fn fmt(&self, f: &mut $crate::__export::Formatter) -> $crate::__export::Result {
                    $crate::__export::write!(f, $crate::__export::stringify!($name))
                }
            }
        };
    }
}

クレート外に公開するマクロ内では、クレート外の識別子は、標準ライブラリ含め一切使用しない(再エクスポートして$crateから使用する)ようにしましょう。特に、crates.ioに公開するようなマクロは、どのような使い方をされるかわからないので、面倒でもこのような書き方をすべきかと思います。

(補足) extern crate

ちなみに、ここで出てきたextern crateですが、Rust 2018 Edition以降ほぼ使用されていないと思われるため、「なんぞこれ?」な人もいるかと思います。改めてどのようなものか補足します。

https://doc.rust-lang.org/reference/items/extern-crates.html

extern crateは、外部クレートをインポートして、プロジェクト内部で使用できるようにする宣言文です。Rust 2015では、Extern Preludeを参照するためには、extern crateでインポートする必要がありましたが、Rust 2018以降では、初めからインポートされていて参照可能なため、基本的には使用することはありません。

具体的な使い方と効果は、useとほぼ同じです(extern crate my_crate as foo;use ::my_crate as foo;)。違う効果としては、extern crateをクレートルートで使用した場合は、Extern Preludeにクレート名が追加(すでに同名がある場合は上書き)され、::fooのようなパスで参照可能になります。つまり、先程の例のように

extern crate std as _std;
extern crate mymacro as std;

をクレートルートで宣言すると、

  • stdクレートが_stdという名前でExtern Preludeに追加され(stdの名前はそのまま使える。)
  • mymacroクレートがstdという名前でExtern Preludeに追加される(元のstdという名前が上書きされる。)

ということが起き、::stdmymacroクレートを指すように変更されてしまいます。

マクロ2.0

このように、macro_rules!はCのマクロと比較すれば健全ですが、部分的に健全でしかありません。そのため、確実に健全なマクロにできる「マクロ2.0」を作ろうという動きがかなり早い段階からありました。

https://github.com/rust-lang/rust/issues/39412

このマクロ2.0は現在も安定化に向けて進められていますが、まだまだ安定化しそうにはないです。しばらくはこの問題を気をつけてマクロを書く必要がありそうです。

その他参考

https://qnighy.hatenablog.com/entry/2019/05/11/190000

https://speakerdeck.com/optim/rust-all-kinds-of-macro

https://gist.github.com/Kestrer/8c05ebd4e0e9347eb05f265dfb7252e1

https://veykril.github.io/tlborm/decl-macros/minutiae/hygiene.html

https://doc.rust-lang.org/reference/macros-by-example.html

Discussion