足を止めて見る #5 〜 RustのSerdeクレート(2) 〜
足を止めて見よう
足を止めて見ようシリーズの5つ目です。
前回は serde クレートについてでした。
構造体を Json 形式にシリアライズして、更にその Json 形式をデシリアライズして、元の構造体に戻る様子を見ました。
今回は serde クレートの Attributes について足を止めて見ようと思います。
serde の attributes とは何なのか
前回は、dervieマクロによって自動的に serde::ser::Serialize や serde::de::Deserialize が実装されることを確認しました。
attributes とは、そのderiveマクロによって実装されるその振る舞いを調整するための機能のようで、3種類あります。
それぞれどこに記述する attributes なのかは、公式Docの例がわかりやすいです。
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)] // <-- this is a container attribute
struct S {
#[serde(default)] // <-- this is a field attribute
f: i32,
}
#[derive(Serialize, Deserialize)]
#[serde(rename = "e")] // <-- this is also a container attribute
enum E {
#[serde(rename = "a")] // <-- this is a variant attribute
A(String),
}
いくつかの attributes をピックアップして動作を見て見ようと思います。
serde クレートの attributes を使ってみる
今回は Person という構造体を用意して、いくつか attributes を使ってみようと思います。3種類の attributes を試すために、Person 構造体のフィールドには、構造体 Age をとる age フィールド、enum Gender をとる gender フィールドなどを用意してみました。
#[derive(serde::Serialize)]
struct Person {
first_name: String,
last_name: String,
age: Age,
gender: Gender,
}
#[derive(serde::Serialize)]
struct Age {
value: u8,
}
#[derive(serde::Serialize)]
enum Gender {
Male,
Female,
Other,
}
fn main() {
let person = Person {
first_name: String::from("太郎"),
last_name: String::from("田中"),
age: Age { value: 30 },
gender: Gender::Male,
};
let serialized = serde_json::to_string(&person).unwrap();
assert_eq!(
serialized,
"{\"first_ame\":\"太郎\",\"last_ame\":\"田中\",\"age\":{\"value\":30},\"gender\":\"Male\"}"
);
}
この実行は成功して、以下のJsonになります。
{
"first_name": "太郎",
"last_name": "田中",
"age": 30,
"gender": "Male"
}
rename_all
rename_all を使うと、スネークケースで記述しているフィールド名をキャメルケースでシリアライズできました
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")] // 追加
struct Person {
first_name: String,
last_name: String,
age: Age,
gender: Gender,
}
シリアライズされた Json の形
{
"firstName": "太郎", // first_name -> firstName
"lastName": "田中", // last_name -> lastName
"age": { "value": 30 },
"gender": "Male"
}
rename
rename を使うと、enum の Variant や struct の フィールド の名前を変更できました
#[derive(serde::Serialize)]
enum Gender {
#[serde(rename = "男")] // 追加
Male,
#[serde(rename = "女")] // 追加
Female,
#[serde(rename = "その他")] // 追加
Other,
}
シリアライズされた Json の形
{
"firstName": "太郎",
"lastName": "田中",
"age": { "value": 30 },
"gender": "男" // Male -> 男
}
transparent
transparent を使うと、フィールドを1つしか持たない構造体がネストされている時に、まるでプリミティブ型のフィールドがそこにあるかのような振る舞いができました。これは New Type パターンを想定した構造体に対して利用する狙いがあるようです。
文字に起こしてもちょっと伝わりづらいので書いてみます。
#[derive(serde::Serialize)]
#[serde(transparent)] // 追加
struct Age {
value: u8,
}
シリアライズされた Json の形
{
"firstName": "太郎",
"lastName": "田中",
"age": 30, // { "value": 30 } -> 30
"gender": "男"
}
わかりやすくなりました。後で試してみたのですが、もし Age がu8をとるユニット様構造体だった場合もこの形になりました。
// 以下2つのシリアライズ結果は同じになりました
#[derive(serde::Serialize)]
#[serde(transparent)]
struct Age {
value: u8,
}
#[derive(serde::Serialize)]
struct Age (u8)
もう一段だけ深ぼってみる(#1)
そもそも #[serde(xxx = "yyy")] といった記述は何者なんでしょうか。
これは自分知ってました、カスタムのdervieマクロってやつでしょう(違ったらごめん)
おそらく serde のどこかに proc_macro_derive といった記述があり、そこにロジックが書かれているはずです。
もう一段だけ深ぼってみる(#2)
では #[serde(xxx = "yyy")] はどのようなメカニズムなのでしょうか?
これを紐解くために、まずは serde クレートの derive という crate feature から辿ってみます。
serdeのCargo.tomlを覗いてみます。
{{ 省略 }}
[dependencies]
serde_derive = { version = "1", optional = true, path = "../serde_derive" }
{{ 省略 }}
### FEATURES #################################################################
[features]
default = ["std"]
# Provide derive(Serialize, Deserialize) macros.
derive = ["serde_derive"]
{{ 省略 }}
derive feature を指定するということは、この Carto.toml の1階層上にある serde_derive というディレクトリに依存するようです。
この serde_derive ですが、crates.io でも検索できるように、1つの独立したクレートのようです。
どうやらこのクレートにロジックがあるようです。宣言部分のソースコード serde_derive/lib.rs をのぞいてみます。
#[proc_macro_derive(Serialize, attributes(serde))]
pub fn derive_serialize(input: TokenStream) -> TokenStream {
let mut input = parse_macro_input!(input as DeriveInput);
ser::expand_derive_serialize(&mut input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
やりました! proc_macro_dervie という記述に辿り着きました!
attributes(serde) とあるので #[serde] という名前のカスタムderiveマクロになっているのでしょうね(TRPLではここまでは教えてくれなかったぞ)。
細かくロジックを追いかけるのは、また今度にします。
振り返り
今回も serde クレートを改めて足を止めて見てみました。
attributes の使い方がざっくりですが分かりました。
また crate feature を Cargo.toml から辿っていき、カスタムのdervieマクロの宣言まで追いかけることができたのは、Rustの習熟を深めるためにも良かったのではないでしょうか。
これで明日から、もっと堂々と serde を使っていけるぞー 🙌
その他
今回書いたRustのコードはこのリポジトリで制作しています。
Discussion
これは違って、
は、
proc_macro_derive(SerializeとあるようにSerializederive macro を定義しています。では
, attributes(serde))は何者かというと、Serializederive macro の helper attributes の名前をserdeと指定しています。( あまり見ないですが複数の helper attributes 名を指定することもできます )
既に記事内で言及されているように、
#[derive(Serialize)]が付与された型宣言の中で#[serde(...)]という attribute をいろんなところに付与することで、ユーザーは#[derive(Serialize)]の 生成する trait impl コードをカスタマイズ できます。これが helper attributes の役割です。
つまり、 derive macro に対してオプションやフラグを指定するためのインターフェースです。
以下、proc macro の仕組み上 helper attributes がどう扱われているかの解説を書いてみたのですが、もしかしたら今後の記事で扱われる予定があるかもしれないと思ったので隠しておきます。必要に応じて読んだり読まなかったりしてください。
proc macro の仕組み上どう扱われているか1
serde_derive::derive_serializeを例に挙げれば、内部でSerializeimpl を生成する処理の過程で「{struct, enum, field, variant} が
serdeという名前の attribute を持っていたら、その中身に応じて〜」という処理があるはずです。
このように、helper attributes は独立した attribute macro ではなく、それ自体は何もしない、本当にただ derive macro に対してオプションやフラグを指定するためのインターフェースです。
実際、
serdecrate からserdeという名前の attribute macro は export されていません。proc macro の仕組み上どう扱われているか2 ( ちょっと deep dive )
ここで、
「derive macro の実装内で
という処理を自前でやっているのであれば、それでいいのでは? わざわざ
#[proc_macro_derive(Serialize, attributes(serde))]みたいな形でも名前を指定する必要はなくないか?」と思ったかもしれません。
ここからは proc macro を実装したことがないとわかりにくい話になるのですが、「 derive macro は元の型定義を触れない 」という性質がこれを必要とする根本的な理由になっています。
具体的には、
まず、attribute macro は
というインターフェースで入力全体を出力で置換します。なので、例えば
を
みたいなコードに置き換えることも可能です。
それに対して、derive macro は
というインターフェースで、元の型定義はそのままに、出力である trait impl コードをソースコードに挿入します。
なので、例えば
が展開されると
となるわけですが、ここで元の型定義に
#[serde(...)]が残っていることが問題になります。proc macro の仕組みを作る立場に立って考えると、これをエラーにしないようにするには
の2つの選択肢がありますが、後者だと
の「入力」を作る時点で削除を終えておく必要があります。
推測になりますが、これは前者に比べて一貫性や効率の面でデメリットがあるのだと思います。
(もしこの議論の一次情報を知っている方がこれを読んでいたら教えてください)
そのため、実際の proc macro の仕組みでは前者が採用されています。cargo-expand などを使うと、実際に
#[serde]helper attribute ごと型定義が残ったまま trait impl が挿入された (エラーになっていない) コードを見ることができます。コメントありがとうございます!
helper attributesは自由に設定できるけど、serde_derive では
serdeしかチェックしませんよ、ということだと理解しました。それと、トグルに折りたたんだコメントもありがとうございます。
仰るとおり、このあと TokenStream をごりごり処理している箇所を見ているところでして、
serdeという名前のattributes以外をスキップしているなあとか、syn::Data Enum のvariantに沿ってそれぞれのattrを確認しているなあ...とか見ていました。今後の記事作成の参考にさせていただきます!