Read OSS

WTF 与内存管理:WebKit 的基础库

中级

前置知识

  • C++ 模板与智能指针(std::shared_ptr / std::weak_ptr 的相关概念)
  • C++ 移动语义与 RAII
  • 第 1 篇:探索 WebKit —— 架构概览与代码库结构

WTF 与内存管理:WebKit 的基础库

WebKit 的每一个组件——从 JavaScript 引擎到 DOM 实现,再到 IPC 层——都建立在 WTF(Web Template Framework)之上。正如第 1 篇所介绍的,WTF 位于构建栈的第 2 层,直接处于 bmalloc 之上。它提供了容器类、智能指针、字符串类型和线程原语,既替代了 C++ 标准库中的对应设施,在许多场景下性能也更胜一筹。

本文将重点介绍贯穿整个代码库的所有权模型。只要深入理解 RefCountedRefRefPtrWeakPtr 以及 ProtectedThis 模式,你就能毫无障碍地读懂 WebKit 中几乎任何一个头文件。

WTF 存在的意义:替代标准库

WebKit 的诞生早于许多现代 C++ 特性。但 WTF 延续至今的原因,远不止历史惯性那么简单:

  • 性能可控。 WTF::Vector 支持内联存储缓冲区,对小尺寸数据可完全避免堆内存分配——在 DOM 树构建过程中需要创建数百万个小型 vector 时,这一优化至关重要。
  • 安全加固。 标准容器无法与 bmalloc 的类型隔离分配机制集成,而 WTF 容器可以,具体通过 WTF_MAKE_TZONE_ALLOCATED 等宏实现。
  • 语义一致。 WTF::HashMap 采用 Robin Hood 哈希的开放寻址方案,比基于链表节点的 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));
    }
};

管理这些对象的智能指针有两种:

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>不可为空。构造时调用 ref(),析构时调用 deref(),不能默认构造。当你确定指针指向一个存活对象时,使用它。
  • RefPtr<T>可为空。行为与 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> 提供了一种非持有引用,当所指对象被销毁后,它会自动变为空。

要让一个类支持弱指针,只需继承 CanMakeWeakPtr<T>

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

其内部实现采用工厂模式。CanMakeWeakPtrBase 持有一个 WeakPtrFactory,该工厂会懒初始化一个共享控制块(WeakPtrImpl)。当对象被销毁时,工厂将 impl 置空,所有现有的 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 头文件中明确指出,由于多了一层间接寻址,其性能比 RefPtr 差。在热路径中,优先选用 RefPtrCheckedPtr

ProtectedThis 模式

在 WebKit 代码库中,protectedThis 是最常见的模式之一。它专门用来防范一种隐蔽而危险的 bug:对象在回调执行过程中将自身销毁。

以 DOM 容器节点移除子节点为例。移除操作可能触发 mutation 事件,事件处理中可能执行任意 JavaScript 代码,而这段脚本可能恰好释放了容器节点的最后一个引用。若没有保护措施,方法后续的代码将在已释放的内存上继续执行。

解决方法很简单,在方法开头加上一行:

Ref<ContainerNode> protectedThis(*this);

这会临时递增引用计数,确保 this 在函数结束、protectedThis 离开作用域之前始终存活。Introduction.mdContainerNode::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() 的调用将造成 use-after-free 漏洞。

核心容器:Vector 与 HashMap

WTF 针对 WebKit 的使用场景,提供了一套经过优化的标准容器替代品。

WTF::Vector 是一个动态数组,其核心特性是可配置的内联存储缓冲区。Vector<int, 8> 可直接在 vector 对象内部存储最多 8 个元素,完全不涉及堆内存分配。由于许多 DOM 操作会创建大量小型临时 vector,这一设计在页面加载过程中可消除数以百万计的内存分配开销。

WTF::HashMap 采用支持自定义哈希特性的开放寻址方案,缓存表现优于 std::unordered_map 的链式结构。它以 KeyTraitsMappedTraits 为模板参数,允许精细控制空值与已删除值的表示方式,避免为哨兵值浪费额外内存。

容器 标准库对应 主要差异
WTF::Vector<T, N> std::vector<T> 内联存储 N 个元素,超出后溢出到堆
WTF::HashMap<K, V> std::unordered_map<K, V> 开放寻址,Robin Hood 哈希
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(隔离堆)——一种将每种 C++ 类型隔离到专属堆页面上的机制。

为什么这很重要?Use-after-free 是浏览器安全漏洞中最常见的一类。攻击者触发 UAF 后,通常会尝试用另一种对象类型填充已释放的内存,从而制造类型混淆。如果 HTMLInputElementJSFunction 共享同一批堆页面,攻击者就可以将已释放的 input element 内存重新分配为 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 的所有权词汇体系:Ref 用于保证存活的非空所有权,RefPtr 用于可空所有权,WeakPtr 用于非持有的观察引用,ProtectedThis 用于在回调中保护对象自身不被销毁。这些模式贯穿代码库的每一个角落。

下一篇文章将沿着构建栈向上一层,深入 JavaScriptCore——WebKit 的 JavaScript 引擎。我们将追踪一个 JavaScript 源文件经历完整四级执行流水线的全过程:LLInt 解释器、Baseline JIT、推测优化的 DFG JIT,以及由 B3 编译器驱动的最高吞吐量 FTL JIT。本文介绍的 WTF 类型,正是 JSC 赖以构建的基石。