*Really* unusual XPC/Swift question

In doing some work, I realized I didn't understand XPC (or at least, the higher-level APIs) at all. So I did what I usually try to do, which is to write a completely, brain-dead simple program to use it, and then keep expanding until I understand it. This also, to my utmost embarrassment, will make it transparently clear how ignorant I am. (Note that, for this, I am not using Xcode -- just using swiftc to compile, and then manually run.)

I started with this, to be the server side:

import Foundation

class ConnectionHandler: NSObject, NSXPCListenerDelegate {
    override init() {
		super.init()
		print("ConnectionHandler.init()")
    }

    func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
		print("ConnectionHandler.listener()")
		return false
    }
}

let handler = ConnectionHandler()
let listener = NSXPCListener(machServiceName: "com.kithrup.test")

listener.delegate = handler
listener.resume()

print("listener = \(listener)")
dispatchMain()

That ... does absolutely nothing, of course, but runs, and then I tried to write the client side:

import Foundation

@objc protocol Hello {
    func hello()
}

class HelloClass: NSObject, Hello {
    override init() {
		super.init()
    }

    func hello() {
		print("In HelloClass.hello()")
    }
}

let hello = HelloClass()

let connection = NSXPCConnection(machServiceName: "com.kithrup.test", options: [])
connection.exportedInterface = NSXPCInterface(with: Hello.self)

let proxy = connection.remoteObjectProxyWithErrorHandler({ error in
							   print("Got error \(error)")
							 }) as? Hello

print("proxy = \(proxy)")

connection.resume()
dispatchMain()

That gets proxy as nil. Because it can't coerce it to something of Hello protocol. And at no point, do I get the message from the server-side listener.

So clearly I am doing everything wrong. Can anyone offer some hints?

Answered by DTS Engineer in 682317022

XPC does not support communication between arbitrary processes. For a client to connect to a listener the listener must be managed by launchd, including:

  • A launchd daemon or agent

  • An XPC Service

  • A Service Management login item

If you plan to ship a product then you must design it around this constraint. However, if you just want to noodle around with XPC then there’s a really good way to do this within a single process, namely an anonymous listener (+[NSXPCListenerEndpoint anonymousListener]). While this is intended to be used as part of more complex XPC strategies, you can use it as a simple loopback mechanisms, allowing you to have both the client and listener running in the same process.

Share and Enjoy

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

What happens if you remove 'as? Hello'. I never practiced, so I am more ignorant than you probably. I found this interesting link: h t t p s : / / matthewminer. com/2018/08/25/creating-an-xpc-service-in-swift.html What they do differently is to define connection.exportedInterface in the listener.

If I remove 'as? Hello', it gives me a value, but then I can't call the method on it. Note that I have connection.exportedInterface = NSXPCInterface(with: Hello.self) in there.

Ok, so I guess I can't make a command-line Mach server. So I'm trying just a minimal gui app that does essentially the same thing, and that's still not working. Perhaps I need a launchd.plist, rather than putting the MachServices dictionary in Info.plist? (And then I was trying my command-line client, as above.)

Accepted Answer

XPC does not support communication between arbitrary processes. For a client to connect to a listener the listener must be managed by launchd, including:

  • A launchd daemon or agent

  • An XPC Service

  • A Service Management login item

If you plan to ship a product then you must design it around this constraint. However, if you just want to noodle around with XPC then there’s a really good way to do this within a single process, namely an anonymous listener (+[NSXPCListenerEndpoint anonymousListener]). While this is intended to be used as part of more complex XPC strategies, you can use it as a simple loopback mechanisms, allowing you to have both the client and listener running in the same process.

Share and Enjoy

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

Oh! So I can't do my test with two programs, but I can do it with one, without having to use xcode or launchd? That'll work for playing-around purposes!

The real programs will be done in xcode, with either XPC Services, or with system extensions -- I'm simply trying to figure out why the bad code I have so far is bad, and (for me) that meant going back to basics with XPC.

Thank you! (Again. And hi again! It's been a very long time since I've seen you.)

My attempt at the comment didn't work with formatting.

Ok. I've got this code, and proxy still ends up as nil.

I'm still playing with it so hopefully I'll figure out on my own what I'm doing wrong.

import Foundation

class ConnectionHandler: NSObject, NSXPCListenerDelegate {
	override init() {
		super.init()
		print("ConnectionHandler.init()")
	}
	func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
		print("ConnectionHandler.listener()")
		newConnection.resume()
		return true
	}
}

let handler = ConnectionHandler()
let listener = NSXPCListener.anonymous()
let endpoint = listener.endpoint
listener.delegate = handler
listener.resume()

@objc protocol Hello {
    func hello()
}

class HelloClass: NSObject, Hello {
    override init() {
		super.init()
    }
    func hello() {
		print("In HelloClass.hello()")
    }
}

let hello = HelloClass()
let connection = NSXPCConnection(listenerEndpoint: endpoint)
connection.exportedInterface = NSXPCInterface(with: Hello.self)

print("Connection = \(connection)")
connection.resume()

let proxy = connection.remoteObjectProxyWithErrorHandler({ error in
							   print("Got error \(error)")
							 }) as? Hello

print("Proxy = \(proxy)")
dispatchMain()

Ok, progress! Yay! Not that I grok how it all works together yet, but I did make progress!

let connection = NSXPCConnection(listenerEndpoint: endpoint)
connection.exportedInterface = NSXPCInterface(with: Hello.self)
connection.exportedObject = hello
print("Connection = \(connection)")
connection.remoteObjectInterface = NSXPCInterface(with: Hello.self) // This is new!

connection.resume()

let proxy = connection.remoteObjectProxyWithErrorHandler({ error in
                                                           print("Got error \(error)")
                                                         }) as? Hello

gives me a valid proxy object, that I can do proxy?.hello() on. Not that it does anything yet but I am getting there!

And with a couple of smaller changes, it now does something. In particular, adding a goodbye object that conforms to Hello and says "goodbye" (I'm not very original here), and doing:

    func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
        print("ConnectionHandler.listener()")
        newConnection.resume()
        newConnection.exportedInterface = NSXPCInterface(with: Hello.self)
        newConnection.exportedObject = goodbye // This is new!
        return true
    }

results in the "client" side being able to have "goodbye" printed.

Ahhhhhhhhhhhh.

*Really* unusual XPC/Swift question
 
 
Q