4
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Organization

serde-jsonのjson!マクロを読む

serde-jsonのjson!マクロ?

Rustでシリアライズ・デシリアライズをするためのライブラリ「serde」の子ライブラリで、JSONのシリアライズ・デシリアライズをする「serde-json」には、JSONを解釈してserde_json::Value型にするためのマクロがついています。

use serde_json::json;

fn main() {
     let my_json = json!{
        {"status": 200,
         "foo": {
             "aaa": 123,
             "bar": [1, 3, "str", ["a", "b", 2]]
         },
         "result": null,
         "bool": [true, false],
         "float": 1.23
        }
     };
    println!("{}", my_json);
    /* 結果
{"bool":[true,false],"float":1.23,"foo":{"aaa":123,"bar":[1,3,"str",["a","b",2]]},"result":null,"status":200}
    */

    // デバッグ表示
    println!("{:?}", my_json);
    /* 結果
Object({"bool": Array([Bool(true), Bool(false)]), "float": Number(1.23), "foo": Object({"aaa": Number(123), "bar": Array([Number(1), Number(3), String("str"), Array([String("a"), String("b"), Number(2)])])}), "result": Null, "status": Number(200)})
    */
}

「構文解析とかがんばってるんだろうなー」と思いきや、案外、力技でなんとかしていたので、今回はそれを解説していきます。

Rustの基本的な文法や、Rustでのmacro_rules!を使ったマクロ定義については、ある程度は理解している読者を想定しています。
マクロについて詳しくない方は、こちらなどが参考になります。

serde_json::Value

JavaScriptをはじめとする動的型付け言語では、普通にあることなのですが、同じ配列や連想配列に、整数や文字列など様々な型のものが混ざっているのは、Rustのような静的型付け言語では基本的にはご法度です。
けれど、実際そういうデータ形式になっているので、enum型でなんとかしています。

#[derive(Clone, PartialEq)]
pub enum Value {
    Null,
    Bool(bool),
    Number(Number),
    String(String),
    Array(Vec<Value>),
    Object(Map<String, Value>),
}

(引用元: src/value/mod.rsより、docstringを削除)

Mapとは? Numberとは? とか思うかもしれませんが、あまり深く気にせずいきましょう。(featureによって切り替わりますが、デフォルトでは、MapBTreeMapを使っていて、Numberはさらなるenum型になっています)

json!マクロは、Value型を作るためのマクロです。

json_internal!マクロ

json!マクロ自体は実質なにもしておらず、json_internal!マクロががんばっています。

macro_rules! json {
    // Hide distracting implementation details from the generated rustdoc.
    ($($json:tt)+) => {
        json_internal!($($json)+)
    };
}

(引用元: src/macros.rs 以下も同じ)

なので、json_internal!マクロを読みます。

長いですが、一旦全部引用します。

