XPCEndpoint cannot be encoded

I am trying to send an anonymous XPC listener endpoint to my daemon from user context in order to be able to do some bidirectional XPC.

I was trying to use the new XPCListener and XPCSession objects and the easiest method I figured was using the Codable version of the send() methods, in which I wanted to send the XPCEndpoint object - alongside the name of the anonymous endpoint (because I want to have more XPCEndpoints sent over, so I want to be able to identify them.

However, trying to manually encode XPCEndpoint throws an exception:

ERROR: Missing CodingUserInfoKey CodingUserInfoKey(rawValue: "_XPCCodable")

Here is a simple command-line tool reproducing the issue:

import Foundation
import XPC

let listener = try XPCListener(service: "mach-service.xxx.yyy", incomingSessionHandler: {
    $0.accept(incomingMessageHandler: { (msg: XPCReceivedMessage) in
        return nil
    })
})

var endpoint = listener.endpoint

do {
    let endpointData = try JSONEncoder().encode(endpoint)
    print("EndpointData object: \(endpointData.count) bytes")
} catch let error {
    print("ERROR: \(error)")
}

Wrapping my object into an XPCDictionary, then adding multiple keys alongside an "endpoint" key with the XPCEndpoint as value works, but XPCDictionaries are less ideal - they don't even support vanilla Data objects, only ones converted to an xpc_object_t with xpc_data_* functions

Is this expected behavior? I shouldn't encode an XPCEndpoint myself? I am using the latest Xcode 26.0 beta, with deployment target of macOS 15.1, running on macOS 15.5.

(Btw it's also incorrect that this XPCEndpoint API is available from macOS 15.0 - it cannot be found in Xcode 15.4 under macOS 15.5. At the very best it's backDeployed but this isn't mentioned in its public declaration.)

Answered by DTS Engineer in 856340022
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()
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()

Thanks Quinn for your very informative answer.

Your code does work on my side as well, and is what I would generally use. However it has a pain point for me - I don't see how I'm able to send various kinds of Codable conforming typed messages over the XPC connection.

That forced me into using XPCDictionary.

How can one XPCSession transfer let's say two distinct types of Codable messages?

struct EndpointCheckinMessage: Codable {
    var endpoint: XPCEndpoint
    var endpointBundleID: String
}

struct SpecificClientMessage: Codable {
    var title: String
    var description: String
}

How would one XPCSession send both of these messages efficiently to the daemon? Because my solution was initially wrap these into a single struct:

struct GeneralClientMessage: Codable {
    var kind: MessageKind // EndpointCheckinMessage, SpecificClientMessage, etc.
    var body: Data // This would contain the above types encoded with `JSONEncoder`.
}

And that's where I hit this issue and had to resort to XPCDictionary.

What would be natural to me is being able to define multiple Codable incomingMessageHandler closures, one for each kind of closure. But I can't see an option for that anywhere - not in the direct initializers of XPCListener, nor in XPCPeerHandler.

Do you have any recommendations for this scenario? I'm pretty sure I'm doing something wrong, I'm fairly unfamiliar with the low-level XPC APIs.

Accepted Answer
How can one XPCSession transfer let's say two distinct types of Codable messages?

Again, I don’t have a lot of concrete experience with the low-level Swift API, but my natural inclination here is to use an enum as your Codable message, with a case for each message type and an associated value with the payload for that message type. So, something like this:

enum WaffleRequest: Codable {
    case cook(Cook)
    case varnish(Varnish)

    struct Cook: Codable {
        var minutes: Int
    }

    struct Varnish: Codable {
        var finish: Finish
    }
    
    enum Finish: Codable {
        case matt
        case gloss
    }
}

Share and Enjoy

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

Damn that was a very good suggestion, it never crossed my mind. It

  • greatly reduced my code complexity
  • and allowed me to use the Codable versions of send() and incomingMessageHandler
  • while still being able to use the XPCEndpoint property
  • and being able to send multiple kinds of messages wrapped inside the associated values of the single enumeration type.

Thanks!

XPCEndpoint cannot be encoded
 
 
Q