概要
構造体(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にしたければ struct
を pub 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"]
マクロは上から評価されるので(本当に?)、フローは次の通りです。
- 初期状態ではA,Bでは受けられないので、Cの部分で評価されます。(なんで?)
- 2回目以降の
make_struct!
では、アトリビュートが残っている場合、Aで評価されます。Aの方が先にあるし、どの道Bでは評価できないと思われます(上記の#が期待されるマクロルールが無い
エラー)。 - アトリビュートが評価され尽くすと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には残しています。
色々わからないことが多いです。。(´・ω・`)