node-swift icon indicating copy to clipboard operation
node-swift copied to clipboard

How can I debug swift with nodejs at the runtime

Open skeyboy opened this issue 1 year ago • 4 comments

Maybe how can I debug the libray at the runtime with source code

skeyboy avatar Aug 14 '24 16:08 skeyboy

AFAICT there is no way to use a debugger on the Swift side when calling from node.js. I found that being able to log from Swift to the node console was useful for me in debugging - at least as useful as inserting print statements anway. You can do this using the NodeClass...

import NodeAPI

/// A facade for the NodeConsole class used in node.js.
@NodeClass final class NodeConsoleFacade {
    
    /// The queue to run the callback on (see https://github.com/kabiroberai/node-swift/issues/17)
    private let nodeQueue: NodeAsyncQueue
    
    /// Initialize the `nodeQueue` callback queue when the instance is created on the node.js side.
    @NodeActor
    @NodeConstructor
    public init() throws {
        nodeQueue = try NodeAsyncQueue(label: "nodeConsoleQueue")
        NodeConsole.init(nodeQueue: nodeQueue)
    }
    
    /// Initialize the NodeConsole singleton that will execute `callback` on `nodeQueue`.
    @NodeActor
    @NodeMethod
    public func registerLogCallback(callback: NodeFunction) throws {
        NodeConsole.console?.setCallback(callback)
    }

}

/// A class that can be used to show a message in the node.js console via `NodeConsole.log`.
public final class NodeConsole {
    
    private let nodeQueue: NodeAsyncQueue?
    fileprivate var callback: NodeFunction? = nil
    
    /// The singleton `NodeConsole`.
    @NodeActor
    fileprivate static var console: NodeConsole?
    
    /// Log a message in the node.js console using the `console` singleton.
    public static func log(_ message: String) throws {
        Task { @NodeActor in
            try console?.log(message)
        }
    }
    
    /// Initialize the NodeConsole singleton when the callback is registered from node.js.
    @NodeActor
    @discardableResult
    fileprivate init(nodeQueue: NodeAsyncQueue) {
        self.nodeQueue = nodeQueue
        Self.console = self
    }
    
    @NodeActor
    fileprivate func setCallback(_ callback: NodeFunction) {
        self.callback = callback
    }
    
    /// Run the `callback` on the `nodeQueue`, passing the `message`.
    private func log(_ message: String) throws {
        guard let nodeQueue, let callback else { return }
        try nodeQueue.run {
            try callback.call([message])
        }
    }
}

You need to expose the facade in your NodeExports:

#NodeModule(exports: [
    "test": try NodeFunction { _ in
        try NodeConsole.log("I'm going to do something.")
        // Do something
        try NodeConsole.log("I did something.")
        return  // Need to return or the compiler becomes confused
    },
    "NodeConsole": NodeConsoleFacade.deferredConstructor,
])

On the node side, you need to register the callback:

const nodeConsole = new NodeConsole();
nodeConsole.registerLogCallback((message) => {
    console.log("Swift> " + message)
});

The test function in the exports shows how you would use NodeConsole.log from Swift.

stevengharris avatar Mar 28 '25 20:03 stevengharris

It's occasionally buggy (as LLDB always tends to be), but you can attach to the Node.js process from LLDB/Xcode and debug your source files. If you're using Xcode, you can do this with Debug > Attach to Process by PID or Name > [enter PID] > Attach.

kabiroberai avatar Apr 06 '25 20:04 kabiroberai

FWIW, I formalized the NodeConsole.log stuff above into a package you can use in a NodeSwift project, and I included a backend for SwiftLog so that normal logging like logger.info("message") sends data back to node in a callback. Details at NodeSwiftLogging.

stevengharris avatar Apr 15 '25 15:04 stevengharris