Read OSS

WTF とメモリ管理:WebKit の基盤ライブラリ

中級

前提知識

  • C++ のテンプレートとスマートポインタ(std::shared_ptr / std::weak_ptr の概念)
  • C++ のムーブセマンティクスと RAII
  • 第1回:WebKit を読み解く — アーキテクチャ概要とコードベースマップ

WTF とメモリ管理:WebKit の基盤ライブラリ

JavaScript エンジンから DOM 実装、IPC レイヤーに至るまで、WebKit のあらゆるコンポーネントは WTF(Web Template Framework)の上に成り立っています。第1回で確認したように、WTF はビルドスタックの第2層に位置し、bmalloc の直上に置かれます。コンテナ、スマートポインタ、文字列型、スレッドプリミティブを提供し、C++ 標準ライブラリの同等機能を置き換える(多くの場合、パフォーマンスでも上回る)存在です。

本記事では、WebKit 全体に浸透しているオーナーシップモデルを解説します。RefCountedRefRefPtrWeakPtr、そして ProtectedThis パターンを理解すれば、WebKit のほぼすべてのヘッダーファイルを迷わず読めるようになります。

なぜ WTF が存在するのか:標準ライブラリの置き換え

WebKit は現代的な C++ 機能の多くが生まれる以前から開発されてきました。しかし WTF が今も使われ続ける理由は、単なる歴史的経緯にとどまりません。

  • パフォーマンスの制御。 WTF::Vector はインラインストレージバッファをサポートしており、小さいサイズではヒープアロケーションを回避できる。DOM ツリーの構築時に数百万もの小さなベクターが生成されることを考えると、極めて重要な最適化である。
  • セキュリティの強化。 標準コンテナは bmalloc の型ゾーンアロケーションと統合されていないが、WTF コンテナは WTF_MAKE_TZONE_ALLOCATED といったマクロを通じてこれを実現している。
  • 一貫したセマンティクス。 WTF::HashMap はロビンフッドハッシュを用いたオープンアドレッシングを採用しており、ノードベースの std::unordered_map に比べてキャッシュ局所性に優れている。
  • WebKit 固有のスレッド保証。 WTF のスマートポインタはデバッグモードでスレッドアサーションを持ち、開発時にスレッド間の不正な使用を検出できる。

公式の Introduction.md では、WTF について「Vector、HashMap、HashSet などの汎用コンテナクラスと、WebKit 全体で使用される Ref、RefPtr、WeakPtr などのスマートポインタ型を提供する」と説明されています。

ヒント: WebKit のコード中に std::unique_ptr のような標準ライブラリ型が直接使われているのを見かけたら、それは意図的な選択です。参照カウントが不要な単一オーナーのセマンティクスを表しています。参照カウントによる共有オーナーシップが必要な場合は、必ず Ref/RefPtr が使われます。

参照カウント:RefCounted、Ref、RefPtr

WebCore のほとんどのオブジェクトはガベージコレクションではなく、侵入的な参照カウントを使用します。基底クラスは RefCountedBase で、初期値 1 の uint32_t 参照カウントを保持します。

class RefCountedBase {
public:
    void ref() const { ++m_refCount; }
    bool hasOneRef() const { return m_refCount == 1; }
protected:
    bool derefBase() const {
        auto tempRefCount = m_refCount - 1;
        if (!tempRefCount) return true;  // caller should delete
        m_refCount = tempRefCount;
        return false;
    }
private:
    mutable uint32_t m_refCount { 1 };
};

テンプレートクラス RefCounted<T>RefCountedBase を継承し、カウントがゼロになったときにオブジェクトを削除する deref() メソッドを追加します。

template<typename T> class RefCounted : public RefCountedBase {
public:
    void deref() const {
        if (derefBase())
            delete const_cast<T*>(static_cast<const T*>(this));
    }
};

これらのオブジェクトを管理するスマートポインタは2種類あります。

classDiagram
    class RefCountedBase {
        +ref()
        +deref() 
        -uint32_t m_refCount
    }
    class RefCounted~T~ {
        +deref()
    }
    class Ref~T~ {
        -T* m_ptr
        +Ref(T& object)
        +operator*()
        +operator->()
    }
    class RefPtr~T~ {
        -T* m_ptr
        +RefPtr(T* ptr)
        +get() T*
        +operator bool()
    }
    RefCountedBase <|-- RefCounted
    RefCounted <.. Ref : "manages"
    RefCounted <.. RefPtr : "manages"
  • Ref<T>null 非許容。 構築時に ref() を呼び、破棄時に deref() を呼びます。デフォルト構築はできません。ポインタが必ず生存していると確信できる場面で使いましょう。
  • RefPtr<T>null 許容。 Ref<T> と同様に動作しますが、nullptr を保持できます。省略可能なオーナーシップに使います。

