XPC Services on macOS apps using Swift Oct 17 2019

We have access to many Inter-Process-Communication(IPC) mechanisms in macOS. One of the newest is XPC1. Before XPC a common way to use IPC, and provide services between processes, was through Sockets or Mach Messages (Using Mach Ports).

Nowadays, we are encouraged by Apple to use XPC in our applications and frameworks that provide services. Also, Apple, in their code, has updated many of its frameworks, daemons and applications to use XPC.

XPC, as you can imagine, is an excellent tool to learn. In this post, we are going to learn:

Let’s examine what we can accomplish using XPC.

* You can check the full code in the GitHub Repository

*Update: You can check the following post if you are interested in creating a Launch Agent that provides XPC services.

The idea behind XPC and its uses

XPC provides us with a new abstraction for Inter-Process-Communication, a simple abstraction that on its own it’s convenient (using MIG is complicated). But aside from having an easier to use IPC mechanism, XPC implementation also gives us some additional benefits. We’ll examine the benefits later but first, let’s explore some use-cases of XPC.

XPC for communicating processes

As mentioned before, the XPC mechanism offers an alternative to sockets (or Mach Services using MIG) for IPC. We could have, for example, a process that acts as a “server” waiting for clients to access it’s API and provide some service.

Imagine we have an application that handles the user’s contacts. The app provides an API that you could access from your applications and query for details on the user’s contacts. That example could very easily be an XPC service provided by AddressBook.app. We don’t access the XPC service directly we use the API provided by Contacts.framework, but the framework is based on XPC services.

XPC services used in this way are an excellent way to communicate different processes. The exchange of data in XPC is through Plists, that also allows for data type validation.

XPC Services on applications

When we talk about XPC Services (capital ‘S’), we are referring to the bundle called XPC Service. Bundles in Apple ecosystem refers to entities represented by a specific directory structure. The most common Bundle you encounter are Application Bundles. If you right-click on any application (For example Chess.app) and select Show content, what you’ll find is a directory structure.

Back to XPC, applications can have may XPC Service bundles. You’ll find them inside the Contents/XPCServices/ directory inside the application bundle. You can search in your /Applications directory and see how many of the applications rely on XPC Services.

1
2
3
4
5
6
7
$ find /Applications/ -name "*.xpc"
# a few results on my computer
/Applications//Safari.app/Contents/XPCServices/com.apple.Safari.SandboxBroker.xpc
/Applications//Safari.app/Contents/XPCServices/com.apple.WebKit.WebContent.Safari.xpc
/Applications//Safari.app/Contents/XPCServices/com.apple.Safari.BrowserDataImportingService.xpc
/Applications//Keynote.app/Contents/XPCServices/com.apple.iWork.ArchiveUpgrader.xpc
/Applications//Keynote.app/Contents/XPCServices/com.apple.iWork.ExternalResourceAccessor.xpc

The XPC Services inside each application provide services that can be easily accessed from the main Application. You can also have XPC Services inside Frameworks (Which are another type of Bundle). You can check the frameworks on your computer for some examples:

1
2
3
4
5
6
7
8
9
10
$ find /System/Library/Framework -name "*.xpc"
# a few results on my computer
/System/Library/Frameworks/MediaAccessibility.framework/Versions/A/XPCServices/com.apple.accessibility.mediaaccessibilityd.xpc
/System/Library/Frameworks/AudioToolbox.framework/XPCServices/RemoteProcessingBlockRegistrar.xpc
/System/Library/Frameworks/AudioToolbox.framework/XPCServices/CAReportingService.xpc
/System/Library/Frameworks/AudioToolbox.framework/XPCServices/com.apple.audio.SandboxHelper.xpc
/System/Library/Frameworks/AudioToolbox.framework/XPCServices/aupbregistrarservice.internal.xpc
/System/Library/Frameworks/AudioToolbox.framework/XPCServices/com.apple.audio.InfoHelper.xpc
/System/Library/Frameworks/AudioToolbox.framework/XPCServices/com.apple.audio.ComponentTagHelper.xpc
/System/Library/Frameworks/ColorSync.framework/Versions/A/XPCServices/com.apple.ColorSyncXPCAgent.xpc

Additional Benefits of XPC Services

Using XPC Services in our apps allow us to break some functionality in separate modules (The XPC Service). We could create an XPC Service that can be in charge of running some costly but infrequent tasks. For example, some crypto task to generate random numbers.

Breaking our application into specific services allows us to keep our main application leaner, and also take less memory while running. Only running our XPC Service on demand.

