やかんです。

最近は日常投稿が多かったですね。

東大生やかんのブログ
やかん

恐れ入ります

ただやっぱり、しばらくは技術系のブログとしてこの「やかん観察日記」を育てていきたいと思います。というか、思い直しました。

ということで、自作ブラウザを作るのでその記録を残していきます!

東大生やかんのブログ
やかん

目標は、毎週ペースで自作ブラウザの進捗報告記事1つと、そのほか何か技術系記事1つ

自作ブラウザを作る

学生時代からずっと作りたいと思っていて、手を出せていなかったので今からやろうと思います。個人的領域がwebとセキュリティ(ネットワーク)なので、ブラウザはどちらも含むポジションとして魅力的です。

制作プラン

Opus4.5が賢すぎるのでね、、、彼にリクエストしながら作ってもらいました。それがこちら↓

# 自作ブラウザ研究プラン

## 研究の目的

「ネットワーク状況を加味した最適なレンダリング」を研究し、LLMが扱いやすい形で体験を記述できるフレームワークの可能性を探る。

### 背景にある問題意識

- 現在のフロントエンド(React等)は「どう見えるか」の記述に最適化されている
- 「どう届けるか」「いつ描くか」の情報が言語仕様に存在しない
- LLMがフロントエンドを生成する際、体験(ネットワーク・描画タイミング)が考慮されにくい構造になっている

### 研究のために必要な理解

1. ブラウザがHTTPでデータを受け取る仕組み
2. HTMLをパースしてDOMを構築する仕組み
3. DOMを画面に描画する仕組み
4. これらが「いつ・どの順序で」起きるか

---

## 開発環境

- OS: macOS
- 言語: C(低レイヤー理解)→ 後半でC++へ移行
- エディタ: 任意
- ビルド: Makefile

---

## 全体マップ

```
Phase 0: 共通基盤    →  ログ・計測の仕組みを用意する
Phase 1: 通信        →  HTTPの中身を自分の手で扱う
Phase 2: 解析        →  HTMLをインクリメンタルにパースする
Phase 3: 描画        →  構造を目に見える形にする
Phase 4: 統合        →  通信→解析→描画をつなげる
Phase 5: 実験        →  レンダリング戦略を比較する
```

---

## Phase別 成果物一覧

| Phase | 成果物 | 確認方法 |
|-------|--------|----------|
| 0 | ログモジュール | 各処理の時刻がログ出力される |
| 1 | HTTPクライアント | `./http_get http://example.com` でHTMLがターミナルに出る |
| 2 | HTMLパーサ | `./parse index.html` でDOM構造がツリー表示される |
| 3 | レンダラ | `./render index.html` でウィンドウにテキストが表示される |
| 4 | ミニブラウザ | `./browser http://example.com` でページが表示される |
| 5 | 実験環境 | 複数のレンダリング戦略を並べて比較できる |

---

## Phase 0: 共通基盤

全Phaseを通して使用するログ・計測の仕組みを用意する。

### ゴール

イベント発生時に時刻とイベント名がログ出力される。

### Steps

| Step | やること | 成果物 | 状態 |
|------|----------|--------|------|
| 0-1 | タイムスタンプ取得 | マイクロ秒精度で現在時刻を取得する関数 | ⬜ |
| 0-2 | ログ出力関数 | イベント名と時刻を出力する関数 | ⬜ |

### 記録するイベント(後のPhaseで追加)

- チャンク到着
- パース開始・終了
- レイアウト開始・終了
- 描画完了
- TTFB, FCP, LCP相当の指標

### 学べること

- 計測の基本
- 研究における定量評価の土台

---

## Phase 1: 通信

HTTPクライアントを自作し、ネットワークからデータを受け取る仕組みを理解する。

### ゴール

`./http_get http://example.com` を実行すると、HTMLがターミナルに表示される。

### Steps

| Step | やること | 成果物 | 状態 |
|------|----------|--------|------|
| 1-1 | TCP接続を理解する | サーバーに接続して文字を送受信 | ⬜ |
| 1-2 | HTTPリクエストを手書きする | GETリクエストを送りレスポンスを受け取る | ⬜ |
| 1-3 | レスポンスを整理する | ヘッダとボディを分離して表示 | ⬜ |
| 1-4 | チャンク到着を観測する | 到着時刻付きでデータを表示 | ⬜ |
| 1-5 | TLS対応(オプション) | HTTPSサイトにも接続できる | ⬜ |

