← Back to DevBytes

macOS XPC Services

Introduction to macOS XPC Services

XPC (Cross-Process Communication) is Apple's framework for secure, structured inter-process communication on macOS. Introduced in Mac OS X Lion (10.7), XPC allows developers to split their applications into separate processes that communicate through typed message passing. An XPC Service is a bundle that runs in a separate process, managed by the system, and communicates with its host application via a well-defined interface.

This tutorial covers what XPC Services are, why they matter, how to build one from scratch, and best practices for production use.

What Is an XPC Service?

An XPC Service is a lightweight helper process bundled inside your application. Unlike traditional helper applications or NSTask-launched processes, XPC Services are managed by launchd and the XPC runtime. They are defined by a protocol (typically an Objective-C or Swift protocol) and communicate with their host through serialized messages.

Key characteristics of XPC Services:

Why XPC Services Matter

Modern macOS development strongly encourages sandboxing and least-privilege design. XPC Services are the canonical mechanism for achieving both. Here's why they matter:

Security and Sandboxing

App Sandbox restricts what your application can access. However, some features—like accessing the camera, network, or specific files—require entitlements you may not want to grant to your entire app. By moving privileged operations into an XPC Service with its own entitlements, you limit exposure. If a vulnerability is exploited in your main app, the attacker still cannot access the privileged resources because they live in a separate, narrowly-scoped process.

Stability

Crash-prone code—such as parsing untrusted file formats, running third-party codecs, or interfacing with flaky hardware—can be isolated. A crash in the service does not bring down the host application. You can simply reconnect and retry.

Concurrency Model

XPC provides a clean concurrency model. Each connection has its own dispatch queue, and the framework handles serialization and thread safety for you. This is far safer than manually managing shared memory or raw sockets.

System Integration

Many macOS system frameworks (such as NSExtension, Endpoint Security clients, and privileged helper tools) are built on XPC. Understanding XPC is essential for advanced macOS development.

How to Use XPC Services

Let's build a complete example: a host application that offloads a CPU-intensive image processing task to an XPC Service. We'll use Swift, but the concepts apply equally to Objective-C.

Step 1: Define the Protocol

The protocol defines the interface between the host and the service. Both sides must agree on this contract. Use the @objc attribute so the protocol is visible to the Objective-C runtime, which XPC relies on.

import Foundation

@objc protocol ImageProcessingProtocol {
    func processImage(
        imageData: Data,
        withReply reply: @escaping (Data?, Error?) -> Void
    )
}

The withReply pattern is the standard XPC idiom for asynchronous responses. The caller passes a completion handler that the service invokes when the work is done.

Step 2: Implement the Service

Create a new XPC Service target in Xcode (File → New → Target → macOS → XPC Service). Xcode generates a main.swift or NSXPCListenerDelegate implementation. Here is a complete service implementation:

import Foundation

class ImageProcessingService: NSObject, ImageProcessingProtocol {
    
    func processImage(
        imageData: Data,
        withReply reply: @escaping (Data?, Error?) -> Void
    ) {
        // Move heavy work to a background queue so we don't
        // block the listener's dispatch queue.
        DispatchQueue.global(qos: .userInitiated).async {
            do {
                let processed = try self.applyFilter(to: imageData)
                reply(processed, nil)
            } catch {
                reply(nil, error)
            }
        }
    }
    
    private func applyFilter(to data: Data) throws -> Data {
        // Simulate expensive work
        Thread.sleep(forTimeInterval: 1.0)
        // In a real app, use CoreImage or vImage here.
        return data
    }
}

class ServiceDelegate: NSObject, NSXPCListenerDelegate {
    func listener(
        _ listener: NSXPCListener,
        shouldAcceptNewConnection newConnection: NSXPCConnection
    ) -> Bool {
        newConnection.exportedInterface = NSXPCInterface(
            with: ImageProcessingProtocol.self
        )
        newConnection.exportedObject = ImageProcessingService()
        newConnection.resume()
        return true
    }
}

let delegate = ServiceDelegate()
let listener = NSXPCListener.service()
listener.delegate = delegate
listener.resume()

The NSXPCListener.service() factory creates a listener configured for an XPC Service bundle. The runtime calls shouldAcceptNewConnection for each incoming connection. You wire up the exported interface and object, then resume the connection.

Step 3: Configure the Service Info.plist

Xcode generates an Info.plist for the XPC Service target. The critical key is XPCService, which declares the service type. For a standard on-demand service, it looks like this:

<key>XPCService</key>
<dict>
    <key>ServiceType</key>
    <string>Application</string>
</dict>

The Application service type means the service runs in the same security context as the host application but in a separate process. For additional isolation, you can add a sandbox entitlements file to the service target with stricter rules than the host.

Step 4: Connect from the Host Application

In your host application, create a connection to the service by its bundle identifier:

import Foundation

class ImageProcessor {
    private var connection: NSXPCConnection?
    
    private func connect() -> NSXPCConnection? {
        let connection = NSXPCConnection(
            serviceName: "com.example.app.ImageProcessingService"
        )
        connection.remoteObjectInterface = NSXPCInterface(
            with: ImageProcessingProtocol.self
        )
        connection.resume()
        return connection
    }
    
