Callback is not invoked when a closure callback-style call is executed in XPC

I noticed a problem while writing a program using XPC on macOS.

When I write it in the form of a closure that receives the result of an XPC call, I can't receive it forever.

I add an XPC target in Xcode, the sample code is used in the pass closure format, but can't I use closure passing with XPC?

My Environment:

  • Xcode 15.3
  • macOS 14.4.1

caller (closure version)

struct ContentView: View {
  @State var callbackResult: String = "Waiting…"
  var body: some View {
    Form {
      Section("Run XPC Call with no argument and no return value using callback") {
        Button("Run…") {
          callbackResult = "Running…"
          let service = NSXPCConnection(serviceName: "net.mtgto.example-nsxpc-throws-error.ExampleXpc")
          service.remoteObjectInterface = NSXPCInterface(with: ExampleXpcProtocol.self)
          service.activate()
          guard let proxy = service.remoteObjectProxy as? any ExampleXpcProtocol else { return }
          defer {
            service.invalidate()
          }
          proxy.performCallback {
            callbackResult = "Done"
          }
        }
        Text(callbackResult)
        ...
     }
  }
}

callee (closure version)

@objc protocol ExampleXpcProtocol {
    func performCallback(with reply: @escaping () -> Void)
}

class ExampleXpc: NSObject, ExampleXpcProtocol {
    @objc func performCallback(with reply: @escaping () -> Void) {
        reply()
    }
}

I found this problem can be solved by receiving asynchronous using Swift Concurrency.

caller (async version)

struct ContentView: View {
  @State var callbackResult: String = "Waiting…"
  var body: some View {
    Form {
      Section("Run XPC Call with no argument and no return value using callback") {
        Button("Run…") {
          simpleAsyncResult = "Running…"
          Task {
            let service = NSXPCConnection(serviceName: "net.mtgto.example-nsxpc-throws-error.ExampleXpc")
            service.remoteObjectInterface = NSXPCInterface(with: ExampleXpcProtocol.self)
            service.activate()
            guard let proxy = service.remoteObjectProxy as? any ExampleXpcProtocol else { return }
            defer {
              service.invalidate()
            }
            await proxy.performNothingAsync()
            simpleAsyncResult = "DONE"
          }
          Text(simpleAsyncResult)
        ...
     }
  }
}

callee (async version)

@objc protocol ExampleXpcProtocol {
    func performNothingAsync() async
}

class ExampleXpc: NSObject, ExampleXpcProtocol {
    @objc func performNothingAsync() async {}
}

To simplify matters, I write source code that omits the arguments and return value, but it is not also invoked by using callback style.

All sample codes are available in https://github.com/mtgto/example-nsxpc-throws-error

Accepted Reply

I’m not 100% sure what the actual problem is here. However:

  • Swift concurrency and NSXPCConnection aren’t a great combo. It’s better to using NSXPCConnection with traditional completion handlers.

  • You’re not setting up any invalidation and interrupt handlers, which is problematic.

  • I generally recommend that you use remoteObjectProxyWithErrorHandler(_:), so that you can receive error callbacks on a per-request basis.

  • The invalidate() call in this sequence is problematic:

let service = NSXPCConnection(serviceName: "net.mtgto.example-nsxpc-throws-error.ExampleXpc")
service.remoteObjectInterface = NSXPCInterface(with: ExampleXpcProtocol.self)
service.activate()
guard let proxy = service.remoteObjectProxy as? any ExampleXpcProtocol else { return }
defer {
	service.invalidate()
}
proxy.performCallback {
	callbackResult = "Done"
}

You’re invalidating the connection at the end of the current scope, which is before the callback has any chance of being called.

In general, XPC connections are meant to be long-lived. Invalidating them after each request is considered poor form.

Finally, if you haven’t already read TN3113 Testing and debugging XPC code with an anonymous listener, you should. It’s the best way to get started with this stuff.

Share and Enjoy

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

Replies

I’m not 100% sure what the actual problem is here. However:

  • Swift concurrency and NSXPCConnection aren’t a great combo. It’s better to using NSXPCConnection with traditional completion handlers.

  • You’re not setting up any invalidation and interrupt handlers, which is problematic.

  • I generally recommend that you use remoteObjectProxyWithErrorHandler(_:), so that you can receive error callbacks on a per-request basis.

  • The invalidate() call in this sequence is problematic:

let service = NSXPCConnection(serviceName: "net.mtgto.example-nsxpc-throws-error.ExampleXpc")
service.remoteObjectInterface = NSXPCInterface(with: ExampleXpcProtocol.self)
service.activate()
guard let proxy = service.remoteObjectProxy as? any ExampleXpcProtocol else { return }
defer {
	service.invalidate()
}
proxy.performCallback {
	callbackResult = "Done"
}

You’re invalidating the connection at the end of the current scope, which is before the callback has any chance of being called.

In general, XPC connections are meant to be long-lived. Invalidating them after each request is considered poor form.

Finally, if you haven’t already read TN3113 Testing and debugging XPC code with an anonymous listener, you should. It’s the best way to get started with this stuff.

Share and Enjoy

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

The invalidate() call in this sequence is problematic: You’re invalidating the connection at the end of the current scope, which is before the callback has any chance of being called.

Yes, you're right. My code calls NSXPCConnection#invalidate before callback called.

My example project works fine after delete NSXPCConnection#invalidate: https://github.com/mtgto/example-nsxpc-throws-error/commit/165488bf1b43e694b7d39c02b76e611542faa8cf

Swift concurrency and NSXPCConnection aren’t a great combo. It’s better to using NSXPCConnection with traditional completion handlers.

Okay, I'll convert my program to use completion handlers.

You’re not setting up any invalidation and interrupt handlers, which is problematic.

Yes, I'll write it.

Finally, if you haven’t already read TN3113 Testing and debugging XPC code with an anonymous listener, you should. It’s the best way to get started with this stuff.

I didn't read yet this article. I don't know NSXPCConnection and NSXPCListener in same process. It helps me for debugging!

I am very grateful to have found the cause of the XPC problem I was struggling with alone. Thank you again!