WebKit でよく使われるファクトリパターンは、プライベートコンストラクタと静的な create() メソッドの組み合わせです。

class MyObject : public RefCounted<MyObject> {
public:
    static Ref<MyObject> create() { return adoptRef(*new MyObject); }
private:
    MyObject() = default;
};

ここで adoptRef() の呼び出しが重要です。新たにアロケートされたオブジェクト(カウントは 1 から始まる)のオーナーシップを、カウントをインクリメントせずに Ref へ移譲します。adoptRef を省略すると、カウントが 2 になってメモリリークが発生します。

弱参照:WeakPtr と CanMakeWeakPtr

すべての参照がオブジェクトを生かし続ける必要はありません。WeakPtr<T> は非所有の参照を提供し、参照先のオブジェクトが破棄されると自動的に null になります。

クラスで弱参照を使えるようにするには、CanMakeWeakPtr<T> を継承します。

class MyWidget : public CanMakeWeakPtr<MyWidget> {
    // WeakPtr<MyWidget> can now point to instances of this class
};

内部実装はファクトリパターンを採用しています。CanMakeWeakPtrBaseWeakPtrFactory を保持しており、共有コントロールブロック(WeakPtrImpl)を遅延初期化します。オブジェクトが破棄されると、ファクトリは impl を null にし、以降すべての WeakPtr インスタンスの get()nullptr を返すようになります。

sequenceDiagram
    participant Owner as Owner Code
    participant WP as WeakPtr<T>
    participant Impl as WeakPtrImpl
    participant Obj as T (CanMakeWeakPtr)
    
    Owner->>Obj: Create WeakPtr
    Obj->>Impl: Lazily create impl
    Owner->>WP: Store WeakPtr
    WP->>Impl: Points to shared impl
    Note right of Obj: Object is alive
    Owner->>WP: wp.get()
    WP->>Impl: Read pointer
    Impl-->>WP: Returns T*
    Note right of Obj: Object destroyed
    Obj->>Impl: Nullify pointer
    Owner->>WP: wp.get()
    WP->>Impl: Read pointer
    Impl-->>WP: Returns nullptr

WTF はさらに WeakHashSet<T>WeakHashMap<K, V> も提供しています。破棄済みのエントリを自動的に除去するコレクションで、WebCore のオブザーバーパターン実装に広く活用されています。観察対象のオブジェクトがオブザーバーの破棄を妨げてしまうのを防ぐためです。

ヒント: ヘッダーのドキュメントには、WeakPtr は間接参照が1段多いため RefPtr よりパフォーマンスが劣ると明記されています。ホットパスでは RefPtrCheckedPtr を優先しましょう。

ProtectedThis パターン

WebKit のコードベースで頻繁に目にするパターンの一つが protectedThis です。これは、コールバック実行中にオブジェクトがメソッドの途中で自分自身を削除してしまうという、発見しにくい危険なバグを防ぐためのものです。

具体的な例を考えてみましょう。DOM コンテナノードが子ノードを削除するとき、その削除操作がミューテーションイベントを発火させます。イベントハンドラから任意の JavaScript が実行され、コンテナノード自身への最後の参照が手放されてしまう可能性があります。対策を施さないと、残りの処理が解放済みのメモリ上で走ることになります。

解決策はメソッドの冒頭に1行加えるだけです。

Ref<ContainerNode> protectedThis(*this);

これにより参照カウントが一時的にインクリメントされ、protectedThis がスコープを抜けるまで this の生存が保証されます。Introduction.md では ContainerNode::removeChild を正規の例として、このパターンの詳しい解説が記載されています。

sequenceDiagram
    participant Caller
    participant Container as ContainerNode
    participant JS as JavaScript Engine
    
    Caller->>Container: removeChild(oldChild)
    Container->>Container: Ref protectedThis(*this)<br/>refCount: 1→2
    Container->>Container: removeNodeWithScriptAssertion()
    Container->>JS: Dispatch mutation event
    JS->>JS: Script drops last external ref
    Note right of Container: refCount: 2→1 (still alive!)
    Container->>Container: dispatchSubtreeModifiedEvent()
    Container->>Container: ~protectedThis()<br/>refCount: 1→0→delete

このガードがなければ、JavaScript コールバックの実行中に参照カウントが 0 になり、dispatchSubtreeModifiedEvent() の呼び出しは解放済みメモリへのアクセスになります。

主要コンテナ:Vector と HashMap