Another additional benefit is that the XPC Service runs on its own process. If that process crashes or it’s killed, it doesn’t affect our main application. Imagine that your application support user-defined plugins. And the plugins are built using XPC Services. If they are poorly coded and crash, they won’t affect the integrity of your main application.

An additional benefit to the XPC Service is that they can have their own entitlements. The application will only require the entitlement when it makes use of a service provided by XPC Service that requires the entitlement. Imagine you have an app that uses location but only for specific features. You could move those features to an XPC Service and add the location entitlement only to that XPC Service. If your user never needs the feature that uses the location, it won’t be prompted for permissions, making the use of your app more trustworthy.

Those are some of the benefits of using XPC. Let’s see how XPC Services are managed.

We’ve discussed that the XPC Service can be run on-demand, but who is in charge of managing your XPC Service?

XPC and our friend launchd.

launchd is the first process to run on our system. It is in charge of launching and managing other processes, services and daemons. launchd is also in charge of scheduling tasks. So it makes sense that launchd will also be responsible for the management of XPC Services.

As I mentioned before, our XPC Service can be stopped if it has been idle for a long time, or be spawned on demand. All the management is done by launchd, and we don’t need to do anything for it to work.

launchd has information about system-wide resource availability and memory pressure, who best to make decisions on how to most effectively use our system’s resources than launchd. This is an ingenious implementation if you ask me.

Ok, enough theory on XPC let’s do something practical.

Creating a macOS application that includes an XPC Service

Let’s create an application that makes use of an XPC Service. We are going to be using an Agent-based Menu bar application. Much like the application we created on the post “Understanding a few concepts of macOS applications by building an agent-based (menu bar) app”.

We’ll build the application and XPC Service using the Swift Package Manager. If you want a refresher on how to create apps without using Xcode, check that post.

Our XPC Service will provide a function that will obtain our public IP. We’ll add the new option to our “menu bar” application(the new option will call our XPC Service).

I’ll go a little faster here until we reach the XPC code, all the rest was covered in the other post.

Start by creating the directory and initialising the Swift project. The name doesn’t matter. I’ll be using the code on the mentioned post as the base so I’ll call my project XPCSquirrel as the code before was only Squirrel:

1
2
$ mkdir XPCSquirrel
$ swift package init --type executable

As I mentioned, we are going to base our code on the previous Squirrel implementation. I’m just going to show you the complete files and not go into detail explaining how they work until we reach the XPC related code.

The main.swift looks like this:

1
2
3
4
5
6
7
import Foundation
import Cocoa

let delegate = AppDelegate()
NSApplication.shared.delegate = delegate

_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

The AppDelegate.swift will have this content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import Cocoa

class AppDelegate: NSObject, NSApplicationDelegate {

  var statusBarItem: NSStatusItem!
  var counter: Int = 0

  func applicationDidFinishLaunching(_ aNotification: Notification) {
    let statusBar = NSStatusBar.system
    statusBarItem = statusBar.statusItem(
      withLength: NSStatusItem.variableLength)
    statusBarItem.button?.title = "🌰 \(counter)"

    let statusBarMenu = NSMenu(title: "Counter Bar Menu")
    statusBarItem.menu = statusBarMenu

    statusBarMenu.addItem(
      withTitle: "Increase",
      action: #selector(AppDelegate.increaseCount),
      keyEquivalent: "")

    statusBarMenu.addItem(
      withTitle: "Decrease",
      action: #selector(AppDelegate.decreaseCount),
      keyEquivalent: "")
    statusBarMenu.addItem(
      withTitle: "Quit",
      action: #selector(AppDelegate.quit),
      keyEquivalent: "")
  }

  @objc func showCounter() {
    statusBarItem.button?.title = "🌰 \(counter)"
  }

  @objc func increaseCount() {
    counter += 1
    showCounter()
  }

  @objc func decreaseCount() {
    counter -= 1
    showCounter()
  }

  @objc func quit() {
    NSApplication.shared.terminate(self)
  }
}

We are going to create the Application Bundle and XPC Service bundle by hand. This means that we are going to manually create the Info.plists for each of the bundles. We are going to create them in a directory called SupportFiles, and then move them to their proper place in the Application and XPC Service bundles.

1
$ mkdir SupportFiles 

We are creating the bare bones of the bundles, so there is a lot of information missing. The application bundle we’ll create, certainly won’t get approved by the App Store. But for understanding how everything works is better to only look at what is required.

Create a file MainInfo.plist that will host our application bundle’s Info.plist. And add the following content: (LSUIElement identifies our application as an Agent so that it won’t show an icon in the Dock or on the task switcher).

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>NSPrincipalClass</key>
    <string>NSApplication</string>
    <key>LSUIElement</key>
    <true/>
