Serdeのソースコードを読んでみる(Understanding Serde の翻訳)
翻訳するきっかけ
Zero to Production in Rust という、RustでWebサイトを作る本を読んでいました。その本を読むと、serdeはシリアライズ/デシリアライズするフレームワークのようなもので、JSONやYAMLなどファイルを実際にシリアライズ/デシリアライズするときは個別のクレートをインストールする必要があると知りました。もう少しSerdeについて知りたいと思ったのでこの本で紹介されている記事を翻訳することにしました。
2019-11-17 に書かれた記事なので実装が変わってたりしてます。
前書き
Serdeは人気のあるcrateの一つです。公式サイトには「Serdeはデータ構造を効率的かつ包括的に、シリアライズ/デシリアライズするためのフレームワーク」と書かれています。私にとって最も印象的なことは、Serdeのデータモデルが堅牢であることや、JSONやYAMLなどに対応していることもありますが、バイナリコードなどのバイナリ形式にも対応していることです。またSerdeが豊富な機能を持つ一方で、高いパフォーマンスが出せることも優れていると思います。
このブログではSerde(とSerdeデータフォーマットのエコシステム)が上記の機能をどのように実現しているかを掘り下げていきます。なお、この記事では「JSON」を「シリアライズ」する仕組みについてのみの扱っており、デシリアライの話題やJSON以外のフォーマットの場合の説明は省略しています。もしデシリアライズや他のフォーマットに興味がある場合は、この記事を読んだ後、ご自身で調べることができることでしょう。
Serde data model
私が使ったことがないライブラリを利用するとき、まずはじめにすることは、自分ならどのようにソースコードを実装するか予想を立てることです。予想に近いこともありますし、的外れなこともあります。今回の場合は大きく間違っていましたが、それも記事にすることで読者にとって理解の助けになると思うので書いておきます。
私はSerde data modelの記事を読んだあと、『データ構造とデータ形式が相互作用するためのAPI(The Serde data model is the API by which data structures and data formats interact)』と説明されていることから、Serdeについてのメンタルモデルはおおよそ次のようなものだと思いました。
Rust structure
↓
-- Serialize --> Structure in terms of the Serde data model
↓
-- Data format (JSON/バイナリコード/その他) --> Convert the Serde data model to the output format
まずはSerdeを使ったサンプルコードを書いておきます。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let point = Point{ x:1, y:2 };
// Point構造体をJSON文字列に変換
let serialized = serde_json::to_string(&point).unwrap();
// 出力結果: {"x":1, "y""2"}
println!("serialized = {}", serialized);
}
次に、Serdeのソースコードについて深掘りする前に、私ならどのような実装にするか予想した内容を示します。先ほどのイメージを上のサンプルコードに当てはめると、私は#[derive(Serialize)]
は次のようなコードを出力すると予想していました。
impl Serialize for Point {
fn serialize(&self) -> SerdeDataModel {
...
}
}
それから、serde_json::to_string
は次のようなものだと思ってました
fn to_string<T(input: T) -> String
where T: Serialize
{
let serde_data_model = input.serialize();
let mut output = String::new();
// Serde data model を使ってJSONを構築するコード
for elem in serde_data_model {
match elem {
struct(content) => // .. 構造体をJSONにシリアライズする
_ => // Serde data model で扱われている他のタイプを処理する
}
}
}
こんな風に実装されているのだろうと思い、答え合わせをしに実際のソースコードを読み進めました。Serde data model という大きなenum
が定義されていると思っていました。懸命な読者はもう気づいたかもしれませんが、そのようなenum
は見つかりませんでした。なぜならそんな実装はされていなかったからです。
コードを読む
先ほど立てたSerdeの実装の予想は間違っていたので、コードを少し覗いてみて、理解できるかどうか確かめました。しかしSerdeのソースコードはジェネリクスを大量に使用しており(その理由は十分理解できます)、Serde crate、 Serde data format crate、 Serde derive マクロにより生成されるコードを行き来する必要があるので、理解するのがとても難しかったです。その時点で、私がライブラリのコードを理解するときに次によく使う手法を取りました。ライブラリの中でよく使う関数を選び、その定義を起点にコードを遡っていくことです。
上記のサンプルコードに沿って、serde_json::to_string
の定義を見てみましょう。
// https://github.com/serde-rs/json/blob/10132f800fd1223ac698fa8c41b201dca152c413/src/ser.rs
// crate: serde_json
pub fn to_writer<W, T: ?Sized>(writer: W, value: &T) -> Result<()>
where
W: io::Write,
T: Serialize,
{
let mut ser = Serializer::new(writer);
try!(value.serialize(&mut ser));
Ok(())
}
pub fn to_vec<T: ?Sized>(value: &T) -> Result<Vec<u8>>
where
T: Serialize,
{
let mut writer = Vec::with_capacity(128);
try!(to_writer(&mut writer, value));
Ok(writer)
}
pub fn to_string<T: ?Sized>(value: &T) -> Result<String>
where
T: Serialize
{
let vec = try!(to_vec(value));
let string = unsafe {
// 無効なUTF-8を出力しない
String::from_utf8_unchecked(vec)
};
Ok(string)
}
serde_json
はシリアライズするJSONの利用方法に応じて、いくつかのエントリーポイントが実装されています。今回の場合、to_string()
のコードを遡りたいのですが、定義のすぐ後にto_vec()
が呼び出され、to_vec
も定義したあとすぐにto_writer()
が呼び出されます。これは興味深いことです。
to_writer()
の定義を見ると、まず初めにSerializer
の新しいインスタンスが作成されていることがわかります。Serializer
はio::Write
の所有権を持っていてます(本記事の場合、実態は &mut Vec<u8>
)。次に、value.serialize(&mut ser)
により、Serializer
への可変参照がPoint
構造体のserialize
メソッドに渡されます。
serialize
はSerialize
トレイトのメソッドです。Serdeトレイトの定義についてはこちらのページにありますが、この記事では#[derive(Serialize)]
属性によって生成されたPoint
構造体のトレイト実装をみていきましょう。cargo-expand
を使うと、deriveマクロの出力が見えるようになります。
// #[derive(Serialize)] マクロによって生成されるコード
use serde::{Serialize, Serializer, ser::SerializeStruct};
impl serde::Serialize for Point {
fn serialize<S>(&self, serializer: S) -> serde::export::Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut serde_state = match Serializer::serialize_struct(
serializer,
"Point", // 名前
false as usize + 1 + 1, // フィールド数
) {
serde::export::Ok(val) => val,
serde::export::Err(err) => {
return serde::export::Err(err);
}
};
match SerializeStruct::serialize_field(&mut serde_state, "x", &self.x) {
serde::export::Ok(val) => val,
serde::export::Err(err) => {
return serde::export::Err(err);
}
};
match SerializeStruct::serialize_field(&mut serde_state, "y", &self.y) {
serde::export::Ok(val) => val,
serde::export::Err(err) => {
return serde::export::Err(err);
}
};
SerializeStruct::end(serde_state)
}
}
可読性を高めるために、実際のコードを少し修正したことをご了承ください。Serdeは生成したコードがあらゆる環境下でも動くように、さまざまなテクニックを使っています。これらのテクニックは興味深いものですが、本筋からずれてしまうので説明は省略します。
しかし、1つだけ修正せずに残したテクニックがあります。それは、Serdeがトレイトメソッドを呼び出す方法です。メソッドの最初の行でSerializer::serialize_struct
を呼び出し、引数にserializer
を渡しています。serializer.serialize_struct
のように呼び出すのが一般的ですが、他に存在するかもしれないserialize_struct
メソッドと明確に区別するためにこのような書き方になっています。修正せずそのままにした理由は、もしこれを書き換えてしまうと、実際のソースコードとサンプルコードがあまりにもかけ離れてしまうのを避けたかったからです。
さて、コードの分析に戻りましょう。私たちは、serde_json
内の呼び出しを遡って、&point.serialize(&mut serializer)
までたどりました。このserializer
はserde_json
に固有のSerializer
トレイトの実装です。この関数で初めに行われることは、serializer
のserialize_struct
メソッドを呼び出し、名前やフィールドの数など、この構造体についての情報を引数に渡しています。もし他のプログラミング言語の経験があるなら、ここで引数として取っている情報は、実行時にリフレクションを通じて得られる型情報と似ていることに気づくでしょう。Rustでは型情報が実行時で利用できないので、これを回避するため、[derive(Serialize)]
マクロは高性能な回避策として存在しています。
原文
Getting back to our analysis now, we were tracing the call in serde_json
to &point.serialize
(&mut serializer
) where serializer is a serde_json
specific implementation of the Serializer
trait. The first thing that happens in this function is it calls the serialize_struct
method on the serializer, passing it some information about this struct (the name and the number of fields in the struct). If you are familiar with other programming languages, you may recognize this information as things you could get from a type at runtime via reflection. The #[derive(Serialize)]
macro exists basically as a high performance work around to the fact that this type information isn't available at runtime in Rust.
// https://github.com/serde-rs/json/blob/10132f800fd1223ac698fa8c41b201dca152c413/src/ser.rs#L427
// crate: serde-json
impl serde::Serialize for Serializer {
// (略)
fn serialize_struct(self, name: &'static str, len: usize) -> Result<Self::SerializeStruct> {
match name {
_ => self.serialize_map(Some(len)),
}
}
}
ご存知の通り、JSONには名前つきの構造体をシリアライズする方法がありません。そのため、serde_json
Serializerのserializer
メソッドは、単にself.serialize_map
を呼び出します。
impl serde::Serialize for Serializer {
// 多くのメソッドがあるが省略
fn serialize_map(self, len:Option<usize>) -> Result<Self::SerializeMap> {
if len == Some(0) {
// 空のJSONオブジェクトの '{}' を作成するコードがあるが省略
} else {
try!(self
.formatter
.begin_object(&mut self.writer)
.map_err(Error::io);
)
Ok(Compound::Map {
ser: self,
state: State::First,
})
}
}
}
// https://github.com/serde-rs/json/blob/10132f800fd1223ac698fa8c41b201dca152c413/src/ser.rs#L1852
trait Formatter {
// 多くのメソッドがあるが省略
fn begin_object<W: ?Sized>(&mut self, writer: &mut W) -> io::Result<()>
where
W: io::Write,
{
writer.write_all(b"{")
}
}
注意深い読者は、serialize_map
メソッドを呼び出す際に構造体のフィールド数(len
)を引数として渡していることに気づいたかもしれません。これは少し奇妙に感じるかもしれません。なぜなら、JSONをシリアライズするためにフィールド数の情報は必要ないからです。実際、フィールド数がゼロかどうかを調べるためにしかlen
は使われていません。
さて、最初のバイトをシリアライズする準備が整いました。self.formatter.begin_object
はVec<u8>
を指す可変参照をとり、開きかっこを表す文字{
を書き込みます。これはJSONのマップの開始を表します。
serialize_map
メソッドは、Compound::Map
のインスタンスを作成して終了します。このCompound::Map
は、serializer
自分自身と、状態を表すState
enumのState::First
を受け取っています。重要なのは、この戻り値の型が、serde::ser::SerializeStruct
トレイトを実装しているということです。
// #[derive(Serialize)] マクロにより作られたコード
use serde::{Serialize, Serializer, ser::SerializeStruct};
impl serde::Serialize for Point {
fn serialize<S>(&self, serializer: S) -> serde::export::Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut serde_state = match Serializer::serialize_struct(
serializer,
"Point",
false as usize + 1 + 1,
) {
serde::export::Ok(val) => val,
serde::export::Err(err) => {
return serde::export::Err(err);
}
};
match SerializeStruct::serialize_field(&mut serde_state, "x", &self.x) {
serde::export::Ok(val) => val,
serde::export::Err(err) => {
return serde::export::Err(err);
}
};
match SerializeStruct::serialize_field(&mut serde_state, "y", &self.y) {
serde::export::Ok(val) => val,
serde::export::Err(err) => {
return serde::export::Err(err);
}
};
SerializeStruct::end(serde_state)
}
}
serde::Serialize
のPoint
の実装に戻ります。説明しやすくするために、少し前に示した#[derive(Serialize)]
で生成されたコードを再掲しました。ここまででの話をまとめるとserde_state
の返り値は最終的にserde-json
のCompound::Map
であることがわかりました。次に、serialize_field
が2回呼び出され、その後end
が呼び出されますので、serialize_field
とend
の実装を見ていきましょう。
// https://github.com/serde-rs/json/blob/10132f800fd1223ac698fa8c41b201dca152c413/src/ser.rs#L755
// crate: serde-json
impl<'a, W, F> ser::SerializeStruct for Compound<'a, W, F>
where
W: io::Write,
F: Formatter,
{
type Ok = ();
type Error = Error;
fn serialize_field<T: ?Sized>(&mut self, key: &'static str, value: &T) -> Result<()>
where
T: Serialize,
{
match *self {
Compound::Map { .. } => {
try!(ser::SerializeMap::serialize_key(self, key));
ser::SerializeMap::serialize_value(self, value)
}
// 他のヴァリアントは省略
}
}
fn end(self) -> Result<()> {
match self {
Compound::Map { .. } => ser::SerializeMap::end(self),
// 他のヴァリアントは省略
}
}
}
Serializer
と同様にSerializeStruct
メソッドは単にSerializeMap
を呼び出して処理を任せているにすぎません。そこでSerializeMap
のソースコードを見ていきましょう。
// https://github.com/serde-rs/json/blob/10132f800fd1223ac698fa8c41b201dca152c413/src/ser.rs#L673
// crate: serde-json
impl<'a, W, F> ser::SerializeMap for Compound<'a, W, F>
where
W: io::Write,
F: Formatter,
{
type Ok = ();
type Error = Error;
fn serialize_key<T: ?Sized>(&mut self, key: &T) -> Result<()>
where
T: Serialize,
{
match *self {
Compound::Map {
ref mut ser,
ref mut state,
} => {
try!(ser
.formatter
.begin_object_key(&mut ser.writer, *state == State::First) // あとで説明
.map_err(Error::io));
*state = State::Rest;
try!(key.serialize(MapKeySerializer { ser: *ser }));
try!(ser
.formatter
.end_object_key(&mut ser.writer)
.map_err(Error::io));
Ok(())
}
// 他のenumのオプションは省略
}
}
fn serialize_value<T: ?Sized>(&mut self, value: &T) -> Result<()>
where
T: Serialize,
{
match *self {
Compound::Map { ref mut ser, .. } => {
try!(ser
.formatter
.begin_object_value(&mut ser.writer)
.map_err(Error::io));
try!(value.serialize(&mut **ser));
try!(ser
.formatter
.end_object_value(&mut ser.writer)
.map_err(Error::io));
Ok(())
}
// .. omitted other enum options
}
}
fn end(self) -> Result<()> {
match self {
Compound::Map { ser, state } => {
match state {
State::Empty => {}
_ => try!(ser.formatter.end_object(&mut ser.writer).map_err(Error::io)),
}
Ok(())
}
// 他のenumのオプションは省略
}
}
}
この部分は、コードが長すぎるため、コードを引用して説明するのではなくリンクを貼って説明します。リンクをクリックして適時コードを確認しながら説明を読んでください。
serialize_key
メソッドから遡ってここまできました。まず初めに着目すメソッドはフォーマッターにあるbegin_object_key
メソッドです。
/// https://github.com/serde-rs/json/blob/10132f800fd1223ac698fa8c41b201dca152c413/src/ser.rs#L1871
/// Called before every object key.
#[inline]
fn begin_object_key<W: ?Sized>(&mut self, writer: &mut W, first: bool) -> io::Result<()>
where
W: io::Write,
{
if first {
Ok(())
} else {
writer.write_all(b",")
}
}
興味深いことに、このメソッドはコード7で引数として使用したState
enumを使用して、Vec<u8>
に,
を書き込む必要があるかどうかを判断しています。(コード11にあるように、最初のフィールドの場合はコンマを書き込む必要はないのでOk()
を返している。そうでなければ,
を書き込んでいる。)
次に、key.serialize
を呼び出し、MapKeySerializer
を引数にとっています。MapKeySerializer
はSerializer
を実装しています。全てのケースでkey
は &'static str
です(この事実はimpl Serialize Struct for Compound
ブロックを見ると確認できます)。直感的な説明をすると、SerializeStruct
トレイトは構造体をシリアライズするためのトレイトであり、そもそも構造体のフィールド名はコンパイル時には既知である必要があります。よって、serialize_field
メソッドの中でキーとして渡されるのフィールド名は&' static str
型であるはずです。
key.serialize
ではMapKeySerializer
インスタンスを作成し、引数としています。そこではserializer.serialize_str
が呼び出されます。これはimpl Serialize for str
ブロックに示されていますが、これは大元のserializerのserialize_str
メソッドに呼び出され、実際のバイトx
をVec<u8>
に書き込むためのformat_escaped_str
が呼び出されます。
原文
key.serialize
immediately calls back to our MapKeySerializer
with serializer.serialize_str
as shown in the impl Serialize for str
block which dispatches back to our root serializer's serialize_str
method which itself calls format_escaped_str
to write the actual bytes, "x", to our Vec<u8>.
// https://github.com/serde-rs/json/blob/10132f800fd1223ac698fa8c41b201dca152c413/src/ser.rs#L1886
fn end_object_key<W: ?Sized>(&mut self, _writer: &mut W) -> io::Result<()>
where
W: io::Write,
{
Ok(())
}
serialize_key
メソッド内部では、最後にformatterのend_object_key
メソッドを呼び出します。このメソッドは何もしません。
fn begin_object_value<W: ?Sized>(&mut self, writer: &mut W) -> io::Result<()>
where
W: io::Write,
{
writer.write_all(b":")
}
もし興味があるなら、serialize_valalue
のはじめに呼び出されるbegin_object_value
メソッドを参照してください。begin_object_value
メソッドでは、JSONマップのキーとバリューの間にコロンを挿入する機能を持ちます。
原文
The serialize_key
method ends with a call to our formatter's end_object_key
method, which does nothing. If you are curious, it is the begin_object_value
method, called at the start of serialize_value
which writes the colon that is required between the key and the value in JSON maps.
この時点で作業が少し同じことの繰り返しになってきます。serialize_value
メソッドは、serialize_key
メソッドとほぼ同じ機能を持ちます。その後、両方のメソッドがPoint
構造体のy
フィールドに対してx
フィールドと同様の処理が行われ、フォーマッターが}
を挿入します。
原文
At this point things start getting a bit repetitive. The serialize_value method works nearly identically to the serialize_key method. Then both methods are repeated for the y field on our Point, then we ask the formatter to print the closing curly brace.
結論
書き終えて気づいたのですが、この記事は「why」というより「how」の記事になってしまいました。あまり満足いかなかった読者もいるかもしれません。私は機械的にソースコードを辿ることはできますが、概念的なレベルで慣れてきはじめたばかりです。物事についてなぜそうなっているかを完全に理解するにはもう少し時間をかけて考える必要があります。
ところで、ソースコードを読む前に私が想像したソースコードのイメージについてはどうでしょうか?この経験から得られたことは、Serdeはとてもパフォーマンスに焦点を当てているということです。私が初めにイメージした通りに実装すると、仲介する構造体が必要になっていたことでしょう。これはSerdeの実際のコードと比べると、パフォーマンスを下げる原因になりかねません。
最初に見落としていたことは、Serdeのデータモデルが構造体やenum
型の形ではなく、代わりに各データフォーマットごとにSerializer
トレイトが実装された関数の形で存在することでした。deriveマクロはSerializeトレイト(Serializerトレイトではない)の実装を生成し、これによってシリアライザは、シリアライズ化されるRustのデータ構造の種類に応じてシリアライざに適切なメソッドを呼び出すことで駆動されます。これが実装の詳細でした。
原文
The thing I missed originally was that the Serde data model doesn't come in the form of a struct or enum, but rather in the form of functions which are implemented by each data format as the Serializer trait. The derive macro generates an implementation of the Serialize (not Serializer) trait, which drives the serializer by calling the appropriate methods on the serializer based on the type of Rust data structure being serialized. Beyond that, its all implementation details.
Discussion