WTF は WebKit の使用パターンに最適化された標準コンテナの代替実装を提供しています。

WTF::Vector は動的配列ですが、インラインストレージバッファを設定できる点が特徴です。Vector<int, 8> はベクターオブジェクト内に最大8要素を直接格納し、小さいサイズではヒープアロケーションを一切行いません。DOM 操作では小さな一時的ベクターが大量に生成されるため、ページ読み込み中に発生する無数のアロケーションを排除できます。

WTF::HashMap はカスタマイズ可能なハッシュトレイトを持つオープンアドレッシング方式を採用しており、std::unordered_map のチェーン方式よりもキャッシュ効率に優れています。KeyTraitsMappedTraits でテンプレート化されており、番兵値のためにメモリを無駄にすることなく、空/削除済みの値表現を細かく制御できます。

コンテナ 標準ライブラリの対応型 主な違い
WTF::Vector<T, N> std::vector<T> N 要素分のインラインバッファ、超過分はヒープへ
WTF::HashMap<K, V> std::unordered_map<K, V> オープンアドレッシング、ロビンフッドハッシュ
WTF::HashSet<T> std::unordered_set<T> HashMap と同じオープンアドレッシング戦略
WTF::String std::string 参照カウント付き、8ビット/16ビット表現、イミュータブル
WTF::Deque<T> std::deque<T> 循環バッファによる実装

bmalloc と IsoHeap:セキュリティ指向のアロケーション

スタックの最下層では、bmalloc が WebKit のカスタムメモリアロケータとして機能します。その最も重要なセキュリティ機能が IsoHeap(Isolated Heap)です。C++ の型ごとに専用のヒープページを確保する仕組みです。

これがなぜ重要なのでしょうか。Use-after-free(UAF)はブラウザのセキュリティ脆弱性として最も一般的なクラスです。攻撃者が UAF を引き起こす際、通常は解放されたメモリに別の型のオブジェクトを充填し、型の混同(type confusion) を生み出そうとします。HTMLInputElementJSFunction が同じヒープページを共有していれば、解放された input 要素のメモリを function オブジェクトとして再アロケートし、制御フローを乗っ取ることができます。

IsoHeap は、HTMLInputElement に使われたページには HTMLInputElement しか配置されない ことを保証することで、この攻撃を封じます。解放されたスロットは同じ型のオブジェクトにしか再利用されません。

オプトインには WTF_MAKE_TZONE_ALLOCATED マクロを使います。

class MySecuritySensitiveObject {
    WTF_MAKE_TZONE_ALLOCATED(MySecuritySensitiveObject);
public:
    // ...
};

USE(TZONE_MALLOC) が有効な場合(Apple プラットフォームではデフォルト)、すべてのアロケーションが bmalloc の型ゾーンアロケータ経由になります。無効な場合は FastMalloc にフォールバックします。

flowchart TD
    A["new HTMLInputElement"] --> B{USE_TZONE_MALLOC?}
    B -->|Yes| C["bmalloc TZoneHeap<br/>Type-segregated page"]
    B -->|No| D["FastMalloc<br/>General-purpose allocator"]
    C --> E["Page contains ONLY<br/>HTMLInputElement objects"]
    
    F["new JSFunction"] --> B2{USE_TZONE_MALLOC?}
    B2 -->|Yes| G["bmalloc TZoneHeap<br/>Different type-segregated page"]
    G --> H["Page contains ONLY<br/>JSFunction objects"]

WTF_MAKE_TZONE_ALLOCATED は WebCore と WebKit の重要なクラスのほぼすべてに登場します。たとえば LayoutContext クラスは 48 行目でこれを宣言しています。JavaScriptCore の Lexer はテンプレートクラスであるため、WTF_MAKE_TZONE_ALLOCATED_TEMPLATE を使用しています。

次回の記事へ向けて

ここまでで WebKit のオーナーシップの語彙を習得しました。生存が保証された非 null オーナーシップには Ref、null を許容するオーナーシップには RefPtr、非所有の監視には WeakPtr、そしてコールバック中の自己保護には ProtectedThis。これらのパターンはコードベースのあらゆる場所に登場します。

次の記事では、ビルドスタックをさらに1層上がり、WebKit の JavaScript エンジンである JavaScriptCore を取り上げます。JavaScript ソースファイルが4つの実行ティアを経て処理される過程を追っていきます。LLInt インタープリタ、Baseline JIT、投機的最適化を行う DFG JIT、そして B3 コンパイラを基盤とする最高スループットの FTL JIT です。今回解説した WTF の型群は、JSC が構築される土台そのものです。