</dict>
</plist>

Now create the Plist file for our XPC Service. Create a file with the name XPCInfo.plist with the following content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleIdentifier</key>
    <string>com.rderik.ServiceProviderXPC</string>
    <key>CFBundlePackageType</key>
    <string>XPC!</string>
    <key>XPCService</key>
    <dict>
        <key>ServiceType</key>
        <string>Application</string>
    </dict>
</dict>
</plist>

Those are the required fields for an XPC Services bundle Info.plist file. The CFBundleIndentifier is used as the service name. Later we’ll use that name when locating the service.

Your file structure should look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── Package.swift
├── README.md
├── Sources
│   └── XPCSquirrel
│       ├── AppDelegate.swift
│       └── main.swift
├── SupportFiles
│   ├── MainInfo.plist
│   └── XPCInfo.plist
└── Tests
    ├── LinuxMain.swift
    └── XPCSquirrelTests
        ├── XCTestManifests.swift
        └── XPCSquirrelTests.swift

Ok, now can focus on the code for the XPC Service.

Finally, let’s work with XPC.

We are going to add a new target to our Package.swift that will represent our XPC Service code.

Edit the Package.swift and add the new target:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// swift-tools-version:5.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "XPCSquirrel",
    dependencies: [
    ],
    targets: [
        .target(
            name: "XPCSquirrel",
            dependencies: []),
        .testTarget(
            name: "XPCSquirrelTests",
            dependencies: ["XPCSquirrel"]),
        .target(
            name: "ServiceProvider",
            dependencies: []),
    ]
)

Now let’s create a new directory inside Sources that will contain our XPC code.

1
$ mkdir Sources/ServiceProvider

Create a new file inside Sources/ServiceProvider called main.swift. This will be our entry point for our XPC Service bundle. From our main will listen for new connections and handle their requests.

The workflow is the following:

(1) We create a listener and (2) set its delegate object. The delegate is in charge of accepting and setting up new incoming connections. Once our listener has a delegate, we call resume that will indicate to our listener to (3) start “listening” for connections.

This is how our main.swift will look:

1
2
3
4
5
6
7
import Foundation

let listener = NSXPCListener.service()
let delegate = ServiceDelegate()
listener.delegate = delegate;
listener.resume()
RunLoop.main.run()

We haven’t created ServiceDelegate yet, so let’s do that. Our delegate should implement the NSXPCListenerDelegate protocol that requires the implementation of the listener function that sets up the incoming connections. listener has the following signature:

1
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool

The boolean that is returned indicates if the connection was accepted. We can do any validations and then decide if we accept the connection or not.

Ok, let’s work on our delegate. Create a new file inside Sources/ServiceProvider called ServiceDelegate.swift and add the following content:

1
2
3
4
5
6
7
8
9
10
11
12
import Foundation

class ServiceDelegate : NSObject, NSXPCListenerDelegate {
    func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
        newConnection.exportedInterface = NSXPCInterface(with: ServiceProviderXPCProtocol.self)

        let exportedObject = ServiceProviderXPC()
        newConnection.exportedObject = exportedObject
        newConnection.resume()
        return true
    }
}

First, we set up the exported interface to be represented by the ServiceProviderXPCProtocol (we’ll be creating that protocol next). This protocol defines the interface for our Service. It defines what functions we provide and their signatures. We are now on the “server” side of the connection, but in the “client” side will also need to have the protocol that defines our interface.

After setting exportedInterface, we create an object that implements our protocol. The object will be the one called when our service functions are requested. We set our connection’s exportedObject property to the Object that implements our protocol.

When a new connection is created, it begins its execution life in a stopped state. We use resume to start it.

If everything went ok, we return true to indicate that the connection was accepted and configured.

As we decided before, our XPC service will provide a service that obtains our public IP and executes the closure we got as a parameter(withReply) with our IP.

Create the file ServiceProviderXPCProtocol.swift and add the following content:

1
2
3
4
5
6
import Foundation

@objc(ServiceProviderXPCProtocol) protocol ServiceProviderXPCProtocol {
  func getPublicIp(withReply reply: @escaping (String) -> Void)
}

Now, let’s create a new file where we’ll implement the protocol. Create the file ServiceProviderXPC.swift inside Sources/ServiceProvider/. And add the following content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import Foundation

@objc class ServiceProviderXPC: NSObject, ServiceProviderXPCProtocol{