    func process(_ data: Data,
                 completion: @escaping (Data?, Error?) -> Void) {
        if connection == nil {
            connection = connect()
        }
        
        guard let proxy = connection?.remoteObjectProxy
            as? ImageProcessingProtocol else {
            completion(nil, NSError(
                domain: "ImageProcessor",
                code: 1,
                userInfo: [NSLocalizedDescriptionKey: "No proxy"]
            ))
            return
        }
        
        proxy.processImage(imageData: data) { result, error in
            DispatchQueue.main.async {
                completion(result, error)
            }
        }
    }
    
    func invalidate() {
        connection?.invalidate()
        connection = nil
    }
}

The remoteObjectProxy is a transparent proxy that serializes method calls and sends them over the XPC connection. When you call processImage(imageData:withReply:), the call is forwarded to the service process, and the reply block is invoked on the connection's dispatch queue when the service responds.

Step 5: Handle Errors and Interruptions

XPC connections can be interrupted if the service crashes or is terminated. Always set an error handler on the proxy:

let proxy = connection.remoteObjectProxyWithErrorHandler { error in
    print("XPC error: \(error)")
} as? ImageProcessingProtocol

proxy?.processImage(imageData: data) { result, error in
    // handle result
}

You should also observe the NSXPCConnection interruption and invalidation handlers:

connection.interruptionHandler = {
    print("Connection interrupted; service may have crashed.")
}
connection.invalidationHandler = {
    print("Connection invalidated; clean up resources.")
}

Interruption means the service died but the connection may be re-established. Invalidation means the connection is gone for good and you must create a new one.

Passing Complex Data

XPC serializes messages using NSCoder-compatible types. Supported types include Data, String, NSNumber, Array, Dictionary, Date, URL, and any NSSecureCoding-conforming class. For custom types, conform to NSSecureCoding:

final class ProcessingResult: NSObject, NSSecureCoding {
    static var supportsSecureCoding: Bool { true }
    
    let output: Data
    let metadata: [String: String]
    
    init(output: Data, metadata: [String: String]) {
        self.output = output
        self.metadata = metadata
    }
    
    func encode(with coder: NSCoder) {
        coder.encode(output, forKey: "output")
        coder.encode(metadata, forKey: "metadata")
    }
    
    required init?(coder: NSCoder) {
        guard let output = coder.decodeObject(of: NSData.self,
                                              forKey: "output") as Data?,
              let metadata = coder.decodeObject(of: [NSDictionary.self,
                                                    NSString.self],
                                               forKey: "metadata")
                as? [String: String] else {
            return nil
        }
        self.output = output
        self.metadata = metadata
    }
}

When using custom types in a protocol method, register them with the interface so XPC knows how to decode them:

let interface = NSXPCInterface(with: ImageProcessingProtocol.self)
interface.setInterface(
    NSXPCInterface(with: ProcessingResult.self),
    for: #selector(ImageProcessingProtocol.processImage(imageData:withReply:)),
    argumentIndex: 0,
    ofReply: true
)

Best Practices

Keep Protocols Minimal

Every method in your protocol is part of your attack surface. Expose only what the host truly needs. Prefer a few well-defined methods over a sprawling API.

Validate All Inputs

Treat the service boundary as untrusted. Even if both sides are your code, a compromised host could send malicious payloads. Validate data sizes, check for nil, and reject malformed input early.

Use Separate Entitlements

Give the XPC Service its own entitlements file with only the permissions it needs. If the service only reads files, do not grant write access. This is the core of least-privilege design.

Avoid Blocking the Listener Queue

The NSXPCListenerDelegate and exported object methods are called on the connection's dispatch queue. Never perform long-running work synchronously there—dispatch to a background queue and call the reply block when finished.

Always Call the Reply Handler

If you accept a connection in shouldAcceptNewConnection, you must eventually call every reply block the host passes in. Failing to do so leaks resources and can deadlock the host. Use defer or careful error paths to guarantee the reply is always invoked.

Reconnect Gracefully

Services can be terminated by the system at any time. Implement reconnection logic in the host: invalidate the old connection, create a new one, and retry the operation. Exponential backoff is useful if the service is repeatedly crashing.

Prefer File Coordination for Large Data

XPC messages are copied and serialized. For large payloads (images, video, models), write the data to a temporary file and pass the URL instead. Use NSFileCoordinator if both sides might access the file concurrently.

Test Crash Scenarios

Deliberately crash your service during development to verify the host recovers. You can call exit(1) or fatalError() inside the service to simulate failures.

Conclusion

XPC Services are a foundational macOS technology for building secure, stable, and well-architected applications. By isolating privileged or crash-prone code into separate, sandboxed processes, you reduce your attack surface and improve resilience. The NSXPCConnection API makes inter-process communication straightforward: define a protocol, implement the service, and connect from the host. While the setup involves some boilerplate, the payoff in security and robustness is substantial. Adopt XPC Services whenever your application touches untrusted data, requires elevated privileges, or performs work that could destabilize the main process—and follow the best practices above to get the most out of the framework.

— Ad —

Google AdSense will appear here after approval

← Back to all articles