Here is a simple command-line tool reproducing the issue:
I’m not surprised that code fails. You’re trying to encode the endpoint using a ‘vanilla’ JSONEncoder
, and that has nowhere to store the Mach port rights.
I’ve always envisaged implementing this using Codable
structs. Pasted in at the end of this reply is an example of what I mean. When I built this with Xcode 26.0b7 and ran it on macOS 15.6.1, it printed this:
will start anonymous listener
did start anonymous listener
will send check-in request, message: Hello Cruel World! 2025-09-02 09:09:13 +0000
did send check-in request
will accept session request
will accept check-in request, message: Hello Cruel World! 2025-09-02 09:09:13 +0000
did accept check-in request, clientID: 21BB21C9-40FE-4F85-B1AC-40E3ED0EA5B1
did receive check-in reply, id: 21BB21C9-40FE-4F85-B1AC-40E3ED0EA5B1
indicating that the reply made it back to the client.
it's also incorrect that this XPCEndpoint API is available from macOS 15.0
I happened to have a macOS 15.0 VM lying about, so I copied my built tool to that VM and ran it there. It works just fine. Yay!
At the very best it's backDeployed
That’s not what’s going on here. Rather, XPCEndpoint
is implemented as documented on macOS 15.0 and later, it just was SPI rather than API. When we decided to make it API, we just changed the availability in the macOS 26 SDK that’s part of Xcode 26.0 beta.
This is unusal, but it’s not that unusal. For an extreme example, check in <sys/fileport.h>
, introduced in the macOS 15.4 SDK but supported all the way back to macOS 10.7!
Folks generally appreciate us doing this because it lets them adopt the API sooner. And, yes, it’s a inconvenient, and potentially confusing, that XPCEndpoint
is only available in the macOS 26 beta SDK, but in a few months that pain will have faded (-:
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
import Foundation
struct CheckInRequest: Codable {
var message: String
}
struct CheckInReply: Codable {
var clientID: UUID
var endpoint: XPCEndpoint
}
func makeClientListener() -> XPCListener {
XPCListener(targetQueue: .main) { request in
// In a real app this would accept new session requests from this client
// to its anonymous listener.
return request.reject(reason: "NYI")
}
}
var listenersByClientID: [UUID: XPCListener] = [:]
func main() throws {
// Start a loopback listener. I’m testing this stuff using the modern equivalent of the technique described in
// TN3113 “Testing and debugging XPC code with an anonymous listener”.
//
// <[TN3113: Testing and debugging XPC code with an anonymous listener | Apple Developer Documentation](https://developer.apple.com/documentation/technotes/tn3113-testing-xpc-code-with-an-anonymous-listener)>
print("will start anonymous listener")
let listener = XPCListener(targetQueue: .main, options: [.inactive]) { request in
print("will accept session request")
return request.accept { (checkIn: CheckInRequest) in
print("will accept check-in request, message: \(checkIn.message)")
let clientID = UUID()
let listener = makeClientListener()
listenersByClientID[clientID] = listener
let reply = CheckInReply(
clientID: clientID,
endpoint: listener.endpoint
)
print("did accept check-in request, clientID: \(clientID)")
return reply
}
}
try listener.activate()
print("did start anonymous listener")
// Create a session to that listener.
let session = try XPCSession(endpoint: listener.endpoint, targetQueue: .main, options: [], cancellationHandler: nil)
// And start a timer that checks in every 5 seconds.
let source = DispatchSource.makeTimerSource(queue: .main)
source.setEventHandler {
do {
let message = "Hello Cruel World! \(Date.now)"
print("will send check-in request, message: \(message)")
try session.send(CheckInRequest(message: message)) { (result: Result<CheckInReply, Error>) in
switch result {
case .failure(let error):
print("did not receive check-in reply, error: \(error)")
case .success(let something):
print("did receive check-in reply, id: \(something.clientID)")
}
}
print("did send check-in request")
} catch {
print("did not send check-in request, error: \(error)")
}
}
source.schedule(deadline: .now() + 1, repeating: 5.0)
source.activate()
// ‘Park’ the main thread in `dispatchMain()`.
withExtendedLifetime( (listener, session) ) {
dispatchMain()
}
}
try main()