LoginSignup
1
2

More than 3 years have passed since last update.

手続きマクロ内でクレート名を取得する

Last updated at Posted at 2019-08-16

Rustにおいて、fooクレートで定義されたfoo::Fooトレイトの実装をfoo-deriveクレートのderiveマクロで生成するようなパターンが頻繁に登場します。この記事ではこのようなマクロの実装における名前解決のちょっとしたコーナーケースについて紹介します。

ナイーブな実装

(この例はRust 1.37.0 (2019-8-13) で安定化されたunderscore_const_namesフィーチャに依存しています)

foo/src/lib.rs
//! # Foo
//!
//! ## Examples
//!
//! ```
//! #[derive(foo::Foo)]
//! struct S;
//! ```

pub use foo_derive::Foo;

pub trait Foo {}
foo-derive/src/lib.rs
extern crate proc_macro;

use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Foo)]
pub fn derive_foo(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = input.ident;

    let output = quote! {
        const _: () = {
            // 呼び出し側の型名前空間で `foo` が上書きされている可能性があるので別名でインポートする
            extern crate foo as _foo;

            impl _foo::Foo for #name {}
        };
    };

    output.into()
}

多くのケースではこれで問題なく動きますが、もしマクロ呼び出し側のCargo.tomlで以下のようにfooがリネームされていたら、extern crate foofooクレートをインポートできないので上手く動きません。

another-crate/Cargo.toml
[dependencies]
bar = { version = "*", package = "foo" }

proc-macro-crateクレート

Cargo.tomlでリネームされたクレートの名前を正攻法で知るのは難しいです。そこで、コンパイル時にマクロ呼び出し側のCargo.tomlを読んでリネーム後の名前を取得するマクロがproc-macro-crateクレートで提供されています。

これを使うとfoo-deriveは以下のように書けます。

foo-derive/src/lib.rs
extern crate proc_macro;

use proc_macro2::{Span, Ident};
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Foo)]
pub fn derive_foo(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = input.ident;
    let foo = proc_macro_crate::crate_name("foo").unwrap();
    let foo = Ident::new(&foo, Span::call_site());

    let output = quote! {
        const _: () = {
            // 呼び出し側の型名前空間で `#foo` が上書きされている可能性があるので別名でインポートする
            extern crate #foo as _foo;

            impl _foo::Foo for #name {}
        };
    };

    output.into()
}

これでマクロ呼び出し側のコンパイルが通るようになり、一件落着……と思いきや、ここでfooのdocテストを走らせると以下のようにエラーになります。

$ cargo test --doc
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
   Doc-tests foo

running 1 test
test src/lib.rs -  (line 5) ... FAILED

failures:

---- src/lib.rs -  (line 5) stdout ----
error: proc-macro derive panicked
 --> src/lib.rs:6:10
  |
4 | #[derive(foo::Foo)]
  |          ^^^^^^^^
  |
  = help: message: called `Result::unwrap()` on an `Err` value: "Could not find `foo` in `dependencies` or `dev-dependencies` in `/proc-macro-crate-name/foo/Cargo.toml`!"

error: aborting due to previous error

Couldn't compile the test.

failures:
    src/lib.rs -  (line 5)

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

error: test failed, to rerun pass '--doc'

foo/Cargo.tomlの依存にfooパッケージが見当たらないため、proc_macro_crate::crate_nameがエラーになっているようです。これはfoo自身のdocテストなので、Cargo.tomlの依存にfooが書かれていないのは当然といえば当然ですが、それでテストが通らないのは困ります。

foo-derive/src/lib.rsunwrap()unwrap_or("foo")としても良いのですが、テスト環境だけで起こるエラーのためにエラーを握りつぶすのが常に望ましい方法とはいえないと思います(それで困ることも稀でしょうが)。

少しトリッキーになりますが、以下のようにfooCargo.tomldev-dependenciesfoo自身を加えれば、foo-deriveを書き換えずともfooのテストを通せます。

foo/Cargo.toml
[dev-dependencies]
foo = { path = "" }

終わりに

この記事で実装したコードは以下のGitリポジトリにまとめてあります。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2