一ヶ月でRustでexample.comをLivingStandardどおりにレンダリングできるブラウザを作った話
はじめに
Web周りの技術をちゃんと理解したり、字句解析/構文解析の練習のために、RustでHTML Living StandardとCSS仕様書に沿ったレンダリングエンジン、そしてついでにDNSとHTTPのクライアントを自作してみました
成果物
リポジトリ:
仕組み
- DNSクライアントで名前解決
- HTTPクライアントでHTMLソースコードを取得
- HTMLモジュールでDOMツリーを生成
- CSSモジュールでCSSOMを生成
- レンダリングモジュールで3, 4をもとにレンダリングツリーとレイアウトツリーを生成
- 5の生成物を使ってレンダリング
HTMLモジュール自作
実装は https://github.com/kntt32/ferrum/tree/main/magnetite/src/html です
ここにパースまわりは書かれています。
仕様書によれば、字句解析の前に2つのモジュールを通るそうです:
- ByteStreamDecoder : 送られてきたバイナリを文字コードとして解釈する部分です。簡単のためutf8として決め打ちしています
- InputStreamPreprocessor : ボムを消したり、改行コードを揃えます
字句解析 (Tokenize)
字句解析はステートマシンとして説明されていて、そのとおりに実装しました。
ステートには例えば次のようなものがあります:
- DataState: タグの開始を待っている状態
- TagOpenState: タグの開始を処理する状態
- TagNameState: タグの名前を処理する状態
例えば
<h1>Title</h1>
<p>ABC</p>
では、
StartTagToken(h1), CharacterToken(T), CharacterToken(i), CharacterToken(t), CharacterToken(l), CharacterToken(e), EndTagToken(h1),
StartTagToken(p), CharacterToken(A), CharacterToken(B), CharacterToken(C), EndTagToken(p),
みたいになります。実際はもう少し一つ一つのトークンの情報量は多いです。
構文解析 (TreeConstruction)
https://html.spec.whatwg.org/multipage/parsing.html#tree-construction にあります。
トークンをもとにDOMツリーを生成します。
HTMLの修復(<p>ABC<p>DEF
を<p>ABC</p><p>DEF</p>
にする)もここで行われます。
CSSモジュール自作
https://www.w3.org/TR/css-syntax-3/ と https://www.w3.org/TR/CSS22/
実装は https://github.com/kntt32/ferrum/tree/main/magnetite/src/css にあります
字句解析
これは「4.3.1. Consume a token」を起点として、必要なTokenizer Algorithmsを順に愚直に実装していくだけでできます。
例えば
h1 {
color: blue;
}
だと、
IdentToken(h1)
LCurlyToken
IdentToken(color)
ColonToken,
IdentToken(blue)
SemicolonToken
RCurlyToken
のように分解されます。
構文解析
スタイルシートを生成するぶんには 5.3.3. Parse a stylesheet をみれば十分です。
例えば、
h1 {
color: blue;
}
だと、
[
QualifiedRule {
prelude: [IdentToken(h1)],
block: [Ident(color), Colon, Ident(blue), Semicolon],
}
]
のようになります。
Cssom
この部分は割と仕様書を読まずに作ってしまった部分です。
いちおう同じように動作はするはずです。
例えば、
h1 {
color: blue;
}
だと、
[
Rule {
selector: [h1],
style: ["color": blue],
}
]
のようになります。
注意点があって、CssomのRuleには優先度があって、これは明確に規定されています。
https://developer.mozilla.org/ja/docs/Web/CSS/CSS_cascade/Cascade と、
https://developer.mozilla.org/ja/docs/Web/CSS/CSS_cascade/Specificity に書かれています。
かんたんに言うと、とりあえずブラウザのデフォルトのスタイルシートよりソースコード中のCSSのほうが重要度が高くて、
同じ重要度の中でも、詳細度によってRuleの優先度が決められる感じです。
レンダリングモジュール自作
実装は https://github.com/kntt32/ferrum/tree/main/magnetite/src/render にあります
この部分は仕様書とかがないので、ChatGPTに聞きながら設計して作りました
レンダリングツリー
DOMツリーにCSSOMからスタイルを適用したやつです。ここでは実際に描画するときのサイズや位置は決めません。
レイアウトツリー
レンダリングツリーをもとに、実際にどこに配置して、どのくらいのサイズにするのかを決めます。ここらへんは https://www.w3.org/TR/CSS22/visudet.html である程度決められています。
DNSクライアント自作
仕様書の日本語訳
実装は https://github.com/kntt32/ferrum/blob/main/copper/src/dns.rs にあります
DNSはバイナリベースのプロトコルです。
仕様書によれば、DNSパケットはこんな感じの構成になっているらしいです:
+---------------------+
| ヘッダー部 |
+---------------------+
| 問い合わせ部 | ネームサーバーへの問い掛け
+---------------------+
| 回答部 | 問い掛けに回答するRR
+---------------------+
| 権威部 | 権威を指し示すRR
+---------------------+
| 付加情報部 | 付加的な情報を保持するRR
+---------------------+
上記仕様書から引用
名前解決に必要なのはヘッダー部と問い合わせ部、回答部だけなので、その部分だけを実装しています。
ヘッダー部は「4.1.1. ヘッダー部のフォーマット」、問い合わせ部は「4.1.2. 問い合わせ部のフォーマット」、そして回答部は「4.1.3. リソースレコードのフォーマット」と「4.1.4. メッセージ圧縮」らへんに書かれています。
再帰的に回答部を見なければ名前解決できないのでめんどくさいですが、丸一日あれば余裕で実装できました
HTTPクライアント自作
実装は https://github.com/kntt32/ferrum/blob/main/copper/src/http.rs にあります
わざわざ原書を見るほどでもないので、とほほのHTTP入門を参考にしました
最低限の実装のためHTTP/1.1で、Keep-Aliveには対応させていません。HTTP/1.1はシンプルなのでここでは説明は省きます
その他
- これを突き進めば自作ブラウザがちゃんとできると思いますが、めんどくさいのでやりません
- example.comをレンダリングできることしか確かめてないので、他のサイトとかがレンダリングできるかは知りません
Discussion