macro_rules! json_internal {
    //////////////////////////////////////////////////////////////////////////
    // TT muncher for parsing the inside of an array [...]. Produces a vec![...]
    // of the elements.
    //
    // Must be invoked as: json_internal!(@array [] $($tt)*)
    //////////////////////////////////////////////////////////////////////////

    // Done with trailing comma.
    (@array [$($elems:expr,)*]) => {
        json_internal_vec![$($elems,)*]
    };

    // Done without trailing comma.
    (@array [$($elems:expr),*]) => {
        json_internal_vec![$($elems),*]
    };

    // Next element is `null`.
    (@array [$($elems:expr,)*] null $($rest:tt)*) => {
        json_internal!(@array [$($elems,)* json_internal!(null)] $($rest)*)
    };

    // Next element is `true`.
    (@array [$($elems:expr,)*] true $($rest:tt)*) => {
        json_internal!(@array [$($elems,)* json_internal!(true)] $($rest)*)
    };

    // Next element is `false`.
    (@array [$($elems:expr,)*] false $($rest:tt)*) => {
        json_internal!(@array [$($elems,)* json_internal!(false)] $($rest)*)
    };

    // Next element is an array.
    (@array [$($elems:expr,)*] [$($array:tt)*] $($rest:tt)*) => {
        json_internal!(@array [$($elems,)* json_internal!([$($array)*])] $($rest)*)
    };

    // Next element is a map.
    (@array [$($elems:expr,)*] {$($map:tt)*} $($rest:tt)*) => {
        json_internal!(@array [$($elems,)* json_internal!({$($map)*})] $($rest)*)
    };

    // Next element is an expression followed by comma.
    (@array [$($elems:expr,)*] $next:expr, $($rest:tt)*) => {
        json_internal!(@array [$($elems,)* json_internal!($next),] $($rest)*)
    };

    // Last element is an expression with no trailing comma.
    (@array [$($elems:expr,)*] $last:expr) => {
        json_internal!(@array [$($elems,)* json_internal!($last)])
    };

    // Comma after the most recent element.
    (@array [$($elems:expr),*] , $($rest:tt)*) => {
        json_internal!(@array [$($elems,)*] $($rest)*)
    };

    // Unexpected token after most recent element.
    (@array [$($elems:expr),*] $unexpected:tt $($rest:tt)*) => {
        json_unexpected!($unexpected)
    };

    //////////////////////////////////////////////////////////////////////////
    // TT muncher for parsing the inside of an object {...}. Each entry is
    // inserted into the given map variable.
    //
    // Must be invoked as: json_internal!(@object $map () ($($tt)*) ($($tt)*))
    //
    // We require two copies of the input tokens so that we can match on one
    // copy and trigger errors on the other copy.
    //////////////////////////////////////////////////////////////////////////

    // Done.
    (@object $object:ident () () ()) => {};

    // Insert the current entry followed by trailing comma.
    (@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => {
        let _ = $object.insert(($($key)+).into(), $value);
        json_internal!(@object $object () ($($rest)*) ($($rest)*));
    };

    // Current entry followed by unexpected token.
    (@object $object:ident [$($key:tt)+] ($value:expr) $unexpected:tt $($rest:tt)*) => {
        json_unexpected!($unexpected);
    };

    // Insert the last entry without trailing comma.
    (@object $object:ident [$($key:tt)+] ($value:expr)) => {
        let _ = $object.insert(($($key)+).into(), $value);
    };

    // Next value is `null`.
    (@object $object:ident ($($key:tt)+) (: null $($rest:tt)*) $copy:tt) => {
        json_internal!(@object $object [$($key)+] (json_internal!(null)) $($rest)*);
    };

    // Next value is `true`.
    (@object $object:ident ($($key:tt)+) (: true $($rest:tt)*) $copy:tt) => {
        json_internal!(@object $object [$($key)+] (json_internal!(true)) $($rest)*);
    };

    // Next value is `false`.
    (@object $object:ident ($($key:tt)+) (: false $($rest:tt)*) $copy:tt) => {
        json_internal!(@object $object [$($key)+] (json_internal!(false)) $($rest)*);
    };

    // Next value is an array.
    (@object $object:ident ($($key:tt)+) (: [$($array:tt)*] $($rest:tt)*) $copy:tt) => {
        json_internal!(@object $object [$($key)+] (json_internal!([$($array)*])) $($rest)*);
    };

    // Next value is a map.
    (@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => {
        json_internal!(@object $object [$($key)+] (json_internal!({$($map)*})) $($rest)*);
    };

    // Next value is an expression followed by comma.
    (@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => {
        json_internal!(@object $object [$($key)+] (json_internal!($value)) , $($rest)*);
    };

    // Last value is an expression with no trailing comma.
    (@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => {
        json_internal!(@object $object [$($key)+] (json_internal!($value)));
    };

    // Missing value for last entry. Trigger a reasonable error message.
    (@object $object:ident ($($key:tt)+) (:) $copy:tt) => {
        // "unexpected end of macro invocation"
        json_internal!();
    };

    // Missing colon and value for last entry. Trigger a reasonable error
    // message.
    (@object $object:ident ($($key:tt)+) () $copy:tt) => {
        // "unexpected end of macro invocation"
        json_internal!();
    };

    // Misplaced colon. Trigger a reasonable error message.
    (@object $object:ident () (: $($rest:tt)*) ($colon:tt $($copy:tt)*)) => {
        // Takes no arguments so "no rules expected the token `:`".
        json_unexpected!($colon);
    };

    // Found a comma inside a key. Trigger a reasonable error message.
    (@object $object:ident ($($key:tt)*) (, $($rest:tt)*) ($comma:tt $($copy:tt)*)) => {
        // Takes no arguments so "no rules expected the token `,`".
        json_unexpected!($comma);
    };

    // Key is fully parenthesized. This avoids clippy double_parens false
    // positives because the parenthesization may be necessary here.
    (@object $object:ident () (($key:expr) : $($rest:tt)*) $copy:tt) => {
        json_internal!(@object $object ($key) (: $($rest)*) (: $($rest)*));
    };

    // Munch a token into the current key.
    (@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => {
        json_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*));
    };

    //////////////////////////////////////////////////////////////////////////
    // The main implementation.
    //
    // Must be invoked as: json_internal!($($json)+)
    //////////////////////////////////////////////////////////////////////////

    (null) => {
        $crate::Value::Null
    };

    (true) => {
        $crate::Value::Bool(true)
    };

    (false) => {
        $crate::Value::Bool(false)
    };

    ([]) => {
        $crate::Value::Array(json_internal_vec![])
    };

    ([ $($tt:tt)+ ]) => {
        $crate::Value::Array(json_internal!(@array [] $($tt)+))
    };

    ({}) => {
        $crate::Value::Object($crate::Map::new())
    };

    ({ $($tt:tt)+ }) => {
        $crate::Value::Object({
            let mut object = $crate::Map::new();
            json_internal!(@object object () ($($tt)+) ($($tt)+));
            object
        })
    };

    // Any Serialize type: numbers, strings, struct literals, variables etc.
    // Must be below every other rule.
    ($other:expr) => {
        $crate::to_value(&$other).unwrap()
    };
}

The main implementation を読む

恐らく、ここから読むのが一番簡単なので、読んでいきます。

//////////////////////////////////////////////////////////////////////////
    // The main implementation.
    //
    // Must be invoked as: json_internal!($($json)+)
    //////////////////////////////////////////////////////////////////////////

    (null) => {
        $crate::Value::Null
    };

    (true) => {
        $crate::Value::Bool(true)
    };

    (false) => {
        $crate::Value::Bool(false)
    };

    ([]) => {
        $crate::Value::Array(json_internal_vec![])
    };

    ([ $($tt:tt)+ ]) => {
        $crate::Value::Array(json_internal!(@array [] $($tt)+))
    };

    ({}) => {
        $crate::Value::Object($crate::Map::new())
    };

    ({ $($tt:tt)+ }) => {
        $crate::Value::Object({
            let mut object = $crate::Map::new();
            json_internal!(@object object () ($($tt)+) ($($tt)+));
            object
        })
    };

    // Any Serialize type: numbers, strings, struct literals, variables etc.
    // Must be below every other rule.
    ($other:expr) => {
        $crate::to_value(&$other).unwrap()
    };

null, true, false, [], {}は直接値を作っています。また、その他の式(数字、文字列も含む)はValue型に変換を試みてunwrapしています。

蛇足ですが、to_value

pub fn to_value<T>(value: T) -> Result<Value, Error>
where
    T: Serialize,
{
    value.serialize(Serializer)
}

(引用元: src/value/mod.rs)
となっていますので、型自体が不適切な場合はコンパイル時にチェックできますが、serializeが実装されているけれど実行時に変換にミスった場合はpanicします。

本題に戻ります。
やや不穏なのは

    ([ $($tt:tt)+ ]) => {
        $crate::Value::Array(json_internal!(@array [] $($tt)+))
    };
    // 略
    ({ $($tt:tt)+ }) => {
        $crate::Value::Object({
            let mut object = $crate::Map::new();
            json_internal!(@object object () ($($tt)+) ($($tt)+));
            object
        })
    };

の実装です。

つまり、
json_internal!( [1, 2, 3] )
Value::Array(json_internal!(@array [] 1, 2, 3))
として、
json_internal!( {"a": 1, "b": 2} )は、
Value::Object({ let mut object = Map::new(); json_internal!(@object object () ("a": 1, "b": 2) ("a": 1, "b": 2)); object })
として再帰的に呼び出されます。

@array@objectは、Rustの文法的には特に意味がなく、単にマクロのパターンマッチのためのマーカーとして使われます。

@arrayたちを読む

適宜、順番を入れ替えながら説明します。
Value::Array(json_internal!(@array [] 1 , 2 , 3))の形に変換されるのでした。これがどう変換されていくのでしょうか。

    // Next element is `null`.
    (@array [$($elems:expr,)*] null $($rest:tt)*) => {
        json_internal!(@array [$($elems,)* json_internal!(null)] $($rest)*)
    };

    // Next element is `true`.
    (@array [$($elems:expr,)*] true $($rest:tt)*) => {
        json_internal!(@array [$($elems,)* json_internal!(true)] $($rest)*)
    };

    // Next element is `false`.
    (@array [$($elems:expr,)*] false $($rest:tt)*) => {
        json_internal!(@array [$($elems,)* json_internal!(false)] $($rest)*)
    };

    // Next element is an array.
    (@array [$($elems:expr,)*] [$($array:tt)*] $($rest:tt)*) => {
        json_internal!(@array [$($elems,)* json_internal!([$($array)*])] $($rest)*)
    };

    // Next element is a map.
    (@array [$($elems:expr,)*] {$($map:tt)*} $($rest:tt)*) => {
        json_internal!(@array [$($elems,)* json_internal!({$($map)*})] $($rest)*)
    };

    // Next element is an expression followed by comma.
    (@array [$($elems:expr,)*] $next:expr, $($rest:tt)*) => {
        json_internal!(@array [$($elems,)* json_internal!($next),] $($rest)*)
    };

ここで挙げたものはすべて
(@array [$($elems)*] $next $($rest)*または(@array [$($elems)*] $next, $($rest)*
$nextをValue化しながら、再帰的に[$($elems)*]の中に付け足していく働きをしています。

また、先頭カンマは、次のように取り除きます。

    // Comma after the most recent element.
    (@array [$($elems:expr),*] , $($rest:tt)*) => {
        json_internal!(@array [$($elems,)*] $($rest)*)
    };

このようにすると、[]に入っていない要素がどんどん減っていき、最後の要素は


    // Last element is an expression with no trailing comma.
    (@array [$($elems:expr,)*] $last:expr) => {
        json_internal!(@array [$($elems,)* json_internal!($last)])
    };

のように処理されます。そして、

    // Done with trailing comma.
    (@array [$($elems:expr,)*]) => {
        json_internal_vec![$($elems,)*]
    };

    // Done without trailing comma.
    (@array [$($elems:expr),*]) => {
        json_internal_vec![$($elems),*]
    };

によって、Value::Array(Vec<Value>)になります。
なお、この過程で、どれにもパターンマッチしない例が出たときは、ここに落ちるようです。(プログラマが直接@arrayを書いたとき以外、こうなることはあるのか、よく分かりませんでした)

    // Unexpected token after most recent element.
    (@array [$($elems:expr),*] $unexpected:tt $($rest:tt)*) => {
        json_unexpected!($unexpected)
    };

@objectたちを読む

json_internal!( {"a": 1, "b": 2} )は、
Value::Object({ let mut object = Map::new(); json_internal!(@object object () ("a": 1, "b": 2) ("a": 1, "b": 2)); object })
となるのでした。
マクロでobjectに値を挿入していくんだろうな、と予想ができます。

なお、中味がなぜか2つある件については、エラー処理に使う、とのコメントが書いてあります。

:の直前まで、@object object () ...の、()の中に入れていきます。

    // Key is fully parenthesized. This avoids clippy double_parens false
    // positives because the parenthesization may be necessary here.
    (@object $object:ident () (($key:expr) : $($rest:tt)*) $copy:tt) => {
        json_internal!(@object $object ($key) (: $($rest)*) (: $($rest)*));
    };

    // Munch a token into the current key.
    (@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => {
        json_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*));
    };

通常は、上のパターンマッチが使われます。
下のパターンマッチのように、式ではないが連想配列のキーになるものが何なのか、私には分かりませんでした。

続いて、コロンが来て、値が来るはずです。

    // Next value is `null`.
    (@object $object:ident ($($key:tt)+) (: null $($rest:tt)*) $copy:tt) => {
        json_internal!(@object $object [$($key)+] (json_internal!(null)) $($rest)*);
    };

    // Next value is `true`.
    (@object $object:ident ($($key:tt)+) (: true $($rest:tt)*) $copy:tt) => {
        json_internal!(@object $object [$($key)+] (json_internal!(true)) $($rest)*);
    };

    // Next value is `false`.
    (@object $object:ident ($($key:tt)+) (: false $($rest:tt)*) $copy:tt) => {
        json_internal!(@object $object [$($key)+] (json_internal!(false)) $($rest)*);
    };

    // Next value is an array.
    (@object $object:ident ($($key:tt)+) (: [$($array:tt)*] $($rest:tt)*) $copy:tt) => {
        json_internal!(@object $object [$($key)+] (json_internal!([$($array)*])) $($rest)*);
    };

    // Next value is a map.
    (@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => {
        json_internal!(@object $object [$($key)+] (json_internal!({$($map)*})) $($rest)*);
    };

    // Next value is an expression followed by comma.
    (@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => {
        json_internal!(@object $object [$($key)+] (json_internal!($value)) , $($rest)*);
    };

    // Last value is an expression with no trailing comma.
    (@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => {
        json_internal!(@object $object [$($key)+] (json_internal!($value)));
    };

面白いのは、(@object $object ($key) ...)だったのが(@object $object [$key] ...)と、角かっこに変わっています。
角かっこのパターンマッチを見ていきましょう。

    // Insert the current entry followed by trailing comma.
    (@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => {
        let _ = $object.insert(($($key)+).into(), $value);
        json_internal!(@object $object () ($($rest)*) ($($rest)*));
    };

    // Current entry followed by unexpected token.
    (@object $object:ident [$($key:tt)+] ($value:expr) $unexpected:tt $($rest:tt)*) => {
        json_unexpected!($unexpected);
    };

    // Insert the last entry without trailing comma.
    (@object $object:ident [$($key:tt)+] ($value:expr)) => {
        let _ = $object.insert(($($key)+).into(), $value);
    };

値を読んだら、次はカンマか何もないか、どちらかになっているはずです。
それを確認したら、$objectにキーと値の組をinsertし、
続きがある場合は$objectの後のキーを()に戻して再びjson_internal!します。

そしてこれは恐らく、最終要素の末尾にカンマがついていた場合に通る処理です。

    // Done.
    (@object $object:ident () () ()) => {};

あと、以下がエラー処理用です。

    // Missing value for last entry. Trigger a reasonable error message.
    (@object $object:ident ($($key:tt)+) (:) $copy:tt) => {
        // "unexpected end of macro invocation"
        json_internal!();
    };

    // Missing colon and value for last entry. Trigger a reasonable error
    // message.
    (@object $object:ident ($($key:tt)+) () $copy:tt) => {
        // "unexpected end of macro invocation"
        json_internal!();
    };

    // Misplaced colon. Trigger a reasonable error message.
    (@object $object:ident () (: $($rest:tt)*) ($colon:tt $($copy:tt)*)) => {
        // Takes no arguments so "no rules expected the token `:`".
        json_unexpected!($colon);
    };

    // Found a comma inside a key. Trigger a reasonable error message.
    (@object $object:ident ($($key:tt)*) (, $($rest:tt)*) ($comma:tt $($copy:tt)*)) => {
        // Takes no arguments so "no rules expected the token `,`".
        json_unexpected!($comma);
    };

マクロの中でエラーが起こると分かりにくいんですが、マクロのマッチングミスで出るエラーメッセージを利用して、マシなエラーになるように心がけているっぽいです。

これでめでたく、json_internal!を読むことができました。

まとめ

今回、serde-jsonのjson!マクロが何をやっているのか読んでみました。

「うまいことやってるなぁ」という気持ちと、「黒魔術では?」という気持ちでいっぱいになりますが、json!マクロ自体は非常に便利です。また、ここまでではなくても、何かマクロでDSLを書きたい場合に「JSONができたんだから、これも頑張ればできるはず」と思えるのは嬉しいことです。

みなさんも、Rustマクロ黒魔術にぜひとも挑戦してみてください。

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
Sign upLogin
4
Help us understand the problem. What are the problem?