  func getPublicIp(withReply reply: @escaping (String) -> Void) {
    let pmset = Process()
      let pipe = Pipe()
      if #available(OSX 13, *) {
        pmset.executableURL = URL(fileURLWithPath: "/usr/bin/env")
      } else {
        pmset.launchPath = "/usr/bin/env"
      }
      // We are going to use `dig` to obtain our public IP using
      // Cisco's opendns.com domain:
      // dig +short myip.opendns.com @resolver1.opendns.com
      pmset.arguments = ["dig", "+shor", "myip.opendns.com", "@resolver1.opendns.com"]
      pmset.standardOutput = pipe
      do {
      if #available(OSX 13, *) {
        do {
          try pmset.run()
        } catch {
         reply("")
        }
      } else {
        pmset.launch()
      }
        pmset.waitUntilExit()
        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        if let output = String(data: data, encoding: String.Encoding.utf8) {
          reply(output.trimmingCharacters(in: .whitespacesAndNewlines))
        }
      }
  }
}

That function uses the dig command to obtain our public IP. The bulk of the code is related to running the dig command, but the code relevant to XPC is only sending our ip obtained by dig to the reply closure.

Alright, we have our XPC Service side complete. Let’s work on accessing our XPC Service form the main application.

Using our XPC Service

Apple has done an excellent job making the use of XPC Services very straight forward. The workflow is the following:

(1) We create a connection to the service we want to use. The service is looked up by name (The CFBundleIdentifier in the XPC Service’s Info.plist remember?). (2) Set the remote object Interface, here we need to know the protocol that describes the interface, the one we used to create the service on the XPC Service bundle. (3) Get an instance of the object that implements the interface (using remoteObjectProxy). (4) And last, make use of the service. Remember that the calls are always asynchronous that means if we are going to work with the UI, we need to send it through the main Queue, so we don’t block the main thread.

Ok, let’s see our implementation. We are going to be working on the AppDelegate.swift file. Let’s add a new option to the statusBarMenu that allows us to call the method to display our public IP.

1
2
3
4
    statusBarMenu.addItem(
      withTitle: "Get public ip",
      action: #selector(AppDelegate.xpcCall),
      keyEquivalent: "")

We need to define our function, xpcCall. Let’s do that:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  @objc func xpcCall() {
    let connection = NSXPCConnection(serviceName: "com.rderik.ServiceProviderXPC")
    connection.remoteObjectInterface = NSXPCInterface(with: ServiceProviderXPCProtocol.self)
    connection.resume()

    let service = connection.remoteObjectProxyWithErrorHandler { error in
      print("Received error:", error)
    } as? ServiceProviderXPCProtocol

    service!.getPublicIp() { (texto) in
      DispatchQueue.main.async {
        self.statusBarItem.button?.title = "\(texto)"
      }
    }
  }

As you can see, we are defining the remoteObjectInterface using ServiceProviderXPProtocol, but we haven’t defined it yet for the main application. You can copy it from the Sources/ServiceProvider/ directory and paste it in Sources/XPCSquirrel directory.

We are using connection.remoteObjectProxyWithErrorHandler that allows us to pass a closure to handle any errors and obtain an instance of the object that implements the ServiceProviderXPCProtocol.

Then we call our function getPublicIp(withReply:) using the inline closure syntactic sugar to pass the closure. In the closure we set our statusBarItem.button?.title to be the ip we get from the service.

Very cool right?

Next, you’ll see the full AppDelegate.swift file. I also included some additional changes. For example, the option to show the counter. This way, we can go back to using our App as a counter after we’ve checked our public ip.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import Cocoa

class AppDelegate: NSObject, NSApplicationDelegate {

  var statusBarItem: NSStatusItem!
  var counter: Int = 0

  func applicationDidFinishLaunching(_ aNotification: Notification) {
    let statusBar = NSStatusBar.system
    statusBarItem = statusBar.statusItem(
      withLength: NSStatusItem.variableLength)
    statusBarItem.button?.title = "🌰 \(counter)"

    let statusBarMenu = NSMenu(title: "Counter Bar Menu")
    statusBarItem.menu = statusBarMenu

    statusBarMenu.addItem(
      withTitle: "Increase",
      action: #selector(AppDelegate.increaseCount),
      keyEquivalent: "")

    statusBarMenu.addItem(
      withTitle: "Decrease",
      action: #selector(AppDelegate.decreaseCount),
      keyEquivalent: "")

    statusBarMenu.addItem(
      withTitle: "Get public ip",
      action: #selector(AppDelegate.xpcCall),
      keyEquivalent: "")

    statusBarMenu.addItem(
      withTitle: "Show Counter",
      action: #selector(AppDelegate.showCounter),
      keyEquivalent: "")

    statusBarMenu.addItem(
      withTitle: "Quit",
      action: #selector(AppDelegate.quit),
      keyEquivalent: "")
  }

