# How the XPC Service Architecture Works in the Apple Container Framework

> Explore the XPC service architecture in Apple Container and understand how XPCServer, XPCClient, and XPCMessage facilitate type-safe communication across processes.

- Repository: [Apple/container](https://github.com/apple/container)
- Tags: internals
- Published: 2026-06-14

---

**The XPC service architecture in Apple Container implements a lightweight RPC framework using Mach services, where `XPCServer` manages incoming connections and route handlers, `XPCClient` establishes persistent sessions, and `XPCMessage` provides type-safe serialization across process boundaries.**

The Apple Container framework relies on secure cross-process communication to enable privileged operations from unprivileged client processes. Its XPC service architecture provides a Swift-native abstraction over macOS XPC primitives, offering async/await support and automatic error serialization. This implementation allows container components to communicate with system-level services while maintaining clear separation between client and server concerns.

## Server-Side Implementation: XPCServer and XPCServerSession

The server side of the XPC service architecture centers around two core types defined in [`Sources/ContainerXPC/XPCServer.swift`](https://github.com/apple/container/blob/main/Sources/ContainerXPC/XPCServer.swift) and [`Sources/ContainerXPC/XPCServerSession.swift`](https://github.com/apple/container/blob/main/Sources/ContainerXPC/XPCServerSession.swift).

### Setting Up the XPC Server

An `XPCServer` instance is initialized with a service identifier and a dictionary of route handlers. Each handler receives an `XPCMessage` and an `XPCServerSession`, returning a response `XPCMessage`. The server creates a Mach-service listener and exposes named routes that clients can invoke.

```swift
// Server side – register a route and start listening
import ContainerXPC
import Logging

let logger = Logger(label: "com.example.myservice")
let server = XPCServer(
    identifier: "com.example.myservice",
    routes: [
        "volume.create": XPCServer.route { message in
            // Extract arguments from the request
            let name = message.string(key: "name") ?? "default"
            // Perform the operation…
            // Return a reply message
            var reply = XPCMessage(route: "volume.create")
            reply.set(key: "result", value: "created \(name)")
            return reply
        }
    ],
    log: logger
)

try await server.listen()          // ← blocks, handling connections forever

```

### Handling Client Connections

The `listen()` method creates an `AsyncStream` that yields incoming `xpc_connection_t` objects from the Mach-service listener. For each new connection, the server instantiates a dedicated `XPCServerSession` and spawns a `handleClientConnection` task. This task reads XPC objects from the connection, wraps them in `XPCMessage`, and dispatches them to the matching route handler. After the handler finishes, the server sends the reply back on the same connection.

### Per-Connection Lifecycle Management

`XPCServerSession` manages the lifecycle of individual client connections. When a client disconnects, the session fires its disconnect callbacks, giving handlers a chance to clean up per-connection state such as resource tracking or temporary file handles. This ensures that even abrupt client terminations do not leak server-side resources.

## Message Abstraction with XPCMessage

Located in [`Sources/ContainerXPC/XPCMessage.swift`](https://github.com/apple/container/blob/main/Sources/ContainerXPC/XPCMessage.swift), this type provides a thin, thread-safe wrapper around the low-level `xpc_object_t`. It offers synchronized getters and setters for common data types including strings, booleans, integers, dates, and file handles.

The `reply()` method creates a new dictionary-based reply object that can be populated and sent back to the client. For error handling, the `set(error:)` method serializes `ContainerXPCError` payloads, while the client side uses `XPCMessage.error()` to reconstruct and re-throw the original error.

## Client-Side Implementation: XPCClient and XPCClientSession

The client side of the XPC service architecture, implemented in [`Sources/ContainerXPC/XPCClient.swift`](https://github.com/apple/container/blob/main/Sources/ContainerXPC/XPCClient.swift) and [`Sources/ContainerXPC/XPCClientSession.swift`](https://github.com/apple/container/blob/main/Sources/ContainerXPC/XPCClientSession.swift), provides complementary functionality for connecting to and communicating with XPC services.

### Establishing Connections

`XPCClient` opens a Mach-service connection using `xpc_connection_create_mach_service` and provides a `send(_:responseTimeout:)` method that returns a response `XPCMessage`. The client also supports a disconnect handler that is invoked when the server side terminates unexpectedly.

### Persistent Sessions and Disconnect Handling

A persistent session (`XPCClientSession`) can be created via `XPCClient.openSession()`. The session maintains a list of disconnect callbacks, guaranteeing that a server crash is detected before any messages are sent. This prevents client hangs on stale connections.

```swift
// Client side – open a persistent session and call the route
import ContainerXPC

let client = XPCClient(service: "com.example.myservice")
let session = client.openSession()

// Optional: react to server disconnects
await session.onDisconnect {
    print("Server vanished – clean up")
}

// Build a request
var request = XPCMessage(route: "volume.create")
request.set(key: "name", value: "myVolume")

do {
    let response = try await session.send(request)
    let result = response.string(key: "result")!
    print("Server replied:", result)
} catch {
    print("XPC error:", error)
}

```

## Error Handling and Serialization

Both sides of the XPC service architecture use `ContainerizationError` to represent protocol-level failures. When a route handler throws a `ContainerizationError`, the server serializes it into a `ContainerXPCError` payload using `XPCMessage.set(error:)`. The client parses the reply and calls `XPCMessage.error()` to re-throw the original error, propagating it to the caller while preserving type information across the process boundary.

## Concurrency Model and Thread Safety

The server implementation uses Swift concurrency with `withThrowingDiscardingTaskGroup` to process multiple connections and messages concurrently while preserving ordering per connection. Access to underlying XPC objects is synchronized with `NSLock` in `XPCMessage` or a `Mutex` in `XPCServerSession`, ensuring thread-safe mutation of shared state without blocking the global actor.

## Summary

- The XPC service architecture separates concerns into `XPCServer` for accepting connections and `XPCClient` for initiating requests across process boundaries.
- `XPCMessage` provides type-safe wrapping around `xpc_object_t` with automatic serialization of `ContainerizationError` via `ContainerXPCError` payloads.
- `XPCServerSession` and `XPCClientSession` manage per-connection lifecycle, offering disconnect callbacks for resource cleanup and crash detection.
- The implementation leverages Swift concurrency with `withThrowingDiscardingTaskGroup` and employs `NSLock` or `Mutex` for thread-safe access to XPC objects.
- Route handlers in [`Sources/ContainerXPC/XPCServer.swift`](https://github.com/apple/container/blob/main/Sources/ContainerXPC/XPCServer.swift) process requests asynchronously and return replies through the same Mach connection.

## Frequently Asked Questions

### What is the role of XPCServerSession in the Container XPC architecture?

`XPCServerSession` manages the lifecycle of individual client connections on the server side. It stores disconnect callbacks that fire when a client vanishes, giving route handlers the opportunity to clean up per-connection resources like file descriptors or temporary state. Each incoming `xpc_connection_t` gets its own session instance instantiated by `XPCServer.listen()`.

### How does XPCMessage handle error serialization between processes?

When a route handler throws a `ContainerizationError`, the server catches it and calls `XPCMessage.set(error:)` to serialize the error into a `ContainerXPCError` payload within the XPC dictionary. On the client side, `XPCMessage.error()` extracts and reconstructs the error, allowing the caller to catch the original exception type despite the cross-process boundary.

### What concurrency primitives does the XPC service use for thread safety?

The architecture uses `NSLock` within `XPCMessage` to synchronize access to the underlying `xpc_object_t`, while `XPCServerSession` employs a `Mutex` to protect its internal state. The server processes connections using `withThrowingDiscardingTaskGroup`, enabling concurrent handling of multiple clients while maintaining message ordering within each individual connection.

### How does XPCClientSession detect server disconnects before sending messages?

`XPCClientSession` maintains a list of disconnect callbacks that trigger when the underlying XPC connection terminates. When you call `openSession()`, the framework immediately registers for connection notifications. This ensures that if the server crashes or exits, the client receives an immediate error on the next `send()` attempt rather than hanging indefinitely waiting for a response.