概要
serde の内部構造を解説するシリーズの第2回です。今回は serialize に焦点を当てて、解説します。
かなり長くなりましたが、コードは多めなので、楽しんで読んでいただけると思います。
1. serde の基本
2. serialize を読む
3. deserialize を読む 1 (予定)
4. deserialize を読む 2 (予定)
5. deserialize を読む 3 (予定)
6. formats (予定)
まとめ
- Rust の強みである「単相化・インライン化を使ったゼロコスト抽象化」により、パフォーマンスを犠牲にすることなく serialize の実装を行うことができる。
serializer プロトタイピング: ゼロコスト抽象化編
他の言語の serializer と比較した時の serde の serializer
serialize は、実はさほど難しい処理ではありません。
直感的には、例えば動的型付き言語の場合には、以下のように単に再帰を行うだけでよさそうです。(以下は JSON への変換を行う処理の擬似コード)
function marshal(object) {
if (object is array) {
return marshal_array(object);
}
if (object is map) {
return marshal_map(object);
}
if (object is int) {
return int_to_string(object);
}
...
}
function marshal_array(array) {
len = length(array);
s = "[";
for (i = 0; i < len; i++) {
s += marshal(array[i]);
if (i + 1 < len) s += ",";
}
s += "]";
return s;
}
...
実際この方法でも serialize はできますし、何らかのリフレクションが使えるのであれば、おそらく楽です。Go ではこのような実装がされています (https://github.com/golang/go/blob/go1.13.7/src/encoding/json/encode.go#L392-L437) が、Rust は違うアプローチを取り、静的型付き言語の強みを活かしています。
以下では、Rust で serialize ライブラリを作ろうとしたらどうなるか、のプロトタイピングをしたいと思います。つまり、ごく一部の例について、「ゼロコスト抽象化」という基本原理を元に serialize 処理を汎用化していって、どういうことになるかを見ていきたいと思います。serde
にはところどころ触れながらも、あくまで必要になったことだけをやります。また、あとで serde
の実際の設計と照らし合わせて答え合わせをします。
serialize ライブラリ、自作編
例えば以下の構造体 A
を (インデントなしの) JSON に serialize するというタスクを考えます。このタスクのことだけを考えるのであれば、以下のような serialize_A_to_json
という関数を作るのが最善です。1
use std::io::{Result, Write};
struct A(i64, i32);
#[allow(non_snake_case)]
fn serialize_A_to_json<W: Write>(a: A, mut writer: W) -> Result<()> {
let A(x, y) = a;
write!(writer, "[{},{}]", x, y)?;
Ok(())
}
この関数を使えば、構造体 A
については JSON に serialize することができます。しかし、ここで2つの問題が現れます:
- 他の型 (構造体、列挙型) についてはどうやって serialize するのか?
- JSON 以外のフォーマットにはどうやって serialize するのか?
前者については、他の型についても同じように実装すればいいです。特に何か考えることはありません。playground へのリンク
use std::io::{Result, Write};
struct A2(i64, i32, i16);
#[allow(non_snake_case)]
fn serialize_A2_to_json<W: Write>(a: A2, mut writer: W) -> Result<()> {
let A2(x, y, z) = a;
write!(writer, "[{},{},{}]", x, y, z)?;
Ok(())
}
後者について、例えば YAML へ serialize するコードは以下のようになるかと思われます。2playground へのリンク
use std::io::{Result, Write};
struct A(i64, i32);
#[allow(non_snake_case)]
fn serialize_A_to_yaml<W: Write>(a: A, mut writer: W) -> Result<()> {
let A(x, y) = a;
write!(writer, "- {}\n- {}", x, y)?;
Ok(())
}
しかし、これを、「全ての型」と「すべてのデータフォーマット」について列挙するのはあまりにも苦痛です。というのは、データ型の個数を N
とし、データフォーマットの個数を M
としたとき、 NM
個の関数を定義しなければならないからです。せめて、それぞれの型について1回、それぞれのデータフォーマットについて1回だけ何かを定義すればいいようにしたいです。つまり N+M
回何かを定義すれば良いようにしたいです。
ここで、serde は Rust らしい解決法をとっています。つまり、
- トレイトによるジェネリクスを使った、ゼロコスト抽象化 (単相化、インライン化に依存する)
- マクロを用いてボイラープレートを自動生成
です。これを模倣していきたいと思います。
トレイトによるジェネリクスを使った、ゼロコスト抽象化
上の構造体 A
の serialize を使って説明します。A
のような、シークエンスとして serialize されるものを抽象化するために、以下のようなヘルパートレイト ToySerializer
を用意することにしましょう。これは serialize_int_seq
を実装します。playground へのリンク
use std::io::{Result, Write};
// 整数の配列を serialize する能力のあるオブジェクト
trait ToySerializer {
// 整数の配列を serialize する関数
fn serialize_int_seq<W: Write>(self, a: impl IntoIterator<Item = i64>, writer: W) -> Result<()>;
}
struct ToJson;
impl ToySerializer for ToJson {
fn serialize_int_seq<W: Write>(self, a: impl IntoIterator<Item = i64>, mut writer: W) -> Result<()> {
write!(writer, "[")?;
let mut count = 0;
for element in a {
if count >= 1 {
write!(writer, ",")?;
}
write!(writer, "{}", element)?;
count += 1;
}
write!(writer, "]")?;
Ok(())
}
}
struct A(i64, i32);
#[allow(non_snake_case)]
fn serialize_A_to_json<W: Write>(a: A, writer: W) -> Result<()> {
let int_serializer = ToJson;
let A(x, y) = a;
let seq = vec![x, y as i64];
int_serializer.serialize_int_seq(seq, writer)?;
Ok(())
}
この方法により、例えば YAML へと serialize するときは、YAML 向けの ToySerializer
を実装し、serialize_A_to_json
内部で ToJSON
の代わりにそれを使えばいいことになります。playground へのリンク
use std::io::{Result, Write};
trait ToySerializer {
fn serialize_int_seq<W: Write>(self, a: impl IntoIterator<Item = i64>, writer: W) -> Result<()>;
}
struct ToYaml;
impl ToySerializer for ToYaml {
fn serialize_int_seq<W: Write>(self, a: impl IntoIterator<Item = i64>, mut writer: W) -> Result<()> {
for element in a {
write!(writer, "- {}\n", element)?;
}
Ok(())
}
}
struct A(i64, i32);
#[allow(non_snake_case)]
fn serialize_A_to_yaml<W: Write>(a: A, writer: W) -> Result<()> {
let int_serializer = ToYaml;
let A(x, y) = a;
let seq = vec![x, y as i64];
int_serializer.serialize_int_seq(seq, writer)?;
Ok(())
}
A
に関する実装を少なくするために、 ToySerializer
を外部から受け取る方式にしてもよいでしょう。playground へのリンク
use std::io::{Result, Write};
trait ToySerializer {
fn serialize_int_seq<W: Write>(self, a: impl IntoIterator<Item = i64>, writer: W) -> Result<()>;
}
// JSON 特有の処理。serialize されるデータ型は関係ない。
struct ToJson;
impl ToySerializer for ToJson {
fn serialize_int_seq<W: Write>(self, a: impl IntoIterator<Item = i64>, mut writer: W) -> Result<()> {
write!(writer, "[")?;
let mut count = 0;
for element in a {
if count >= 1 {
write!(writer, ",")?;
}
write!(writer, "{}", element)?;
count += 1;
}
write!(writer, "]")?;
Ok(())
}
}
// YAML 特有の処理。serialize されるデータ型は関係ない。
struct ToYaml;
impl ToySerializer for ToYaml {
fn serialize_int_seq<W: Write>(self, a: impl IntoIterator<Item = i64>, mut writer: W) -> Result<()> {
for element in a {
write!(writer, "- {}\n", element)?;
}
Ok(())
}
}
struct A(i64, i32);
#[allow(non_snake_case)]
fn serialize_A<S: ToySerializer, W: Write>(a: A, int_serializer: S, writer: W) -> Result<()> {
let A(x, y) = a;
let seq = vec![x, y as i64];
int_serializer.serialize_int_seq(seq, writer)?;
Ok(())
}
fn main() {
serialize_A(A(4,3), ToJson, std::io::stdout()).unwrap(); // `[4,3]` が出力される
println!();
serialize_A(A(4,3), ToYaml, std::io::stdout()).unwrap(); // `- 4\n- 3\n` が出力される
}
ここまでくると、本物の serde の設計にかなり近くなります。ただ、まだ不満点が残ります。
- serialize_A で
Vec
を作っているが、元のserialize_A_to_json
にはそんなものはなかった。 - int だけ特別扱いせずに、serialize できるものをまとめたトレイトが欲しい。(仮にそのトレイトが
ToySerialize
という名前だとする。) - int の配列だけでなく
ToySerialize
の配列を扱おうとすると、引数として<T: ToySerialize> impl IntoIterator<Item = T>
のようなものを持つ必要が出てくる。しかし、要素の型が全部同じとは限らないので、それはできない。よって、ToySerialize は object safe なトレイトではない (serialize を行うメソッドが必然的に型パラメータimpl Iterator<Item = Box<dyn ToySerialize>>
みたいな型を受け取るより他ないと思われるが、これだと box 化による実行時オーバヘッドのせいで多少重くなる。S: ToySerializer
を持つ) ため、このような方法はそもそも使えない。(2020-08-07 追記)
serialize_A で Vec を作りたくない
これについては、ジェネリックなシークエンスを扱うためにどうする?
でまとめて扱います。
int だけ特別扱いせずに、serialize できるものをまとめたトレイトが欲しい
これは、serialize_A
のシグニチャを真似て、以下のようなトレイトを定義すればいいでしょう。
trait ToySerialize {
fn serialize<S: ToySerializer, W: Write>(self, serializer: S, writer: W) -> Result<()>;
}
ジェネリックなシークエンスを扱うためにどうする?
上の実装では int のシークエンスに決め打ちしてメソッドを定義していました。実際の利用時には、 serialize できる全ての型について serialize できるようにしたいでしょう。単純に思いつくのは、以下のようなメソッドを定義することです。
fn serialize_seq<T: ToySerialize, W: Write>(self, a: impl IntoIterator<Item = T>, writer: W) -> Result<()>;
しかし、これでは要素の型がすべて同じではない配列 ([2, "a", {}]
など) の serialize に失敗してしまいます。上の構造体の例でいうと、フィールドの型が同じである保証はどこにもないので、struct A(i64, String)
のような型で使うことはできません。ちなみに、こうした要素の型がすべて同じではない配列のことを、ヘテロジーニアスな配列と呼びます。対義語はホモジーニアスです。
次に思いつくのは、Box
で包むことでしょう。
fn serialize_seq<W: Write>(self, a: impl IntoIterator<Item = Box<dyn ToySerialize>>, writer: W) -> Result<()>;
これはヘテロジーニアスな配列を問題なく受け取れますが、 ToySerialize は object safe なトレイトではない (Box
で包んでいるために動的ディスパッチが発生し、遅いという問題があります。元の serailize_A_to_json
にはこのようなオーバヘッドはなかったはずです。serialize
が型パラメータ S: ToySerializer, W: Write
を持つ) ため、このような方法はそもそも使えません。(2020-08-07 追記)
そこでどうするか? この問題は serializer 側でリストの要素の処理をさせようとしたことによって発生したので、serialize 側でリストの要素の処理をさせればよいのです!
serialize 側でリストの要素の処理をさせるために、考えるべきことは以下の 2 個です。
- serialize 側は、要素を
Vec
に包んで渡す代わりに、要素を渡して"消費"してもらえるオブジェクトを受け取り、そのオブジェクトに要素を次々に渡していく。要素を消費するメソッドはToySerialize
を実装している任意のオブジェクトを受け取れないといけないので、必然的にfn name<T: ToySerialize>(self, element: T, ...)
というシグニチャを持つことになる。この要請をToySeqSerializer
というトレイトで表すことにし、要素の消費をするメソッドの名前をreceive_an_element
とする。 - JSON の出力を見ればわかるように、データフォーマットの種類によっては、要素の消費の開始時と終了時に特別な処理をする必要があることもある。開始時はどちらにせよ
ToySeqSerializer
を返す必要があるのでついでにやればよいが、終了時には明示的に処理を行う必要がある。これをToySeqSerializer
のfinish_receiving
というメソッドにやらせることにする。
また、i32
, i64
, A
に ToySerialize
を実装します。i32
と i64
は今後 ToySeqSerializer
で使うため、 A
には実装する必要はないが、できるならした方が扱いやすいためです。
以上の工夫を実装したコードは以下のようになります。playground へのリンク
use std::io::{Result, Write};
trait ToySerializer {
type Seq: ToySeqSerializer;
fn serialize_seq<W: Write>(self, writer: W) -> Result<Self::Seq>;
}
// 要素を渡すと serialize してくれる。要素を渡し終わったら finish_receiving を呼ぶ。
trait ToySeqSerializer {
fn receive_an_element<T: ToySerialize, W: Write>(&mut self, element: T, writer: W) -> Result<()>;
fn finish_receiving<W: Write>(self, writer: W) -> Result<()>;
}
trait ToySerialize {
fn serialize<S: ToySerializer, W: Write>(self, serializer: S, writer: W) -> Result<()>;
}
impl ToySerialize for i64 {
fn serialize<S: ToySerializer, W: Write>(self, _serializer: S, mut writer: W) -> Result<()> {
write!(writer, "{}", self)
}
}
impl ToySerialize for i32 {
fn serialize<S: ToySerializer, W: Write>(self, _serializer: S, mut writer: W) -> Result<()> {
write!(writer, "{}", self)
}
}
// JSON 特有の処理。serialize されるデータ型は関係ない。
struct ToJson;
struct SeqToJson { first: bool }
impl ToySerializer for ToJson {
type Seq = SeqToJson;
fn serialize_seq<W: Write>(self, mut writer: W) -> Result<Self::Seq> {
write!(writer, "[")?;
Ok(SeqToJson { first: true })
}
}
impl ToySeqSerializer for SeqToJson {
fn receive_an_element<T: ToySerialize, W: Write>(&mut self, element: T, mut writer: W) -> Result<()> {
if !self.first {
write!(writer, ",")?;
}
element.serialize(ToJson, writer)?;
self.first = false;
Ok(())
}
fn finish_receiving<W: Write>(self, mut writer: W) -> Result<()> {
write!(writer, "]")
}
}
// YAML 特有の処理。serialize されるデータ型は関係ない。
struct ToYaml;
struct SeqToYaml;
impl ToySerializer for ToYaml {
type Seq = SeqToYaml;
fn serialize_seq<W: Write>(self, _writer: W) -> Result<Self::Seq> {
Ok(SeqToYaml)
}
}
impl ToySeqSerializer for SeqToYaml {
fn receive_an_element<T: ToySerialize, W: Write>(&mut self, element: T, mut writer: W) -> Result<()> {
write!(writer, "- ")?;
element.serialize(ToYaml, &mut writer)?;
write!(writer, "\n")?;
Ok(())
}
fn finish_receiving<W: Write>(self, _writer: W) -> Result<()> {
Ok(())
}
}
// データ型固有の処理。
struct A(i64, i32);
impl ToySerialize for A {
fn serialize<S: ToySerializer, W: Write>(self, serializer: S, mut writer: W) -> Result<()> {
let A(x, y) = self;
let mut accessor = serializer.serialize_seq(&mut writer)?;
accessor.receive_an_element(x, &mut writer)?;
accessor.receive_an_element(y, &mut writer)?;
accessor.finish_receiving(&mut writer)?;
Ok(())
}
}
// 実際の使い方
fn main() {
A::serialize(A(4,3), ToJson, std::io::stdout()).unwrap(); // `[4,3]` が出力される
println!();
A::serialize(A(4,3), ToYaml, std::io::stdout()).unwrap(); // `- 4\n- 3\n` が出力される
}
serialize 側に Write いる?
いらないです。全部 serializer に押し付けましょう。やるべきことは以下の4つです。
-
ToySerialize
,ToySerializer
,ToySeqSerializer
すべてからWrite
への言及を消す。そのかわり、ToJson
とToYaml
にWrite
のインスタンスを持たせる。 -
Write
への言及が消えたことで、エラーがstd::io::Error
だけとは限らなくなった。このため、Serializer
にError
型を定義し、エラー時にはそれを返すことにする。 - 「
Write
に書き出す」以外の処理をさせたくなるかもしれない。そのために、成功時に任意の値を返せるようにする。そのため、Serializer
にOk
型を定義し、成功時にはそれを返すことにする。 -
i64
,i32
のToySerialize
実装がWrite
を参照できなくなったので、serializer に serialize してもらうしかなくなった。このため、ToySerializer
にserialize_i64
,serialize_i32
というメソッドを追加する。とはいえ、整数型を serialize する方法を知っているのは serializer だけなので、これはあるべき姿と言える。
以上を実装すると以下のようになります。だいぶ複雑になりましたね。playground へのリンク
use std::io::Write;
trait ToySerializer {
type Ok;
type Error;
// ToySeqSerializer でも同じエラー型を使うことを強制する。呼び出し側のエラー処理を楽にするため。
type Seq: ToySeqSerializer<Ok = Self::Ok, Error = Self::Error>;
fn serialize_seq(self) -> Result<Self::Seq, Self::Error>;
fn serialize_i64(self, x: i64) -> Result<Self::Ok, Self::Error>;
fn serialize_i32(self, x: i32) -> Result<Self::Ok, Self::Error>;
}
// 要素を渡すと serialize してくれる。要素を渡し終わったら finish_receiving を呼ぶ。
trait ToySeqSerializer {
type Ok;
type Error;
fn receive_an_element<T: ToySerialize>(&mut self, element: T) -> Result<(), Self::Error>;
fn finish_receiving(self) -> Result<Self::Ok, Self::Error>;
}
trait ToySerialize {
fn serialize<S: ToySerializer>(self, serializer: S) -> Result<<S as ToySerializer>::Ok, <S as ToySerializer>::Error>;
}
impl ToySerialize for i64 {
fn serialize<S: ToySerializer>(self, serializer: S) -> Result<<S as ToySerializer>::Ok, <S as ToySerializer>::Error> {
serializer.serialize_i64(self)
}
}
impl ToySerialize for i32 {
fn serialize<S: ToySerializer>(self, serializer: S) -> Result<<S as ToySerializer>::Ok, <S as ToySerializer>::Error> {
serializer.serialize_i32(self)
}
}
// JSON 特有の処理。serialize されるデータ型は関係ない。
struct ToJson<W> { writer: W }
struct SeqToJson<W> { first: bool, writer: W }
impl<W: Write> ToySerializer for ToJson<W> {
type Ok = ();
type Error = std::io::Error;
type Seq = SeqToJson<W>;
fn serialize_seq(mut self) -> Result<Self::Seq, std::io::Error> {
write!(self.writer, "[")?;
Ok(SeqToJson { first: true, writer: self.writer })
}
fn serialize_i64(mut self, x: i64) -> Result<(), std::io::Error> {
write!(self.writer, "{}", x)
}
fn serialize_i32(mut self, x: i32) -> Result<(), std::io::Error> {
write!(self.writer, "{}", x)
}
}
impl<W: Write> ToySeqSerializer for SeqToJson<W> {
type Ok = ();
type Error = std::io::Error;
fn receive_an_element<T: ToySerialize>(&mut self, element: T) -> Result<(), std::io::Error> {
if !self.first {
write!(self.writer, ",")?;
}
element.serialize(ToJson { writer: &mut self.writer})?;
self.first = false;
Ok(())
}
fn finish_receiving(mut self) -> Result<(), std::io::Error> {
write!(self.writer, "]")
}
}
// YAML 特有の処理。serialize されるデータ型は関係ない。
struct ToYaml<W> { writer: W }
struct SeqToYaml<W> { writer: W }
impl<W: Write> ToySerializer for ToYaml<W> {
type Ok = ();
type Error = std::io::Error;
type Seq = SeqToYaml<W>;
fn serialize_seq(self) -> Result<Self::Seq, std::io::Error> {
Ok(SeqToYaml { writer: self.writer })
}
fn serialize_i64(mut self, x: i64) -> Result<(), std::io::Error> {
write!(self.writer, "{}", x)
}
fn serialize_i32(mut self, x: i32) -> Result<(), std::io::Error> {
write!(self.writer, "{}", x)
}
}
impl<W: Write> ToySeqSerializer for SeqToYaml<W> {
type Ok = ();
type Error = std::io::Error;
fn receive_an_element<T: ToySerialize>(&mut self, element: T) -> Result<(), std::io::Error> {
write!(self.writer, "- ")?;
element.serialize(ToYaml { writer: &mut self.writer })?;
write!(self.writer, "\n")?;
Ok(())
}
fn finish_receiving(self) -> Result<(), std::io::Error> {
Ok(())
}
}
// データ型固有の処理。
struct A(i64, i32);
impl ToySerialize for A {
fn serialize<S: ToySerializer>(self, serializer: S) -> Result<<S as ToySerializer>::Ok, <S as ToySerializer>::Error> {
let A(x, y) = self;
let mut accessor = serializer.serialize_seq()?;
accessor.receive_an_element(x)?;
accessor.receive_an_element(y)?;
let result = accessor.finish_receiving()?;
Ok(result)
}
}
// 実際の使い方
fn main() {
A::serialize(A(4,3), ToJson { writer: std::io::stdout() }).unwrap(); // `[4,3]` が出力される
println!();
A::serialize(A(4,3), ToYaml { writer: std::io::stdout() }).unwrap(); // `- 4\n- 3\n` が出力される
}
お疲れ様でした! これでほとんど余計なことをしなくなり、オーバーヘッドがなくなりました。また、 serialize の結果を Write
への書き込みだけでなく、他のことにすることも可能になりました。(例えば、受け取ったタームを serde_json::Value
のような項に変換するなど。)
マクロを用いてボイラープレートを自動生成
Rust には procedural macro という機能があり、コンパイラがパースした構文木に対してかなり広範な処理を行うことができます。自作 derive マクロもその一つです。
上の A
に対する ToySerialize
実装はほとんど典型的な処理しか行なっていないので、実装を自動で生成できたら楽です。実際に自動でできます。
serde の基本・答えあわせ
上で苦労して導いた実装が実際に合理的であることを見ていきましょう。
実際の serde
では、データ型側は Serialize
を実装して、データフォーマット側は Serializer
を実装するのでした。この2者が協働してデータの serialize を行うというのは第1回で説明した通りです。
Serialize
Serialize
を実装するためには、以下のメソッドを実装する必要があります:
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer;
このメソッドで受け取った serializer
に対して、自分のフィールドをこれこれこういう順番で、こういうフォーマットで serialize してほしい、という要求を出すわけです。要求の出し方は、Serializer
に定義されている serialize_xxx
という名前のメソッドを呼ぶことです。Serializer
には以下の30個3のメソッドが提供されています (ser/mod.rs)4:
// コメントなど省略
fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error>;
fn serialize_i8(self, v: i8) -> Result<Self::Ok, Self::Error>;
fn serialize_i16(self, v: i16) -> Result<Self::Ok, Self::Error>;
fn serialize_i32(self, v: i32) -> Result<Self::Ok, Self::Error>;
fn serialize_i64(self, v: i64) -> Result<Self::Ok, Self::Error>;
fn serialize_i128(self, v: i128) -> Result<Self::Ok, Self::Error> {
fn serialize_u8(self, v: u8) -> Result<Self::Ok, Self::Error>;
fn serialize_u16(self, v: u16) -> Result<Self::Ok, Self::Error>;
fn serialize_u32(self, v: u32) -> Result<Self::Ok, Self::Error>;
fn serialize_u64(self, v: u64) -> Result<Self::Ok, Self::Error>;
fn serialize_u128(self, v: u128) -> Result<Self::Ok, Self::Error> {
fn serialize_f32(self, v: f32) -> Result<Self::Ok, Self::Error>;
fn serialize_f64(self, v: f64) -> Result<Self::Ok, Self::Error>;
fn serialize_char(self, v: char) -> Result<Self::Ok, Self::Error>;
fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error>;
fn serialize_bytes(self, v: &[u8]) -> Result<Self::Ok, Self::Error>;
fn serialize_none(self) -> Result<Self::Ok, Self::Error>;
fn serialize_some<T: ?Sized>(self, value: &T) -> Result<Self::Ok, Self::Error>
fn serialize_unit(self) -> Result<Self::Ok, Self::Error>;
fn serialize_unit_struct(self, name: &'static str) -> Result<Self::Ok, Self::Error>;
fn serialize_unit_variant(
self,
name: &'static str,
variant_index: u32,
variant: &'static str,
) -> Result<Self::Ok, Self::Error>;
fn serialize_newtype_struct<T: ?Sized>(
self,
name: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error>
where
T: Serialize;
fn serialize_newtype_variant<T: ?Sized>(
self,
name: &'static str,
variant_index: u32,
variant: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error>
where
T: Serialize;
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error>;
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error>;
fn serialize_tuple_struct(
self,
name: &'static str,
len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error>;
fn serialize_tuple_variant(
self,
name: &'static str,
variant_index: u32,
variant: &'static str,
len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error>;
fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap, Self::Error>;
fn serialize_struct(
self,
name: &'static str,
len: usize,
) -> Result<Self::SerializeStruct, Self::Error>;
fn serialize_struct_variant(
self,
name: &'static str,
variant_index: u32,
variant: &'static str,
len: usize,
) -> Result<Self::SerializeStructVariant, Self::Error>;
とはいえこれらは Self::Ok
を返すか、上で扱ったような 「Serialize
を受け取って何かをするオブジェクト」を返すかのどちらかなので、取り立てて目新しいことはないでしょう。
ちなみに、serialize_seq
が返す SerializeSeq
は以下のようになっています (ser/mod.rs):
pub trait SerializeSeq {
/// Must match the `Ok` type of our `Serializer`.
type Ok;
/// Must match the `Error` type of our `Serializer`.
type Error: Error;
/// Serialize a sequence element.
fn serialize_element<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: Serialize;
/// Finish serializing a sequence.
fn end(self) -> Result<Self::Ok, Self::Error>;
}
ほとんど上の ToySeqSerializer
と同じです。このことからも、ゼロコスト抽象化を推し進めると必然的にこのような設計になるであろうということがわかります。
データ型 <=> 呼ばれる関数 の対応
Rust での表現 | データ型の例 | serialize 時に呼ばれる関数 | JSON での表現例 |
Serialize の実装場所 |
---|---|---|---|---|
整数型 | i64 | serialize_i64 | 1 |
ser/impls.rs @ serde |
newtype struct | struct A(i64) |
serialize_newtype_struct | 1 |
ser.rs @ serde_derive |
tuple struct | struct A(i64, i32, String) |
serialize_tuple_struct | [1, 2, "a"] |
ser.rs @ serde_derive |
named struct | struct A { x: i64, y: i32, z: String } |
serialize_struct | {"x": 1, "y": 2, "z": "a"} |
ser.rs @ serde_derive |
enum variant (newtype struct) | enum A { V(i64) } |
serialize_newtype_variant | {"V": 0} |
ser.rs @ serde_derive |
enum variant (tuple struct) | enum A { V(i64, i32, String) } |
serialize_newtype_variant | {"V": [1, 2, "a"]} |
ser.rs @ serde_derive |
enum variant (struct) | enum A { V { x: i64, y: i32, z: String }} |
serialize_newtype_variant | {"V": {"x": 1, "y": 2, "z": "a"}} |
ser.rs @ serde_derive |
serde-derive
で derive される Serialize
実装の例
struct B<T>(i64, i32, String, T);
に対して、#[derive(Serialize)]
をかけてみた結果です。
cargo-expand
で展開した結果なので、正確とは限りません。雰囲気だけ楽しんでください。
また、try!
マクロまで展開されていて読みにくいです。
なお、完全なコードは https://github.com/koba-e964/cargo-serde-test/tree/a7cc2c2130a36630ba592c9889bbf12922c942c7 にあります。
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std;
use serde::Serialize;
struct B<T>(i64, i32, String, T);
#[allow(non_upper_case_globals, unused_attributes, unused_qualifications)]
const _IMPL_SERIALIZE_FOR_B: () = {
#[allow(unknown_lints)]
#[allow(rust_2018_idioms)]
extern crate serde as _serde;
#[allow(unused_macros)]
macro_rules! try {
($ __expr : expr) => {
match $__expr {
_serde::export::Ok(__val) => __val,
_serde::export::Err(__err) => {
return _serde::export::Err(__err);
}
}
};
}
#[automatically_derived]
impl<T> _serde::Serialize for B<T>
where
T: _serde::Serialize,
{
fn serialize<__S>(&self, __serializer: __S) -> _serde::export::Result<__S::Ok, __S::Error>
where
__S: _serde::Serializer,
{
let mut __serde_state = match _serde::Serializer::serialize_tuple_struct(
__serializer,
"B",
0 + 1 + 1 + 1 + 1,
) {
_serde::export::Ok(__val) => __val,
_serde::export::Err(__err) => {
return _serde::export::Err(__err);
}
};
match _serde::ser::SerializeTupleStruct::serialize_field(&mut __serde_state, &self.0) {
_serde::export::Ok(__val) => __val,
_serde::export::Err(__err) => {
return _serde::export::Err(__err);
}
};
match _serde::ser::SerializeTupleStruct::serialize_field(&mut __serde_state, &self.1) {
_serde::export::Ok(__val) => __val,
_serde::export::Err(__err) => {
return _serde::export::Err(__err);
}
};
match _serde::ser::SerializeTupleStruct::serialize_field(&mut __serde_state, &self.2) {
_serde::export::Ok(__val) => __val,
_serde::export::Err(__err) => {
return _serde::export::Err(__err);
}
};
match _serde::ser::SerializeTupleStruct::serialize_field(&mut __serde_state, &self.3) {
_serde::export::Ok(__val) => __val,
_serde::export::Err(__err) => {
return _serde::export::Err(__err);
}
};
_serde::ser::SerializeTupleStruct::end(__serde_state)
}
}
};
fn main() {}
一つずつ見ていきましょう。
_IMPL_SERIALIZE_FOR_B
#[allow(non_upper_case_globals, unused_attributes, unused_qualifications)]
const _IMPL_SERIALIZE_FOR_B: () = { ... }
生成している箇所
これは B
についての自動生成された実装を隠蔽するためのブロックです。
この隠蔽は serde-rs#159 で導入されました。extern crate serde;
がトップレベル以外の場所に書かれていても良いようにするための変更ですが、ここでは触れません。
try!
マクロの自前定義
#[allow(unused_macros)]
macro_rules! try {
($ __expr : expr) => {
match $__expr {
_serde::export::Ok(__val) => __val,
_serde::export::Err(__err) => {
return _serde::export::Err(__err);
}
}
};
}
生成している箇所
コメントを読むとわかるように、この try!
マクロはコンパイル時間の短縮のための工夫です。into
を挟まないことで型推論の手間が少なくなり、コンパイルが速くなります。
トレイト境界
#[automatically_derived]
impl<T> _serde::Serialize for B<T>
where
T: _serde::Serialize,
生成している箇所
#[automatically_derived]
という属性と T: Serialize
という境界に目が行くと思います。順番に見ていきましょう。
#[automatically_derived]
という属性がついている要素は、未使用でも警告が出ません。参考
T: Serialize
という境界は bound.rs @ serde_derive で生成されます。bound::with_bound
は、構造体の型パラメータのうち、実際に serialize されるもの (つまり、 #[skip_serialize]
がついていないもの) すべてを見つけて、それらに Serialize
を要請する、という処理を行います。この場合、フィールドに T
があり、serialize されるので、T: Serialize
という境界が自動で付加されます。
begin
let mut __serde_state = match _serde::Serializer::serialize_tuple_struct(
__serializer,
"B",
0 + 1 + 1 + 1 + 1,
) {
_serde::export::Ok(__val) => __val,
_serde::export::Err(__err) => {
return _serde::export::Err(__err);
}
};
Serializer::serialize_tuple_struct
を呼んで、serialize を開始する処理が書かれています。
Serializer::serialize_tuple_struct
のシグニチャは以下のようになっています:
fn serialize_tuple_struct(
self,
name: &'static str,
len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error>;
今回の場合は name = "B", len = 4
です。name
が必要なのはそうだろうという気がします (構造体の名前 serialize したい場合があるはずなので) ですが、長さを serialize する必要があるというのは謎です。実は、serde-json では長さ 0 の場合とそれ以外の場合を場合分けしています。長さ 0 の場合は単に []
を出力して終わり、長さが 1 以上の場合は [
だけを出力して残りは別の状態 Compound::Map
に委ねています。
(追記: 例えば msgpack では長さを serialize しています。 (https://github.com/3Hren/msgpack-rust/blob/84fb34b57b6df35813779cd54735240ba1115558/rmp-serde/src/encode.rs#L531-L537))
serialize_field
match _serde::ser::SerializeTupleStruct::serialize_field(&mut __serde_state, &self.0) {
_serde::export::Ok(__val) => __val,
_serde::export::Err(__err) => {
return _serde::export::Err(__err);
}
};
match _serde::ser::SerializeTupleStruct::serialize_field(&mut __serde_state, &self.1) {
_serde::export::Ok(__val) => __val,
_serde::export::Err(__err) => {
return _serde::export::Err(__err);
}
};
match _serde::ser::SerializeTupleStruct::serialize_field(&mut __serde_state, &self.2) {
_serde::export::Ok(__val) => __val,
_serde::export::Err(__err) => {
return _serde::export::Err(__err);
}
};
match _serde::ser::SerializeTupleStruct::serialize_field(&mut __serde_state, &self.3) {
_serde::export::Ok(__val) => __val,
_serde::export::Err(__err) => {
return _serde::export::Err(__err);
}
};
生成している箇所
フィールドを1個ずつ調べて、serialize_field しています。serde-json では tuple_struct はシークエンスへと serialize されます。
end
_serde::ser::SerializeTupleStruct::end(__serde_state)
生成している箇所
始めたものは終えないといけません。これが最後に呼ばれる関数なので、Err
の場合に early return をする必要はありません。
まとめ
- Rust の強みである「単相化・インライン化を使ったゼロコスト抽象化」により、パフォーマンスを犠牲にすることなく serialize の実装を行うことができる。
今後の構成予定
今後は deserialize を解説していきたいと思っています。Deserialize を理解するには以下の物事を理解しなくてはならないため、少なくとも 3 倍は難しいです。
-
Visitor
パターン - in-place deserialization
- deserializer 本体
-
serde-json では Write を実装する型に対して書き込むという形式で serialize を実装しているため、今回もそれに倣いました。playground へのリンク ↩
-
実は serde_yaml は
Write
を受け取る設計ではなく、YAML 用の型を用意して、その型の値を吐き出す設計になっています。YAML ファイルを作るところは yaml-rust という別のライブラリに任せています。 ↩ -
serialize_none
とserialize_some
が同じOption
型という扱いなので、データ型は 29 個です。 ↩ -
serde のデータモデルは、公式には explicit なデータ型として提供されていません。 (serde-value というクレートにはありますが、例えば
serialize_unit_struct
に対応する enum variant がないなど、微妙に差異があります。) ↩