xcodemacosnsxpcconnection

Using custom class in XPC protocol


I’m attempting to write an XPC service using my own type on the withReply signature. The type/class has Xcode’s “target membership” of both the main app and the XPC service. However I am getting incompatible reply block signature in the debug output even though the same class is being used in the withReply signature however the Xcode target differs as I will explain below.

Note: This is being done in Swift using this project to get me started. Except there they use NSData instead of a custom type.

Details

For the purposes of this question I’ll use the following as an example

and then all the usual XPC boilerplate where I export an object conforming to TweetTransfer. The XPC service appears to launch but then transfer between it and the main app fails with

XPCWorker[11069:402719] <NSXPCConnection: 0x61800010e220> connection from pid 11066 received an undecodable message

The full message is below[1] but the only difference between the “wire” and “local” is that argument one is

Where the Xcode target is different. Is that enough to throw it off? How then do I share code between an app and it's XPC service?

[1] Full error text

<NSXPCConnection: 0x61800010e220> connection from pid 11066 received an undecodable message (incompatible reply block signature (wire: <NSMethodSignature: 0x618000074ec0>
    number of arguments = 2
    frame size = 224
    is special struct return? NO
    return value: -------- -------- -------- --------
        type encoding (v) 'v'
        flags {}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
        memory {offset = 0, size = 0}
    argument 0: -------- -------- -------- --------
        type encoding (@) '@?'
        flags {isObject, isBlock}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 1: -------- -------- -------- --------
        type encoding (@) '@"_TtC17MainApp5Tweet"'
        flags {isObject}
        modifiers {}
        frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
            class '_TtC17MainApp5Tweet'
 vs local: <NSMethodSignature: 0x610000074740>
    number of arguments = 2
    frame size = 224
    is special struct return? NO
    return value: -------- -------- -------- --------
        type encoding (v) 'v'
        flags {}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
        memory {offset = 0, size = 0}
    argument 0: -------- -------- -------- --------
        type encoding (@) '@?'
        flags {isObject, isBlock}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 1: -------- -------- -------- --------
        type encoding (@) '@"_TtC23XPCWorker5Tweet"'
        flags {isObject}
        modifiers {}
        frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
            class '_TtC23XPCWorker5Tweet'
)

Update

Some more info regarding the protocol, remoteObjectProxy connection and Tweet object. This is the protocol used for the XPC calls:

@objc(TweetTransfer)
protocol TweetTransfer {
  func take(_ count: Int, withReply: replyType)
}

typealias replyType = ((Tweet) -> Void)

I'm using a type alias for convenience. And then the Tweet object is very simple and just for testing (although somewhat complicated by supporting NSSecureCoding):

final class Tweet: NSObject, NSSecureCoding {
  var name: String
  var text: String
  static var supportsSecureCoding = true

  init(name: String, text: String) {
    self.name = name
    self.text = text
  }

  init?(coder aDecoder: NSCoder) {
    guard let name = aDecoder.decodeObject(forKey: "name") as? String else {
      fatalError("Could not deserialise name!")
    }

    guard let text = aDecoder.decodeObject(forKey: "text") as? String else {
      fatalError("Could not deseralise text!")
    }

    self.name = name
    self.text = text
  }

  func encode(with aCoder: NSCoder) {
    aCoder.encode(name, forKey: "name")
    aCoder.encode(text, forKey: "text")
  }
}

and finally the point at which we call the remoteObjectProxy

guard let loader = workerConnection.remoteObjectProxyWithErrorHandler(handler) as? TweetTransfer else {
  fatalError("Could not map worker to TweetTransfer protocol!")
}

var tweets = [Tweet]()
loader.take(1) { tweet in
  tweets.append(tweet)
}

Solution

  • The full message is below but the only difference between the “wire” and “local” is that argument one is

    • wire - _TtC17MainApp5Tweet
    • local - _TtC23XPCWorker5Tweet

    Where the Xcode target is different. Is that enough to throw it off? How then do I share code between an app and it's XPC service?

    That is indeed enough to throw it off. Swift's namespacing makes the archived object appear as a different class. You can disable name spacing by declaring your Tweet object with;

    @objc(Tweet) class Tweet: NSObject, NSSecureCoding { ... }
    

    The name in @objc(name) is often presented as a way to present a different name in objc vs Swift, but it also has the effect of disabling Swift's name spacing.

    From Using Swift with Cocoa and Objective-C

    When you use the @objc(name) attribute on a Swift class, the class is made available in Objective-C without any namespacing. As a result, this attribute can also be useful when migrating an archivable Objective-C class to Swift. Because archived objects store the name of their class in the archive, you should use the @objc(name) attribute to specify the same name as your Objective-C class so that older archives can be unarchived by your new Swift class.

    Another alternative is to move your custom object to a Framework. That framework target then becomes the namespace, and both the XPC and App would refer to the same namespace/class in the framework.