### 学べること

- ソケットAPI(connect, send, recv)
- HTTPプロトコルの実際
- データが「いつ届くか」という感覚
- TLS/SSLの基礎(オプション)

---

## Phase 2: 解析

HTMLをインクリメンタルにパースしてDOM構造を構築する。

### 設計方針

- **インクリメンタルパース**: チャンク到着ごとにパースを進める
- パーサは状態を保持し、逐次的にDOMを構築する
- これにより「ネットワーク×レンダリング」の研究テーマと直結する

### ゴール

`./parse index.html` を実行すると、DOM構造がツリー形式で表示される。
また、チャンク単位で入力してもパースが進行する。

### Steps

| Step | やること | 成果物 | 状態 |
|------|----------|--------|------|
| 2-1 | HTMLトークナイザ | タグ・テキストをトークンに分解 | ⬜ |
| 2-2 | 状態保持型パーサ | 途中で中断・再開できるパーサ | ⬜ |
| 2-3 | DOMツリー構築 | トークンからツリー構造を構築 | ⬜ |
| 2-4 | ツリー表示 | 構築したDOMを見やすく出力 | ⬜ |

### 学べること

- 状態マシンによるパース
- インクリメンタル処理の設計
- 木構造のデータ表現(Cでの実装)
- HTML仕様の複雑さの一端

---

## Phase 3: 描画

DOMをウィンドウに描画する。最小のレイアウトアルゴリズムを含む。

### 設計方針

- **最小レイアウト**: blockレイアウト(縦積み)のみ
- padding / marginの基本は実装する
- inline、flexは対象外

### ゴール

`./render index.html` を実行すると、ウィンドウが開き、ブロック要素が縦に並んでテキストが表示される。

### Steps

| Step | やること | 成果物 | 状態 |
|------|----------|--------|------|
| 3-1 | ウィンドウを開く | SDL2で空のウィンドウを表示 | ⬜ |
| 3-2 | テキスト描画 | ウィンドウ内にテキストを表示 | ⬜ |
| 3-3 | 最小レイアウト | block要素の縦積み、padding/margin | ⬜ |
| 3-4 | DOM→描画 | DOMツリーを走査してレイアウト→描画 | ⬜ |

### 学べること

- SDL2の基本
- フォントレンダリングの基礎
- レイアウトアルゴリズムの入口
- 「構造→見た目」の変換

---

## Phase 4: 統合

Phase 1〜3を統合し、URLを指定するとページが表示されるミニブラウザを作る。

### ゴール

`./browser http://example.com` を実行すると、ウィンドウにページが表示される。

### Steps

| Step | やること | 成果物 | 状態 |
|------|----------|--------|------|
| 4-1 | パイプライン構築 | HTTP→パース→描画を接続 | ⬜ |
| 4-2 | 逐次処理 | データ到着→即パース→即描画 | ⬜ |

### 学べること

- コンポーネント間のデータの流れ
- 「ストリーミング」の実感

---

## Phase 5: 実験

ネットワーク遅延をシミュレーションし、レンダリング戦略の違いを視覚的に比較する。

### ゴール

複数のレンダリング戦略(到着即描画、DOM構築後描画など)を並べて表示し、体験の違いを視覚的に比較できる環境を作る。

### Steps

| Step | やること | 成果物 | 状態 |
|------|----------|--------|------|
| 5-1 | 遅延シミュレータ | 人工的にチャンク到着を遅延させる | ⬜ |
| 5-2 | 戦略実装 | 複数のレンダリング戦略を実装 | ⬜ |
| 5-3 | 計測統合 | Phase 0のログを使いTTFB/FCP/LCPを計測 | ⬜ |
| 5-4 | 比較UI | 複数戦略を並べて表示するUI | ⬜ |

### レンダリング戦略の例

- **到着即描画**: チャンクが届くたびに即座に描画
- **DOM構築後描画**: DOMが完成してから描画
- **閾値ベース**: 一定量のDOMが構築されたら描画開始
- **優先度ベース**: 重要な要素を先に描画

