kabiroberai / node-swift

Create Node modules in Swift
MIT License
492 stars 16 forks source link

How to expose existing classes to Node? #27

Closed justjake closed 4 months ago

justjake commented 4 months ago

Hey, I'm new to node-swift. I'm trying to wrap ScreenCaptureKit for access from NodeJS or Electron applications. By looking over the existing code in the repo, it appears the way to expose instances of a Swift class to NodeJS is to implement NodeClass as an extension.

However when I try to implement NodeClass for SCDisplay, I get this error:

Protocol 'NodeClass' requirement 'construct' cannot be satisfied by a non-final class ('SCDisplay') because it uses 'Self' in a non-parameter, non-result type position

Method 'from' in non-final class 'SCDisplay' cannot be implemented in a protocol extension because it returns 'Self' and has associated type requirements

image

So, my work-around currently is to create my own wrapper classes with @NodeProperty attributes for each property I want to expose to Node:

@available(macOS 12.3, *)
@NodeClass final class Display {
  let inner: SCDisplay

  init(_ inner: SCDisplay) {
    self.inner = inner
  }

  @NodeProperty var displayID: String {
    inner.displayID.formatted()
  }

  @NodeProperty var frame: CGRect {
    inner.frame
  }

  @NodeProperty var width: Int {
    inner.width
  }

  @NodeProperty var height: Int {
    inner.height
  }
}

Once I've written my wrapper class, what's the right way to automatically promote SCDisplay to my wrapper Display class? I tried this but I'm not sure if coerce is correct:

@available(macOS 12.3, *)
extension SCDisplay: NodeValueConvertible {
  public func nodeValue() throws -> any NodeAPI.NodeValue {
    try NodeObject(coercing: Display(self))
  }
}

Even with that extension, when I try to convert [SCDisplay] to a NodeObject property, I get an error. Is the manual .map { inner in MyWrapper(inner) } the best way to go about this?

image
kabiroberai commented 4 months ago

:wave: I wouldn't recommend conforming an existing third-party class to NodeClass — retroactive conformances are discouraged across Swift, see SE-0364 for one explanation. Creating a wrapper class is a reasonable solution, so if possible avoid conforming SCDisplay to any NodeSwift protocols yourself and just deal with your own Display type.

If you really want the retroactive NodeValueConvertible conformance (eg you're sure nobody else is going to try to add their own extension SCDisplay: NodeValueConvertible) then your answer is to return Display(self).nodeValue(). For completeness, I'd also recommend adding a NodeValueCreatable conformance to handle the case of going from JS to your class. You can probably do something like (untested):

extension SCDisplay: NodeValueCreatable {
  static func from(_ value: NodeObject) throws -> Self {
    // force cast because SCDisplay isn't final so Self != SCDisplay. Consider throwing instead.
    try value.as(Display.self).inner as! Self
  }
}

As for the issue with arrays, that's because Swift protocols can't conform to themselves. I could have either allowed for [some NodeValueConvertible]: NodeValueConvertible or [any NodeValueConvertible]: NodeValueConvertible and I went with the latter. But the two cases are actually equivalent barring some syntactic cruft; you just have to write it as self.displays as [any NodeValueConvertible].