4

I'm attempting to rebuild Apple's XPC "lowerCase" sample code from Objective-C to Swift. I understand XPC will, but I'm relatively new to Swift and Objective-C interoperability.

When I use their exact sample code, which passes a String from the app to the target and back, it works. But when I try and replace that with my own custom class, Person, I get the error:

NSXPCConnection: ... connection on anonymousListener or serviceListener from pid 4133:

Exception caught during decoding of received selector upperCasePerson:withReply:, dropping incoming message.

Exception: Exception while decoding argument 0 (#2 of invocation):

Exception: decodeObjectForKey: class "CombineCycle.Person" not loaded or does not exist.

(The demo apps name is 'CombineCycle')

Person is a Swift class that inherits from NSObject and conforms to NSSecureCoding. It has a single property for name:String. It's accompanying .swift source file is included in the XPC target's build.

The exception seems pretty self-explanatory but I'm not sure which build settings I need to change, or which files I need to generate, to fix the underlying issue.

This is just a demo app. The main app was created as a Swift app and contains no Objective-C files. The XPC target was created using Xcode's new target feature which, in Xcode 11, generates an Objective-C skeleton. I replaced all of those files with Swift implementations.

// Person.swift (Included in both targets)

@objc public class Person : NSObject, NSSecureCoding  {
  var name:String?

  init(_ name:String) {
    self.name = name
  }

  public static var supportsSecureCoding: Bool {
    return true
  }

  public required init?(coder: NSCoder) {
    self.name = coder.decodeObject(forKey: "name") as? String
  }

  public func encode(with coder: NSCoder) {
    coder.encode(self.name, forKey: "name")
  }
}

Update

The XPC service is able to instantiate an instance of Person just fine, so the class is being included. The problem appears to be that NSXPCDecoder (a private class) is unable to deserialize it...

    __exceptionPreprocess + 250
    objc_exception_throw + 48
    _decodeObject + 1413
    -[NSXPCDecoder _decodeObjectOfClasses:atObject:] + 63
    _NSXPCSerializationDecodeInvocationObjectOnlyArgumentArray + 463
kennyc
  • 5,490
  • 5
  • 34
  • 57
  • I haven't worked with NSXPCConnection yet, but my guess would be that you have to mark all classes/properties with `@objc`. – Martin R Jun 13 '19 at 17:33
  • Did you check this https://matthewminer.com/2018/08/25/creating-an-xpc-service-in-swift.html ? – Martin R Jun 13 '19 at 17:34
  • I did refer to that resource and, like the Apple sample code, it uses String as the transport. That works great, but if I replace String with `Person`, which is my Swift class, then I get the exception. If I add `@objc` to the `Person` class, it doesn't seem to help. Are there any build settings I need to change? The reference above actually requires a few changes to make Swift, in general, work. Perhaps I've overlooked something else along the lines of a bridging header... – kennyc Jun 13 '19 at 17:40

3 Answers3

6

The problems appears to be related to how NSCoder does archiving and unarchiving by relying on the classe's name. When the Person object is encoded on the application side, the name used is <ModuleName.ClassName>.

When attempting to deserialize that in the service, ModuleName is naturally wrong. Using @objc(Person) on the Person to set the Objective-C names appears to be working.

kennyc
  • 5,490
  • 5
  • 34
  • 57
  • 1
    Thanks for good information on this. But even after using @objc(Person) I continue to get the same error: 'Exception: decodeObjectForKey: class "Person" not loaded or does not exist'. The Person source code is included in both targets. As in your case, it all works fine if I use String instead of Person. Any ideas? – rene Mar 09 '21 at 20:32
  • Hard to stay what issues you might be having, but I would try and instantiate a `Person` object in your XPC target somewhere generic just to make sure that the XPC target does, indeed, have the appropriate classes included. Be sure to test from a Swift file and an ObjC file. That should help narrow the issue down. Also worth checking to make sure you're allowing `Person` classes to be decoded through `NSSecureCoding` methods. – kennyc Mar 14 '21 at 06:06
  • 1
    If anyone needs just a little bit more information to make it clear how to use @objc here, this article got me there - https://developer.apple.com/forums/thread/7296 – Chris May 28 '21 at 12:10
0

I found it a lot easier to simply put all of the shared objects in a separate framework and import that framework everywhere I'm dealing with these objects. I never managed to fix this error, but it simply went away once the objects were inside a separate framework.

Lucas van Dongen
  • 9,328
  • 7
  • 39
  • 60
0

You may find it quite a bit simpler to use SecureXPC instead which provides a pure Swift API. It uses Codable which is a very well support protocol within the Swift ecosystem.

I built this after finding the Objective-C API to be quite awkward and generally disliked how much the use of @objc ended up spreading through my codebase.

Under the hood it makes use of the XPC C framework; this is not a wrapper for the Objective-C API nor does it resemble the C framework. It's designed from the ground up for Swift.

Joshua Kaplan
  • 843
  • 5
  • 9
  • Thanks for sharing. This looks like a potentially useful library, nice work. It's been awhile, but my use-case involved decoding and processing media files and made extensive use of `IOSurface`, which is not easily representable as `Codable`. Another class, `MTLSharedTextureHandle` appears to only be encodable using the `ObjC` API. `NSXPCConnection` is a third type that is commonly used when you want a backchannel connection to "stream" responses or progress updates back. (See WWDC 2013-702). Support for `IOSurface` and `NSXPCConnection` are, IMHO, critical for a lot of XPC use-cases. – kennyc Jul 08 '22 at 18:43
  • 1
    Thanks for the follow up. Adding support for `IOSurface` ought to be feasible, although I haven’t looked into the specifics since it’s never been requested. File descriptors/handles are similarly live and I was able to rather easily add support for them. SecureXPC allows for passing around connections as `XPCServerEndpoint` instances which is the equivalent of an `NSXPCConnection`. But unfortunately looks like `MTLSharedTextureHandle` can’t be supported. – Joshua Kaplan Jul 08 '22 at 23:58
  • [1/2] If you could use something like an `AsyncStream` to allow for the client to receive multiple replies, then you'd go a long way to simplifying how an XPC connection can be used when progress reporting or multi-part replies are required. Just make sure that the client receives the replies in a deterministic order. A use-case might be decoding video frames when you want the frames to come back in timestamp order to the client. – kennyc Jul 09 '22 at 18:30
  • [2/2] Another use-case to consider is supporting back-channel cancellation requests. Not sure how this would work with `Task` but there might be a way to leverage the existing cancellation support. Back-channel cancellation allows you to cancel an inflight job on the server without invalidating the entire connection. A use-case is asking the server to render thumbnails for a batch of files but then cancelling a subset of those request (like when they scroll off the screen) but not cancelling all of them. – kennyc Jul 09 '22 at 18:32
  • Would you be up for starting a discussion to share more thoughts/feedback on the [Github page](https://github.com/trilemma-dev/SecureXPC/discussions)? I very much appreciate your perspective as it's quite different than what I've gotten so far with folks using XPC for different purposes. – Joshua Kaplan Jul 10 '22 at 07:23
  • [1] This was implemented as part of the [0.5.0](https://github.com/trilemma-dev/SecureXPC/releases/tag/0.5.0) release from April. Would be curious to hear your thoughts on it. On the client side it's exposed as an `AsyncThrowingStream` (there's also a closure-based version) and on the server side uses a custom `SequentialResultProvider` class. – Joshua Kaplan Jul 10 '22 at 07:24
  • [2] There's no built-in support for anything like that, but it's my initial reaction without giving it considerable thought is it's not clear to me that ought to be part of an XPC framework itself. – Joshua Kaplan Jul 10 '22 at 07:26
  • Adding support for `IOSurface` appears to be trivial: https://github.com/trilemma-dev/SecureXPC/pull/106 – Joshua Kaplan Jul 10 '22 at 11:56