### 学べること

- ネットワーク×レンダリングの関係
- 「体験」を定量化する方法
- 研究の種になる発見

---

## 進捗管理

- ⬜ 未着手
- 🔄 進行中
- ✅ 完了

---

## メモ欄

(学んだこと、疑問、アイデアを随時記録)

うーん、すごすぎ。

Phase 0

ということで今日はフェーズ0です。C久しぶりだからな、、、基本的な文法にも立ち返りながらやっていきます。

上の手順書に倣えば、ロギングの関数群の作成が目標です。

関数群の作成であり、かつ共通基盤として今後も利用していきたいのでヘッダーファイル書くところから始めます。

東大生やかんのブログ
やかん

C言語の標準設計として、.hファイルには宣言のみを記述し、実装は.cファイルで行うというアーキテクチャが存在するそうです。不勉強ですね、、知りませんでした。

↑だから、.hファイルは「APIの仕様書」みたいに理解してあげるとよさそうですね。これ章として分けるか。

なぜ.hファイルに実装を含めないか?

端的に述べると理由は2つあって、

  1. 重複定義を避けたい
  2. コンパイルを「敢えて非効率に」したくない

です。というか、だと思います。手元で実験せず、gpt-5.1が言ってる内容に基づいているので悪しからず。

そもそも.hファイルをincludeしたときに行われるのは、「テキスト展開」のみだそうです。つまり、.hファイルに記述した宣言がそのまま展開されるだけで、このとき実装は含まれません。というか、実装という概念はケアしていないんじゃないかなあ、、、

実際に実装された関数本体と、その関数を呼び出している箇所はコンパイル後にリンカが結びつけるそうです。だから、プログラム上で「その関数の実体がどこにあるか」は指定しないんですね。

というわけなので、先ほどの2つの理由に立ち返ると、1つ目はなんとなくイメージ湧きますよね。リンカからしたら「実体複数あるけど、どれだよ」となります。テキスト展開が、対象関数が読まれる.cファイル全てで行われるので。

2つ目の理由についてはこちらも「テキスト展開だ」という点を強く意識してあげて、わざわざ分量を増やす必要はないよな、という気持ちになってきませんか。

東大生やかんのブログ
やかん

現状、この程度の理解で先に進もうと思います。しばらくしたら振り返って理解を再度心みたい。

今回作成したファイル

もちろん、0バイブコーディングです。と言っても、opus4.5が提示してくれたのを一生懸命手打ちしただけなんですけどね。。

今回作成したのは次の4つのファイル。

  1. log.h
  2. log.c
  3. test_log.c
  4. Makefile

まあmakefileはご愛嬌ですね。

まず、log.hがこちら。


/*
 * log.h - ログモジュール
 * 
 * 研究用ブラウザ Phase 0
 * イベントの発生時刻を記録し、後で分析できるようにする
 */
#ifndef LOG_H
#define LOG_H

#include <stdint.h>

/*
 * 現在時刻をマイクロ秒で取得
 * 戻り値: プログラム開始からの経過マイクロ秒
 */
 uint64_t log_now(void);

 /*
 * ログを初期化(プログラム開始時に1回呼ぶ)
 */
 void log_init(void);

 /*
 * イベントをログ出力
 * event: イベント名(例: "chunk_arrived", "parse_start")
 * detail: 追加情報(不要ならNULL)
 */
void log_event(const char *event, const char *detail);

/*
 * 数値付きイベントをログ出力
 * event: イベント名
 * value: 数値(バイト数など)
 */
 void log_event_num(const char *event, int64_t value);

 #endif /* LOG_H */

#ifndefあたりはもう定型というか、ヘッダーファイルを書くときの呪文みたいなものですね。重複宣言を避けたい意図です。

void log_event(const char *event, const char *detail);

この辺りはギョッとしますが、なんかもう言語仕様、と言う言葉に尽きる気がします。C言語に「文字列型(複数の文字からなる値)」は存在しないので、文字列を扱いたい場合は

  • 文字列の先頭ポインタを渡して「ここから」だよと伝える(「ここまでだよ」という終点についての情報は、渡した文字列に含有されます)
  • 文字の配列として表現する

