The Multi-Process Architecture and IPC System
Prerequisites
- ›Article 1: Architecture Overview (multi-process model introduction)
- ›Understanding of inter-process communication concepts (message passing, serialization)
- ›C++ virtual dispatch patterns and template metaprogramming basics
The Multi-Process Architecture and IPC System
WebKit2's multi-process architecture is the project's most significant architectural innovation since the original KHTML fork. By running web content in isolated processes, WebKit ensures that a compromised web page can't access the user's file system, a crashed tab doesn't bring down the browser, and GPU driver bugs don't escalate into security vulnerabilities.
But multi-process architecture is only as good as its IPC system. This article examines WebKit's custom message-passing framework — from the .messages.in definition files through code generation to runtime dispatch — and traces a page load across process boundaries to show how it all fits together.
The Four Process Types
As introduced in Part 1, WebKit2 uses four process types. Let's now look at their implementation classes:
flowchart TD
subgraph UI["UI Process"]
APP["Host App (Safari)"]
WPP["WebPageProxy<br/>4,000+ LOC"]
NPP["NetworkProcessProxy"]
GPP["GPUProcessProxy"]
end
subgraph WC["WebContent Process"]
WP["WebPage<br/>wraps WebCore::Page"]
WCORE["WebCore engine"]
JSC_R["JSC runtime"]
end
subgraph NP["Network Process"]
NETP["NetworkProcess"]
NETC["NetworkConnectionToWebProcess"]
end
subgraph GP["GPU Process"]
GPUP["GPUProcess"]
GPUC["GPUConnectionToWebProcess"]
end
WPP <-->|"IPC::Connection"| WP
NPP <-->|"IPC::Connection"| NETP
GPP <-->|"IPC::Connection"| GPUP
WP <-->|"IPC::Connection"| NETC
WP <-->|"IPC::Connection"| GPUC
The UI Process is the application itself. It owns WebPageProxy — a proxy object that mirrors the state of a web page and forwards user actions via IPC. WebPageProxy implements IPC::MessageReceiver to handle responses from the WebContent process.
The WebContent Process runs WebPage, which wraps a WebCore::Page (the rendering engine) and implements both IPC::MessageReceiver and IPC::MessageSender. Each tab typically gets its own WebContent process.
The Network Process centralizes all HTTP networking. By routing all network requests through a single process, WebKit can enforce cookie policies, manage caches, and handle storage without giving WebContent processes direct network access.
The GPU Process handles GPU-accelerated operations. This isolates GPU driver code — historically a major source of security vulnerabilities — from both the WebContent sandbox and the UI process.
AuxiliaryProcess: The Uniform Process Lifecycle
All child processes (WebContent, Network, GPU) inherit from AuxiliaryProcess:
classDiagram
class IPC_Connection_Client {
<<interface>>
+didReceiveMessage()
+didClose()
}
class IPC_MessageSender {
<<interface>>
+send()
+sendSync()
#messageSenderConnection() Connection*
}
class AuxiliaryProcess {
+initialize()
+disableTermination()
+enableTermination()
+addMessageReceiver()
+removeMessageReceiver()
}
class WebProcess {
-WebPage pages
}
class NetworkProcess {
-NetworkSession sessions
}
class GPUProcess {
-GPUConnectionToWebProcess connections
}
IPC_Connection_Client <|-- AuxiliaryProcess
IPC_MessageSender <|-- AuxiliaryProcess
AuxiliaryProcess <|-- WebProcess
AuxiliaryProcess <|-- NetworkProcess
AuxiliaryProcess <|-- GPUProcess
The key design decision here is that AuxiliaryProcess inherits from both IPC::Connection::Client (to receive messages) and IPC::MessageSender (to send messages). This establishes a uniform lifecycle: initialize, receive and process messages, terminate.
The addMessageReceiver and removeMessageReceiver methods allow the process to dynamically register handlers for different message types. Messages are routed by ReceiverName (an enum identifying which subsystem should handle the message) and optionally by a destination ID (identifying a specific instance, like a particular web page).
The IPC Framework: Connection, Sender, Receiver
The core IPC infrastructure lives in Source/WebKit/Platform/IPC/. Three classes form the backbone:
IPC::Connection is a thread-safe, ref-counted IPC channel built on platform primitives (Mach ports on macOS, Unix domain sockets on Linux). It handles message serialization, deserialization, and dispatch.
IPC::MessageSender is an interface providing send() and sendSync() methods. The sender doesn't need to know the details of the connection — it just calls send(SomeMessage { args... }) and the framework handles serialization and delivery.
IPC::MessageReceiver is the receiving counterpart. It defines virtual methods didReceiveMessage(), didReceiveMessageWithReplyHandler(), and didReceiveSyncMessage(). Each receiver parses the incoming Decoder and dispatches to the appropriate handler.
Tip: When debugging IPC issues, the
Connection.hfile is your starting point. It defines the message dispatch thread, the sync message timeout behavior, and the error handling for when connections are interrupted.
Message Definitions and Code Generation
WebKit doesn't hand-write IPC dispatch code. Instead, it defines messages in .messages.in files using a custom IDL, and a Python script generates the C++ dispatch code.
Look at NetworkProcess.messages.in:
[
DispatchedFrom=UI,
DispatchedTo=Networking,
ExceptionForEnabledBy
]
messages -> NetworkProcess : AuxiliaryProcess WantsAsyncDispatchMessage {
InitializeNetworkProcess(struct WebKit::NetworkProcessCreationParameters ...) -> ()
CreateNetworkConnectionToWebProcess(...) -> (...) AllowedWhenWaitingForSyncReply
...
}
Several things to note:
- The header annotations (
DispatchedFrom=UI,DispatchedTo=Networking) document the message flow direction. - The
-> ()syntax defines reply messages (async by default). AllowedWhenWaitingForSyncReplyindicates messages that can be processed even when the process is blocked waiting for a synchronous reply.- Platform-specific messages are wrapped in
#if USE(SOUP)/#if USE(CURL)guards.
flowchart LR
MSG["NetworkProcess.messages.in"] --> PARSER["webkit/parser.py"]
PARSER --> MODEL["webkit/model.py<br/>(AST of messages)"]
MODEL --> GEN["generate-message-receiver.py"]
GEN --> HEADER["NetworkProcessMessages.h<br/>(message enums, structs)"]
GEN --> RECV["NetworkProcessMessageReceiver.cpp<br/>(dispatch switch)"]
The generate-message-receiver.py script uses webkit.parser to parse the .messages.in file, then webkit.messages to generate C++ code. The generated receiver contains a large switch statement that deserializes each message type and calls the corresponding handler method.
This code generation approach is critical: it ensures type-safe serialization (the compiler checks that message argument types match), eliminates boilerplate, and makes it easy to add new messages by editing a single .messages.in file.
The Proxy Pattern: WebPageProxy ↔ WebPage
The canonical example of WebKit2's architecture is the mirroring between WebPageProxy (UI process) and WebPage (WebContent process). Let's trace a user-initiated page load:
sequenceDiagram
participant User
participant WPP as WebPageProxy<br/>(UI Process)
participant IPC as IPC::Connection
participant WP as WebPage<br/>(WebContent Process)
participant WC as WebCore::Page
User->>WPP: loadURL("https://example.com")
WPP->>WPP: Update navigation state
WPP->>IPC: send(LoadURL { url, ... })
IPC->>WP: didReceiveMessage(LoadURL)
WP->>WC: mainFrame().loader().load(request)
WC-->>WP: didStartProvisionalNavigation
WP->>IPC: send(DidStartProvisionalLoadForFrame { ... })
IPC->>WPP: didReceiveMessage(DidStartProvisionalLoad)
WPP->>WPP: Update UI (show loading indicator)
Note over WC: Network process fetches HTML...
WC-->>WP: didCommitNavigation
WP->>IPC: send(DidCommitLoadForFrame { ... })
IPC->>WPP: didReceiveMessage(DidCommitLoad)
WPP->>WPP: Update URL bar, title
Several things are happening:
-
State mirroring —
WebPageProxymaintains its own copy of navigation state (current URL, title, loading progress) so the UI process can query it without IPC round-trips. -
Async by default — Most messages are asynchronous. The UI process sends
LoadURLand continues; it doesn't block waiting for the page to load. -
Bidirectional messaging —
WebPageProxysends commands toWebPage, andWebPagesends events back toWebPageProxy. Both sides implementMessageReceiver. -
WebCore isolation — The WebCore
Pageobject is only touched in the WebContent process. The UI process never directly accesses WebCore APIs.
This proxy pattern repeats throughout WebKit2. There are NetworkProcessProxy ↔ NetworkProcess, GPUProcessProxy ↔ GPUProcess, and many more specialized proxy pairs. The pattern is so consistent that once you understand one pair, you can navigate them all.
Tip: To find all IPC messages for a component, search for
.messages.infiles:find Source/WebKit -name "*.messages.in". There are dozens, and they serve as a complete index of the IPC surface area.
What's Next
We've now covered the architectural layers from memory management through DOM rendering to multi-process IPC. In Part 5, we'll descend into JavaScriptCore — the JavaScript engine — to trace how source code moves through four compilation tiers from interpretation to optimizing JIT, how the B3 compiler backend generates machine code, and how the Riptide garbage collector manages object lifetimes. The IPC patterns from this article will reappear when we discuss how JSC's garbage collector coordinates with WebCore's reference-counted objects.