Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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リポジトリにまとめてあります。

https://github.com/tesaguri/proc-macro-crate-name

tesaguri
Ferris the Crab🦀愛好家
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした