LoginSignup
5
2

More than 3 years have passed since last update.

Rustのマクロで構造体のフィールド名を取得する

Posted at

概要

構造体(struct)のフィールド名が必要になったので作りました。

次のような用途を想定しています。

  • 設定をConfig的な構造体にまとめたい
  • 設定ファイルや環境変数から読み込んだ時に、全ての値が定義されているか検証したい
  • $フィールド名 \subset 設定ファイル等$

TL;DR の後にマクロの作り方も書いています。

環境: rust 1.41

TL; DR

macro_rules! make_struct {
  (@collect_attrs_flag
   ($(#[$attrs:meta])*)
   #[$attr:meta]
   $($tail:tt)*
  ) => {
    make_struct! {
      @collect_attrs_flag
        ($(#[$attrs])* #[$attr])
        $($tail)*
    }
  };

  (@collect_attrs_flag
    ($(#[$attr:meta])*)
    $pub:vis struct $name:ident {
      $($fpub:vis $field:ident: $type:ty,)*
    }) => {
    $(#[$attr])*
      $pub struct $name {
        $($fpub $field: $type,)*
      }
    impl $name {
      fn fields() -> Vec<&'static str> {
        vec![$(stringify!($field)),*]
      }
    }
  };

  ($($tt:tt)*) => {
    make_struct! {@collect_attrs_flag () $($tt)* }
  };
}
make_struct! {
  #[derive(Debug, Clone)]
  #[allow(dead_code)]
  pub struct S {
    pub a: String,
    b: String,
  }
}

fn main() {
  println!("{:?}", S::fields());
  // ["a", "b"]
}


作り方

1. 単に構造体を作る例

マクロを使用する予定の構造体が1つの場合はとても簡単です。
pubにしたければstructpub structに置換して、フィールド名の頭にpubをつければ良いです。

macro_rules! make_struct {
  (struct $name:ident { // name: 構造体の名称 識別子なのでident
    $($field:ident: $type:ty,)* // field: フィールド名 識別子, type: タイプなのでty。 繰り返しがあるので$()* 
  }) => {
    struct $name {
      $($field: $type,)*
    }
    impl $name {
      fn fields() -> Vec<&'static str> {
        vec![$(stringify!($field)),*]
      }
    }
  };
}
make_struct! {
  struct S {
    a: String,
    b: String,
  }
}

fn main() {
  for field in S1::fields() {
    println!("{}", field);
  }
}

2. pub な構造体を許容する例

pubな構造体も許容する場合はメタ変数visを使う必要があります。
これはmacro by exampleには載っていますが、日本語のドキュメントには見当たりません。

フィールド名にも適用すると、一部だけpubな構造にも対応できます。

macro_rules! make_struct {
  ($pub:vis struct $name:ident { // vis: visibilityのフラグ指定子
    $($fpub:vis $field:ident: $type:ty,)*
  }) => {
    $pub struct $name {
      $($fpub $field: $type,)*
    }
    impl $name {
      fn fields() -> Vec<&'static str> {
        vec![$(stringify!($field)),*]
      }
    }
  };

}
make_struct! {
  pub struct S {
    pub a: String,
    b: String,
  }
}

fn main() {
  for field in S::fields() {
    println!("{}", field);
  }
}

3. #[derive()]とかを許容する例

この場合は少しややこしくなります。
先程までの例を元に考えると、次のような実装が思いつきますが、これはコンパイルエラーになります。

macro_rules! make_struct {
  (
    ($(#[$attr:meta])*) // メタ変数 metaでアトリビュートをキャッチして
    $pub:vis struct $name:ident {
      $($fpub:vis $field:ident: $type:ty,)*
    }) => {
    $(#[$attr])* // 吐き出す
      $pub struct $name {
        $($fpub $field: $type,)*
      }
    impl $name {
      fn fields() -> Vec<&'static str> {
        vec![$(stringify!($field)),*]
      }
    }
  };

}
make_struct! {
  #[derive(Debug, Clone)]
  #[allow(dead_code)]
  pub struct S {
    pub a: String,
    b: String,
  }
}
// error: no rules expected the token `#`

#が期待されるマクロルールが無いとのことなので一旦ttでトークンツリーとしてキャッチすれば良いです。

macro_rules! make_struct {
  // A. アトリビュートが1つ以上存在する場合はこっちで評価される
  (($(#[$attrs:meta])*)
   #[$attr:meta]
   $($tail:tt)*
  ) => {
    make_struct! {
      ($(#[$attrs])* #[$attr])
      $($tail)*
    }
  };

  // B. アトリビュートが0個(元々0個または手順Aで評価され尽くして0個)の場合はこっち
  (($(#[$attr:meta])*)
   $pub:vis struct $name:ident {
     $($fpub:vis $field:ident: $type:ty,)*
   }) => {
    $(#[$attr])*
      $pub struct $name {
        $($fpub $field: $type,)*
      }
    impl $name {
      fn fields() -> Vec<&'static str> {
        vec![$(stringify!($field)),*]
      }
    }
  };

  // C. 一旦ttで受ける
  ($($tt:tt)*) => {
    make_struct! { () $($tt)* } // 前半の()が無いと全てにマッチして無限ループする
  };
}

make_struct! {
  #[derive(Debug)]
  #[derive(Clone)]
  #[allow(dead_code)]
  pub struct S {
    pub a: String,
    b: String,
  }
}

fn main() {
  println!("{:?}", S::fields());
}
// ["a", "b"]

マクロは上から評価されるので(本当に?)、フローは次の通りです。

  1. 初期状態ではA,Bでは受けられないので、Cの部分で評価されます。(なんで?
  2. 2回目以降のmake_struct!では、アトリビュートが残っている場合、Aで評価されます。Aの方が先にあるし、どの道Bでは評価できないと思われます(上記の#が期待されるマクロルールが無いエラー)。
  3. アトリビュートが評価され尽くすとBで評価されてstructとして吐き出されます。

このフローは恐らく正しくて、次のようにするとmake_struct!が何回呼ばれているか確認できます。

rustup default nightly && rustc -Z unstable-options --pretty expanded,hygiene src/main.rs

ちなみにこのマクロを書くにあたってReddit - macro_rules! make it hard to destruct Rust structs.を参考にしたのですが、そちらでは@collect_attrsを使っています。
こいつは恐らく分岐Cに到達したことのマーカーな気がするのですが、正しいかどうか分かりません。一応、TL;DRには残しています。

色々わからないことが多いです。。(´・ω・`)

5
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
5
2