  @objc func showCounter() {
    statusBarItem.button?.title = "🌰 \(counter)"
  }

  @objc func increaseCount() {
    counter += 1
    showCounter()
  }

  @objc func decreaseCount() {
    counter -= 1
    showCounter()
  }

  @objc func xpcCall() {
    let connection = NSXPCConnection(serviceName: "com.rderik.ServiceProviderXPC")
    connection.remoteObjectInterface = NSXPCInterface(with: ServiceProviderXPCProtocol.self)
    connection.resume()

    let service = connection.remoteObjectProxyWithErrorHandler { error in
      print("Received error:", error)
    } as? ServiceProviderXPCProtocol

    service!.getPublicIp() { (texto) in
      DispatchQueue.main.async {
        self.statusBarItem.button?.title = "\(texto)"
      }
    }
  }

  @objc func quit() {
    NSApplication.shared.terminate(self)
  }
}

We now have all the code, but we still need to put everything together. We need to build the application and add the XPCService to the proper place in the main application’s bundle. Application bundles can have as many XPC Service bundles as they need, they are located inside the XPCServices folder inside Contents.

I’ll automate the process using make so we don’t have to do it all by hand. You can see the Makefile next:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
SUPPORTFILES=./SupportFiles/
PLATFORM=x86_64-apple-macosx
BUILD_DIRECTORY = ./.build/${PLATFORM}/debug
APP_DIRECTORY=./XPCSquirrel.app
CFBUNDLEEXECUTABLE=XPCSquirrel
XPCEXECUTABLE=ServiceProvider

install: build copySupportFiles

build:
    swift build --product ${CFBUNDLEEXECUTABLE}
    swift build --product ${XPCEXECUTABLE}

copySupportFiles:
    mkdir -p ${APP_DIRECTORY}/Contents/MacOS/
    mkdir -p ${APP_DIRECTORY}/Contents/XPCServices/${XPCEXECUTABLE}.xpc/Contents/MacOS/
    cp ${SUPPORTFILES}/MainInfo.plist ${APP_DIRECTORY}/Contents/Info.plist
    cp ${SUPPORTFILES}/XPCInfo.plist ${APP_DIRECTORY}/Contents/XPCServices/${XPCEXECUTABLE}.xpc/Contents/Info.plist
    cp ${BUILD_DIRECTORY}/${CFBUNDLEEXECUTABLE} ${APP_DIRECTORY}/Contents/MacOS/
    cp ${BUILD_DIRECTORY}/${XPCEXECUTABLE} ${APP_DIRECTORY}/Contents/XPCServices/${XPCEXECUTABLE}.xpc/Contents/MacOS/

run: build
    open -a ${APP_DIRECTORY}

clean:
    rm -rf .build
    rm -rf ${APP_DIRECTORY}

.PHONY: run build copySupportFiles clean

As you can see, we create all the Application Bundle structure and create the XPC bundle. Let’s run the make command and generate our XPCSquirrel.app.

1
2
3
$ make
# this will create the `XPCSquirrel.app` Application bundle
# you can use `make clean` to remove it

This generates the XPCSquirrel.app bundle that has the following structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
XPCSquirrel.app
└── Contents
    ├── Info.plist
    ├── MacOS
    │   └── XPCSquirrel
    └── XPCServices
        └── ServiceProvider.xpc
            └── Contents
                ├── Info.plist
                └── MacOS
                    └── ServiceProvider

6 directories, 4 files

Now you can run the application by double-clicking it on Finder or using the open command on the shell:

1
$ open XPCSquirrel.app

If you can’t see the app in your taskbar, switch to Finder, it might be covered by some menu items in your current application. Now you can click Get public ip, and you should see your IP in the taskbar.

Congratulations! You just used XPC Services in your application.

Final thoughts

There are more uses for XPC as Inter-Process-Communication, other than creating XPC Service bundles in your applications. One of the most useful, in my opinion, is creating a LaunchAgent that provides some services that many apps can connect to and obtain information.

For example, an application that does grammar and spelling checking (Grammarly maybe?). The app can expose an XPC service that you can send a block of text, and it’ll return an object with the corrections.

Notice the lower-case ‘s’ in that use of XPC service, we are not talking about a bundle but a service that can be accessed from other applications.

Ok, that’s it for this week. Let me know if you like the post and any other use you give toXPC.

* You can check the full code in the GitHub Repository

Related topics/notes of interest


  1. I have no idea what XPC stands for, if you have any clues, please let me know :). 


** There is no comment system yet, but you can send me a message on twitter @rderik or send me an email: derik[at]rderik[dot]com.