Safari Web Extensions & NSXPCConnection

I have a basic setup following WWDC 2020 on Safari Web Extensions and another one on XPC. The video even mentions that one can use UserDefaults or XPC to communicate with the host app. Here is my setup.

macOS 15.2, Xcode 16.2 A macOS app (all targets sandboxed, with an app group) with 3 targets:

  • SwiftUI Hello World
  • web extension
  • XPC Service

The web extension itself works and can update UserDefaults, which can then be read by SwiftUI app - everything works by the book.

The app can communicate to the XPC service via NSXPCConnection - again, everything works fine.

The problem is that the web extension does not communicate with XPC, and this is what I need so that I can avoid using UserDefaults for larger and more complex payloads.

Web Ext handler code:

class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
func beginRequest(with context: NSExtensionContext) {
// Unpack the message from Safari Web Extension.
let item = context.inputItems[0] as? NSExtensionItem
let message = item?.userInfo?[SFExtensionMessageKey]
// Update the value in UserDefaults.
let defaults = UserDefaults(suiteName: "com.***.AppName.group")
let messageDictionary = message as? [String: String]
if messageDictionary?["message"] == "Word highlighted" {
var currentValue = defaults?.integer(forKey: "WordHighlightedCount") ?? 0
currentValue += 1
defaults?.set(currentValue, forKey: "WordHighlightedCount")
}
let response = NSExtensionItem()
response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ]
os_log(.default, "setting up XPC connection")
let xpcConnection = NSXPCConnection(serviceName: "com.***.AppName.AppName-XPC-Service")
xpcConnection.remoteObjectInterface = NSXPCInterface(with: AppName_XPC_ServiceProtocol.self)
xpcConnection.resume()
let service = xpcConnection.remoteObjectProxyWithErrorHandler { error in
os_log(.default, "Received error: %{public}@", error as CVarArg)
} as? AppName_XPC_ServiceProtocol
service?.performCalculation(firstNumber: 23, secondNumber: 19) { result in
NSLog("Result of calculation XPC is: \(result)")
os_log(.default, "Result of calculation XPC is: \(result)")
context.completeRequest(returningItems: [response], completionHandler: nil)
}
}
}

The error I'm getting:

Error Domain=NSCocoaErrorDomain Code=4099 "The connection to service named com.***.AppName.AppName-XPC-Service was invalidated: failed at lookup with error 3 - No such process."

What am I missing?

Answered by DTS Engineer in 831880022

Thanks for the clarifications.

So, yeah, that pretty much confirms what I was talking about in my previous post. You can’t use an XPC service for this task because there’s no way that the same XPC service can be visible to both your app and your appex. I see this a lot with folks building other extension types, like Finder Sync extensions.

How you get around that depends on your specific circumstances. What do you want to happen when the appex connects to the service when the container app isn’t running?

There are two standard answers to that:

  • The IPC is optional, so you want it to fail.

  • The IPC is critical, so you want it to launch the app.

I suspect it’s the latter, but that’s not really feasible because launching an app is a heavyweight operation. I talk about this whole issue in more detail in XPC and App-to-App Communication DevForums

The solution is for you to install something smaller than your app that gets launched on demand. Ideally that’d be an XPC service but, as I’ve noted above, that doesn’t work. However, there’s another option, namely a launchd agent.

These have a number of useful properties:

  • They’re relatively easy to install, using SMAppService.

  • They support XPC, via the MachServices property in the launchd property list.

  • And they support launch on demand, so the agent only need to runs when your appex (or app) calls upon its services.


Oh, and one thing to watch out for here. Your appex is sandboxed, and the app sandbox prevents it from connecting to arbitrary XPC named endpoints. To make this work the named endpoint must be ‘within’ an app group claimed by the appex. For example, you might have an app group ID like group.com.example.my-app.group and then set the XPC endpoint name to be group.com.example.my-app.group.xpc.

Note that app groups themselves have some sharp edges on macOS. See App Groups: macOS vs iOS: Fight!

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Written by ilia_saz in 778297021
The problem is that the web extension does not communicate with XPC

From an XPC perspective that not super surprising. An XPC service is only available to the app in which it’s embedded. Your XPC service is embedded in your app, and thus it’s visible to your app, but not to your appex. If moved it to your appex, the opposite would be true.

However, there’s a few web extension things I’d like to confirm. To start, how did you create your web extension? From the macOS > Safari Extension template?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I followed this WWDC talk to add a Safari Web Extension to my app. So I went to add a new target, selected Safari Extensions, then picked Web Extensions and select my app in "Embed in Application" dropdown.

Similarly, I added a new target for a XPC Service. And I tried with both high-level using NSXPCConnection and low-level using XPCSession.

When I build my app, I get both targets (Web Extension and XPC Service) embedded in my app's .app file - one under Plugins, and the other under XPCServices.

Thanks for the clarifications.

So, yeah, that pretty much confirms what I was talking about in my previous post. You can’t use an XPC service for this task because there’s no way that the same XPC service can be visible to both your app and your appex. I see this a lot with folks building other extension types, like Finder Sync extensions.

How you get around that depends on your specific circumstances. What do you want to happen when the appex connects to the service when the container app isn’t running?

There are two standard answers to that:

  • The IPC is optional, so you want it to fail.

  • The IPC is critical, so you want it to launch the app.

I suspect it’s the latter, but that’s not really feasible because launching an app is a heavyweight operation. I talk about this whole issue in more detail in XPC and App-to-App Communication DevForums

The solution is for you to install something smaller than your app that gets launched on demand. Ideally that’d be an XPC service but, as I’ve noted above, that doesn’t work. However, there’s another option, namely a launchd agent.

These have a number of useful properties:

  • They’re relatively easy to install, using SMAppService.

  • They support XPC, via the MachServices property in the launchd property list.

  • And they support launch on demand, so the agent only need to runs when your appex (or app) calls upon its services.


Oh, and one thing to watch out for here. Your appex is sandboxed, and the app sandbox prevents it from connecting to arbitrary XPC named endpoints. To make this work the named endpoint must be ‘within’ an app group claimed by the appex. For example, you might have an app group ID like group.com.example.my-app.group and then set the XPC endpoint name to be group.com.example.my-app.group.xpc.

Note that app groups themselves have some sharp edges on macOS. See App Groups: macOS vs iOS: Fight!

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thank you for the very detailed answer!

I get the idea, and I have a few follow-up questions. And let me give a bit more context about what I'm trying to achieve. The Web Extension captures quite a bit of data from an active page and sends it to the app for background processing - doing things like parsing, storing it in a DB, some compute intensive work, REST calls, and etc. The user can see the progress and the results in the app whenever they would like but usually this will happen some time later. I don't want to distract or impact the user's browsing activity in any way, it should be "press a button and forget" kind of experience, and hence your suggestion of implementing this with launchd makes perfect sense.

  1. While I'm in the early development phase, it might be ok to just fail if the app is not running. How does this assumption make things any simpler if web ext still can't talk to the app? Or do you suggest using a socket or even an embedded web server rather than XPC?

  2. If I go with launchd approach, how will this work on iOS?

Safari Web Extensions & NSXPCConnection
 
 
Q