の2通りです(少なくとも)。

そして、これにconstを付すことで実現される挙動は「ポインタが指し示す先の値の書き換えを禁止」となります。ちょっとこの辺は特に理解が浅いんですけどね〜、先に進みます。

また、後ほど登場しますがこの宣言に基づく関数は以下のように呼ばれます。

log_event("after_sleep", "100ms経過");

文字列は、内部的に始点となるポインタと終点として解釈されるので、呼び出し時に明示的にポインタを渡す必要がありません。

こうしてみるとC言語も十分高水準な言語な気がしてきます。

東大生やかんのブログ
やかん

理解が浅いところ多いけど先に進む!

お次はlog.cです。

/*
 * log.c - ログモジュール実装
 */
#include "log.h"
#include <stdio.h>
#include <sys/time.h>

/* プログラム開始時刻(マイクロ秒) */
static uint64_t start_time_us = 0;

/*
 * 現在時刻をマイクロ秒で取得(内部用)
 */
 static uint64_t get_time_us(void) {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return (uint64_t)tv.tv_sec * 100000 + tv.tv_usec;
 }

 /*
 * ログを初期化
 */
 void log_init(void){
    start_time_us = get_time_us();
    log_event("log_init", "logging started");
 }

 /*
 * プログラム開始からの経過時間(マイクロ秒)
 */
 uint64_t log_now(void) {
   return get_time_us() - start_time_us;
 }

 /*
 * イベントをログ出力
 * 形式: [経過時間ms] イベント名 (詳細)
 */
void log_event(const char *event, const char *detail) {
   uint64_t elapsed = log_now();
   double ms = elapsed / 1000.0;

   if(detail){
      printf("[%10.3f ms] %s (%s)\n", ms, event, detail);
   } else {
      printf("[%10.3f ms] %s\n", ms, event);
   }
}

/*
 * 数値付きイベントをログ出力
 */
 void log_event_num(const char *event, int64_t value) {
   uint64_t elapsed = log_now();
   double ms = elapsed / 1000.0;

   printf("[%10.3f ms] %s %lld\n", ms, event, (long long)value);
 }

まああんまり言うことないかな〜といったところ。

staticは曲者ですよね。使われる局面によって何パターンか意味がありますが、今回はファイル内に変数のスコープを限定する意味で使われています。

続いて、デバッグ用のtest_log.c。

/*
 * test_log.c - ログモジュールの動作確認
 */
#include "log.h"
#include <unistd.h> /* usleep用 */

int main(void) {
    /* ログ初期化 */
    log_init();

    /* いくつかのイベントをログ */
    log_event("test_start", NULL);

    /* 100ms待つ */
    usleep(100000);
    log_event("after_sleep", "100ms経過");

    /* 数値付きログ */
    log_event_num("bytes_received", 1024);

    /* もう少し待つ */
    usleep(50000);
    log_event_num("bytes_received", 2048);

    log_event("test_end", NULL);

    return 0;
}

まあ、呼んでるだけですね。出力はこんな感じ↓

make test
clang -Wall -Wextra -std=c11 -o test_log src/test_log.c src/log.c
./test_log
[     0.002 ms] log_init (logging started)
[     0.021 ms] test_start
[   105.051 ms] after_sleep (100ms経過)
[   105.077 ms] bytes_received 1024
[   160.103 ms] bytes_received 2048
[   160.131 ms] test_end

そしてMakefileがこちら。


# Research Browser - Makefile
# Phase 0: ログモジュール

CC = clang
CFLAGS = -Wall -Wextra -std=c11
SRCDIR = src

.PHONY: all clean test

all: test_log

test_log: $(SRCDIR)/test_log.c $(SRCDIR)/log.c
	$(CC) $(CFLAGS) -o $@ $^

test: test_log
	./test_log

clean:
	rm -f test_log

ということで、無事phase 0完了!

今回はブラウザというよりc言語のおさらいがメインでした。

※作成しているものはこちらのgithubに載せています。

ということで、以上!やっぱこれ系の技術系記事は書いてて楽しいですね。目指せ継続です。あと、前に着手したreact読破PJも進めたいところ。。

最後までお読みいただきありがとうございます!