From eb5f1696d9e685bd433127edec1a525fbfe29df3 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 8 Oct 2025 19:52:37 +0100 Subject: [PATCH 001/107] Basic LLDB remote protocol scaffolding --- Package@swift-6.1.swift | 194 ++++++++++++++++++ .../LLDBRemoteProtocol/CommandDecoder.swift | 121 +++++++++++ Sources/LLDBRemoteProtocol/HostCommand.swift | 41 ++++ Sources/LLDBRemoteProtocol/Package.swift | 12 ++ .../LLDBRemoteProtocol/ResponseEncoder.swift | 24 +++ .../LLDBRemoteProtocol/TargetResponse.swift | 15 ++ .../WasmKitLLDBHandler/WasmKitHandler.swift | 55 +++++ Sources/wasmkit-lldb/Entrypoint.swift | 45 ++++ .../RemoteProtocolTests.swift | 33 +++ 9 files changed, 540 insertions(+) create mode 100644 Package@swift-6.1.swift create mode 100644 Sources/LLDBRemoteProtocol/CommandDecoder.swift create mode 100644 Sources/LLDBRemoteProtocol/HostCommand.swift create mode 100644 Sources/LLDBRemoteProtocol/Package.swift create mode 100644 Sources/LLDBRemoteProtocol/ResponseEncoder.swift create mode 100644 Sources/LLDBRemoteProtocol/TargetResponse.swift create mode 100644 Sources/WasmKitLLDBHandler/WasmKitHandler.swift create mode 100644 Sources/wasmkit-lldb/Entrypoint.swift create mode 100644 Tests/LLDBRemoteProtocolTests/RemoteProtocolTests.swift diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift new file mode 100644 index 00000000..ffb392a7 --- /dev/null +++ b/Package@swift-6.1.swift @@ -0,0 +1,194 @@ +// swift-tools-version:6.1 + +import PackageDescription + +import class Foundation.ProcessInfo + +let DarwinPlatforms: [Platform] = [.macOS, .iOS, .watchOS, .tvOS, .visionOS] + +let package = Package( + name: "WasmKit", + platforms: [.macOS(.v10_13), .iOS(.v12)], + products: [ + .executable(name: "wasmkit-cli", targets: ["CLI"]), + .library(name: "WasmKit", targets: ["WasmKit"]), + .library(name: "WasmKitWASI", targets: ["WasmKitWASI"]), + .library(name: "WASI", targets: ["WASI"]), + .library(name: "WasmParser", targets: ["WasmParser"]), + .library(name: "WAT", targets: ["WAT"]), + .library(name: "WIT", targets: ["WIT"]), + .library(name: "_CabiShims", targets: ["_CabiShims"]), + ], + traits: [ + .default(enabledTraits: []), + "WasmDebuggingSupport" + ], + targets: [ + .executableTarget( + name: "CLI", + dependencies: [ + "WAT", + "WasmKit", + "WasmKitWASI", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "SystemPackage", package: "swift-system"), + ], + exclude: ["CMakeLists.txt"] + ), + + .target( + name: "WasmKit", + dependencies: [ + "_CWasmKit", + "WasmParser", + "WasmTypes", + "SystemExtras", + .product(name: "SystemPackage", package: "swift-system"), + ], + exclude: ["CMakeLists.txt"] + ), + .target(name: "_CWasmKit"), + .target( + name: "WasmKitFuzzing", + dependencies: ["WasmKit"], + path: "FuzzTesting/Sources/WasmKitFuzzing" + ), + .testTarget( + name: "WasmKitTests", + dependencies: ["WasmKit", "WAT", "WasmKitFuzzing"], + exclude: ["ExtraSuite"] + ), + + .target( + name: "WAT", + dependencies: ["WasmParser"], + exclude: ["CMakeLists.txt"] + ), + .testTarget(name: "WATTests", dependencies: ["WAT"]), + + .target( + name: "WasmParser", + dependencies: [ + "WasmTypes", + .product(name: "SystemPackage", package: "swift-system"), + ], + exclude: ["CMakeLists.txt"] + ), + .testTarget(name: "WasmParserTests", dependencies: ["WasmParser"]), + + .target(name: "WasmTypes", exclude: ["CMakeLists.txt"]), + + .target( + name: "WasmKitWASI", + dependencies: ["WasmKit", "WASI"], + exclude: ["CMakeLists.txt"] + ), + .target( + name: "WASI", + dependencies: ["WasmTypes", "SystemExtras"], + exclude: ["CMakeLists.txt"] + ), + .testTarget(name: "WASITests", dependencies: ["WASI", "WasmKitWASI"]), + + .target( + name: "SystemExtras", + dependencies: [ + .product(name: "SystemPackage", package: "swift-system") + ], + exclude: ["CMakeLists.txt"], + swiftSettings: [ + .define("SYSTEM_PACKAGE_DARWIN", .when(platforms: DarwinPlatforms)) + ] + ), + + .executableTarget( + name: "WITTool", + dependencies: [ + "WIT", + "WITOverlayGenerator", + "WITExtractor", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ] + ), + + .target(name: "WIT"), + .testTarget(name: "WITTests", dependencies: ["WIT"]), + + .target(name: "WITOverlayGenerator", dependencies: ["WIT"]), + .target(name: "_CabiShims"), + + .target(name: "WITExtractor"), + .testTarget(name: "WITExtractorTests", dependencies: ["WITExtractor", "WIT"]), + + .target(name: "LLDBRemoteProtocol", + dependencies: [ + .product(name: "NIOCore", package: "swift-nio"), + ] + ), + .testTarget(name: "LLDBRemoteProtocolTests", dependencies: ["LLDBRemoteProtocol"]), + + .target( + name: "WasmKitLLDBHandler", + dependencies: [ + .product(name: "NIOCore", package: "swift-nio"), + "WasmKit", + "LLDBRemoteProtocol", + ], + ), + + .executableTarget( + name: "wasmkit-lldb", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + "LLDBRemoteProtocol", + "WasmKitLLDBHandler", + ] + ), + ], +) + +if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { + package.dependencies += [ + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.1"), + .package(url: "https://github.com/apple/swift-system", from: "1.5.0"), + .package(url: "https://github.com/apple/swift-nio", from: "2.86.2"), + ] +} else { + package.dependencies += [ + .package(path: "../swift-argument-parser"), + .package(path: "../swift-system"), + ] +} + +#if !os(Windows) + // Add build tool plugins only for non-Windows platforms + package.products.append(contentsOf: [ + .plugin(name: "WITOverlayPlugin", targets: ["WITOverlayPlugin"]), + .plugin(name: "WITExtractorPlugin", targets: ["WITExtractorPlugin"]), + ]) + + package.targets.append(contentsOf: [ + .plugin(name: "WITOverlayPlugin", capability: .buildTool(), dependencies: ["WITTool"]), + .plugin(name: "GenerateOverlayForTesting", capability: .buildTool(), dependencies: ["WITTool"]), + .testTarget( + name: "WITOverlayGeneratorTests", + dependencies: ["WITOverlayGenerator", "WasmKit", "WasmKitWASI"], + exclude: ["Fixtures", "Compiled", "Generated", "EmbeddedSupport"], + plugins: [.plugin(name: "GenerateOverlayForTesting")] + ), + .plugin( + name: "WITExtractorPlugin", + capability: .command( + intent: .custom(verb: "extract-wit", description: "Extract WIT definition from Swift module"), + permissions: [] + ), + dependencies: ["WITTool"] + ), + .testTarget( + name: "WITExtractorPluginTests", + exclude: ["Fixtures"] + ), + ]) +#endif diff --git a/Sources/LLDBRemoteProtocol/CommandDecoder.swift b/Sources/LLDBRemoteProtocol/CommandDecoder.swift new file mode 100644 index 00000000..6acd798e --- /dev/null +++ b/Sources/LLDBRemoteProtocol/CommandDecoder.swift @@ -0,0 +1,121 @@ +import NIOCore + +import struct Foundation.Date + +extension ByteBuffer { + var isChecksumDelimiterAtReader: Bool { + self.peekInteger(as: UInt8.self) == UInt8(ascii: "#") + } + + var isArgumentsDelimiterAtReader: Bool { + self.peekInteger(as: UInt8.self) == UInt8(ascii: ":") + } +} + +package struct CommandDecoder: ByteToMessageDecoder { + enum Error: Swift.Error { + case expectedCommandStart + case unknownCommandKind(String) + case expectedChecksum + case checksumIncorrect + } + + package typealias InboundOut = Packet + + private var accummulatedKind = [UInt8]() + private var accummulatedArguments = [UInt8]() + + package init() {} + + private var accummulatedSum = 0 + package var accummulatedChecksum: UInt8 { + UInt8(self.accummulatedSum % 256) + } + + mutating package func decode(buffer: inout ByteBuffer) throws -> Packet? { + // Command start delimiters. + guard + buffer.readInteger(as: UInt8.self) == UInt8(ascii: "+") + && buffer.readInteger(as: UInt8.self) == UInt8(ascii: "$") + else { + throw Error.expectedCommandStart + } + + // Byte offset for command start. + while !buffer.isChecksumDelimiterAtReader && !buffer.isArgumentsDelimiterAtReader, + let char = buffer.readInteger(as: UInt8.self) + { + self.accummulatedSum += Int(char) + self.accummulatedKind.append(char) + } + + if buffer.isArgumentsDelimiterAtReader, + let argumentsDelimiter = buffer.readInteger(as: UInt8.self) + { + self.accummulatedSum += Int(argumentsDelimiter) + + while !buffer.isChecksumDelimiterAtReader, let char = buffer.readInteger(as: UInt8.self) { + self.accummulatedSum += Int(char) + self.accummulatedArguments.append(char) + } + } + + // Command checksum delimiter. + if !buffer.isChecksumDelimiterAtReader { + // If delimiter not available yet, return `nil` to indicate that the caller needs to top up the buffer. + return nil + } + + defer { + self.accummulatedKind = [] + self.accummulatedArguments = [] + self.accummulatedSum = 0 + } + + let kindString = String(decoding: self.accummulatedKind, as: UTF8.self) + + if let commandKind = HostCommand.Kind(rawValue: kindString) { + buffer.moveReaderIndex(forwardBy: 1) + + guard let checksumString = buffer.readString(length: 2), + let first = checksumString.first?.hexDigitValue, + let last = checksumString.last?.hexDigitValue + else { + throw Error.expectedChecksum + } + + guard (first * 16) + last == self.accummulatedChecksum else { + // FIXME: better diagnostics + throw Error.checksumIncorrect + } + + return .init( + payload: .init( + kind: commandKind, + arguments: String(decoding: self.accummulatedArguments, as: UTF8.self) + ), + checksum: accummulatedChecksum, + ) + } else { + throw Error.unknownCommandKind(kindString) + } + } + + mutating package func decode( + context: ChannelHandlerContext, + buffer: inout ByteBuffer + ) throws -> DecodingState { + print(buffer.peekString(length: buffer.readableBytes)!) + + guard let command = try self.decode(buffer: &buffer) else { + return .needMoreData + } + + // Shift by checksum bytes + context.fireChannelRead(wrapInboundOut(command)) + return .continue + // } else { + // throw Error.unknownCommand(accummulated) + // } + } +} diff --git a/Sources/LLDBRemoteProtocol/HostCommand.swift b/Sources/LLDBRemoteProtocol/HostCommand.swift new file mode 100644 index 00000000..cb7f63fc --- /dev/null +++ b/Sources/LLDBRemoteProtocol/HostCommand.swift @@ -0,0 +1,41 @@ +/// See https://lldb.llvm.org/resources/lldbgdbremote.html for more details. +package struct HostCommand: Equatable { + package enum Kind: Equatable { + // Currently listed in the order that LLDB sends them in. + case generalRegisters + case startNoAckMode + case firstThreadInfo + case supportedFeatures + case isThreadSuffixSupported + case listThreadsInStopReply + case hostInfo + + package init?(rawValue: String) { + switch rawValue { + case "g": + self = .generalRegisters + case "QStartNoAckMode": + self = .startNoAckMode + case "qSupported": + self = .supportedFeatures + case "QThreadSuffixSupported": + self = .isThreadSuffixSupported + case "QListThreadsInStopReply": + self = .listThreadsInStopReply + case "qHostInfo": + self = .hostInfo + default: + return nil + } + } + } + + package let kind: Kind + + package let arguments: String + + package init(kind: Kind, arguments: String) { + self.kind = kind + self.arguments = arguments + } +} diff --git a/Sources/LLDBRemoteProtocol/Package.swift b/Sources/LLDBRemoteProtocol/Package.swift new file mode 100644 index 00000000..87faa03a --- /dev/null +++ b/Sources/LLDBRemoteProtocol/Package.swift @@ -0,0 +1,12 @@ +package struct Packet { + package let payload: Payload + + package let checksum: UInt8 + + package init(payload: Payload, checksum: UInt8) { + self.payload = payload + self.checksum = checksum + } +} + +extension Packet: Equatable where Payload: Equatable {} diff --git a/Sources/LLDBRemoteProtocol/ResponseEncoder.swift b/Sources/LLDBRemoteProtocol/ResponseEncoder.swift new file mode 100644 index 00000000..33a254a1 --- /dev/null +++ b/Sources/LLDBRemoteProtocol/ResponseEncoder.swift @@ -0,0 +1,24 @@ +import Foundation +import NIOCore + +package struct ResponseEncoder: MessageToByteEncoder { + package init() {} + package func encode(data: TargetResponse, out: inout ByteBuffer) throws { + if !data.isNoAckModeActive { + out.writeInteger(UInt8(ascii: "+")) + } + out.writeInteger(UInt8(ascii: "$")) + + switch data.kind { + case .ok: + out.writeBytes("ok#da".utf8) + + case .raw(let str): + out.writeBytes( + "\(str)#\(String(format:"%02X", str.utf8.reduce(0, { $0 + Int($1) }) % 256))".utf8) + + case .empty: + fatalError("unhandled") + } + } +} diff --git a/Sources/LLDBRemoteProtocol/TargetResponse.swift b/Sources/LLDBRemoteProtocol/TargetResponse.swift new file mode 100644 index 00000000..46960b50 --- /dev/null +++ b/Sources/LLDBRemoteProtocol/TargetResponse.swift @@ -0,0 +1,15 @@ +package struct TargetResponse { + package enum Kind { + case ok + case raw(String) + case empty + } + + package let kind: Kind + package let isNoAckModeActive: Bool + + package init(kind: Kind, isNoAckModeActive: Bool) { + self.kind = kind + self.isNoAckModeActive = isNoAckModeActive + } +} diff --git a/Sources/WasmKitLLDBHandler/WasmKitHandler.swift b/Sources/WasmKitLLDBHandler/WasmKitHandler.swift new file mode 100644 index 00000000..97b309f2 --- /dev/null +++ b/Sources/WasmKitLLDBHandler/WasmKitHandler.swift @@ -0,0 +1,55 @@ +import LLDBRemoteProtocol +import NIOCore +import WasmKit + +import struct Foundation.Date + +package final class WasmKitHandler: ChannelInboundHandler { + package typealias InboundIn = Packet + package typealias OutboundOut = TargetResponse + + /// Whether `QStartNoAckMode` command was previously sent. + private var isNoAckModeActive = false + + package init() {} + + package func channelRead( + context: ChannelHandlerContext, + data: NIOAny + ) { + let command = self.unwrapInboundIn(data).payload + let responseKind: TargetResponse.Kind + print(command.kind) + + switch command.kind { + case .startNoAckMode, .isThreadSuffixSupported, .listThreadsInStopReply: + responseKind = .ok + case .supportedFeatures: + responseKind = .raw(command.arguments) + default: + fatalError() + } + + context.writeAndFlush( + wrapOutboundOut(.init(kind: responseKind, isNoAckModeActive: self.isNoAckModeActive)), + promise: nil) + if command.kind == .startNoAckMode { + self.isNoAckModeActive = true + } + } + + package func channelReadComplete( + context: ChannelHandlerContext + ) { + context.flush() + } + + package func errorCaught( + context: ChannelHandlerContext, + error: Error + ) { + print(error) + + context.close(promise: nil) + } +} diff --git a/Sources/wasmkit-lldb/Entrypoint.swift b/Sources/wasmkit-lldb/Entrypoint.swift new file mode 100644 index 00000000..00ccaba7 --- /dev/null +++ b/Sources/wasmkit-lldb/Entrypoint.swift @@ -0,0 +1,45 @@ +import ArgumentParser +import LLDBRemoteProtocol +import NIOCore +import NIOPosix +import WasmKitLLDBHandler + +@main +struct Entrypoint: ParsableCommand { + @Option(help: "TCP port that a debugger can connect to") + var port = 8080 + + func run() throws { + let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + let bootstrap = ServerBootstrap(group: group) + // Specify backlog and enable SO_REUSEADDR for the server itself + .serverChannelOption(.backlog, value: 256) + .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) + + // Set the handlers that are applied to the accepted child `Channel`s. + .childChannelInitializer { channel in + // Ensure we don't read faster then we can write by adding the BackPressureHandler into the pipeline. + channel.eventLoop.makeCompletedFuture { + try channel.pipeline.syncOperations.addHandler(BackPressureHandler()) + // make sure to instantiate your `ChannelHandlers` inside of + // the closure as it will be invoked once per connection. + try channel.pipeline.syncOperations.addHandlers([ + ByteToMessageHandler(CommandDecoder()), + MessageToByteHandler(ResponseEncoder()), + WasmKitHandler(), + ]) + } + } + + // Enable SO_REUSEADDR for the accepted Channels + .childChannelOption(.socketOption(.so_reuseaddr), value: 1) + .childChannelOption(.maxMessagesPerRead, value: 16) + .childChannelOption(.recvAllocator, value: AdaptiveRecvByteBufferAllocator()) + let channel = try bootstrap.bind(host: "127.0.0.1", port: port).wait() + /* the server will now be accepting connections */ + print("listening on port \(port)") + + try channel.closeFuture.wait() // wait forever as we never close the Channel + try group.syncShutdownGracefully() + } +} diff --git a/Tests/LLDBRemoteProtocolTests/RemoteProtocolTests.swift b/Tests/LLDBRemoteProtocolTests/RemoteProtocolTests.swift new file mode 100644 index 00000000..c3805f7c --- /dev/null +++ b/Tests/LLDBRemoteProtocolTests/RemoteProtocolTests.swift @@ -0,0 +1,33 @@ +import LLDBRemoteProtocol +import NIOCore +import Testing + +@Suite +struct LLDBRemoteProtocolTests { + @Test + func decoding() throws { + var decoder = CommandDecoder() + + var buffer = ByteBuffer(string: "+$g#67") + var packet = try decoder.decode(buffer: &buffer) + #expect(packet == Packet(payload: HostCommand(kind: .generalRegisters, arguments: ""), checksum: 103)) + #expect(decoder.accummulatedChecksum == 0) + + buffer = ByteBuffer( + string: """ + +$qSupported:xmlRegisters=i386,arm,mips,arc;multiprocess+;fork-events+;vfork-events+#2e + """ + ) + + packet = try decoder.decode(buffer: &buffer) + let expectedPacket = Packet( + payload: HostCommand( + kind: .supportedFeatures, + arguments: "xmlRegisters=i386,arm,mips,arc;multiprocess+;fork-events+;vfork-events+" + ), + checksum: 0x2e, + ) + #expect(packet == expectedPacket) + #expect(decoder.accummulatedChecksum == 0) + } +} From 0baf7a51d9e8b59be3aaa5f940499b0dac45aebb Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 8 Oct 2025 21:56:18 +0100 Subject: [PATCH 002/107] Implement more packet handling, update naming --- Package@swift-6.1.swift | 14 ++++---- .../CommandDecoder.swift | 18 ++++++---- .../HostCommand.swift | 21 +++++++++-- .../Package.swift | 0 .../GDBRemoteProtocol/ResponseEncoder.swift | 35 +++++++++++++++++++ .../GDBRemoteProtocol/TargetResponse.swift | 27 ++++++++++++++ .../LLDBRemoteProtocol/ResponseEncoder.swift | 24 ------------- .../LLDBRemoteProtocol/TargetResponse.swift | 15 -------- .../WasmKitHandler.swift | 29 ++++++++++++--- .../Entrypoint.swift | 8 ++--- .../RemoteProtocolTests.swift | 8 ++--- 11 files changed, 132 insertions(+), 67 deletions(-) rename Sources/{LLDBRemoteProtocol => GDBRemoteProtocol}/CommandDecoder.swift (83%) rename Sources/{LLDBRemoteProtocol => GDBRemoteProtocol}/HostCommand.swift (58%) rename Sources/{LLDBRemoteProtocol => GDBRemoteProtocol}/Package.swift (100%) create mode 100644 Sources/GDBRemoteProtocol/ResponseEncoder.swift create mode 100644 Sources/GDBRemoteProtocol/TargetResponse.swift delete mode 100644 Sources/LLDBRemoteProtocol/ResponseEncoder.swift delete mode 100644 Sources/LLDBRemoteProtocol/TargetResponse.swift rename Sources/{WasmKitLLDBHandler => WasmKitGDBHandler}/WasmKitHandler.swift (64%) rename Sources/{wasmkit-lldb => wasmkit-gdb-tool}/Entrypoint.swift (90%) rename Tests/{LLDBRemoteProtocolTests => GDBRemoteProtocolTests}/RemoteProtocolTests.swift (79%) diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index ffb392a7..f70371f1 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -120,30 +120,30 @@ let package = Package( .target(name: "WITExtractor"), .testTarget(name: "WITExtractorTests", dependencies: ["WITExtractor", "WIT"]), - .target(name: "LLDBRemoteProtocol", + .target(name: "GDBRemoteProtocol", dependencies: [ .product(name: "NIOCore", package: "swift-nio"), ] ), - .testTarget(name: "LLDBRemoteProtocolTests", dependencies: ["LLDBRemoteProtocol"]), + .testTarget(name: "GDBRemoteProtocolTests", dependencies: ["GDBRemoteProtocol"]), .target( - name: "WasmKitLLDBHandler", + name: "WasmKitGDBHandler", dependencies: [ .product(name: "NIOCore", package: "swift-nio"), "WasmKit", - "LLDBRemoteProtocol", + "GDBRemoteProtocol", ], ), .executableTarget( - name: "wasmkit-lldb", + name: "wasmkit-gdb-tool", dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), - "LLDBRemoteProtocol", - "WasmKitLLDBHandler", + "GDBRemoteProtocol", + "WasmKitGDBHandler", ] ), ], diff --git a/Sources/LLDBRemoteProtocol/CommandDecoder.swift b/Sources/GDBRemoteProtocol/CommandDecoder.swift similarity index 83% rename from Sources/LLDBRemoteProtocol/CommandDecoder.swift rename to Sources/GDBRemoteProtocol/CommandDecoder.swift index 6acd798e..c46b7a51 100644 --- a/Sources/LLDBRemoteProtocol/CommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/CommandDecoder.swift @@ -12,7 +12,7 @@ extension ByteBuffer { } } -package struct CommandDecoder: ByteToMessageDecoder { +package struct GDBHostCommandDecoder: ByteToMessageDecoder { enum Error: Swift.Error { case expectedCommandStart case unknownCommandKind(String) @@ -20,7 +20,7 @@ package struct CommandDecoder: ByteToMessageDecoder { case checksumIncorrect } - package typealias InboundOut = Packet + package typealias InboundOut = Packet private var accummulatedKind = [UInt8]() private var accummulatedArguments = [UInt8]() @@ -32,12 +32,16 @@ package struct CommandDecoder: ByteToMessageDecoder { UInt8(self.accummulatedSum % 256) } - mutating package func decode(buffer: inout ByteBuffer) throws -> Packet? { + mutating package func decode(buffer: inout ByteBuffer) throws -> Packet? { // Command start delimiters. - guard - buffer.readInteger(as: UInt8.self) == UInt8(ascii: "+") - && buffer.readInteger(as: UInt8.self) == UInt8(ascii: "$") + let firstStartDelimiter = buffer.readInteger(as: UInt8.self) + let secondStartDelimiter = buffer.readInteger(as: UInt8.self) + guard firstStartDelimiter == UInt8(ascii: "+") + && secondStartDelimiter == UInt8(ascii: "$") else { + if let firstStartDelimiter, let secondStartDelimiter { + print("unexpected delimiter: \(Character(UnicodeScalar(firstStartDelimiter)))\(Character(UnicodeScalar(secondStartDelimiter)))") + } throw Error.expectedCommandStart } @@ -74,7 +78,7 @@ package struct CommandDecoder: ByteToMessageDecoder { let kindString = String(decoding: self.accummulatedKind, as: UTF8.self) - if let commandKind = HostCommand.Kind(rawValue: kindString) { + if let commandKind = GDBHostCommand.Kind(rawValue: kindString) { buffer.moveReaderIndex(forwardBy: 1) guard let checksumString = buffer.readString(length: 2), diff --git a/Sources/LLDBRemoteProtocol/HostCommand.swift b/Sources/GDBRemoteProtocol/HostCommand.swift similarity index 58% rename from Sources/LLDBRemoteProtocol/HostCommand.swift rename to Sources/GDBRemoteProtocol/HostCommand.swift index cb7f63fc..754270fd 100644 --- a/Sources/LLDBRemoteProtocol/HostCommand.swift +++ b/Sources/GDBRemoteProtocol/HostCommand.swift @@ -1,5 +1,7 @@ -/// See https://lldb.llvm.org/resources/lldbgdbremote.html for more details. -package struct HostCommand: Equatable { +/// See GDB and LLDB remote protocol documentation for more details: +/// * https://sourceware.org/gdb/current/onlinedocs/gdb.html/General-Query-Packets.html +/// * https://lldb.llvm.org/resources/lldbgdbremote.html +package struct GDBHostCommand: Equatable { package enum Kind: Equatable { // Currently listed in the order that LLDB sends them in. case generalRegisters @@ -9,6 +11,11 @@ package struct HostCommand: Equatable { case isThreadSuffixSupported case listThreadsInStopReply case hostInfo + case vContSupportedActions + case isVAttachOrWaitSupported + case enableErrorStrings + case processInfo + case currentThreadID package init?(rawValue: String) { switch rawValue { @@ -24,6 +31,16 @@ package struct HostCommand: Equatable { self = .listThreadsInStopReply case "qHostInfo": self = .hostInfo + case "vCont?": + self = .vContSupportedActions + case "qVAttachOrWaitSupported": + self = .isVAttachOrWaitSupported + case "QEnableErrorStrings": + self = .enableErrorStrings + case "qProcessInfo": + self = .processInfo + case "qC": + self = .currentThreadID default: return nil } diff --git a/Sources/LLDBRemoteProtocol/Package.swift b/Sources/GDBRemoteProtocol/Package.swift similarity index 100% rename from Sources/LLDBRemoteProtocol/Package.swift rename to Sources/GDBRemoteProtocol/Package.swift diff --git a/Sources/GDBRemoteProtocol/ResponseEncoder.swift b/Sources/GDBRemoteProtocol/ResponseEncoder.swift new file mode 100644 index 00000000..ddcbe872 --- /dev/null +++ b/Sources/GDBRemoteProtocol/ResponseEncoder.swift @@ -0,0 +1,35 @@ +import Foundation +import NIOCore + +extension String { + fileprivate var appendedChecksum: String.UTF8View { + "\(self)#\(String(format:"%02X", self.utf8.reduce(0, { $0 + Int($1) }) % 256))".utf8 + } +} + +package struct GDBTargetResponseEncoder: MessageToByteEncoder { + package init() {} + package func encode(data: TargetResponse, out: inout ByteBuffer) throws { + if !data.isNoAckModeActive { + out.writeInteger(UInt8(ascii: "+")) + } + out.writeInteger(UInt8(ascii: "$")) + + switch data.kind { + case .ok: + out.writeBytes("ok#da".utf8) + + case .hostInfo(let info): + out.writeBytes(info.map { (key, value) in "\(key):\(value);"}.joined().appendedChecksum) + + case .vContSupportedActions(let actions): + out.writeBytes("vCont;\(actions.map(\.rawValue).joined())".appendedChecksum) + + case .raw(let str): + out.writeBytes(str.appendedChecksum) + + case .empty: + out.writeBytes("".appendedChecksum) + } + } +} diff --git a/Sources/GDBRemoteProtocol/TargetResponse.swift b/Sources/GDBRemoteProtocol/TargetResponse.swift new file mode 100644 index 00000000..4373f1f2 --- /dev/null +++ b/Sources/GDBRemoteProtocol/TargetResponse.swift @@ -0,0 +1,27 @@ +/// Actions supported in the `vCont` host command. +package enum VContActions: String { + case `continue` = "c" + case continueWithSignal = "C" + case step = "s" + case stepWithSignal = "S" + case stop = "t" + case stepInRange = "r" +} + +package struct TargetResponse { + package enum Kind { + case ok + case hostInfo(KeyValuePairs) + case vContSupportedActions([VContActions]) + case raw(String) + case empty + } + + package let kind: Kind + package let isNoAckModeActive: Bool + + package init(kind: Kind, isNoAckModeActive: Bool) { + self.kind = kind + self.isNoAckModeActive = isNoAckModeActive + } +} diff --git a/Sources/LLDBRemoteProtocol/ResponseEncoder.swift b/Sources/LLDBRemoteProtocol/ResponseEncoder.swift deleted file mode 100644 index 33a254a1..00000000 --- a/Sources/LLDBRemoteProtocol/ResponseEncoder.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation -import NIOCore - -package struct ResponseEncoder: MessageToByteEncoder { - package init() {} - package func encode(data: TargetResponse, out: inout ByteBuffer) throws { - if !data.isNoAckModeActive { - out.writeInteger(UInt8(ascii: "+")) - } - out.writeInteger(UInt8(ascii: "$")) - - switch data.kind { - case .ok: - out.writeBytes("ok#da".utf8) - - case .raw(let str): - out.writeBytes( - "\(str)#\(String(format:"%02X", str.utf8.reduce(0, { $0 + Int($1) }) % 256))".utf8) - - case .empty: - fatalError("unhandled") - } - } -} diff --git a/Sources/LLDBRemoteProtocol/TargetResponse.swift b/Sources/LLDBRemoteProtocol/TargetResponse.swift deleted file mode 100644 index 46960b50..00000000 --- a/Sources/LLDBRemoteProtocol/TargetResponse.swift +++ /dev/null @@ -1,15 +0,0 @@ -package struct TargetResponse { - package enum Kind { - case ok - case raw(String) - case empty - } - - package let kind: Kind - package let isNoAckModeActive: Bool - - package init(kind: Kind, isNoAckModeActive: Bool) { - self.kind = kind - self.isNoAckModeActive = isNoAckModeActive - } -} diff --git a/Sources/WasmKitLLDBHandler/WasmKitHandler.swift b/Sources/WasmKitGDBHandler/WasmKitHandler.swift similarity index 64% rename from Sources/WasmKitLLDBHandler/WasmKitHandler.swift rename to Sources/WasmKitGDBHandler/WasmKitHandler.swift index 97b309f2..cbdff83c 100644 --- a/Sources/WasmKitLLDBHandler/WasmKitHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitHandler.swift @@ -1,11 +1,11 @@ -import LLDBRemoteProtocol +import GDBRemoteProtocol import NIOCore import WasmKit import struct Foundation.Date package final class WasmKitHandler: ChannelInboundHandler { - package typealias InboundIn = Packet + package typealias InboundIn = Packet package typealias OutboundOut = TargetResponse /// Whether `QStartNoAckMode` command was previously sent. @@ -24,15 +24,36 @@ package final class WasmKitHandler: ChannelInboundHandler { switch command.kind { case .startNoAckMode, .isThreadSuffixSupported, .listThreadsInStopReply: responseKind = .ok + + case .hostInfo: + responseKind = .hostInfo([ + "arch": "wasm32", + "ptrsize": "4", + "endian": "little", + "ostype": "wasip1", + "vendor": "WasmKit" + ]) + case .supportedFeatures: responseKind = .raw(command.arguments) - default: + + case .vContSupportedActions: + responseKind = .vContSupportedActions([.continue, .step, .stop]) + + case .isVAttachOrWaitSupported, .enableErrorStrings, .processInfo: + responseKind = .empty + + case .currentThreadID: + responseKind = .raw("QC 1") + + case .generalRegisters, .firstThreadInfo: fatalError() } context.writeAndFlush( wrapOutboundOut(.init(kind: responseKind, isNoAckModeActive: self.isNoAckModeActive)), - promise: nil) + promise: nil + ) if command.kind == .startNoAckMode { self.isNoAckModeActive = true } diff --git a/Sources/wasmkit-lldb/Entrypoint.swift b/Sources/wasmkit-gdb-tool/Entrypoint.swift similarity index 90% rename from Sources/wasmkit-lldb/Entrypoint.swift rename to Sources/wasmkit-gdb-tool/Entrypoint.swift index 00ccaba7..8fa6095c 100644 --- a/Sources/wasmkit-lldb/Entrypoint.swift +++ b/Sources/wasmkit-gdb-tool/Entrypoint.swift @@ -1,8 +1,8 @@ import ArgumentParser -import LLDBRemoteProtocol +import GDBRemoteProtocol import NIOCore import NIOPosix -import WasmKitLLDBHandler +import WasmKitGDBHandler @main struct Entrypoint: ParsableCommand { @@ -24,8 +24,8 @@ struct Entrypoint: ParsableCommand { // make sure to instantiate your `ChannelHandlers` inside of // the closure as it will be invoked once per connection. try channel.pipeline.syncOperations.addHandlers([ - ByteToMessageHandler(CommandDecoder()), - MessageToByteHandler(ResponseEncoder()), + ByteToMessageHandler(GDBHostCommandDecoder()), + MessageToByteHandler(GDBTargetResponseEncoder()), WasmKitHandler(), ]) } diff --git a/Tests/LLDBRemoteProtocolTests/RemoteProtocolTests.swift b/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift similarity index 79% rename from Tests/LLDBRemoteProtocolTests/RemoteProtocolTests.swift rename to Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift index c3805f7c..22483310 100644 --- a/Tests/LLDBRemoteProtocolTests/RemoteProtocolTests.swift +++ b/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift @@ -1,4 +1,4 @@ -import LLDBRemoteProtocol +import GDBRemoteProtocol import NIOCore import Testing @@ -6,11 +6,11 @@ import Testing struct LLDBRemoteProtocolTests { @Test func decoding() throws { - var decoder = CommandDecoder() + var decoder = GDBHostCommandDecoder() var buffer = ByteBuffer(string: "+$g#67") var packet = try decoder.decode(buffer: &buffer) - #expect(packet == Packet(payload: HostCommand(kind: .generalRegisters, arguments: ""), checksum: 103)) + #expect(packet == Packet(payload: GDBHostCommand(kind: .generalRegisters, arguments: ""), checksum: 103)) #expect(decoder.accummulatedChecksum == 0) buffer = ByteBuffer( @@ -21,7 +21,7 @@ struct LLDBRemoteProtocolTests { packet = try decoder.decode(buffer: &buffer) let expectedPacket = Packet( - payload: HostCommand( + payload: GDBHostCommand( kind: .supportedFeatures, arguments: "xmlRegisters=i386,arm,mips,arc;multiprocess+;fork-events+;vfork-events+" ), From 85a695a6a4ddca855f1d9c616f1eb9a8dc09eec1 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 8 Oct 2025 21:58:48 +0100 Subject: [PATCH 003/107] Provide original license notice in `wasmkit-gdb-tool/Entrypoint.swift` --- Sources/wasmkit-gdb-tool/Entrypoint.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Sources/wasmkit-gdb-tool/Entrypoint.swift b/Sources/wasmkit-gdb-tool/Entrypoint.swift index 8fa6095c..3d1fa0b1 100644 --- a/Sources/wasmkit-gdb-tool/Entrypoint.swift +++ b/Sources/wasmkit-gdb-tool/Entrypoint.swift @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2017-2025 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + import ArgumentParser import GDBRemoteProtocol import NIOCore From eba97a0b6d3701c99ada5a663bdd054692f74a43 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 8 Oct 2025 22:00:43 +0100 Subject: [PATCH 004/107] Apply formatter --- Sources/GDBRemoteProtocol/CommandDecoder.swift | 3 ++- Sources/GDBRemoteProtocol/ResponseEncoder.swift | 2 +- Sources/WasmKitGDBHandler/WasmKitHandler.swift | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/GDBRemoteProtocol/CommandDecoder.swift b/Sources/GDBRemoteProtocol/CommandDecoder.swift index c46b7a51..46e035de 100644 --- a/Sources/GDBRemoteProtocol/CommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/CommandDecoder.swift @@ -36,7 +36,8 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { // Command start delimiters. let firstStartDelimiter = buffer.readInteger(as: UInt8.self) let secondStartDelimiter = buffer.readInteger(as: UInt8.self) - guard firstStartDelimiter == UInt8(ascii: "+") + guard + firstStartDelimiter == UInt8(ascii: "+") && secondStartDelimiter == UInt8(ascii: "$") else { if let firstStartDelimiter, let secondStartDelimiter { diff --git a/Sources/GDBRemoteProtocol/ResponseEncoder.swift b/Sources/GDBRemoteProtocol/ResponseEncoder.swift index ddcbe872..96d89350 100644 --- a/Sources/GDBRemoteProtocol/ResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/ResponseEncoder.swift @@ -20,7 +20,7 @@ package struct GDBTargetResponseEncoder: MessageToByteEncoder { out.writeBytes("ok#da".utf8) case .hostInfo(let info): - out.writeBytes(info.map { (key, value) in "\(key):\(value);"}.joined().appendedChecksum) + out.writeBytes(info.map { (key, value) in "\(key):\(value);" }.joined().appendedChecksum) case .vContSupportedActions(let actions): out.writeBytes("vCont;\(actions.map(\.rawValue).joined())".appendedChecksum) diff --git a/Sources/WasmKitGDBHandler/WasmKitHandler.swift b/Sources/WasmKitGDBHandler/WasmKitHandler.swift index cbdff83c..f5817122 100644 --- a/Sources/WasmKitGDBHandler/WasmKitHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitHandler.swift @@ -31,7 +31,7 @@ package final class WasmKitHandler: ChannelInboundHandler { "ptrsize": "4", "endian": "little", "ostype": "wasip1", - "vendor": "WasmKit" + "vendor": "WasmKit", ]) case .supportedFeatures: From 65467541e149fec23c98f03ccae7c5bf1b4fddee Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 8 Oct 2025 22:05:21 +0100 Subject: [PATCH 005/107] Make naming of types more specific to avoid future collisions --- .../{HostCommand.swift => GDBHostCommand.swift} | 0 .../{CommandDecoder.swift => GDBHostCommandDecoder.swift} | 4 ++-- .../GDBRemoteProtocol/{Package.swift => GDBPacket.swift} | 4 ++-- .../{TargetResponse.swift => GDBTargetResponse.swift} | 2 +- ...ResponseEncoder.swift => GDBTargetResponseEncoder.swift} | 2 +- Sources/WasmKitGDBHandler/WasmKitHandler.swift | 6 +++--- Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) rename Sources/GDBRemoteProtocol/{HostCommand.swift => GDBHostCommand.swift} (100%) rename Sources/GDBRemoteProtocol/{CommandDecoder.swift => GDBHostCommandDecoder.swift} (97%) rename Sources/GDBRemoteProtocol/{Package.swift => GDBPacket.swift} (67%) rename Sources/GDBRemoteProtocol/{TargetResponse.swift => GDBTargetResponse.swift} (94%) rename Sources/GDBRemoteProtocol/{ResponseEncoder.swift => GDBTargetResponseEncoder.swift} (92%) diff --git a/Sources/GDBRemoteProtocol/HostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift similarity index 100% rename from Sources/GDBRemoteProtocol/HostCommand.swift rename to Sources/GDBRemoteProtocol/GDBHostCommand.swift diff --git a/Sources/GDBRemoteProtocol/CommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift similarity index 97% rename from Sources/GDBRemoteProtocol/CommandDecoder.swift rename to Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift index 46e035de..6847e766 100644 --- a/Sources/GDBRemoteProtocol/CommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -20,7 +20,7 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { case checksumIncorrect } - package typealias InboundOut = Packet + package typealias InboundOut = GDBPacket private var accummulatedKind = [UInt8]() private var accummulatedArguments = [UInt8]() @@ -32,7 +32,7 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { UInt8(self.accummulatedSum % 256) } - mutating package func decode(buffer: inout ByteBuffer) throws -> Packet? { + mutating package func decode(buffer: inout ByteBuffer) throws -> GDBPacket? { // Command start delimiters. let firstStartDelimiter = buffer.readInteger(as: UInt8.self) let secondStartDelimiter = buffer.readInteger(as: UInt8.self) diff --git a/Sources/GDBRemoteProtocol/Package.swift b/Sources/GDBRemoteProtocol/GDBPacket.swift similarity index 67% rename from Sources/GDBRemoteProtocol/Package.swift rename to Sources/GDBRemoteProtocol/GDBPacket.swift index 87faa03a..1878efb4 100644 --- a/Sources/GDBRemoteProtocol/Package.swift +++ b/Sources/GDBRemoteProtocol/GDBPacket.swift @@ -1,4 +1,4 @@ -package struct Packet { +package struct GDBPacket { package let payload: Payload package let checksum: UInt8 @@ -9,4 +9,4 @@ package struct Packet { } } -extension Packet: Equatable where Payload: Equatable {} +extension GDBPacket: Equatable where Payload: Equatable {} diff --git a/Sources/GDBRemoteProtocol/TargetResponse.swift b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift similarity index 94% rename from Sources/GDBRemoteProtocol/TargetResponse.swift rename to Sources/GDBRemoteProtocol/GDBTargetResponse.swift index 4373f1f2..b827ee55 100644 --- a/Sources/GDBRemoteProtocol/TargetResponse.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift @@ -8,7 +8,7 @@ package enum VContActions: String { case stepInRange = "r" } -package struct TargetResponse { +package struct GDBTargetResponse { package enum Kind { case ok case hostInfo(KeyValuePairs) diff --git a/Sources/GDBRemoteProtocol/ResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift similarity index 92% rename from Sources/GDBRemoteProtocol/ResponseEncoder.swift rename to Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index 96d89350..83e738b0 100644 --- a/Sources/GDBRemoteProtocol/ResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -9,7 +9,7 @@ extension String { package struct GDBTargetResponseEncoder: MessageToByteEncoder { package init() {} - package func encode(data: TargetResponse, out: inout ByteBuffer) throws { + package func encode(data: GDBTargetResponse, out: inout ByteBuffer) throws { if !data.isNoAckModeActive { out.writeInteger(UInt8(ascii: "+")) } diff --git a/Sources/WasmKitGDBHandler/WasmKitHandler.swift b/Sources/WasmKitGDBHandler/WasmKitHandler.swift index f5817122..1c1b9c95 100644 --- a/Sources/WasmKitGDBHandler/WasmKitHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitHandler.swift @@ -5,8 +5,8 @@ import WasmKit import struct Foundation.Date package final class WasmKitHandler: ChannelInboundHandler { - package typealias InboundIn = Packet - package typealias OutboundOut = TargetResponse + package typealias InboundIn = GDBPacket + package typealias OutboundOut = GDBTargetResponse /// Whether `QStartNoAckMode` command was previously sent. private var isNoAckModeActive = false @@ -18,7 +18,7 @@ package final class WasmKitHandler: ChannelInboundHandler { data: NIOAny ) { let command = self.unwrapInboundIn(data).payload - let responseKind: TargetResponse.Kind + let responseKind: GDBTargetResponse.Kind print(command.kind) switch command.kind { diff --git a/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift b/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift index 22483310..43a971d9 100644 --- a/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift +++ b/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift @@ -10,7 +10,7 @@ struct LLDBRemoteProtocolTests { var buffer = ByteBuffer(string: "+$g#67") var packet = try decoder.decode(buffer: &buffer) - #expect(packet == Packet(payload: GDBHostCommand(kind: .generalRegisters, arguments: ""), checksum: 103)) + #expect(packet == GDBPacket(payload: GDBHostCommand(kind: .generalRegisters, arguments: ""), checksum: 103)) #expect(decoder.accummulatedChecksum == 0) buffer = ByteBuffer( @@ -20,7 +20,7 @@ struct LLDBRemoteProtocolTests { ) packet = try decoder.decode(buffer: &buffer) - let expectedPacket = Packet( + let expectedPacket = GDBPacket( payload: GDBHostCommand( kind: .supportedFeatures, arguments: "xmlRegisters=i386,arm,mips,arc;multiprocess+;fork-events+;vfork-events+" From 7b2c6f661b3ec0234cd4a3abb34a1bc7ee98c139 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 8 Oct 2025 22:08:09 +0100 Subject: [PATCH 006/107] More cleanups for type naming --- .../{WasmKitHandler.swift => WasmKitGDBHandler.swift} | 2 +- Sources/wasmkit-gdb-tool/Entrypoint.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename Sources/WasmKitGDBHandler/{WasmKitHandler.swift => WasmKitGDBHandler.swift} (96%) diff --git a/Sources/WasmKitGDBHandler/WasmKitHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift similarity index 96% rename from Sources/WasmKitGDBHandler/WasmKitHandler.swift rename to Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 1c1b9c95..3814f720 100644 --- a/Sources/WasmKitGDBHandler/WasmKitHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -4,7 +4,7 @@ import WasmKit import struct Foundation.Date -package final class WasmKitHandler: ChannelInboundHandler { +package final class WasmKitGDBHandler: ChannelInboundHandler { package typealias InboundIn = GDBPacket package typealias OutboundOut = GDBTargetResponse diff --git a/Sources/wasmkit-gdb-tool/Entrypoint.swift b/Sources/wasmkit-gdb-tool/Entrypoint.swift index 3d1fa0b1..97f3bf62 100644 --- a/Sources/wasmkit-gdb-tool/Entrypoint.swift +++ b/Sources/wasmkit-gdb-tool/Entrypoint.swift @@ -40,7 +40,7 @@ struct Entrypoint: ParsableCommand { try channel.pipeline.syncOperations.addHandlers([ ByteToMessageHandler(GDBHostCommandDecoder()), MessageToByteHandler(GDBTargetResponseEncoder()), - WasmKitHandler(), + WasmKitGDBHandler(), ]) } } From 4a1c998c12f4a5101f81e20ee8f7eadbe010f16a Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 8 Oct 2025 22:12:20 +0100 Subject: [PATCH 007/107] Remove unused `import struct Foundation.Date` --- Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift | 2 -- Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift | 2 -- 2 files changed, 4 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift index 6847e766..b187b704 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -1,7 +1,5 @@ import NIOCore -import struct Foundation.Date - extension ByteBuffer { var isChecksumDelimiterAtReader: Bool { self.peekInteger(as: UInt8.self) == UInt8(ascii: "#") diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 3814f720..aec24de5 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -2,8 +2,6 @@ import GDBRemoteProtocol import NIOCore import WasmKit -import struct Foundation.Date - package final class WasmKitGDBHandler: ChannelInboundHandler { package typealias InboundIn = GDBPacket package typealias OutboundOut = GDBTargetResponse From f8c08dd03953bca25edfb37c56e61b1701ff043c Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 8 Oct 2025 23:10:18 +0100 Subject: [PATCH 008/107] Remove unused `else` clause from command decoder --- Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift index b187b704..a0091e0e 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -117,8 +117,5 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { // Shift by checksum bytes context.fireChannelRead(wrapInboundOut(command)) return .continue - // } else { - // throw Error.unknownCommand(accummulated) - // } } } From 77bdb0489fd3e2da7e42e7a58df4a016e036908a Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 8 Oct 2025 23:12:27 +0100 Subject: [PATCH 009/107] Add `swift-nio` in `SWIFTCI_USE_LOCAL_DEPS` clause --- Package@swift-6.1.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index f70371f1..a63ef594 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -159,6 +159,7 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { package.dependencies += [ .package(path: "../swift-argument-parser"), .package(path: "../swift-system"), + .package(path: "../swift-nio"), ] } From 256c70c4da59f0cb42bb06f542a462d129822be8 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 8 Oct 2025 23:16:27 +0100 Subject: [PATCH 010/107] Add FIXME note for `.supportedFeatures` response --- Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index aec24de5..1e7d6cde 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -33,6 +33,7 @@ package final class WasmKitGDBHandler: ChannelInboundHandler { ]) case .supportedFeatures: + // FIXME: should return a different set of supported features instead of echoing. responseKind = .raw(command.arguments) case .vContSupportedActions: From 111deb8314f8ee6d62c5936e14ef1e07f2d87a21 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 9 Oct 2025 13:06:24 +0100 Subject: [PATCH 011/107] Use `Logger`, `NIOAsyncChannel` --- .../GDBRemoteProtocol/GDBHostCommand.swift | 12 ++- .../GDBHostCommandDecoder.swift | 28 +++++-- Sources/GDBRemoteProtocol/GDBPacket.swift | 2 +- .../GDBRemoteProtocol/GDBTargetResponse.swift | 2 +- .../WasmKitGDBHandler/WasmKitDebugger.swift | 68 +++++++++++++++++ .../WasmKitGDBHandler/WasmKitGDBHandler.swift | 75 ------------------- Sources/wasmkit-gdb-tool/Entrypoint.swift | 72 ++++++++++++++++-- 7 files changed, 164 insertions(+), 95 deletions(-) create mode 100644 Sources/WasmKitGDBHandler/WasmKitDebugger.swift delete mode 100644 Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index 754270fd..e83e2eea 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -2,11 +2,9 @@ /// * https://sourceware.org/gdb/current/onlinedocs/gdb.html/General-Query-Packets.html /// * https://lldb.llvm.org/resources/lldbgdbremote.html package struct GDBHostCommand: Equatable { - package enum Kind: Equatable { + package enum Kind: String, Equatable { // Currently listed in the order that LLDB sends them in. - case generalRegisters case startNoAckMode - case firstThreadInfo case supportedFeatures case isThreadSuffixSupported case listThreadsInStopReply @@ -16,6 +14,10 @@ package struct GDBHostCommand: Equatable { case enableErrorStrings case processInfo case currentThreadID + case firstThreadInfo + case subsequentThreadInfo + + case generalRegisters package init?(rawValue: String) { switch rawValue { @@ -41,6 +43,10 @@ package struct GDBHostCommand: Equatable { self = .processInfo case "qC": self = .currentThreadID + case "qfThreadInfo": + self = .firstThreadInfo + case "qsThreadInfo": + self = .subsequentThreadInfo default: return nil } diff --git a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift index a0091e0e..46b696c0 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -1,3 +1,4 @@ +import Logging import NIOCore extension ByteBuffer { @@ -20,10 +21,14 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { package typealias InboundOut = GDBPacket + private var accumulatedDelimiter: UInt8? + private var accummulatedKind = [UInt8]() private var accummulatedArguments = [UInt8]() - package init() {} + private let logger: Logger + + package init(logger: Logger) { self.logger = logger } private var accummulatedSum = 0 package var accummulatedChecksum: UInt8 { @@ -31,16 +36,24 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { } mutating package func decode(buffer: inout ByteBuffer) throws -> GDBPacket? { + guard let firstStartDelimiter = self.accumulatedDelimiter ?? buffer.readInteger(as: UInt8.self) else { + // Not enough data to parse. + return nil + } + guard let secondStartDelimiter = buffer.readInteger(as: UInt8.self) else { + // Preserve what we already read. + self.accumulatedDelimiter = firstStartDelimiter + + // Not enough data to parse. + return nil + } + // Command start delimiters. - let firstStartDelimiter = buffer.readInteger(as: UInt8.self) - let secondStartDelimiter = buffer.readInteger(as: UInt8.self) guard firstStartDelimiter == UInt8(ascii: "+") && secondStartDelimiter == UInt8(ascii: "$") else { - if let firstStartDelimiter, let secondStartDelimiter { - print("unexpected delimiter: \(Character(UnicodeScalar(firstStartDelimiter)))\(Character(UnicodeScalar(secondStartDelimiter)))") - } + logger.error("unexpected delimiter: \(Character(UnicodeScalar(firstStartDelimiter)))\(Character(UnicodeScalar(secondStartDelimiter)))") throw Error.expectedCommandStart } @@ -70,6 +83,7 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { } defer { + self.accumulatedDelimiter = nil self.accummulatedKind = [] self.accummulatedArguments = [] self.accummulatedSum = 0 @@ -108,7 +122,7 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { context: ChannelHandlerContext, buffer: inout ByteBuffer ) throws -> DecodingState { - print(buffer.peekString(length: buffer.readableBytes)!) + logger.trace(.init(stringLiteral: buffer.peekString(length: buffer.readableBytes)!)) guard let command = try self.decode(buffer: &buffer) else { return .needMoreData diff --git a/Sources/GDBRemoteProtocol/GDBPacket.swift b/Sources/GDBRemoteProtocol/GDBPacket.swift index 1878efb4..10a24fa6 100644 --- a/Sources/GDBRemoteProtocol/GDBPacket.swift +++ b/Sources/GDBRemoteProtocol/GDBPacket.swift @@ -1,4 +1,4 @@ -package struct GDBPacket { +package struct GDBPacket: Sendable { package let payload: Payload package let checksum: UInt8 diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift index b827ee55..5f55430d 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift @@ -11,7 +11,7 @@ package enum VContActions: String { package struct GDBTargetResponse { package enum Kind { case ok - case hostInfo(KeyValuePairs) + case keyValuePairs(KeyValuePairs) case vContSupportedActions([VContActions]) case raw(String) case empty diff --git a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift new file mode 100644 index 00000000..4dbaaf74 --- /dev/null +++ b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift @@ -0,0 +1,68 @@ +import GDBRemoteProtocol +import Logging +import SystemPackage +import WasmKit + +package actor WasmKitDebugger { + /// Whether `QStartNoAckMode` command was previously sent. + private var isNoAckModeActive = false + + private let module: Module + private let logger: Logger + + package init(logger: Logger, moduleFilePath: FilePath) throws { + self.logger = logger + self.module = try parseWasm(filePath: moduleFilePath) + } + + package func handle(command: GDBHostCommand) -> GDBTargetResponse { + let responseKind: GDBTargetResponse.Kind + logger.trace("handling GDB host command", metadata: ["GDBHostCommand": .string(command.kind.rawValue)]) + + responseKind = switch command.kind { + case .startNoAckMode, .isThreadSuffixSupported, .listThreadsInStopReply: + .ok + + case .hostInfo: + .keyValuePairs([ + "arch": "wasm32", + "ptrsize": "4", + "endian": "little", + "ostype": "wasip1", + "vendor": "WasmKit", + ]) + + case .supportedFeatures: + // FIXME: should return a different set of supported features instead of echoing. + .raw(command.arguments) + + case .vContSupportedActions: + .vContSupportedActions([.continue, .step, .stop]) + + case .isVAttachOrWaitSupported, .enableErrorStrings: + .empty + case .processInfo: + .raw("pid:1;parent-pid:1;arch:wasm32;endian:little;ptrsize:4;") + + case .currentThreadID: + .raw("QC1") + + case .firstThreadInfo: + .raw("m1") + + case .subsequentThreadInfo: + .raw("l") + + case .generalRegisters: + fatalError() + } + + defer { + if command.kind == .startNoAckMode { + self.isNoAckModeActive = true + } + } + return .init(kind: responseKind, isNoAckModeActive: self.isNoAckModeActive) + } + +} diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift deleted file mode 100644 index 1e7d6cde..00000000 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ /dev/null @@ -1,75 +0,0 @@ -import GDBRemoteProtocol -import NIOCore -import WasmKit - -package final class WasmKitGDBHandler: ChannelInboundHandler { - package typealias InboundIn = GDBPacket - package typealias OutboundOut = GDBTargetResponse - - /// Whether `QStartNoAckMode` command was previously sent. - private var isNoAckModeActive = false - - package init() {} - - package func channelRead( - context: ChannelHandlerContext, - data: NIOAny - ) { - let command = self.unwrapInboundIn(data).payload - let responseKind: GDBTargetResponse.Kind - print(command.kind) - - switch command.kind { - case .startNoAckMode, .isThreadSuffixSupported, .listThreadsInStopReply: - responseKind = .ok - - case .hostInfo: - responseKind = .hostInfo([ - "arch": "wasm32", - "ptrsize": "4", - "endian": "little", - "ostype": "wasip1", - "vendor": "WasmKit", - ]) - - case .supportedFeatures: - // FIXME: should return a different set of supported features instead of echoing. - responseKind = .raw(command.arguments) - - case .vContSupportedActions: - responseKind = .vContSupportedActions([.continue, .step, .stop]) - - case .isVAttachOrWaitSupported, .enableErrorStrings, .processInfo: - responseKind = .empty - - case .currentThreadID: - responseKind = .raw("QC 1") - - case .generalRegisters, .firstThreadInfo: - fatalError() - } - - context.writeAndFlush( - wrapOutboundOut(.init(kind: responseKind, isNoAckModeActive: self.isNoAckModeActive)), - promise: nil - ) - if command.kind == .startNoAckMode { - self.isNoAckModeActive = true - } - } - - package func channelReadComplete( - context: ChannelHandlerContext - ) { - context.flush() - } - - package func errorCaught( - context: ChannelHandlerContext, - error: Error - ) { - print(error) - - context.close(promise: nil) - } -} diff --git a/Sources/wasmkit-gdb-tool/Entrypoint.swift b/Sources/wasmkit-gdb-tool/Entrypoint.swift index 97f3bf62..5ca8adf5 100644 --- a/Sources/wasmkit-gdb-tool/Entrypoint.swift +++ b/Sources/wasmkit-gdb-tool/Entrypoint.swift @@ -14,16 +14,46 @@ import ArgumentParser import GDBRemoteProtocol +import Logging import NIOCore import NIOPosix +import SystemPackage import WasmKitGDBHandler +#if hasFeature(RetroactiveAttribute) + extension Logger.Level: @retroactive ExpressibleByArgument {} + extension FilePath: @retroactive ExpressibleByArgument { + public init?(argument: String) { + self.init(argument) + } + } +#else + extension Logger.Level: ExpressibleByArgument {} + extension FilePath: ExpressibleByArgument { + public init?(argument: String) { + self.init(argument) + } + } +#endif + @main -struct Entrypoint: ParsableCommand { +struct Entrypoint: AsyncParsableCommand { @Option(help: "TCP port that a debugger can connect to") var port = 8080 - func run() throws { + @Option(name: .shortAndLong) + var logLevel = Logger.Level.info + + @Argument + var wasmModulePath: FilePath + + func run() async throws { + let logger = { + var result = Logger(label: "org.swiftwasm.WasmKit") + result.logLevel = self.logLevel + return result + }() + let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) let bootstrap = ServerBootstrap(group: group) // Specify backlog and enable SO_REUSEADDR for the server itself @@ -38,9 +68,8 @@ struct Entrypoint: ParsableCommand { // make sure to instantiate your `ChannelHandlers` inside of // the closure as it will be invoked once per connection. try channel.pipeline.syncOperations.addHandlers([ - ByteToMessageHandler(GDBHostCommandDecoder()), + ByteToMessageHandler(GDBHostCommandDecoder(logger: logger)), MessageToByteHandler(GDBTargetResponseEncoder()), - WasmKitGDBHandler(), ]) } } @@ -49,11 +78,38 @@ struct Entrypoint: ParsableCommand { .childChannelOption(.socketOption(.so_reuseaddr), value: 1) .childChannelOption(.maxMessagesPerRead, value: 16) .childChannelOption(.recvAllocator, value: AdaptiveRecvByteBufferAllocator()) - let channel = try bootstrap.bind(host: "127.0.0.1", port: port).wait() + + let serverChannel = try await bootstrap.bind(host: "127.0.0.1", port: port) { childChannel in + childChannel.eventLoop.makeCompletedFuture { + try NIOAsyncChannel, GDBTargetResponse>( + wrappingChannelSynchronously: childChannel + ) + } + } /* the server will now be accepting connections */ - print("listening on port \(port)") + logger.info("listening on port \(port)") + + let debugger = try WasmKitDebugger(logger: logger, moduleFilePath: self.wasmModulePath) + + try await withThrowingDiscardingTaskGroup { group in + try await serverChannel.executeThenClose { serverChannelInbound in + for try await connectionChannel in serverChannelInbound { + group.addTask { + do { + try await connectionChannel.executeThenClose { connectionChannelInbound, connectionChannelOutbound in + for try await inboundData in connectionChannelInbound { + // Let's echo back all inbound data + try await connectionChannelOutbound.write(debugger.handle(command: inboundData.payload)) + } + } + } catch { + logger.error("Error in GDB remote protocol connection channel", metadata: ["error": "\(error)"]) + } + } + } + } + } - try channel.closeFuture.wait() // wait forever as we never close the Channel - try group.syncShutdownGracefully() + try await group.shutdownGracefully() } } From e392d3026f3a497367c26e76e8db46ec268b8e36 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 9 Oct 2025 13:06:42 +0100 Subject: [PATCH 012/107] Add required dependencies to `Package@swift-6.1.swift` --- Package@swift-6.1.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index a63ef594..2cafb158 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -8,7 +8,7 @@ let DarwinPlatforms: [Platform] = [.macOS, .iOS, .watchOS, .tvOS, .visionOS] let package = Package( name: "WasmKit", - platforms: [.macOS(.v10_13), .iOS(.v12)], + platforms: [.macOS(.v14), .iOS(.v12)], products: [ .executable(name: "wasmkit-cli", targets: ["CLI"]), .library(name: "WasmKit", targets: ["WasmKit"]), @@ -122,6 +122,7 @@ let package = Package( .target(name: "GDBRemoteProtocol", dependencies: [ + .product(name: "Logging", package: "swift-log"), .product(name: "NIOCore", package: "swift-nio"), ] ), @@ -131,6 +132,7 @@ let package = Package( name: "WasmKitGDBHandler", dependencies: [ .product(name: "NIOCore", package: "swift-nio"), + .product(name: "SystemPackage", package: "swift-system"), "WasmKit", "GDBRemoteProtocol", ], @@ -140,8 +142,10 @@ let package = Package( name: "wasmkit-gdb-tool", dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Logging", package: "swift-log"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "SystemPackage", package: "swift-system"), "GDBRemoteProtocol", "WasmKitGDBHandler", ] @@ -154,12 +158,14 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.1"), .package(url: "https://github.com/apple/swift-system", from: "1.5.0"), .package(url: "https://github.com/apple/swift-nio", from: "2.86.2"), + .package(url: "https://github.com/apple/swift-log", from: "1.6.4"), ] } else { package.dependencies += [ .package(path: "../swift-argument-parser"), .package(path: "../swift-system"), .package(path: "../swift-nio"), + .package(path: "../swift-log"), ] } From a7c62b0354e4b05f14a44221f4e7a705507a5e84 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 9 Oct 2025 18:05:29 +0100 Subject: [PATCH 013/107] Add `iSeqToWasmMapping` to `InstanceEntity` --- .../GDBTargetResponseEncoder.swift | 2 +- Sources/WasmKit/Execution/Errors.swift | 20 ++++++++++++++++--- Sources/WasmKit/Execution/Execution.swift | 10 +++------- Sources/WasmKit/Execution/Function.swift | 12 +++++++---- Sources/WasmKit/Execution/Instances.swift | 6 +++++- .../WasmKit/Execution/StoreAllocator.swift | 7 +++++-- Sources/WasmKit/Module.swift | 17 ++++++++++++++-- Sources/WasmKit/Translator.swift | 8 ++------ .../RemoteProtocolTests.swift | 5 ++++- Tests/WasmKitTests/ExecutionTests.swift | 4 ++-- Utilities/Sources/WasmGen.swift | 5 +++++ 11 files changed, 67 insertions(+), 29 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index 83e738b0..20ad3928 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -19,7 +19,7 @@ package struct GDBTargetResponseEncoder: MessageToByteEncoder { case .ok: out.writeBytes("ok#da".utf8) - case .hostInfo(let info): + case .keyValuePairs(let info): out.writeBytes(info.map { (key, value) in "\(key):\(value);" }.joined().appendedChecksum) case .vContSupportedActions(let actions): diff --git a/Sources/WasmKit/Execution/Errors.swift b/Sources/WasmKit/Execution/Errors.swift index 07c1880b..fefba1ef 100644 --- a/Sources/WasmKit/Execution/Errors.swift +++ b/Sources/WasmKit/Execution/Errors.swift @@ -8,16 +8,30 @@ struct Backtrace: CustomStringConvertible, Sendable { struct Symbol { /// The name of the symbol. let name: String? + let debuggingAddress: DebuggingAddress + + /// Address of the symbol for debugging purposes. + enum DebuggingAddress: CustomStringConvertible, @unchecked Sendable { + case iseq(Pc) + case wasm(UInt64) + + var description: String { + switch self { + case .iseq(let pc): "iseq(\(Int(bitPattern: pc)))" + case .wasm(let wasmAddress): "wasm(\(wasmAddress))" + } + } + } } /// The symbols in the backtrace. - let symbols: [Symbol?] + let symbols: [Symbol] /// Textual description of the backtrace. var description: String { symbols.enumerated().map { (index, symbol) in - let name = symbol?.name ?? "unknown" - return " \(index): \(name)" + let name = symbol.name ?? "unknown" + return " \(symbol.debuggingAddress): \(name)" }.joined(separator: "\n") } } diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index b0d09dfa..56fe4bc1 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -61,18 +61,14 @@ struct Execution { static func captureBacktrace(sp: Sp, store: Store) -> Backtrace { var frames = FrameIterator(sp: sp) - var symbols: [Backtrace.Symbol?] = [] + var symbols: [Backtrace.Symbol] = [] while let frame = frames.next() { guard let function = frame.function else { - symbols.append(nil) + symbols.append(.init(name: nil, debuggingAddress: .iseq(frame.pc))) continue } let symbolName = store.nameRegistry.symbolicate(.wasm(function)) - symbols.append( - Backtrace.Symbol( - name: symbolName - ) - ) + symbols.append(.init(name: symbolName, debuggingAddress: .iseq(frame.pc))) } return Backtrace(symbols: symbols) } diff --git a/Sources/WasmKit/Execution/Function.swift b/Sources/WasmKit/Execution/Function.swift index 3786cf16..4af8c740 100644 --- a/Sources/WasmKit/Execution/Function.swift +++ b/Sources/WasmKit/Execution/Function.swift @@ -243,7 +243,7 @@ struct WasmFunctionEntity { switch code { case .uncompiled(let code): return try compile(store: store, code: code) - case .compiled(let iseq), .compiledAndPatchable(_, let iseq): + case .compiled(let iseq), .debuggable(_, let iseq): return iseq } } @@ -280,10 +280,14 @@ extension EntityHandle { case .uncompiled(let code): return try self.withValue { let iseq = try $0.compile(store: store, code: code) - $0.code = .compiled(iseq) + if $0.instance.isDebuggable { + $0.code = .debuggable(code, iseq) + } else { + $0.code = .compiled(iseq) + } return iseq } - case .compiled(let iseq), .compiledAndPatchable(_, let iseq): + case .compiled(let iseq), .debuggable(_, let iseq): return iseq } } @@ -316,7 +320,7 @@ struct InstructionSequence { enum CodeBody { case uncompiled(InternalUncompiledCode) case compiled(InstructionSequence) - case compiledAndPatchable(InternalUncompiledCode, InstructionSequence) + case debuggable(InternalUncompiledCode, InstructionSequence) } extension Reference { diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index 27a2452a..89ae3435 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -83,6 +83,8 @@ struct InstanceEntity /* : ~Copyable */ { var functionRefs: Set var features: WasmFeatureSet var dataCount: UInt32? + var isDebuggable: Bool + var iSeqToWasmMapping: [Pc: UInt64] static var empty: InstanceEntity { InstanceEntity( @@ -96,7 +98,9 @@ struct InstanceEntity /* : ~Copyable */ { exports: [:], functionRefs: [], features: [], - dataCount: nil + dataCount: nil, + isDebuggable: false, + iSeqToWasmMapping: [:] ) } diff --git a/Sources/WasmKit/Execution/StoreAllocator.swift b/Sources/WasmKit/Execution/StoreAllocator.swift index f060dc56..73109c51 100644 --- a/Sources/WasmKit/Execution/StoreAllocator.swift +++ b/Sources/WasmKit/Execution/StoreAllocator.swift @@ -251,7 +251,8 @@ extension StoreAllocator { module: Module, engine: Engine, resourceLimiter: any ResourceLimiter, - imports: Imports + imports: Imports, + isDebuggable: Bool ) throws -> InternalInstance { // Step 1 of module allocation algorithm, according to Wasm 2.0 spec. @@ -450,7 +451,9 @@ extension StoreAllocator { exports: exports, functionRefs: functionRefs, features: module.features, - dataCount: module.dataCount + dataCount: module.dataCount, + isDebuggable: isDebuggable, + iSeqToWasmMapping: [:] ) instancePointer.initialize(to: instanceEntity) instanceInitialized = true diff --git a/Sources/WasmKit/Module.swift b/Sources/WasmKit/Module.swift index 30070d1a..164264e9 100644 --- a/Sources/WasmKit/Module.swift +++ b/Sources/WasmKit/Module.swift @@ -138,9 +138,21 @@ public struct Module { Instance(handle: try self.instantiateHandle(store: store, imports: imports), store: store) } +#if WasmDebuggingSupport + /// Instantiate this module in the given imports. + /// + /// - Parameters: + /// - store: The ``Store`` to allocate the instance in. + /// - imports: The imports to use for instantiation. All imported entities + /// must be allocated in the given store. + public func instantiate(store: Store, imports: Imports = [:], isDebuggable: Bool) throws -> Instance { + Instance(handle: try self.instantiateHandle(store: store, imports: imports, isDebuggable: isDebuggable), store: store) + } +#endif + /// > Note: /// - private func instantiateHandle(store: Store, imports: Imports) throws -> InternalInstance { + private func instantiateHandle(store: Store, imports: Imports, isDebuggable: Bool = false) throws -> InternalInstance { try ModuleValidator(module: self).validate() // Steps 5-8. @@ -152,7 +164,8 @@ public struct Module { let instance = try store.allocator.allocate( module: self, engine: store.engine, resourceLimiter: store.resourceLimiter, - imports: imports + imports: imports, + isDebuggable: isDebuggable ) if let nameSection = customSections.first(where: { $0.name == "name" }) { diff --git a/Sources/WasmKit/Translator.swift b/Sources/WasmKit/Translator.swift index 6f3e1116..1f07a7c9 100644 --- a/Sources/WasmKit/Translator.swift +++ b/Sources/WasmKit/Translator.swift @@ -822,8 +822,6 @@ struct InstructionTranslator: InstructionVisitor { let functionIndex: FunctionIndex /// Whether a call to this function should be intercepted let isIntercepting: Bool - /// Whether Wasm debugging facilities are currently enabled. - let isDebugging: Bool var constantSlots: ConstSlots let validator: InstructionValidator @@ -836,8 +834,7 @@ struct InstructionTranslator: InstructionVisitor { locals: [WasmTypes.ValueType], functionIndex: FunctionIndex, codeSize: Int, - isIntercepting: Bool, - isDebugging: Bool = false + isIntercepting: Bool ) throws { self.allocator = allocator self.funcTypeInterner = funcTypeInterner @@ -854,7 +851,6 @@ struct InstructionTranslator: InstructionVisitor { self.locals = Locals(types: type.parameters + locals) self.functionIndex = functionIndex self.isIntercepting = isIntercepting - self.isDebugging = isDebugging self.constantSlots = ConstSlots(stackLayout: stackLayout) self.validator = InstructionValidator(context: module) @@ -2262,7 +2258,7 @@ struct InstructionTranslator: InstructionVisitor { } mutating func visitUnknown(_ opcode: [UInt8]) throws -> Bool { - guard self.isDebugging && opcode.count == 1 && opcode[0] == 0xFF else { + guard self.module.isDebuggable && opcode.count == 1 && opcode[0] == 0xFF else { return false } diff --git a/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift b/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift index 43a971d9..4a30f0e1 100644 --- a/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift +++ b/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift @@ -1,4 +1,5 @@ import GDBRemoteProtocol +import Logging import NIOCore import Testing @@ -6,7 +7,9 @@ import Testing struct LLDBRemoteProtocolTests { @Test func decoding() throws { - var decoder = GDBHostCommandDecoder() + var logger = Logger(label: "com.swiftwasm.WasmKit.tests") + logger.logLevel = .critical + var decoder = GDBHostCommandDecoder(logger: logger) var buffer = ByteBuffer(string: "+$g#67") var packet = try decoder.decode(buffer: &buffer) diff --git a/Tests/WasmKitTests/ExecutionTests.swift b/Tests/WasmKitTests/ExecutionTests.swift index e5b61c87..42c8759d 100644 --- a/Tests/WasmKitTests/ExecutionTests.swift +++ b/Tests/WasmKitTests/ExecutionTests.swift @@ -111,7 +111,7 @@ """ ) { trap in #expect( - trap.backtrace?.symbols.compactMap(\.?.name) == [ + trap.backtrace?.symbols.compactMap(\.name) == [ "foo", "bar", "_start", @@ -138,7 +138,7 @@ """ ) { trap in #expect( - trap.backtrace?.symbols.compactMap(\.?.name) == [ + trap.backtrace?.symbols.compactMap(\.name) == [ "wasm function[1]", "bar", "_start", diff --git a/Utilities/Sources/WasmGen.swift b/Utilities/Sources/WasmGen.swift index af354614..541f409e 100644 --- a/Utilities/Sources/WasmGen.swift +++ b/Utilities/Sources/WasmGen.swift @@ -96,6 +96,8 @@ enum WasmGen { /// The visitor pattern is used while parsing WebAssembly expressions to allow for easy extensibility. /// See the expression parsing method ``Code/parseExpression(visitor:)`` public protocol InstructionVisitor { + /// Current offset in visitor's instruction stream. + var currentOffset: Int { get set } """ for instruction in instructions.categorized { @@ -534,6 +536,9 @@ enum WasmGen { /// Claim the next byte to be decoded @inlinable func claimNextByte() throws -> UInt8 + /// Current offset in decoder's instruction stream. + var currentOffset: Int { get } + /// Throw an error due to unknown opcode. func throwUnknown(_ opcode: [UInt8]) throws -> Never From 21e1b44a7b742b94fcefee59fe600843fab24eb2 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 9 Oct 2025 20:17:08 +0100 Subject: [PATCH 014/107] Fix formatting and iOS compatibility --- Package@swift-6.1.swift | 2 +- Sources/WasmKit/Execution/Errors.swift | 4 +- Sources/WasmKit/Module.swift | 22 +++---- .../WasmKitGDBHandler/WasmKitDebugger.swift | 59 ++++++++++--------- 4 files changed, 44 insertions(+), 43 deletions(-) diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index 2cafb158..33d7b2cc 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -8,7 +8,7 @@ let DarwinPlatforms: [Platform] = [.macOS, .iOS, .watchOS, .tvOS, .visionOS] let package = Package( name: "WasmKit", - platforms: [.macOS(.v14), .iOS(.v12)], + platforms: [.macOS(.v14), .iOS(.v13)], products: [ .executable(name: "wasmkit-cli", targets: ["CLI"]), .library(name: "WasmKit", targets: ["WasmKit"]), diff --git a/Sources/WasmKit/Execution/Errors.swift b/Sources/WasmKit/Execution/Errors.swift index fefba1ef..6bfab3ac 100644 --- a/Sources/WasmKit/Execution/Errors.swift +++ b/Sources/WasmKit/Execution/Errors.swift @@ -17,8 +17,8 @@ struct Backtrace: CustomStringConvertible, Sendable { var description: String { switch self { - case .iseq(let pc): "iseq(\(Int(bitPattern: pc)))" - case .wasm(let wasmAddress): "wasm(\(wasmAddress))" + case .iseq(let pc): "iseq(\(Int(bitPattern: pc)))" + case .wasm(let wasmAddress): "wasm(\(wasmAddress))" } } } diff --git a/Sources/WasmKit/Module.swift b/Sources/WasmKit/Module.swift index 164264e9..c2a5b0b9 100644 --- a/Sources/WasmKit/Module.swift +++ b/Sources/WasmKit/Module.swift @@ -138,17 +138,17 @@ public struct Module { Instance(handle: try self.instantiateHandle(store: store, imports: imports), store: store) } -#if WasmDebuggingSupport - /// Instantiate this module in the given imports. - /// - /// - Parameters: - /// - store: The ``Store`` to allocate the instance in. - /// - imports: The imports to use for instantiation. All imported entities - /// must be allocated in the given store. - public func instantiate(store: Store, imports: Imports = [:], isDebuggable: Bool) throws -> Instance { - Instance(handle: try self.instantiateHandle(store: store, imports: imports, isDebuggable: isDebuggable), store: store) - } -#endif + #if WasmDebuggingSupport + /// Instantiate this module in the given imports. + /// + /// - Parameters: + /// - store: The ``Store`` to allocate the instance in. + /// - imports: The imports to use for instantiation. All imported entities + /// must be allocated in the given store. + public func instantiate(store: Store, imports: Imports = [:], isDebuggable: Bool) throws -> Instance { + Instance(handle: try self.instantiateHandle(store: store, imports: imports, isDebuggable: isDebuggable), store: store) + } + #endif /// > Note: /// diff --git a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift index 4dbaaf74..53043486 100644 --- a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift +++ b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift @@ -19,43 +19,44 @@ package actor WasmKitDebugger { let responseKind: GDBTargetResponse.Kind logger.trace("handling GDB host command", metadata: ["GDBHostCommand": .string(command.kind.rawValue)]) - responseKind = switch command.kind { - case .startNoAckMode, .isThreadSuffixSupported, .listThreadsInStopReply: - .ok + responseKind = + switch command.kind { + case .startNoAckMode, .isThreadSuffixSupported, .listThreadsInStopReply: + .ok - case .hostInfo: - .keyValuePairs([ - "arch": "wasm32", - "ptrsize": "4", - "endian": "little", - "ostype": "wasip1", - "vendor": "WasmKit", - ]) + case .hostInfo: + .keyValuePairs([ + "arch": "wasm32", + "ptrsize": "4", + "endian": "little", + "ostype": "wasip1", + "vendor": "WasmKit", + ]) - case .supportedFeatures: - // FIXME: should return a different set of supported features instead of echoing. - .raw(command.arguments) + case .supportedFeatures: + // FIXME: should return a different set of supported features instead of echoing. + .raw(command.arguments) - case .vContSupportedActions: - .vContSupportedActions([.continue, .step, .stop]) + case .vContSupportedActions: + .vContSupportedActions([.continue, .step, .stop]) - case .isVAttachOrWaitSupported, .enableErrorStrings: - .empty - case .processInfo: - .raw("pid:1;parent-pid:1;arch:wasm32;endian:little;ptrsize:4;") + case .isVAttachOrWaitSupported, .enableErrorStrings: + .empty + case .processInfo: + .raw("pid:1;parent-pid:1;arch:wasm32;endian:little;ptrsize:4;") - case .currentThreadID: - .raw("QC1") + case .currentThreadID: + .raw("QC1") - case .firstThreadInfo: - .raw("m1") + case .firstThreadInfo: + .raw("m1") - case .subsequentThreadInfo: - .raw("l") + case .subsequentThreadInfo: + .raw("l") - case .generalRegisters: - fatalError() - } + case .generalRegisters: + fatalError() + } defer { if command.kind == .startNoAckMode { From 329e4f5ac9112d3aa9c54a5c28285a1af9ead7a2 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 9 Oct 2025 20:25:24 +0100 Subject: [PATCH 015/107] Update Package@swift-6.1.swift --- Package@swift-6.1.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index 33d7b2cc..316ebdac 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -8,7 +8,7 @@ let DarwinPlatforms: [Platform] = [.macOS, .iOS, .watchOS, .tvOS, .visionOS] let package = Package( name: "WasmKit", - platforms: [.macOS(.v14), .iOS(.v13)], + platforms: [.macOS(.v14), .iOS(.v17)], products: [ .executable(name: "wasmkit-cli", targets: ["CLI"]), .library(name: "WasmKit", targets: ["WasmKit"]), From a3d20affc73b3f1031731083399caee61b27f631 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 9 Oct 2025 20:42:55 +0100 Subject: [PATCH 016/107] Update doc comment of `func instantiate` in `Module.swift` --- Sources/WasmKit/Module.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/WasmKit/Module.swift b/Sources/WasmKit/Module.swift index c2a5b0b9..499cb93b 100644 --- a/Sources/WasmKit/Module.swift +++ b/Sources/WasmKit/Module.swift @@ -145,6 +145,8 @@ public struct Module { /// - store: The ``Store`` to allocate the instance in. /// - imports: The imports to use for instantiation. All imported entities /// must be allocated in the given store. + /// - isDebuggable: Whether the module should support debugging actions + /// (breakpoints etc) after instantiation. public func instantiate(store: Store, imports: Imports = [:], isDebuggable: Bool) throws -> Instance { Instance(handle: try self.instantiateHandle(store: store, imports: imports, isDebuggable: isDebuggable), store: store) } From 8d15b99cec225462b3e4c57d6701e621fde8f1a5 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 10 Oct 2025 18:19:26 +0100 Subject: [PATCH 017/107] Add `targetStatus`/`?` host command --- Sources/GDBRemoteProtocol/GDBHostCommand.swift | 4 ++++ Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index e83e2eea..13f94d9c 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -16,6 +16,7 @@ package struct GDBHostCommand: Equatable { case currentThreadID case firstThreadInfo case subsequentThreadInfo + case targetStatus case generalRegisters @@ -47,6 +48,9 @@ package struct GDBHostCommand: Equatable { self = .firstThreadInfo case "qsThreadInfo": self = .subsequentThreadInfo + case "?": + self = .targetStatus + default: return nil } diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index 20ad3928..b9344152 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -23,7 +23,7 @@ package struct GDBTargetResponseEncoder: MessageToByteEncoder { out.writeBytes(info.map { (key, value) in "\(key):\(value);" }.joined().appendedChecksum) case .vContSupportedActions(let actions): - out.writeBytes("vCont;\(actions.map(\.rawValue).joined())".appendedChecksum) + out.writeBytes("vCont;\(actions.map { "\($0.rawValue);" }.joined())".appendedChecksum) case .raw(let str): out.writeBytes(str.appendedChecksum) From d9b60b15f10297e1bbadcf46f3fc6ca1088d06a8 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 10 Oct 2025 18:20:15 +0100 Subject: [PATCH 018/107] Fix use of `OK#9a` instead of incorrect `ok#da` response --- Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index b9344152..c70aa556 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -17,7 +17,7 @@ package struct GDBTargetResponseEncoder: MessageToByteEncoder { switch data.kind { case .ok: - out.writeBytes("ok#da".utf8) + out.writeBytes("OK#9a".utf8) case .keyValuePairs(let info): out.writeBytes(info.map { (key, value) in "\(key):\(value);" }.joined().appendedChecksum) From 05583d037043dca20e5f33db77fb5bda3ffb5295 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 10 Oct 2025 18:25:06 +0100 Subject: [PATCH 019/107] Add `CSystemExtras` to `Package@swift-6.1` --- Package@swift-6.1.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index 316ebdac..8162f827 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -8,7 +8,7 @@ let DarwinPlatforms: [Platform] = [.macOS, .iOS, .watchOS, .tvOS, .visionOS] let package = Package( name: "WasmKit", - platforms: [.macOS(.v14), .iOS(.v17)], + platforms: [.macOS(.v15), .iOS(.v17)], products: [ .executable(name: "wasmkit-cli", targets: ["CLI"]), .library(name: "WasmKit", targets: ["WasmKit"]), @@ -93,7 +93,8 @@ let package = Package( .target( name: "SystemExtras", dependencies: [ - .product(name: "SystemPackage", package: "swift-system") + .product(name: "SystemPackage", package: "swift-system"), + .target(name: "CSystemExtras", condition: .when(platforms: [.wasi])), ], exclude: ["CMakeLists.txt"], swiftSettings: [ @@ -101,6 +102,8 @@ let package = Package( ] ), + .target(name: "CSystemExtras"), + .executableTarget( name: "WITTool", dependencies: [ From 39242d6446874539c126876e1c52eac783a13480 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 10 Oct 2025 18:26:16 +0100 Subject: [PATCH 020/107] Handle `targetStatus` in `WasmKitDebugger` --- Sources/WasmKitGDBHandler/WasmKitDebugger.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift index 53043486..061ed255 100644 --- a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift +++ b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift @@ -54,6 +54,13 @@ package actor WasmKitDebugger { case .subsequentThreadInfo: .raw("l") + case .targetStatus: + .keyValuePairs([ + "T05thread": "1", + "reason": "trace" + ] + ) + case .generalRegisters: fatalError() } From cbff9741030e629fc08c26a7e020b5f873bbf9b2 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 10 Oct 2025 18:30:29 +0100 Subject: [PATCH 021/107] Build only `wasmkit-cli` product for WASI in `main.yml` --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f88eacb8..2ecbad41 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -266,5 +266,5 @@ jobs: - name: Install Swift SDK run: swift sdk install https://download.swift.org/swift-6.2-release/wasm/swift-6.2-RELEASE/swift-6.2-RELEASE_wasm.artifactbundle.tar.gz --checksum fe4e8648309fce86ea522e9e0d1dc48e82df6ba6e5743dbf0c53db8429fb5224 - name: Build with the Swift SDK - run: swift build --swift-sdk "$(swiftc -print-target-info | jq -r '.swiftCompilerTag')_wasm" + run: swift build --swift-sdk "$(swiftc -print-target-info | jq -r '.swiftCompilerTag')_wasm" --product wasmkit-cli From 982fa91b1177f552e7e69f6c7ad18fdbe0c51176 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 10 Oct 2025 18:32:15 +0100 Subject: [PATCH 022/107] Fix formatting in `WasmKitDebugger.swift` --- Sources/WasmKitGDBHandler/WasmKitDebugger.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift index 061ed255..3905a4bb 100644 --- a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift +++ b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift @@ -56,10 +56,9 @@ package actor WasmKitDebugger { case .targetStatus: .keyValuePairs([ - "T05thread": "1", - "reason": "trace" - ] - ) + "T05thread": "1", + "reason": "trace", + ]) case .generalRegisters: fatalError() From 3a74a91a06cea5227e5aacf32614a456620b4ae5 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 14 Oct 2025 15:45:01 +0100 Subject: [PATCH 023/107] Fix `QStartNoAckMode` handling --- .../GDBHostCommandDecoder.swift | 43 ++++++++++++++----- Sources/GDBRemoteProtocol/GDBPacket.swift | 1 - .../GDBRemoteProtocol/GDBTargetResponse.swift | 6 +-- .../GDBTargetResponseEncoder.swift | 9 +++- .../WasmKitGDBHandler/WasmKitDebugger.swift | 22 +++++----- 5 files changed, 53 insertions(+), 28 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift index 46b696c0..96b7fcad 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -13,6 +13,7 @@ extension ByteBuffer { package struct GDBHostCommandDecoder: ByteToMessageDecoder { enum Error: Swift.Error { + case expectedAck case expectedCommandStart case unknownCommandKind(String) case expectedChecksum @@ -35,25 +36,41 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { UInt8(self.accummulatedSum % 256) } + private var isNoAckModeRequested = false + private var isNoAckModeActive = false + mutating package func decode(buffer: inout ByteBuffer) throws -> GDBPacket? { - guard let firstStartDelimiter = self.accumulatedDelimiter ?? buffer.readInteger(as: UInt8.self) else { + guard var startDelimiter = self.accumulatedDelimiter ?? buffer.readInteger(as: UInt8.self) else { // Not enough data to parse. return nil } - guard let secondStartDelimiter = buffer.readInteger(as: UInt8.self) else { - // Preserve what we already read. - self.accumulatedDelimiter = firstStartDelimiter - // Not enough data to parse. - return nil + if !isNoAckModeActive { + let firstStartDelimiter = startDelimiter + + guard firstStartDelimiter == UInt8(ascii: "+") else { + logger.error("unexpected ack character: \(Character(UnicodeScalar(startDelimiter)))") + throw Error.expectedAck + } + + if isNoAckModeRequested { + self.isNoAckModeActive = true + } + + guard let secondStartDelimiter = buffer.readInteger(as: UInt8.self) else { + // Preserve what we already read. + self.accumulatedDelimiter = firstStartDelimiter + + // Not enough data to parse. + return nil + } + + startDelimiter = secondStartDelimiter } // Command start delimiters. - guard - firstStartDelimiter == UInt8(ascii: "+") - && secondStartDelimiter == UInt8(ascii: "$") - else { - logger.error("unexpected delimiter: \(Character(UnicodeScalar(firstStartDelimiter)))\(Character(UnicodeScalar(secondStartDelimiter)))") + guard startDelimiter == UInt8(ascii: "$") else { + logger.error("unexpected delimiter: \(Character(UnicodeScalar(startDelimiter)))") throw Error.expectedCommandStart } @@ -106,6 +123,10 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { throw Error.checksumIncorrect } + if commandKind == .startNoAckMode { + self.isNoAckModeRequested = true + } + return .init( payload: .init( kind: commandKind, diff --git a/Sources/GDBRemoteProtocol/GDBPacket.swift b/Sources/GDBRemoteProtocol/GDBPacket.swift index 10a24fa6..483e9cb1 100644 --- a/Sources/GDBRemoteProtocol/GDBPacket.swift +++ b/Sources/GDBRemoteProtocol/GDBPacket.swift @@ -1,6 +1,5 @@ package struct GDBPacket: Sendable { package let payload: Payload - package let checksum: UInt8 package init(payload: Payload, checksum: UInt8) { diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift index 5f55430d..7728ba39 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift @@ -18,10 +18,10 @@ package struct GDBTargetResponse { } package let kind: Kind - package let isNoAckModeActive: Bool + package let isNoAckModeActivated: Bool - package init(kind: Kind, isNoAckModeActive: Bool) { + package init(kind: Kind, isNoAckModeActivated: Bool) { self.kind = kind - self.isNoAckModeActive = isNoAckModeActive + self.isNoAckModeActivated = isNoAckModeActivated } } diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index c70aa556..5001c077 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -7,12 +7,17 @@ extension String { } } -package struct GDBTargetResponseEncoder: MessageToByteEncoder { +package class GDBTargetResponseEncoder: MessageToByteEncoder { + private var isNoAckModeActive = false + package init() {} package func encode(data: GDBTargetResponse, out: inout ByteBuffer) throws { - if !data.isNoAckModeActive { + if !isNoAckModeActive { out.writeInteger(UInt8(ascii: "+")) } + if data.isNoAckModeActivated { + self.isNoAckModeActive = true + } out.writeInteger(UInt8(ascii: "$")) switch data.kind { diff --git a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift index 3905a4bb..dc950a81 100644 --- a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift +++ b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift @@ -1,12 +1,10 @@ import GDBRemoteProtocol import Logging +import Synchronization import SystemPackage import WasmKit package actor WasmKitDebugger { - /// Whether `QStartNoAckMode` command was previously sent. - private var isNoAckModeActive = false - private let module: Module private let logger: Logger @@ -19,9 +17,14 @@ package actor WasmKitDebugger { let responseKind: GDBTargetResponse.Kind logger.trace("handling GDB host command", metadata: ["GDBHostCommand": .string(command.kind.rawValue)]) + var isNoAckModeActivated = false responseKind = switch command.kind { - case .startNoAckMode, .isThreadSuffixSupported, .listThreadsInStopReply: + case .startNoAckMode: + isNoAckModeActivated = true + fallthrough + + case .isThreadSuffixSupported, .listThreadsInStopReply: .ok case .hostInfo: @@ -38,7 +41,7 @@ package actor WasmKitDebugger { .raw(command.arguments) case .vContSupportedActions: - .vContSupportedActions([.continue, .step, .stop]) + .vContSupportedActions([.continue, .step]) case .isVAttachOrWaitSupported, .enableErrorStrings: .empty @@ -64,12 +67,9 @@ package actor WasmKitDebugger { fatalError() } - defer { - if command.kind == .startNoAckMode { - self.isNoAckModeActive = true - } - } - return .init(kind: responseKind, isNoAckModeActive: self.isNoAckModeActive) + logger.trace("handler produced a response", metadata: ["GDBTargetResponse": .string("\(responseKind)")]) + + return .init(kind: responseKind, isNoAckModeActivated: isNoAckModeActivated) } } From bc539e8a6d6c02f59a5a9b0e68394a5109e96c14 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 14 Oct 2025 16:57:34 +0100 Subject: [PATCH 024/107] Add `qRegisterInfo` parsing to `GDBHostCommand.init` --- .../GDBRemoteProtocol/GDBHostCommand.swift | 28 +++++++++-- .../GDBHostCommandDecoder.swift | 49 +++++++++---------- .../WasmKitGDBHandler/WasmKitDebugger.swift | 14 ++++-- 3 files changed, 58 insertions(+), 33 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index 13f94d9c..bdb8b198 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -1,7 +1,12 @@ /// See GDB and LLDB remote protocol documentation for more details: /// * https://sourceware.org/gdb/current/onlinedocs/gdb.html/General-Query-Packets.html /// * https://lldb.llvm.org/resources/lldbgdbremote.html -package struct GDBHostCommand: Equatable { +package struct GDBHostCommand { + enum Error: Swift.Error { + case unexpectedArgumentsValue + case unknownCommand(kind: String, arguments: String) + } + package enum Kind: String, Equatable { // Currently listed in the order that LLDB sends them in. case startNoAckMode @@ -17,6 +22,7 @@ package struct GDBHostCommand: Equatable { case firstThreadInfo case subsequentThreadInfo case targetStatus + case registerInfo case generalRegisters @@ -61,8 +67,22 @@ package struct GDBHostCommand: Equatable { package let arguments: String - package init(kind: Kind, arguments: String) { - self.kind = kind - self.arguments = arguments + package init(kind: String, arguments: String) throws { + let registerInfoPrefix = "qRegisterInfo" + if kind.starts(with: registerInfoPrefix) { + self.kind = .registerInfo + + guard arguments.isEmpty else { + throw Error.unexpectedArgumentsValue + } + self.arguments = String(kind.dropFirst(registerInfoPrefix.count)) + return + } else if let kind = Kind(rawValue: kind) { + self.kind = kind + } else { + throw Error.unknownCommand(kind: kind, arguments: arguments) + } + + self.arguments = arguments } } diff --git a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift index 96b7fcad..eaf974ae 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -17,7 +17,7 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { case expectedCommandStart case unknownCommandKind(String) case expectedChecksum - case checksumIncorrect + case checksumIncorrect(expectedChecksum: Int, receivedChecksum: UInt8) } package typealias InboundOut = GDBPacket @@ -106,37 +106,34 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { self.accummulatedSum = 0 } - let kindString = String(decoding: self.accummulatedKind, as: UTF8.self) + buffer.moveReaderIndex(forwardBy: 1) - if let commandKind = GDBHostCommand.Kind(rawValue: kindString) { - buffer.moveReaderIndex(forwardBy: 1) + guard let checksumString = buffer.readString(length: 2), + let first = checksumString.first?.hexDigitValue, + let last = checksumString.last?.hexDigitValue + else { + throw Error.expectedChecksum + } - guard let checksumString = buffer.readString(length: 2), - let first = checksumString.first?.hexDigitValue, - let last = checksumString.last?.hexDigitValue - else { - throw Error.expectedChecksum - } + let expectedChecksum = (first * 16) + last - guard (first * 16) + last == self.accummulatedChecksum else { - // FIXME: better diagnostics - throw Error.checksumIncorrect - } + guard expectedChecksum == self.accummulatedChecksum else { + throw Error.checksumIncorrect( + expectedChecksum: expectedChecksum, + receivedChecksum: self.accummulatedChecksum + ) + } - if commandKind == .startNoAckMode { - self.isNoAckModeRequested = true - } + let payload = try GDBHostCommand( + kind: String(decoding: self.accummulatedKind, as: UTF8.self), + arguments: String(decoding: self.accummulatedArguments, as: UTF8.self) + ) - return .init( - payload: .init( - kind: commandKind, - arguments: String(decoding: self.accummulatedArguments, as: UTF8.self) - ), - checksum: accummulatedChecksum, - ) - } else { - throw Error.unknownCommandKind(kindString) + if payload.kind == .startNoAckMode { + self.isNoAckModeRequested = true } + + return .init(payload: payload, checksum: accummulatedChecksum) } mutating package func decode( diff --git a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift index dc950a81..d7378cda 100644 --- a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift +++ b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift @@ -37,8 +37,7 @@ package actor WasmKitDebugger { ]) case .supportedFeatures: - // FIXME: should return a different set of supported features instead of echoing. - .raw(command.arguments) + .raw("qXfer:libraries:read+;PacketSize=1000;") case .vContSupportedActions: .vContSupportedActions([.continue, .step]) @@ -46,7 +45,13 @@ package actor WasmKitDebugger { case .isVAttachOrWaitSupported, .enableErrorStrings: .empty case .processInfo: - .raw("pid:1;parent-pid:1;arch:wasm32;endian:little;ptrsize:4;") + .keyValuePairs([ + "pid": "1", + "parent-pid": "1", + "arch": "wasm32", + "endian": "little", + "ptrsize": "4", + ]) case .currentThreadID: .raw("QC1") @@ -63,6 +68,9 @@ package actor WasmKitDebugger { "reason": "trace", ]) + case .registerInfo: + .raw("name:pc;alt-name:pc;bitsize:64;offset:0;encoding:uint;format:hex;set:General Purpose Registers;gcc:16;dwarf:16;generic:pc;") + case .generalRegisters: fatalError() } From fb4e2a2e285b26fa7b5b34cded074abdf36761b1 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 14 Oct 2025 18:17:15 +0100 Subject: [PATCH 025/107] Use `KeyValuePairs` response for `.registerInfo` --- Sources/WasmKitGDBHandler/WasmKitDebugger.swift | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift index d7378cda..cca82408 100644 --- a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift +++ b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift @@ -69,7 +69,21 @@ package actor WasmKitDebugger { ]) case .registerInfo: - .raw("name:pc;alt-name:pc;bitsize:64;offset:0;encoding:uint;format:hex;set:General Purpose Registers;gcc:16;dwarf:16;generic:pc;") + if command.arguments == "0" { + .keyValuePairs([ + "name": "pc", + "bitsize": "64", + "offset": "0", + "encoding": "uint", + "format": "hex", + "set": "General Purpose Registers", + "gcc": "16", + "dwarf": "16", + "generic": "pc" + ]) + } else { + .raw("E45") + } case .generalRegisters: fatalError() From 042a2c4c4bbdce3fe888f09cbe8d54f89fae5fdd Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 14 Oct 2025 18:20:06 +0100 Subject: [PATCH 026/107] Fix formatting and tests build error --- .../GDBRemoteProtocol/GDBHostCommand.swift | 19 ++++++++++++------- .../GDBHostCommandDecoder.swift | 2 +- .../WasmKitGDBHandler/WasmKitDebugger.swift | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index bdb8b198..39593f4e 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -1,7 +1,7 @@ /// See GDB and LLDB remote protocol documentation for more details: /// * https://sourceware.org/gdb/current/onlinedocs/gdb.html/General-Query-Packets.html /// * https://lldb.llvm.org/resources/lldbgdbremote.html -package struct GDBHostCommand { +package struct GDBHostCommand: Equatable { enum Error: Swift.Error { case unexpectedArgumentsValue case unknownCommand(kind: String, arguments: String) @@ -67,22 +67,27 @@ package struct GDBHostCommand { package let arguments: String - package init(kind: String, arguments: String) throws { + package init(kindString: String, arguments: String) throws { let registerInfoPrefix = "qRegisterInfo" - if kind.starts(with: registerInfoPrefix) { + if kindString.starts(with: registerInfoPrefix) { self.kind = .registerInfo guard arguments.isEmpty else { throw Error.unexpectedArgumentsValue } - self.arguments = String(kind.dropFirst(registerInfoPrefix.count)) + self.arguments = String(kindString.dropFirst(registerInfoPrefix.count)) return - } else if let kind = Kind(rawValue: kind) { + } else if let kind = Kind(rawValue: kindString) { self.kind = kind } else { - throw Error.unknownCommand(kind: kind, arguments: arguments) + throw Error.unknownCommand(kind: kindString, arguments: arguments) } - self.arguments = arguments + self.arguments = arguments + } + + package init(kind: Kind, arguments: String) { + self.kind = kind + self.arguments = arguments } } diff --git a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift index eaf974ae..bd5650e8 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -125,7 +125,7 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { } let payload = try GDBHostCommand( - kind: String(decoding: self.accummulatedKind, as: UTF8.self), + kindString: String(decoding: self.accummulatedKind, as: UTF8.self), arguments: String(decoding: self.accummulatedArguments, as: UTF8.self) ) diff --git a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift index cca82408..591c2e9b 100644 --- a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift +++ b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift @@ -79,7 +79,7 @@ package actor WasmKitDebugger { "set": "General Purpose Registers", "gcc": "16", "dwarf": "16", - "generic": "pc" + "generic": "pc", ]) } else { .raw("E45") From 197abd37f0e9fae8a8cd919ee4067f07804b0503 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 14 Oct 2025 22:15:48 +0100 Subject: [PATCH 027/107] Exclude `wasmkit-gdb-tool` on Windows, address PR feedback --- Package@swift-6.1.swift | 26 ++--- .../GDBTargetResponseEncoder.swift | 14 +-- Sources/wasmkit-gdb-tool/Entrypoint.swift | 95 ++++++++++--------- 3 files changed, 71 insertions(+), 64 deletions(-) diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index 8162f827..9f9ced87 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -140,19 +140,6 @@ let package = Package( "GDBRemoteProtocol", ], ), - - .executableTarget( - name: "wasmkit-gdb-tool", - dependencies: [ - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "Logging", package: "swift-log"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), - .product(name: "SystemPackage", package: "swift-system"), - "GDBRemoteProtocol", - "WasmKitGDBHandler", - ] - ), ], ) @@ -200,5 +187,18 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { name: "WITExtractorPluginTests", exclude: ["Fixtures"] ), + + .executableTarget( + name: "wasmkit-gdb-tool", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Logging", package: "swift-log"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "SystemPackage", package: "swift-system"), + "GDBRemoteProtocol", + "WasmKitGDBHandler", + ] + ), ]) #endif diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index 5001c077..aa8decc7 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -2,8 +2,8 @@ import Foundation import NIOCore extension String { - fileprivate var appendedChecksum: String.UTF8View { - "\(self)#\(String(format:"%02X", self.utf8.reduce(0, { $0 + Int($1) }) % 256))".utf8 + fileprivate var appendedChecksum: String { + "\(self)#\(String(format:"%02X", self.utf8.reduce(0, { $0 + Int($1) }) % 256))" } } @@ -22,19 +22,19 @@ package class GDBTargetResponseEncoder: MessageToByteEncoder { switch data.kind { case .ok: - out.writeBytes("OK#9a".utf8) + out.writeString("OK#9a") case .keyValuePairs(let info): - out.writeBytes(info.map { (key, value) in "\(key):\(value);" }.joined().appendedChecksum) + out.writeString(info.map { (key, value) in "\(key):\(value);" }.joined().appendedChecksum) case .vContSupportedActions(let actions): - out.writeBytes("vCont;\(actions.map { "\($0.rawValue);" }.joined())".appendedChecksum) + out.writeString("vCont;\(actions.map { "\($0.rawValue);" }.joined())".appendedChecksum) case .raw(let str): - out.writeBytes(str.appendedChecksum) + out.writeString(str.appendedChecksum) case .empty: - out.writeBytes("".appendedChecksum) + out.writeString("".appendedChecksum) } } } diff --git a/Sources/wasmkit-gdb-tool/Entrypoint.swift b/Sources/wasmkit-gdb-tool/Entrypoint.swift index 5ca8adf5..6e08c604 100644 --- a/Sources/wasmkit-gdb-tool/Entrypoint.swift +++ b/Sources/wasmkit-gdb-tool/Entrypoint.swift @@ -41,7 +41,15 @@ struct Entrypoint: AsyncParsableCommand { @Option(help: "TCP port that a debugger can connect to") var port = 8080 - @Option(name: .shortAndLong) + @Option( + name: .shortAndLong, + transform: { stringValue in + guard let logLevel = Logger.Level(rawValue: stringValue.lowercased()) else { + throw ValidationError("not a valid log level: \(stringValue)") + } + return logLevel + } + ) var logLevel = Logger.Level.info @Argument @@ -54,62 +62,61 @@ struct Entrypoint: AsyncParsableCommand { return result }() - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - let bootstrap = ServerBootstrap(group: group) - // Specify backlog and enable SO_REUSEADDR for the server itself - .serverChannelOption(.backlog, value: 256) - .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) + try await MultiThreadedEventLoopGroup.withEventLoopGroup(numberOfThreads: System.coreCount) { group in + let bootstrap = ServerBootstrap(group: group) + // Specify backlog and enable SO_REUSEADDR for the server itself + .serverChannelOption(.backlog, value: 256) + .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) - // Set the handlers that are applied to the accepted child `Channel`s. - .childChannelInitializer { channel in - // Ensure we don't read faster then we can write by adding the BackPressureHandler into the pipeline. - channel.eventLoop.makeCompletedFuture { - try channel.pipeline.syncOperations.addHandler(BackPressureHandler()) - // make sure to instantiate your `ChannelHandlers` inside of - // the closure as it will be invoked once per connection. - try channel.pipeline.syncOperations.addHandlers([ - ByteToMessageHandler(GDBHostCommandDecoder(logger: logger)), - MessageToByteHandler(GDBTargetResponseEncoder()), - ]) + // Set the handlers that are applied to the accepted child `Channel`s. + .childChannelInitializer { channel in + // Ensure we don't read faster then we can write by adding the BackPressureHandler into the pipeline. + channel.eventLoop.makeCompletedFuture { + try channel.pipeline.syncOperations.addHandler(BackPressureHandler()) + // make sure to instantiate your `ChannelHandlers` inside of + // the closure as it will be invoked once per connection. + try channel.pipeline.syncOperations.addHandlers([ + ByteToMessageHandler(GDBHostCommandDecoder(logger: logger)), + MessageToByteHandler(GDBTargetResponseEncoder()), + ]) + } } - } - // Enable SO_REUSEADDR for the accepted Channels - .childChannelOption(.socketOption(.so_reuseaddr), value: 1) - .childChannelOption(.maxMessagesPerRead, value: 16) - .childChannelOption(.recvAllocator, value: AdaptiveRecvByteBufferAllocator()) + // Enable SO_REUSEADDR for the accepted Channels + .childChannelOption(.socketOption(.so_reuseaddr), value: 1) + .childChannelOption(.maxMessagesPerRead, value: 16) + .childChannelOption(.recvAllocator, value: AdaptiveRecvByteBufferAllocator()) - let serverChannel = try await bootstrap.bind(host: "127.0.0.1", port: port) { childChannel in - childChannel.eventLoop.makeCompletedFuture { - try NIOAsyncChannel, GDBTargetResponse>( - wrappingChannelSynchronously: childChannel - ) + let serverChannel = try await bootstrap.bind(host: "127.0.0.1", port: port) { childChannel in + childChannel.eventLoop.makeCompletedFuture { + try NIOAsyncChannel, GDBTargetResponse>( + wrappingChannelSynchronously: childChannel + ) + } } - } - /* the server will now be accepting connections */ - logger.info("listening on port \(port)") + /* the server will now be accepting connections */ + logger.info("listening on port \(port)") - let debugger = try WasmKitDebugger(logger: logger, moduleFilePath: self.wasmModulePath) + let debugger = try WasmKitDebugger(logger: logger, moduleFilePath: self.wasmModulePath) - try await withThrowingDiscardingTaskGroup { group in - try await serverChannel.executeThenClose { serverChannelInbound in - for try await connectionChannel in serverChannelInbound { - group.addTask { - do { - try await connectionChannel.executeThenClose { connectionChannelInbound, connectionChannelOutbound in - for try await inboundData in connectionChannelInbound { - // Let's echo back all inbound data - try await connectionChannelOutbound.write(debugger.handle(command: inboundData.payload)) + try await withThrowingDiscardingTaskGroup { group in + try await serverChannel.executeThenClose { serverChannelInbound in + for try await connectionChannel in serverChannelInbound { + group.addTask { + do { + try await connectionChannel.executeThenClose { connectionChannelInbound, connectionChannelOutbound in + for try await inboundData in connectionChannelInbound { + // Let's echo back all inbound data + try await connectionChannelOutbound.write(debugger.handle(command: inboundData.payload)) + } } + } catch { + logger.error("Error in GDB remote protocol connection channel", metadata: ["error": "\(error)"]) } - } catch { - logger.error("Error in GDB remote protocol connection channel", metadata: ["error": "\(error)"]) } } } } } - - try await group.shutdownGracefully() } } From a2899f74e15ce9abe90f6bb09f3ce30daf3dcd4d Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 14 Oct 2025 22:18:23 +0100 Subject: [PATCH 028/107] Remove retroactive `FilePath` conformance --- Sources/wasmkit-gdb-tool/Entrypoint.swift | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/Sources/wasmkit-gdb-tool/Entrypoint.swift b/Sources/wasmkit-gdb-tool/Entrypoint.swift index 6e08c604..08d36b09 100644 --- a/Sources/wasmkit-gdb-tool/Entrypoint.swift +++ b/Sources/wasmkit-gdb-tool/Entrypoint.swift @@ -20,22 +20,6 @@ import NIOPosix import SystemPackage import WasmKitGDBHandler -#if hasFeature(RetroactiveAttribute) - extension Logger.Level: @retroactive ExpressibleByArgument {} - extension FilePath: @retroactive ExpressibleByArgument { - public init?(argument: String) { - self.init(argument) - } - } -#else - extension Logger.Level: ExpressibleByArgument {} - extension FilePath: ExpressibleByArgument { - public init?(argument: String) { - self.init(argument) - } - } -#endif - @main struct Entrypoint: AsyncParsableCommand { @Option(help: "TCP port that a debugger can connect to") @@ -52,7 +36,7 @@ struct Entrypoint: AsyncParsableCommand { ) var logLevel = Logger.Level.info - @Argument + @Argument(transform: { FilePath($0) }) var wasmModulePath: FilePath func run() async throws { From 0b42724213c124cdc145408353a040367e458242 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 14 Oct 2025 23:00:25 +0100 Subject: [PATCH 029/107] Handle more host commands, up to `qWasmCallStack` --- .../GDBRemoteProtocol/GDBHostCommand.swift | 22 +++++++- .../WasmKitGDBHandler/WasmKitDebugger.swift | 52 +++++++++++++------ 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index 39593f4e..df765ce1 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -23,6 +23,11 @@ package struct GDBHostCommand: Equatable { case subsequentThreadInfo case targetStatus case registerInfo + case structuredDataPlugins + case transfer + case readMemoryBinaryData + case readMemory + case wasmCallStack case generalRegisters @@ -56,6 +61,12 @@ package struct GDBHostCommand: Equatable { self = .subsequentThreadInfo case "?": self = .targetStatus + case "qStructuredDataPlugins": + self = .structuredDataPlugins + case "qXfer": + self = .transfer + case "qWasmCallStack": + self = .wasmCallStack default: return nil @@ -69,7 +80,16 @@ package struct GDBHostCommand: Equatable { package init(kindString: String, arguments: String) throws { let registerInfoPrefix = "qRegisterInfo" - if kindString.starts(with: registerInfoPrefix) { + + if kindString.starts(with: "x") { + self.kind = .readMemoryBinaryData + self.arguments = String(kindString.dropFirst()) + return + } else if kindString.starts(with: "m") { + self.kind = .readMemory + self.arguments = String(kindString.dropFirst()) + return + } else if kindString.starts(with: registerInfoPrefix) { self.kind = .registerInfo guard arguments.isEmpty else { diff --git a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift index 591c2e9b..1c373a8f 100644 --- a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift +++ b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift @@ -5,30 +5,35 @@ import SystemPackage import WasmKit package actor WasmKitDebugger { + enum Error: Swift.Error { + case unknownTransferArguments + } + private let module: Module + private let moduleFilePath: FilePath private let logger: Logger package init(logger: Logger, moduleFilePath: FilePath) throws { self.logger = logger self.module = try parseWasm(filePath: moduleFilePath) + self.moduleFilePath = moduleFilePath } - package func handle(command: GDBHostCommand) -> GDBTargetResponse { + package func handle(command: GDBHostCommand) throws -> GDBTargetResponse { let responseKind: GDBTargetResponse.Kind logger.trace("handling GDB host command", metadata: ["GDBHostCommand": .string(command.kind.rawValue)]) var isNoAckModeActivated = false - responseKind = switch command.kind { case .startNoAckMode: isNoAckModeActivated = true fallthrough case .isThreadSuffixSupported, .listThreadsInStopReply: - .ok + responseKind = .ok case .hostInfo: - .keyValuePairs([ + responseKind = .keyValuePairs([ "arch": "wasm32", "ptrsize": "4", "endian": "little", @@ -37,15 +42,16 @@ package actor WasmKitDebugger { ]) case .supportedFeatures: - .raw("qXfer:libraries:read+;PacketSize=1000;") + responseKind = .raw("qXfer:libraries:read+;PacketSize=1000;") case .vContSupportedActions: - .vContSupportedActions([.continue, .step]) + responseKind = .vContSupportedActions([.continue, .step]) + + case .isVAttachOrWaitSupported, .enableErrorStrings, .structuredDataPlugins, .readMemoryBinaryData: + responseKind = .empty - case .isVAttachOrWaitSupported, .enableErrorStrings: - .empty case .processInfo: - .keyValuePairs([ + responseKind = .keyValuePairs([ "pid": "1", "parent-pid": "1", "arch": "wasm32", @@ -54,23 +60,23 @@ package actor WasmKitDebugger { ]) case .currentThreadID: - .raw("QC1") + responseKind = .raw("QC1") case .firstThreadInfo: - .raw("m1") + responseKind = .raw("m1") case .subsequentThreadInfo: - .raw("l") + responseKind = .raw("l") case .targetStatus: - .keyValuePairs([ + responseKind = .keyValuePairs([ "T05thread": "1", "reason": "trace", ]) case .registerInfo: if command.arguments == "0" { - .keyValuePairs([ + responseKind = .keyValuePairs([ "name": "pc", "bitsize": "64", "offset": "0", @@ -82,10 +88,24 @@ package actor WasmKitDebugger { "generic": "pc", ]) } else { - .raw("E45") + responseKind = .raw("E45") } - case .generalRegisters: + case .transfer: + if command.arguments.starts(with: "libraries:read:") { + responseKind = .raw(""" + l +
+ + """) + } else { + throw Error.unknownTransferArguments + } + + case .readMemory: + responseKind = .empty + + case .wasmCallStack, .generalRegisters: fatalError() } From 1783176bd8910ab88b6791c6030f8573392eb29c Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 14 Oct 2025 23:11:53 +0100 Subject: [PATCH 030/107] Fix formatting --- .../GDBRemoteProtocol/GDBHostCommand.swift | 12 +- .../WasmKitGDBHandler/WasmKitDebugger.swift | 151 +++++++++--------- 2 files changed, 82 insertions(+), 81 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index df765ce1..278d07e5 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -82,13 +82,13 @@ package struct GDBHostCommand: Equatable { let registerInfoPrefix = "qRegisterInfo" if kindString.starts(with: "x") { - self.kind = .readMemoryBinaryData - self.arguments = String(kindString.dropFirst()) - return + self.kind = .readMemoryBinaryData + self.arguments = String(kindString.dropFirst()) + return } else if kindString.starts(with: "m") { - self.kind = .readMemory - self.arguments = String(kindString.dropFirst()) - return + self.kind = .readMemory + self.arguments = String(kindString.dropFirst()) + return } else if kindString.starts(with: registerInfoPrefix) { self.kind = .registerInfo diff --git a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift index 1c373a8f..fed823ce 100644 --- a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift +++ b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift @@ -24,90 +24,91 @@ package actor WasmKitDebugger { logger.trace("handling GDB host command", metadata: ["GDBHostCommand": .string(command.kind.rawValue)]) var isNoAckModeActivated = false - switch command.kind { - case .startNoAckMode: - isNoAckModeActivated = true - fallthrough - - case .isThreadSuffixSupported, .listThreadsInStopReply: - responseKind = .ok - - case .hostInfo: + switch command.kind { + case .startNoAckMode: + isNoAckModeActivated = true + fallthrough + + case .isThreadSuffixSupported, .listThreadsInStopReply: + responseKind = .ok + + case .hostInfo: + responseKind = .keyValuePairs([ + "arch": "wasm32", + "ptrsize": "4", + "endian": "little", + "ostype": "wasip1", + "vendor": "WasmKit", + ]) + + case .supportedFeatures: + responseKind = .raw("qXfer:libraries:read+;PacketSize=1000;") + + case .vContSupportedActions: + responseKind = .vContSupportedActions([.continue, .step]) + + case .isVAttachOrWaitSupported, .enableErrorStrings, .structuredDataPlugins, .readMemoryBinaryData: + responseKind = .empty + + case .processInfo: + responseKind = .keyValuePairs([ + "pid": "1", + "parent-pid": "1", + "arch": "wasm32", + "endian": "little", + "ptrsize": "4", + ]) + + case .currentThreadID: + responseKind = .raw("QC1") + + case .firstThreadInfo: + responseKind = .raw("m1") + + case .subsequentThreadInfo: + responseKind = .raw("l") + + case .targetStatus: + responseKind = .keyValuePairs([ + "T05thread": "1", + "reason": "trace", + ]) + + case .registerInfo: + if command.arguments == "0" { responseKind = .keyValuePairs([ - "arch": "wasm32", - "ptrsize": "4", - "endian": "little", - "ostype": "wasip1", - "vendor": "WasmKit", - ]) - - case .supportedFeatures: - responseKind = .raw("qXfer:libraries:read+;PacketSize=1000;") - - case .vContSupportedActions: - responseKind = .vContSupportedActions([.continue, .step]) - - case .isVAttachOrWaitSupported, .enableErrorStrings, .structuredDataPlugins, .readMemoryBinaryData: - responseKind = .empty - - case .processInfo: - responseKind = .keyValuePairs([ - "pid": "1", - "parent-pid": "1", - "arch": "wasm32", - "endian": "little", - "ptrsize": "4", - ]) - - case .currentThreadID: - responseKind = .raw("QC1") - - case .firstThreadInfo: - responseKind = .raw("m1") - - case .subsequentThreadInfo: - responseKind = .raw("l") - - case .targetStatus: - responseKind = .keyValuePairs([ - "T05thread": "1", - "reason": "trace", + "name": "pc", + "bitsize": "64", + "offset": "0", + "encoding": "uint", + "format": "hex", + "set": "General Purpose Registers", + "gcc": "16", + "dwarf": "16", + "generic": "pc", ]) + } else { + responseKind = .raw("E45") + } - case .registerInfo: - if command.arguments == "0" { - responseKind = .keyValuePairs([ - "name": "pc", - "bitsize": "64", - "offset": "0", - "encoding": "uint", - "format": "hex", - "set": "General Purpose Registers", - "gcc": "16", - "dwarf": "16", - "generic": "pc", - ]) - } else { - responseKind = .raw("E45") - } - - case .transfer: - if command.arguments.starts(with: "libraries:read:") { - responseKind = .raw(""" + case .transfer: + if command.arguments.starts(with: "libraries:read:") { + responseKind = .raw( + """ l
""") - } else { - throw Error.unknownTransferArguments - } + } else { + throw Error.unknownTransferArguments + } - case .readMemory: - responseKind = .empty + case .readMemory: + responseKind = .empty - case .wasmCallStack, .generalRegisters: - fatalError() - } + case .wasmCallStack, .generalRegisters: + fatalError() + } logger.trace("handler produced a response", metadata: ["GDBTargetResponse": .string("\(responseKind)")]) From c0a9cf61d0ad5ebff9bfc7a172a740384b59e5b0 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 16 Oct 2025 11:56:03 +0100 Subject: [PATCH 031/107] Address PR feedback, add comment for `DebuggingAddress` sendability --- Sources/WasmKit/Execution/Errors.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/WasmKit/Execution/Errors.swift b/Sources/WasmKit/Execution/Errors.swift index 6bfab3ac..72dd6f90 100644 --- a/Sources/WasmKit/Execution/Errors.swift +++ b/Sources/WasmKit/Execution/Errors.swift @@ -11,7 +11,10 @@ struct Backtrace: CustomStringConvertible, Sendable { let debuggingAddress: DebuggingAddress /// Address of the symbol for debugging purposes. - enum DebuggingAddress: CustomStringConvertible, @unchecked Sendable { + enum DebuggingAddress: CustomStringConvertible, + // `Pc` is a pointer, which is not `Sendable` by default. + @unchecked Sendable + { case iseq(Pc) case wasm(UInt64) @@ -31,7 +34,7 @@ struct Backtrace: CustomStringConvertible, Sendable { var description: String { symbols.enumerated().map { (index, symbol) in let name = symbol.name ?? "unknown" - return " \(symbol.debuggingAddress): \(name)" + return " \(index): (\(symbol.debuggingAddress)) \(name)" }.joined(separator: "\n") } } From 806e7eccdff8cc4e1acf5f6f993ed282f765d3c7 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 16 Oct 2025 13:08:54 +0100 Subject: [PATCH 032/107] Add `DebuggerExecution` wrapper type --- Sources/WasmKit/Execution/Debugging.swift | 21 +++++++++++++++++++ Sources/WasmKit/Execution/Errors.swift | 9 ++++---- Sources/WasmKit/Execution/Execution.swift | 7 ++++++- .../WasmKitGDBHandler/WasmKitDebugger.swift | 13 ++++++++++-- Sources/wasmkit-gdb-tool/Entrypoint.swift | 2 +- 5 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 Sources/WasmKit/Execution/Debugging.swift diff --git a/Sources/WasmKit/Execution/Debugging.swift b/Sources/WasmKit/Execution/Debugging.swift new file mode 100644 index 00000000..77dc5756 --- /dev/null +++ b/Sources/WasmKit/Execution/Debugging.swift @@ -0,0 +1,21 @@ +package struct DebuggerExecution: ~Copyable { + let valueStack: Sp + let execution: Execution + let store: Store + + package init(store: Store) { + let limit = store.engine.configuration.stackSize / MemoryLayout.stride + self.valueStack = UnsafeMutablePointer.allocate(capacity: limit) + self.store = store + self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit)) + } + + package func captureBacktrace() -> Backtrace { + return Execution.captureBacktrace(sp: self.valueStack, store: self.store) + } + + deinit { + valueStack.deallocate() + } +} + diff --git a/Sources/WasmKit/Execution/Errors.swift b/Sources/WasmKit/Execution/Errors.swift index 72dd6f90..1a5e1463 100644 --- a/Sources/WasmKit/Execution/Errors.swift +++ b/Sources/WasmKit/Execution/Errors.swift @@ -3,9 +3,9 @@ import WasmTypes import struct WasmParser.Import /// The backtrace of the trap. -struct Backtrace: CustomStringConvertible, Sendable { +package struct Backtrace: CustomStringConvertible, Sendable { /// A symbol in the backtrace. - struct Symbol { + package struct Symbol { /// The name of the symbol. let name: String? let debuggingAddress: DebuggingAddress @@ -31,8 +31,9 @@ struct Backtrace: CustomStringConvertible, Sendable { let symbols: [Symbol] /// Textual description of the backtrace. - var description: String { - symbols.enumerated().map { (index, symbol) in + package var description: String { + print("backtrace contains \(symbols.count) symbols") + return symbols.enumerated().map { (index, symbol) in let name = symbol.name ?? "unknown" return " \(index): (\(symbol.debuggingAddress)) \(name)" }.joined(separator: "\n") diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index 56fe4bc1..f6a3ef34 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -4,7 +4,7 @@ import _CWasmKit /// /// Each new invocation through exported function has a separate ``Execution`` /// even though the invocation happens during another invocation. -struct Execution { +struct Execution: ~Copyable { /// The reference to the ``Store`` associated with the execution. let store: StoreRef /// The end of the VM stack space. @@ -14,6 +14,11 @@ struct Execution { /// - Note: If the trap is set, it must be released manually. private var trap: (error: UnsafeRawPointer, sp: Sp)? = nil + package init(store: StoreRef, stackEnd: UnsafeMutablePointer) { + self.store = store + self.stackEnd = stackEnd + } + /// Executes the given closure with a new execution state associated with /// the given ``Store`` instance. static func with( diff --git a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift index fed823ce..4732b26f 100644 --- a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift +++ b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift @@ -4,7 +4,7 @@ import Synchronization import SystemPackage import WasmKit -package actor WasmKitDebugger { +package actor WasmKitGDBHandler { enum Error: Swift.Error { case unknownTransferArguments } @@ -12,11 +12,16 @@ package actor WasmKitDebugger { private let module: Module private let moduleFilePath: FilePath private let logger: Logger + private let debuggerExecution: DebuggerExecution + private let instance: Instance package init(logger: Logger, moduleFilePath: FilePath) throws { self.logger = logger self.module = try parseWasm(filePath: moduleFilePath) self.moduleFilePath = moduleFilePath + let store = Store(engine: Engine()) + self.debuggerExecution = DebuggerExecution(store: store) + self.instance = try module.instantiate(store: store) } package func handle(command: GDBHostCommand) throws -> GDBTargetResponse { @@ -106,7 +111,11 @@ package actor WasmKitDebugger { case .readMemory: responseKind = .empty - case .wasmCallStack, .generalRegisters: + case .wasmCallStack: + print(self.debuggerExecution.captureBacktrace()) + responseKind = .empty + + case .generalRegisters: fatalError() } diff --git a/Sources/wasmkit-gdb-tool/Entrypoint.swift b/Sources/wasmkit-gdb-tool/Entrypoint.swift index 08d36b09..9e200ab1 100644 --- a/Sources/wasmkit-gdb-tool/Entrypoint.swift +++ b/Sources/wasmkit-gdb-tool/Entrypoint.swift @@ -81,7 +81,7 @@ struct Entrypoint: AsyncParsableCommand { /* the server will now be accepting connections */ logger.info("listening on port \(port)") - let debugger = try WasmKitDebugger(logger: logger, moduleFilePath: self.wasmModulePath) + let debugger = try WasmKitGDBHandler(logger: logger, moduleFilePath: self.wasmModulePath) try await withThrowingDiscardingTaskGroup { group in try await serverChannel.executeThenClose { serverChannelInbound in From 26ea4e5b48d3b9d25e88e5ad113623866d712681 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 16 Oct 2025 17:05:33 +0100 Subject: [PATCH 033/107] Implement `.readMemory` command handling --- Package@swift-6.1.swift | 2 + .../GDBRemoteProtocol/GDBTargetResponse.swift | 5 +- .../GDBTargetResponseEncoder.swift | 6 +- ...Debugger.swift => WasmKitGDBHandler.swift} | 67 ++++++++++++++++--- Sources/wasmkit-gdb-tool/Entrypoint.swift | 2 +- 5 files changed, 69 insertions(+), 13 deletions(-) rename Sources/WasmKitGDBHandler/{WasmKitDebugger.swift => WasmKitGDBHandler.swift} (60%) diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index 9f9ced87..f426b8f5 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -134,9 +134,11 @@ let package = Package( .target( name: "WasmKitGDBHandler", dependencies: [ + .product(name: "_NIOFileSystem", package: "swift-nio"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "SystemPackage", package: "swift-system"), "WasmKit", + "WasmKitWASI", "GDBRemoteProtocol", ], ), diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift index 7728ba39..d367cc29 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift @@ -1,3 +1,5 @@ +import NIOCore + /// Actions supported in the `vCont` host command. package enum VContActions: String { case `continue` = "c" @@ -13,7 +15,8 @@ package struct GDBTargetResponse { case ok case keyValuePairs(KeyValuePairs) case vContSupportedActions([VContActions]) - case raw(String) + case string(String) + case hexEncodedBinary(ByteBufferView) case empty } diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index aa8decc7..7a889334 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -30,9 +30,13 @@ package class GDBTargetResponseEncoder: MessageToByteEncoder { case .vContSupportedActions(let actions): out.writeString("vCont;\(actions.map { "\($0.rawValue);" }.joined())".appendedChecksum) - case .raw(let str): + case .string(let str): out.writeString(str.appendedChecksum) + case .hexEncodedBinary(let binary): + let hexDump = ByteBuffer(bytes: binary).hexDump(format: .compact) + out.writeString(hexDump.appendedChecksum) + case .empty: out.writeString("".appendedChecksum) } diff --git a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift similarity index 60% rename from Sources/WasmKitGDBHandler/WasmKitDebugger.swift rename to Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 4732b26f..513aa2a8 100644 --- a/Sources/WasmKitGDBHandler/WasmKitDebugger.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -1,27 +1,61 @@ import GDBRemoteProtocol import Logging +import NIOCore +import NIOFileSystem import Synchronization import SystemPackage import WasmKit +import WasmKitWASI + +extension BinaryInteger { + init?(hexEncoded: Substring) { + var result = Self.zero + for (offset, element) in hexEncoded.reversed().enumerated() { + guard let digit = element.hexDigitValue else { return nil } + result += Self(digit) << (offset * 4) + } + + self = result + } +} package actor WasmKitGDBHandler { enum Error: Swift.Error { case unknownTransferArguments + case unknownReadMemoryArguments + case entrypointFunctionNotFound } + private let wasmBinary: ByteBuffer private let module: Module private let moduleFilePath: FilePath private let logger: Logger private let debuggerExecution: DebuggerExecution private let instance: Instance + private let entrypointFunction: Function + private let functionsRLE: [(wasmAddress: Int, iSeqAddress: Int)] = [] - package init(logger: Logger, moduleFilePath: FilePath) throws { + package init(logger: Logger, moduleFilePath: FilePath) async throws { self.logger = logger - self.module = try parseWasm(filePath: moduleFilePath) + + self.wasmBinary = try await FileSystem.shared.withFileHandle(forReadingAt: moduleFilePath) { + try await $0.readToEnd(maximumSizeAllowed: .unlimited) + } + + self.module = try parseWasm(bytes: .init(buffer: self.wasmBinary)) self.moduleFilePath = moduleFilePath let store = Store(engine: Engine()) self.debuggerExecution = DebuggerExecution(store: store) - self.instance = try module.instantiate(store: store) + + var imports = Imports() + let wasi = try WASIBridgeToHost() + wasi.link(to: &imports, store: store) + self.instance = try module.instantiate(store: store, imports: imports) + + guard case .function(let entrypointFunction) = self.instance.exports["_start"] else { + throw Error.entrypointFunctionNotFound + } + self.entrypointFunction = entrypointFunction } package func handle(command: GDBHostCommand) throws -> GDBTargetResponse { @@ -47,7 +81,7 @@ package actor WasmKitGDBHandler { ]) case .supportedFeatures: - responseKind = .raw("qXfer:libraries:read+;PacketSize=1000;") + responseKind = .string("qXfer:libraries:read+;PacketSize=1000;") case .vContSupportedActions: responseKind = .vContSupportedActions([.continue, .step]) @@ -65,13 +99,13 @@ package actor WasmKitGDBHandler { ]) case .currentThreadID: - responseKind = .raw("QC1") + responseKind = .string("QC1") case .firstThreadInfo: - responseKind = .raw("m1") + responseKind = .string("m1") case .subsequentThreadInfo: - responseKind = .raw("l") + responseKind = .string("l") case .targetStatus: responseKind = .keyValuePairs([ @@ -93,12 +127,12 @@ package actor WasmKitGDBHandler { "generic": "pc", ]) } else { - responseKind = .raw("E45") + responseKind = .string("E45") } case .transfer: if command.arguments.starts(with: "libraries:read:") { - responseKind = .raw( + responseKind = .string( """ l
@@ -109,7 +143,20 @@ package actor WasmKitGDBHandler { } case .readMemory: - responseKind = .empty + let argumentsArray = command.arguments.split(separator: ",") + guard + argumentsArray.count == 2, + let address = UInt64(hexEncoded: argumentsArray[0]), + var length = Int(hexEncoded: argumentsArray[1]) + else { throw Error.unknownReadMemoryArguments } + + let binaryOffset = Int(address - 0x4000000000000000) + + if binaryOffset + length > wasmBinary.readableBytes { + length = wasmBinary.readableBytes - binaryOffset + } + + responseKind = .hexEncodedBinary(wasmBinary.readableBytesView[binaryOffset..<(binaryOffset + length)]) case .wasmCallStack: print(self.debuggerExecution.captureBacktrace()) diff --git a/Sources/wasmkit-gdb-tool/Entrypoint.swift b/Sources/wasmkit-gdb-tool/Entrypoint.swift index 9e200ab1..3a2a7e3e 100644 --- a/Sources/wasmkit-gdb-tool/Entrypoint.swift +++ b/Sources/wasmkit-gdb-tool/Entrypoint.swift @@ -81,7 +81,7 @@ struct Entrypoint: AsyncParsableCommand { /* the server will now be accepting connections */ logger.info("listening on port \(port)") - let debugger = try WasmKitGDBHandler(logger: logger, moduleFilePath: self.wasmModulePath) + let debugger = try await WasmKitGDBHandler(logger: logger, moduleFilePath: self.wasmModulePath) try await withThrowingDiscardingTaskGroup { group in try await serverChannel.executeThenClose { serverChannelInbound in From 834390201d30757e9f1adda9d0bdf2306a072bee Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 16 Oct 2025 17:08:05 +0100 Subject: [PATCH 034/107] Fix formatting --- Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 513aa2a8..0327128e 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -150,7 +150,7 @@ package actor WasmKitGDBHandler { var length = Int(hexEncoded: argumentsArray[1]) else { throw Error.unknownReadMemoryArguments } - let binaryOffset = Int(address - 0x4000000000000000) + let binaryOffset = Int(address - 0x4000_0000_0000_0000) if binaryOffset + length > wasmBinary.readableBytes { length = wasmBinary.readableBytes - binaryOffset From ff2fa0de6121d739840aafc8565488bdcf539708 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 16 Oct 2025 17:12:50 +0100 Subject: [PATCH 035/107] Pass `-package-name` in CMake to enable use of `package` --- cmake/modules/AddSwiftHostLibrary.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/modules/AddSwiftHostLibrary.cmake b/cmake/modules/AddSwiftHostLibrary.cmake index 26b72478..e7bf9949 100644 --- a/cmake/modules/AddSwiftHostLibrary.cmake +++ b/cmake/modules/AddSwiftHostLibrary.cmake @@ -91,7 +91,7 @@ function(add_wasmkit_library name) endif() target_compile_options(${name} PRIVATE - $<$:-color-diagnostics> + $<$:-color-diagnostics;-package-name;WasmKit> ) if(LLVM_USE_LINKER) From 3ed90137dbb0e7ddfdeb06594c90d0f57ddce1e5 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 16 Oct 2025 17:23:01 +0100 Subject: [PATCH 036/107] Disable `WasmKitGDBHandler` on Windows --- Package@swift-6.1.swift | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index f426b8f5..edafee37 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -130,18 +130,6 @@ let package = Package( ] ), .testTarget(name: "GDBRemoteProtocolTests", dependencies: ["GDBRemoteProtocol"]), - - .target( - name: "WasmKitGDBHandler", - dependencies: [ - .product(name: "_NIOFileSystem", package: "swift-nio"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "SystemPackage", package: "swift-system"), - "WasmKit", - "WasmKitWASI", - "GDBRemoteProtocol", - ], - ), ], ) @@ -202,5 +190,17 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { "WasmKitGDBHandler", ] ), + + .target( + name: "WasmKitGDBHandler", + dependencies: [ + .product(name: "_NIOFileSystem", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "SystemPackage", package: "swift-system"), + "WasmKit", + "WasmKitWASI", + "GDBRemoteProtocol", + ], + ), ]) #endif From 3ea5aab03242227bb3557b562677e492fb5110bf Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 16 Oct 2025 17:23:23 +0100 Subject: [PATCH 037/107] Fix formatting --- Sources/WasmKit/Execution/Debugging.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugging.swift b/Sources/WasmKit/Execution/Debugging.swift index 77dc5756..3dc08ddf 100644 --- a/Sources/WasmKit/Execution/Debugging.swift +++ b/Sources/WasmKit/Execution/Debugging.swift @@ -10,12 +10,15 @@ package struct DebuggerExecution: ~Copyable { self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit)) } + package func toggleBreakpoint() { + + } + package func captureBacktrace() -> Backtrace { - return Execution.captureBacktrace(sp: self.valueStack, store: self.store) + return Execution.captureBacktrace(sp: self.valueStack, store: self.store) } deinit { valueStack.deallocate() } } - From 3fc8fdca07c2a45221155ec9e248eeacab7e1569 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 17 Oct 2025 12:19:49 +0100 Subject: [PATCH 038/107] Try nightly `main` for `build-cmake` job --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2ecbad41..ea540a8d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -242,7 +242,7 @@ jobs: build-cmake: runs-on: ubuntu-24.04 container: - image: swift:6.2-noble + image: "swiftlang/swift:nightly-main-noble" steps: - uses: actions/checkout@v4 - name: Install Ninja From ea831348c8f11a34e0c917a1e1395a681e6366be Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 17 Oct 2025 12:34:10 +0100 Subject: [PATCH 039/107] Revert "Try nightly `main` for `build-cmake` job" This reverts commit dd317cf07b634ef6d6d731633c13a7aa5440aa11. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ea540a8d..2ecbad41 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -242,7 +242,7 @@ jobs: build-cmake: runs-on: ubuntu-24.04 container: - image: "swiftlang/swift:nightly-main-noble" + image: swift:6.2-noble steps: - uses: actions/checkout@v4 - name: Install Ninja From 2893799029ec9e087be06523138d43ae0f0a3214 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 17 Oct 2025 16:31:21 +0100 Subject: [PATCH 040/107] Rename `DebuggerExecution` to `Debugger`, hide behind a trait --- .../Execution/{Debugging.swift => Debugger.swift} | 6 +++++- Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift | 12 ++++++++---- Sources/wasmkit-gdb-tool/Entrypoint.swift | 4 ++++ 3 files changed, 17 insertions(+), 5 deletions(-) rename Sources/WasmKit/Execution/{Debugging.swift => Debugger.swift} (90%) diff --git a/Sources/WasmKit/Execution/Debugging.swift b/Sources/WasmKit/Execution/Debugger.swift similarity index 90% rename from Sources/WasmKit/Execution/Debugging.swift rename to Sources/WasmKit/Execution/Debugger.swift index 3dc08ddf..11742dcc 100644 --- a/Sources/WasmKit/Execution/Debugging.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -1,4 +1,6 @@ -package struct DebuggerExecution: ~Copyable { +#if WasmDebuggingSupport + +package struct Debugger: ~Copyable { let valueStack: Sp let execution: Execution let store: Store @@ -22,3 +24,5 @@ package struct DebuggerExecution: ~Copyable { valueStack.deallocate() } } + +#endif diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 0327128e..3b2c61a4 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -1,3 +1,5 @@ +#if WasmDebuggingSupport + import GDBRemoteProtocol import Logging import NIOCore @@ -30,7 +32,7 @@ package actor WasmKitGDBHandler { private let module: Module private let moduleFilePath: FilePath private let logger: Logger - private let debuggerExecution: DebuggerExecution + private let debugger: Debugger private let instance: Instance private let entrypointFunction: Function private let functionsRLE: [(wasmAddress: Int, iSeqAddress: Int)] = [] @@ -45,7 +47,7 @@ package actor WasmKitGDBHandler { self.module = try parseWasm(bytes: .init(buffer: self.wasmBinary)) self.moduleFilePath = moduleFilePath let store = Store(engine: Engine()) - self.debuggerExecution = DebuggerExecution(store: store) + self.debugger = Debugger(store: store) var imports = Imports() let wasi = try WASIBridgeToHost() @@ -150,7 +152,7 @@ package actor WasmKitGDBHandler { var length = Int(hexEncoded: argumentsArray[1]) else { throw Error.unknownReadMemoryArguments } - let binaryOffset = Int(address - 0x4000_0000_0000_0000) + let binaryOffset = Int(address - 0x4000000000000000) if binaryOffset + length > wasmBinary.readableBytes { length = wasmBinary.readableBytes - binaryOffset @@ -159,7 +161,7 @@ package actor WasmKitGDBHandler { responseKind = .hexEncodedBinary(wasmBinary.readableBytesView[binaryOffset..<(binaryOffset + length)]) case .wasmCallStack: - print(self.debuggerExecution.captureBacktrace()) + print(self.debugger.captureBacktrace()) responseKind = .empty case .generalRegisters: @@ -172,3 +174,5 @@ package actor WasmKitGDBHandler { } } + +#endif diff --git a/Sources/wasmkit-gdb-tool/Entrypoint.swift b/Sources/wasmkit-gdb-tool/Entrypoint.swift index 3a2a7e3e..107e3525 100644 --- a/Sources/wasmkit-gdb-tool/Entrypoint.swift +++ b/Sources/wasmkit-gdb-tool/Entrypoint.swift @@ -40,6 +40,7 @@ struct Entrypoint: AsyncParsableCommand { var wasmModulePath: FilePath func run() async throws { +#if WasmDebuggingSupport let logger = { var result = Logger(label: "org.swiftwasm.WasmKit") result.logLevel = self.logLevel @@ -102,5 +103,8 @@ struct Entrypoint: AsyncParsableCommand { } } } +#else + fatalError("Build WasmKit with `WasmDebuggingSupport` trait") +#endif } } From 7bdb16c3718c73011c33fdcc7ff77c07b430f91c Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 17 Oct 2025 16:33:10 +0100 Subject: [PATCH 041/107] Enable `WasmDebuggingSupport` on CI where possible --- .github/workflows/main.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2ecbad41..93fba759 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -102,29 +102,32 @@ jobs: wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" + test-args: "--traits WasmDebuggingSupport" - swift: "swift:6.1-noble" development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz" wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" + test-args: "--traits WasmDebuggingSupport" - swift: "swift:6.2-amazonlinux2" development-toolchain-download: "https://download.swift.org/development/amazonlinux2/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-amazonlinux2.tar.gz" wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" + test-args: "--traits WasmDebuggingSupport" - swift: "swift:6.2-noble" development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz" wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" - test-args: "--enable-code-coverage" + test-args: "--traits WasmDebuggingSupport --enable-code-coverage" build-dev-dashboard: true - swift: "swiftlang/swift:nightly-main-noble" development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz" wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" - test-args: "-Xswiftc -DWASMKIT_CI_TOOLCHAIN_NIGHTLY" + test-args: "--traits WasmDebuggingSupport -Xswiftc -DWASMKIT_CI_TOOLCHAIN_NIGHTLY" runs-on: ubuntu-24.04 name: "build-linux (${{ matrix.swift }})" @@ -206,9 +209,9 @@ jobs: run: ./build-exec swift sdk install "${{ matrix.musl-swift-sdk-download }}" --checksum "${{ matrix.musl-swift-sdk-checksum }}" - name: Build (x86_64-swift-linux-musl) - run: ./build-exec swift build --swift-sdk x86_64-swift-linux-musl + run: ./build-exec swift build --swift-sdk x86_64-swift-linux-musl --traits WasmDebuggingSupport - name: Build (aarch64-swift-linux-musl) - run: ./build-exec swift build --swift-sdk aarch64-swift-linux-musl + run: ./build-exec swift build --swift-sdk aarch64-swift-linux-musl --traits WasmDebuggingSupport build-android: runs-on: ubuntu-24.04 @@ -267,4 +270,4 @@ jobs: run: swift sdk install https://download.swift.org/swift-6.2-release/wasm/swift-6.2-RELEASE/swift-6.2-RELEASE_wasm.artifactbundle.tar.gz --checksum fe4e8648309fce86ea522e9e0d1dc48e82df6ba6e5743dbf0c53db8429fb5224 - name: Build with the Swift SDK run: swift build --swift-sdk "$(swiftc -print-target-info | jq -r '.swiftCompilerTag')_wasm" --product wasmkit-cli - + From 4ea0fb0fb55fd6199ce63381083e1a6bc8496a81 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 17 Oct 2025 16:33:34 +0100 Subject: [PATCH 042/107] Fix formatting --- Sources/WasmKit/Execution/Debugger.swift | 36 +-- .../WasmKitGDBHandler/WasmKitGDBHandler.swift | 302 +++++++++--------- Sources/wasmkit-gdb-tool/Entrypoint.swift | 102 +++--- 3 files changed, 220 insertions(+), 220 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 11742dcc..dc27283e 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -1,28 +1,28 @@ #if WasmDebuggingSupport -package struct Debugger: ~Copyable { - let valueStack: Sp - let execution: Execution - let store: Store + package struct Debugger: ~Copyable { + let valueStack: Sp + let execution: Execution + let store: Store - package init(store: Store) { - let limit = store.engine.configuration.stackSize / MemoryLayout.stride - self.valueStack = UnsafeMutablePointer.allocate(capacity: limit) - self.store = store - self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit)) - } + package init(store: Store) { + let limit = store.engine.configuration.stackSize / MemoryLayout.stride + self.valueStack = UnsafeMutablePointer.allocate(capacity: limit) + self.store = store + self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit)) + } - package func toggleBreakpoint() { + package func toggleBreakpoint() { - } + } - package func captureBacktrace() -> Backtrace { - return Execution.captureBacktrace(sp: self.valueStack, store: self.store) - } + package func captureBacktrace() -> Backtrace { + return Execution.captureBacktrace(sp: self.valueStack, store: self.store) + } - deinit { - valueStack.deallocate() + deinit { + valueStack.deallocate() + } } -} #endif diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 3b2c61a4..c0c5ec59 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -1,178 +1,178 @@ #if WasmDebuggingSupport -import GDBRemoteProtocol -import Logging -import NIOCore -import NIOFileSystem -import Synchronization -import SystemPackage -import WasmKit -import WasmKitWASI - -extension BinaryInteger { - init?(hexEncoded: Substring) { - var result = Self.zero - for (offset, element) in hexEncoded.reversed().enumerated() { - guard let digit = element.hexDigitValue else { return nil } - result += Self(digit) << (offset * 4) - } + import GDBRemoteProtocol + import Logging + import NIOCore + import NIOFileSystem + import Synchronization + import SystemPackage + import WasmKit + import WasmKitWASI + + extension BinaryInteger { + init?(hexEncoded: Substring) { + var result = Self.zero + for (offset, element) in hexEncoded.reversed().enumerated() { + guard let digit = element.hexDigitValue else { return nil } + result += Self(digit) << (offset * 4) + } - self = result + self = result + } } -} -package actor WasmKitGDBHandler { - enum Error: Swift.Error { - case unknownTransferArguments - case unknownReadMemoryArguments - case entrypointFunctionNotFound - } + package actor WasmKitGDBHandler { + enum Error: Swift.Error { + case unknownTransferArguments + case unknownReadMemoryArguments + case entrypointFunctionNotFound + } - private let wasmBinary: ByteBuffer - private let module: Module - private let moduleFilePath: FilePath - private let logger: Logger - private let debugger: Debugger - private let instance: Instance - private let entrypointFunction: Function - private let functionsRLE: [(wasmAddress: Int, iSeqAddress: Int)] = [] + private let wasmBinary: ByteBuffer + private let module: Module + private let moduleFilePath: FilePath + private let logger: Logger + private let debugger: Debugger + private let instance: Instance + private let entrypointFunction: Function + private let functionsRLE: [(wasmAddress: Int, iSeqAddress: Int)] = [] - package init(logger: Logger, moduleFilePath: FilePath) async throws { - self.logger = logger + package init(logger: Logger, moduleFilePath: FilePath) async throws { + self.logger = logger - self.wasmBinary = try await FileSystem.shared.withFileHandle(forReadingAt: moduleFilePath) { - try await $0.readToEnd(maximumSizeAllowed: .unlimited) - } + self.wasmBinary = try await FileSystem.shared.withFileHandle(forReadingAt: moduleFilePath) { + try await $0.readToEnd(maximumSizeAllowed: .unlimited) + } - self.module = try parseWasm(bytes: .init(buffer: self.wasmBinary)) - self.moduleFilePath = moduleFilePath - let store = Store(engine: Engine()) - self.debugger = Debugger(store: store) + self.module = try parseWasm(bytes: .init(buffer: self.wasmBinary)) + self.moduleFilePath = moduleFilePath + let store = Store(engine: Engine()) + self.debugger = Debugger(store: store) - var imports = Imports() - let wasi = try WASIBridgeToHost() - wasi.link(to: &imports, store: store) - self.instance = try module.instantiate(store: store, imports: imports) + var imports = Imports() + let wasi = try WASIBridgeToHost() + wasi.link(to: &imports, store: store) + self.instance = try module.instantiate(store: store, imports: imports) - guard case .function(let entrypointFunction) = self.instance.exports["_start"] else { - throw Error.entrypointFunctionNotFound + guard case .function(let entrypointFunction) = self.instance.exports["_start"] else { + throw Error.entrypointFunctionNotFound + } + self.entrypointFunction = entrypointFunction } - self.entrypointFunction = entrypointFunction - } - package func handle(command: GDBHostCommand) throws -> GDBTargetResponse { - let responseKind: GDBTargetResponse.Kind - logger.trace("handling GDB host command", metadata: ["GDBHostCommand": .string(command.kind.rawValue)]) - - var isNoAckModeActivated = false - switch command.kind { - case .startNoAckMode: - isNoAckModeActivated = true - fallthrough - - case .isThreadSuffixSupported, .listThreadsInStopReply: - responseKind = .ok - - case .hostInfo: - responseKind = .keyValuePairs([ - "arch": "wasm32", - "ptrsize": "4", - "endian": "little", - "ostype": "wasip1", - "vendor": "WasmKit", - ]) - - case .supportedFeatures: - responseKind = .string("qXfer:libraries:read+;PacketSize=1000;") - - case .vContSupportedActions: - responseKind = .vContSupportedActions([.continue, .step]) - - case .isVAttachOrWaitSupported, .enableErrorStrings, .structuredDataPlugins, .readMemoryBinaryData: - responseKind = .empty - - case .processInfo: - responseKind = .keyValuePairs([ - "pid": "1", - "parent-pid": "1", - "arch": "wasm32", - "endian": "little", - "ptrsize": "4", - ]) - - case .currentThreadID: - responseKind = .string("QC1") - - case .firstThreadInfo: - responseKind = .string("m1") - - case .subsequentThreadInfo: - responseKind = .string("l") - - case .targetStatus: - responseKind = .keyValuePairs([ - "T05thread": "1", - "reason": "trace", - ]) - - case .registerInfo: - if command.arguments == "0" { + package func handle(command: GDBHostCommand) throws -> GDBTargetResponse { + let responseKind: GDBTargetResponse.Kind + logger.trace("handling GDB host command", metadata: ["GDBHostCommand": .string(command.kind.rawValue)]) + + var isNoAckModeActivated = false + switch command.kind { + case .startNoAckMode: + isNoAckModeActivated = true + fallthrough + + case .isThreadSuffixSupported, .listThreadsInStopReply: + responseKind = .ok + + case .hostInfo: responseKind = .keyValuePairs([ - "name": "pc", - "bitsize": "64", - "offset": "0", - "encoding": "uint", - "format": "hex", - "set": "General Purpose Registers", - "gcc": "16", - "dwarf": "16", - "generic": "pc", + "arch": "wasm32", + "ptrsize": "4", + "endian": "little", + "ostype": "wasip1", + "vendor": "WasmKit", ]) - } else { - responseKind = .string("E45") - } - case .transfer: - if command.arguments.starts(with: "libraries:read:") { - responseKind = .string( - """ - l -
- - """) - } else { - throw Error.unknownTransferArguments - } + case .supportedFeatures: + responseKind = .string("qXfer:libraries:read+;PacketSize=1000;") - case .readMemory: - let argumentsArray = command.arguments.split(separator: ",") - guard - argumentsArray.count == 2, - let address = UInt64(hexEncoded: argumentsArray[0]), - var length = Int(hexEncoded: argumentsArray[1]) - else { throw Error.unknownReadMemoryArguments } + case .vContSupportedActions: + responseKind = .vContSupportedActions([.continue, .step]) - let binaryOffset = Int(address - 0x4000000000000000) + case .isVAttachOrWaitSupported, .enableErrorStrings, .structuredDataPlugins, .readMemoryBinaryData: + responseKind = .empty - if binaryOffset + length > wasmBinary.readableBytes { - length = wasmBinary.readableBytes - binaryOffset - } + case .processInfo: + responseKind = .keyValuePairs([ + "pid": "1", + "parent-pid": "1", + "arch": "wasm32", + "endian": "little", + "ptrsize": "4", + ]) - responseKind = .hexEncodedBinary(wasmBinary.readableBytesView[binaryOffset..<(binaryOffset + length)]) + case .currentThreadID: + responseKind = .string("QC1") - case .wasmCallStack: - print(self.debugger.captureBacktrace()) - responseKind = .empty + case .firstThreadInfo: + responseKind = .string("m1") - case .generalRegisters: - fatalError() - } + case .subsequentThreadInfo: + responseKind = .string("l") - logger.trace("handler produced a response", metadata: ["GDBTargetResponse": .string("\(responseKind)")]) + case .targetStatus: + responseKind = .keyValuePairs([ + "T05thread": "1", + "reason": "trace", + ]) - return .init(kind: responseKind, isNoAckModeActivated: isNoAckModeActivated) - } + case .registerInfo: + if command.arguments == "0" { + responseKind = .keyValuePairs([ + "name": "pc", + "bitsize": "64", + "offset": "0", + "encoding": "uint", + "format": "hex", + "set": "General Purpose Registers", + "gcc": "16", + "dwarf": "16", + "generic": "pc", + ]) + } else { + responseKind = .string("E45") + } + + case .transfer: + if command.arguments.starts(with: "libraries:read:") { + responseKind = .string( + """ + l +
+ + """) + } else { + throw Error.unknownTransferArguments + } + + case .readMemory: + let argumentsArray = command.arguments.split(separator: ",") + guard + argumentsArray.count == 2, + let address = UInt64(hexEncoded: argumentsArray[0]), + var length = Int(hexEncoded: argumentsArray[1]) + else { throw Error.unknownReadMemoryArguments } + + let binaryOffset = Int(address - 0x4000_0000_0000_0000) + + if binaryOffset + length > wasmBinary.readableBytes { + length = wasmBinary.readableBytes - binaryOffset + } + + responseKind = .hexEncodedBinary(wasmBinary.readableBytesView[binaryOffset..<(binaryOffset + length)]) + + case .wasmCallStack: + print(self.debugger.captureBacktrace()) + responseKind = .empty + + case .generalRegisters: + fatalError() + } + + logger.trace("handler produced a response", metadata: ["GDBTargetResponse": .string("\(responseKind)")]) -} + return .init(kind: responseKind, isNoAckModeActivated: isNoAckModeActivated) + } + + } #endif diff --git a/Sources/wasmkit-gdb-tool/Entrypoint.swift b/Sources/wasmkit-gdb-tool/Entrypoint.swift index 107e3525..ffe793ec 100644 --- a/Sources/wasmkit-gdb-tool/Entrypoint.swift +++ b/Sources/wasmkit-gdb-tool/Entrypoint.swift @@ -40,71 +40,71 @@ struct Entrypoint: AsyncParsableCommand { var wasmModulePath: FilePath func run() async throws { -#if WasmDebuggingSupport - let logger = { - var result = Logger(label: "org.swiftwasm.WasmKit") - result.logLevel = self.logLevel - return result - }() + #if WasmDebuggingSupport + let logger = { + var result = Logger(label: "org.swiftwasm.WasmKit") + result.logLevel = self.logLevel + return result + }() - try await MultiThreadedEventLoopGroup.withEventLoopGroup(numberOfThreads: System.coreCount) { group in - let bootstrap = ServerBootstrap(group: group) - // Specify backlog and enable SO_REUSEADDR for the server itself - .serverChannelOption(.backlog, value: 256) - .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) + try await MultiThreadedEventLoopGroup.withEventLoopGroup(numberOfThreads: System.coreCount) { group in + let bootstrap = ServerBootstrap(group: group) + // Specify backlog and enable SO_REUSEADDR for the server itself + .serverChannelOption(.backlog, value: 256) + .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) - // Set the handlers that are applied to the accepted child `Channel`s. - .childChannelInitializer { channel in - // Ensure we don't read faster then we can write by adding the BackPressureHandler into the pipeline. - channel.eventLoop.makeCompletedFuture { - try channel.pipeline.syncOperations.addHandler(BackPressureHandler()) - // make sure to instantiate your `ChannelHandlers` inside of - // the closure as it will be invoked once per connection. - try channel.pipeline.syncOperations.addHandlers([ - ByteToMessageHandler(GDBHostCommandDecoder(logger: logger)), - MessageToByteHandler(GDBTargetResponseEncoder()), - ]) + // Set the handlers that are applied to the accepted child `Channel`s. + .childChannelInitializer { channel in + // Ensure we don't read faster then we can write by adding the BackPressureHandler into the pipeline. + channel.eventLoop.makeCompletedFuture { + try channel.pipeline.syncOperations.addHandler(BackPressureHandler()) + // make sure to instantiate your `ChannelHandlers` inside of + // the closure as it will be invoked once per connection. + try channel.pipeline.syncOperations.addHandlers([ + ByteToMessageHandler(GDBHostCommandDecoder(logger: logger)), + MessageToByteHandler(GDBTargetResponseEncoder()), + ]) + } } - } - // Enable SO_REUSEADDR for the accepted Channels - .childChannelOption(.socketOption(.so_reuseaddr), value: 1) - .childChannelOption(.maxMessagesPerRead, value: 16) - .childChannelOption(.recvAllocator, value: AdaptiveRecvByteBufferAllocator()) + // Enable SO_REUSEADDR for the accepted Channels + .childChannelOption(.socketOption(.so_reuseaddr), value: 1) + .childChannelOption(.maxMessagesPerRead, value: 16) + .childChannelOption(.recvAllocator, value: AdaptiveRecvByteBufferAllocator()) - let serverChannel = try await bootstrap.bind(host: "127.0.0.1", port: port) { childChannel in - childChannel.eventLoop.makeCompletedFuture { - try NIOAsyncChannel, GDBTargetResponse>( - wrappingChannelSynchronously: childChannel - ) + let serverChannel = try await bootstrap.bind(host: "127.0.0.1", port: port) { childChannel in + childChannel.eventLoop.makeCompletedFuture { + try NIOAsyncChannel, GDBTargetResponse>( + wrappingChannelSynchronously: childChannel + ) + } } - } - /* the server will now be accepting connections */ - logger.info("listening on port \(port)") + /* the server will now be accepting connections */ + logger.info("listening on port \(port)") - let debugger = try await WasmKitGDBHandler(logger: logger, moduleFilePath: self.wasmModulePath) + let debugger = try await WasmKitGDBHandler(logger: logger, moduleFilePath: self.wasmModulePath) - try await withThrowingDiscardingTaskGroup { group in - try await serverChannel.executeThenClose { serverChannelInbound in - for try await connectionChannel in serverChannelInbound { - group.addTask { - do { - try await connectionChannel.executeThenClose { connectionChannelInbound, connectionChannelOutbound in - for try await inboundData in connectionChannelInbound { - // Let's echo back all inbound data - try await connectionChannelOutbound.write(debugger.handle(command: inboundData.payload)) + try await withThrowingDiscardingTaskGroup { group in + try await serverChannel.executeThenClose { serverChannelInbound in + for try await connectionChannel in serverChannelInbound { + group.addTask { + do { + try await connectionChannel.executeThenClose { connectionChannelInbound, connectionChannelOutbound in + for try await inboundData in connectionChannelInbound { + // Let's echo back all inbound data + try await connectionChannelOutbound.write(debugger.handle(command: inboundData.payload)) + } } + } catch { + logger.error("Error in GDB remote protocol connection channel", metadata: ["error": "\(error)"]) } - } catch { - logger.error("Error in GDB remote protocol connection channel", metadata: ["error": "\(error)"]) } } } } } - } -#else - fatalError("Build WasmKit with `WasmDebuggingSupport` trait") -#endif + #else + fatalError("Build WasmKit with `WasmDebuggingSupport` trait") + #endif } } From ee3c7bfe75ef1ad184eef3cc3b055f426ee05e27 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 17 Oct 2025 16:35:14 +0100 Subject: [PATCH 043/107] Revert "Pass `-package-name` in CMake to enable use of `package`" This reverts commit e56e093ae6302c4aa11b291d52f601b373d2366e. --- cmake/modules/AddSwiftHostLibrary.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/modules/AddSwiftHostLibrary.cmake b/cmake/modules/AddSwiftHostLibrary.cmake index e7bf9949..26b72478 100644 --- a/cmake/modules/AddSwiftHostLibrary.cmake +++ b/cmake/modules/AddSwiftHostLibrary.cmake @@ -91,7 +91,7 @@ function(add_wasmkit_library name) endif() target_compile_options(${name} PRIVATE - $<$:-color-diagnostics;-package-name;WasmKit> + $<$:-color-diagnostics> ) if(LLVM_USE_LINKER) From a6d07498f401395e10208b2a1135380f26c89e68 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 17 Oct 2025 16:42:07 +0100 Subject: [PATCH 044/107] Disable traits in pre-6.1, guard use of `package` on trait --- .github/workflows/main.yml | 5 ++--- Sources/WasmKit/Execution/Execution.swift | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 93fba759..851ac57e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,7 +29,7 @@ jobs: wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" - test-args: "--sanitize address" + test-args: "--sanitize address --traits WasmDebuggingSupport" # Swift 6.2 - os: macos-15 xcode: Xcode_26.0 @@ -37,7 +37,7 @@ jobs: wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" - test-args: "--sanitize address" + test-args: "--sanitize address --traits WasmDebuggingSupport" runs-on: ${{ matrix.os }} name: "build-macos (${{ matrix.xcode }})" @@ -102,7 +102,6 @@ jobs: wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" - test-args: "--traits WasmDebuggingSupport" - swift: "swift:6.1-noble" development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz" wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index f6a3ef34..d40a888c 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -14,10 +14,12 @@ struct Execution: ~Copyable { /// - Note: If the trap is set, it must be released manually. private var trap: (error: UnsafeRawPointer, sp: Sp)? = nil +#if WasmDebuggingSupport package init(store: StoreRef, stackEnd: UnsafeMutablePointer) { self.store = store self.stackEnd = stackEnd } +#endif /// Executes the given closure with a new execution state associated with /// the given ``Store`` instance. From 4909d3b3930c2554bd39e5b388926363c1e25815 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 20 Oct 2025 12:10:57 +0100 Subject: [PATCH 045/107] Remove use of `package` from CMake-built code --- Sources/WasmKit/Execution/Debugger.swift | 10 ++++++++-- Sources/WasmKit/Execution/Errors.swift | 6 +++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index dc27283e..7b93c0c7 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -16,8 +16,14 @@ } - package func captureBacktrace() -> Backtrace { - return Execution.captureBacktrace(sp: self.valueStack, store: self.store) + /// Array of addresses in the Wasm binary of executed instructions on the call stack. + package var currentCallStack: [UInt64] { + return Execution.captureBacktrace(sp: self.valueStack, store: self.store).symbols.map { + switch $0.debuggingAddress { + case .iseq: fatalError() + case .wasm(let pc): return pc + } + } } deinit { diff --git a/Sources/WasmKit/Execution/Errors.swift b/Sources/WasmKit/Execution/Errors.swift index 1a5e1463..3a35bc5f 100644 --- a/Sources/WasmKit/Execution/Errors.swift +++ b/Sources/WasmKit/Execution/Errors.swift @@ -3,9 +3,9 @@ import WasmTypes import struct WasmParser.Import /// The backtrace of the trap. -package struct Backtrace: CustomStringConvertible, Sendable { +struct Backtrace: CustomStringConvertible, Sendable { /// A symbol in the backtrace. - package struct Symbol { + struct Symbol { /// The name of the symbol. let name: String? let debuggingAddress: DebuggingAddress @@ -31,7 +31,7 @@ package struct Backtrace: CustomStringConvertible, Sendable { let symbols: [Symbol] /// Textual description of the backtrace. - package var description: String { + var description: String { print("backtrace contains \(symbols.count) symbols") return symbols.enumerated().map { (index, symbol) in let name = symbol.name ?? "unknown" From cce210578e473650e04e9e1c31a9e10027742007 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 20 Oct 2025 12:16:53 +0100 Subject: [PATCH 046/107] Fix non-CMake build breakage --- Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index c0c5ec59..b321a050 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -161,7 +161,7 @@ responseKind = .hexEncodedBinary(wasmBinary.readableBytesView[binaryOffset..<(binaryOffset + length)]) case .wasmCallStack: - print(self.debugger.captureBacktrace()) + print(self.debugger.currentCallStack) responseKind = .empty case .generalRegisters: From 811589f42075beda8a4cc226387874d0e7a291c0 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 20 Oct 2025 12:18:59 +0100 Subject: [PATCH 047/107] Fix formatting --- Sources/WasmKit/Execution/Debugger.swift | 4 ++-- Sources/WasmKit/Execution/Execution.swift | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 7b93c0c7..643e8bf5 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -20,8 +20,8 @@ package var currentCallStack: [UInt64] { return Execution.captureBacktrace(sp: self.valueStack, store: self.store).symbols.map { switch $0.debuggingAddress { - case .iseq: fatalError() - case .wasm(let pc): return pc + case .iseq: fatalError() + case .wasm(let pc): return pc } } } diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index d40a888c..4f695efa 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -14,12 +14,12 @@ struct Execution: ~Copyable { /// - Note: If the trap is set, it must be released manually. private var trap: (error: UnsafeRawPointer, sp: Sp)? = nil -#if WasmDebuggingSupport - package init(store: StoreRef, stackEnd: UnsafeMutablePointer) { - self.store = store - self.stackEnd = stackEnd - } -#endif + #if WasmDebuggingSupport + package init(store: StoreRef, stackEnd: UnsafeMutablePointer) { + self.store = store + self.stackEnd = stackEnd + } + #endif /// Executes the given closure with a new execution state associated with /// the given ``Store`` instance. From bc2230d29e3ab02a0a2471b4c6577be044810a1e Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 20 Oct 2025 18:35:22 +0100 Subject: [PATCH 048/107] Enable iseq -> wasm instruction mapping --- .sourcekit-lsp/config.json | 5 ++ Sources/WAT/Encoder.swift | 2 + Sources/WAT/Parser/WastParser.swift | 1 + Sources/WasmKit/Execution/Debugger.swift | 55 ++++++++++++++++--- Sources/WasmKit/Execution/Errors.swift | 22 +------- Sources/WasmKit/Execution/Execution.swift | 7 ++- Sources/WasmKit/Execution/Instances.swift | 2 +- Sources/WasmKit/Translator.swift | 46 ++++++++++++++-- .../WasmKitGDBHandler/WasmKitGDBHandler.swift | 15 +---- .../WasmParser/BinaryInstructionDecoder.swift | 5 ++ Sources/WasmParser/InstructionVisitor.swift | 3 + Sources/WasmParser/WasmParser.swift | 2 + Sources/WasmParser/WasmTypes.swift | 4 ++ Utilities/Sources/WasmGen.swift | 7 ++- 14 files changed, 129 insertions(+), 47 deletions(-) create mode 100644 .sourcekit-lsp/config.json diff --git a/.sourcekit-lsp/config.json b/.sourcekit-lsp/config.json new file mode 100644 index 00000000..f6740959 --- /dev/null +++ b/.sourcekit-lsp/config.json @@ -0,0 +1,5 @@ +{ + "swiftPM": { + "traits": ["WasmDebuggingSupport"] + } +} diff --git a/Sources/WAT/Encoder.swift b/Sources/WAT/Encoder.swift index 52b90740..baa28a3f 100644 --- a/Sources/WAT/Encoder.swift +++ b/Sources/WAT/Encoder.swift @@ -158,6 +158,7 @@ extension TableType: WasmEncodable { struct ElementExprCollector: AnyInstructionVisitor { typealias Output = Void + var binaryOffset: Int = 0 var isAllRefFunc: Bool = true var instructions: [Instruction] = [] @@ -443,6 +444,7 @@ extension WatParser.DataSegmentDecl { } struct ExpressionEncoder: BinaryInstructionEncoder { + var binaryOffset: Int = 0 var encoder = Encoder() var hasDataSegmentInstruction: Bool = false diff --git a/Sources/WAT/Parser/WastParser.swift b/Sources/WAT/Parser/WastParser.swift index cfc69fb1..70258283 100644 --- a/Sources/WAT/Parser/WastParser.swift +++ b/Sources/WAT/Parser/WastParser.swift @@ -54,6 +54,7 @@ struct WastParser { } struct ConstExpressionCollector: WastConstInstructionVisitor { + var binaryOffset: Int = 0 let addValue: (Value) -> Void mutating func visitI32Const(value: Int32) throws { addValue(.i32(UInt32(bitPattern: value))) } diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 643e8bf5..5caef354 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -1,28 +1,69 @@ #if WasmDebuggingSupport package struct Debugger: ~Copyable { + enum Error: Swift.Error { + case entrypointFunctionNotFound + } + let valueStack: Sp let execution: Execution let store: Store - package init(store: Store) { + private let module: Module + private let instance: Instance + + /// Addresses of each function in the code section of ``module`` + let functionAddresses: [Int] + let entrypointFunction: Function + + package init(module: Module, store: Store, imports: Imports) throws(Error) { let limit = store.engine.configuration.stackSize / MemoryLayout.stride + let instance = try module.instantiate(store: store, imports: imports, isDebuggable: true) + + guard case .function(let entrypointFunction) = instance.exports["_start"] else { + throw Error.entrypointFunctionNotFound + } + + self.instance = instance + self.module = module + self.functionAddresses = module.functions.map { $0.code.originalAddress } + self.entrypointFunction = entrypointFunction self.valueStack = UnsafeMutablePointer.allocate(capacity: limit) self.store = store self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit)) } - package func toggleBreakpoint() { + package func stopAtEntrypoint() throws(Error) { + try self.toggleBreakpoint(address: self.originalAddress(function: entrypointFunction)) + } + + package func originalAddress(function: Function) throws(Error) -> Int { + precondition(function.handle.isWasm) + + switch function.handle.wasm.code { + case .debuggable(let wasm, _): + return wasm.originalAddress + case .uncompiled: + try function.handle.wasm.ensureCompiled(store: StoreRef(self.store)) + return try self.originalAddress(function: function) + case .compiled: + print(function.handle.wasm.code) + fatalError() + } + } + package mutating func toggleBreakpoint(address: Int) throws(Error) { + print("attempt to toggle a breakpoint at \(address)") } /// Array of addresses in the Wasm binary of executed instructions on the call stack. - package var currentCallStack: [UInt64] { + package var currentCallStack: [Int] { + guard let instance = self.valueStack.currentInstance else { return [] } + let isDebuggable = instance.isDebuggable + print("isDebuggable is \(isDebuggable)") + return Execution.captureBacktrace(sp: self.valueStack, store: self.store).symbols.map { - switch $0.debuggingAddress { - case .iseq: fatalError() - case .wasm(let pc): return pc - } + instance.iSeqToWasmMapping[$0.address]! } } diff --git a/Sources/WasmKit/Execution/Errors.swift b/Sources/WasmKit/Execution/Errors.swift index 3a35bc5f..5d714d02 100644 --- a/Sources/WasmKit/Execution/Errors.swift +++ b/Sources/WasmKit/Execution/Errors.swift @@ -5,26 +5,10 @@ import struct WasmParser.Import /// The backtrace of the trap. struct Backtrace: CustomStringConvertible, Sendable { /// A symbol in the backtrace. - struct Symbol { + struct Symbol: @unchecked Sendable { /// The name of the symbol. let name: String? - let debuggingAddress: DebuggingAddress - - /// Address of the symbol for debugging purposes. - enum DebuggingAddress: CustomStringConvertible, - // `Pc` is a pointer, which is not `Sendable` by default. - @unchecked Sendable - { - case iseq(Pc) - case wasm(UInt64) - - var description: String { - switch self { - case .iseq(let pc): "iseq(\(Int(bitPattern: pc)))" - case .wasm(let wasmAddress): "wasm(\(wasmAddress))" - } - } - } + let address: Pc } /// The symbols in the backtrace. @@ -35,7 +19,7 @@ struct Backtrace: CustomStringConvertible, Sendable { print("backtrace contains \(symbols.count) symbols") return symbols.enumerated().map { (index, symbol) in let name = symbol.name ?? "unknown" - return " \(index): (\(symbol.debuggingAddress)) \(name)" + return " \(index): (\(symbol.address)) \(name)" }.joined(separator: "\n") } } diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index 4f695efa..ab29e86c 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -69,13 +69,14 @@ struct Execution: ~Copyable { static func captureBacktrace(sp: Sp, store: Store) -> Backtrace { var frames = FrameIterator(sp: sp) var symbols: [Backtrace.Symbol] = [] + while let frame = frames.next() { guard let function = frame.function else { - symbols.append(.init(name: nil, debuggingAddress: .iseq(frame.pc))) + symbols.append(.init(name: nil, address: frame.pc)) continue } let symbolName = store.nameRegistry.symbolicate(.wasm(function)) - symbols.append(.init(name: symbolName, debuggingAddress: .iseq(frame.pc))) + symbols.append(.init(name: symbolName, address: frame.pc)) } return Backtrace(symbols: symbols) } @@ -251,7 +252,7 @@ extension Sp { nonmutating set { self[-1] = UInt64(UInt(bitPattern: newValue)) } } - fileprivate var currentInstance: InternalInstance? { + var currentInstance: InternalInstance? { currentFunction?.instance } } diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index 89ae3435..64fc71d9 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -84,7 +84,7 @@ struct InstanceEntity /* : ~Copyable */ { var features: WasmFeatureSet var dataCount: UInt32? var isDebuggable: Bool - var iSeqToWasmMapping: [Pc: UInt64] + var iSeqToWasmMapping: [Pc: Int] static var empty: InstanceEntity { InstanceEntity( diff --git a/Sources/WasmKit/Translator.swift b/Sources/WasmKit/Translator.swift index 1f07a7c9..60d44282 100644 --- a/Sources/WasmKit/Translator.swift +++ b/Sources/WasmKit/Translator.swift @@ -811,7 +811,7 @@ struct InstructionTranslator: InstructionVisitor { let allocator: ISeqAllocator let funcTypeInterner: Interner - let module: InternalInstance + var module: InternalInstance private var iseqBuilder: ISeqBuilder var controlStack: ControlStack var valueStack: ValueStack @@ -825,6 +825,14 @@ struct InstructionTranslator: InstructionVisitor { var constantSlots: ConstSlots let validator: InstructionValidator + // Wasm debugging support. + + /// Current offset to an instruction in the original Wasm binary processed by this translator. + var binaryOffset: Int = 0 + + /// Mapping from `self.iseqBuilder.instructions` to Wasm instructions + var iSeqToWasmMapping = [Int: Int]() + init( allocator: ISeqAllocator, engineConfiguration: EngineConfiguration, @@ -874,12 +882,14 @@ struct InstructionTranslator: InstructionVisitor { } private mutating func emit(_ instruction: Instruction, resultRelink: ISeqBuilder.ResultRelink? = nil) { + self.updateInstructionMapping() iseqBuilder.emit(instruction, resultRelink: resultRelink) } @discardableResult private mutating func emitCopyStack(from source: VReg, to dest: VReg) -> Bool { guard source != dest else { return false } + self.updateInstructionMapping() emit(.copyStack(Instruction.CopyStackOperand(source: LVReg(source), dest: LVReg(dest)))) return true } @@ -1065,6 +1075,7 @@ struct InstructionTranslator: InstructionVisitor { emit(.onExit(functionIndex)) } try visitReturnLike() + self.updateInstructionMapping() iseqBuilder.emit(._return) } private mutating func markUnreachable() throws { @@ -1084,9 +1095,15 @@ struct InstructionTranslator: InstructionVisitor { let instructions = iseqBuilder.finalize() // TODO: Figure out a way to avoid the copy here while keeping the execution performance. let buffer = allocator.allocateInstructions(capacity: instructions.count) - for (idx, instruction) in instructions.enumerated() { - buffer[idx] = instruction + let initializedElementsIndex = buffer.initialize(fromContentsOf: instructions) + assert(initializedElementsIndex == instructions.endIndex) + + for (iseq, wasm) in self.iSeqToWasmMapping { + self.module.withValue { + $0.iSeqToWasmMapping[iseq + buffer.baseAddress.unsafelyUnwrapped] = wasm + } } + let constants = allocator.allocateConstants(self.constantSlots.values) return InstructionSequence( instructions: buffer, @@ -1095,6 +1112,15 @@ struct InstructionTranslator: InstructionVisitor { ) } + private mutating func updateInstructionMapping() { + // This is a hot path, so best to exclude the code altogether if the trait isn't enabled. +#if WasmDebuggingSupport + guard self.module.isDebuggable else { return } + + self.iSeqToWasmMapping[self.iseqBuilder.insertingPC.offsetFromHead] = self.binaryOffset +#endif + } + // MARK: Main entry point /// Translate a Wasm expression into a sequence of instructions. @@ -1122,7 +1148,9 @@ struct InstructionTranslator: InstructionVisitor { emit(.unreachable) try markUnreachable() } - mutating func visitNop() -> Output { emit(.nop) } + mutating func visitNop() -> Output { + emit(.nop) + } mutating func visitBlock(blockType: WasmParser.BlockType) throws -> Output { let blockType = try module.resolveBlockType(blockType) @@ -1169,6 +1197,7 @@ struct InstructionTranslator: InstructionVisitor { ) ) guard let condition = condition else { return } + self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.brIfNot, endLabel) { iseqBuilder, selfPC, endPC in let targetPC: MetaProgramCounter if let elsePC = iseqBuilder.resolveLabel(elseLabel) { @@ -1189,6 +1218,8 @@ struct InstructionTranslator: InstructionVisitor { preserveOnStack(depth: valueStack.height - frame.stackHeight) try controlStack.resetReachability() iseqBuilder.resetLastEmission() + + self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.br, endLabel) { _, selfPC, endPC in let offset = endPC.offsetFromHead - selfPC.offsetFromHead return Int32(offset) @@ -1284,6 +1315,8 @@ struct InstructionTranslator: InstructionVisitor { currentFrame: try controlStack.currentFrame(), currentHeight: valueStack.height ) + + self.updateInstructionMapping() iseqBuilder.emitWithLabel(makeInstruction, frame.continuation) { _, selfPC, continuation in let relativeOffset = continuation.offsetFromHead - selfPC.offsetFromHead return make(Int32(relativeOffset), UInt32(copyCount), popCount) @@ -1317,6 +1350,7 @@ struct InstructionTranslator: InstructionVisitor { if frame.copyCount == 0 { guard let condition else { return } // Optimization where we don't need copying values when the branch taken + self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.brIf, frame.continuation) { _, selfPC, continuation in let relativeOffset = continuation.offsetFromHead - selfPC.offsetFromHead return Instruction.BrIfOperand( @@ -1349,11 +1383,13 @@ struct InstructionTranslator: InstructionVisitor { // [0x06] (local.get 1 reg:2) <----|---------+ // [0x07] ... <-------+ let onBranchNotTaken = iseqBuilder.allocLabel() + self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.brIfNot, onBranchNotTaken) { _, conditionCheckAt, continuation in let relativeOffset = continuation.offsetFromHead - conditionCheckAt.offsetFromHead return Instruction.BrIfOperand(condition: LVReg(condition), offset: Int32(relativeOffset)) } try copyOnBranch(targetFrame: frame) + self.updateInstructionMapping() try emitBranch(Instruction.br, relativeDepth: relativeDepth) { offset, copyCount, popCount in return offset } @@ -1380,6 +1416,7 @@ struct InstructionTranslator: InstructionVisitor { baseAddress: tableBuffer.baseAddress!, count: UInt16(tableBuffer.count), index: index ) + self.updateInstructionMapping() iseqBuilder.emit(.brTable(operand)) let brTableAt = iseqBuilder.insertingPC @@ -1429,6 +1466,7 @@ struct InstructionTranslator: InstructionVisitor { } let emittedCopy = try copyOnBranch(targetFrame: frame) if emittedCopy { + self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.br, frame.continuation) { _, brAt, continuation in let relativeOffset = continuation.offsetFromHead - brAt.offsetFromHead return Int32(relativeOffset) diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index b321a050..4a3a7df2 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -25,16 +25,12 @@ enum Error: Swift.Error { case unknownTransferArguments case unknownReadMemoryArguments - case entrypointFunctionNotFound } private let wasmBinary: ByteBuffer - private let module: Module private let moduleFilePath: FilePath private let logger: Logger private let debugger: Debugger - private let instance: Instance - private let entrypointFunction: Function private let functionsRLE: [(wasmAddress: Int, iSeqAddress: Int)] = [] package init(logger: Logger, moduleFilePath: FilePath) async throws { @@ -44,20 +40,15 @@ try await $0.readToEnd(maximumSizeAllowed: .unlimited) } - self.module = try parseWasm(bytes: .init(buffer: self.wasmBinary)) self.moduleFilePath = moduleFilePath - let store = Store(engine: Engine()) - self.debugger = Debugger(store: store) + let store = Store(engine: Engine()) var imports = Imports() let wasi = try WASIBridgeToHost() wasi.link(to: &imports, store: store) - self.instance = try module.instantiate(store: store, imports: imports) - guard case .function(let entrypointFunction) = self.instance.exports["_start"] else { - throw Error.entrypointFunctionNotFound - } - self.entrypointFunction = entrypointFunction + self.debugger = try Debugger(module: parseWasm(bytes: .init(buffer: self.wasmBinary)), store: store, imports: imports) + try self.debugger.stopAtEntrypoint() } package func handle(command: GDBHostCommand) throws -> GDBTargetResponse { diff --git a/Sources/WasmParser/BinaryInstructionDecoder.swift b/Sources/WasmParser/BinaryInstructionDecoder.swift index 5cc84908..4e19c93f 100644 --- a/Sources/WasmParser/BinaryInstructionDecoder.swift +++ b/Sources/WasmParser/BinaryInstructionDecoder.swift @@ -6,6 +6,9 @@ import WasmTypes @usableFromInline protocol BinaryInstructionDecoder { + /// Current offset in the decoded Wasm binary. + var offset: Int { get } + /// Claim the next byte to be decoded @inlinable func claimNextByte() throws -> UInt8 @@ -91,6 +94,8 @@ protocol BinaryInstructionDecoder { @inlinable func parseBinaryInstruction(visitor: inout some InstructionVisitor, decoder: inout some BinaryInstructionDecoder) throws -> Bool { + visitor.binaryOffset = decoder.offset + let opcode0 = try decoder.claimNextByte() switch opcode0 { case 0x00: diff --git a/Sources/WasmParser/InstructionVisitor.swift b/Sources/WasmParser/InstructionVisitor.swift index 2a6b0271..8c8facd3 100644 --- a/Sources/WasmParser/InstructionVisitor.swift +++ b/Sources/WasmParser/InstructionVisitor.swift @@ -310,6 +310,9 @@ extension AnyInstructionVisitor { /// The visitor pattern is used while parsing WebAssembly expressions to allow for easy extensibility. /// See the expression parsing method ``Code/parseExpression(visitor:)`` public protocol InstructionVisitor { + /// Current offset in the original Wasm binary. + var binaryOffset: Int { get set } + /// Visiting `unreachable` instruction. mutating func visitUnreachable() throws /// Visiting `nop` instruction. diff --git a/Sources/WasmParser/WasmParser.swift b/Sources/WasmParser/WasmParser.swift index 0bca6049..0b7c7199 100644 --- a/Sources/WasmParser/WasmParser.swift +++ b/Sources/WasmParser/WasmParser.swift @@ -757,6 +757,8 @@ extension Parser: BinaryInstructionDecoder { @usableFromInline struct InstructionFactory: AnyInstructionVisitor { + @usableFromInline var binaryOffset: Int = 0 + @usableFromInline var insts: [Instruction] = [] @inlinable init() {} diff --git a/Sources/WasmParser/WasmTypes.swift b/Sources/WasmParser/WasmTypes.swift index df95b6c2..5eb8c3c6 100644 --- a/Sources/WasmParser/WasmTypes.swift +++ b/Sources/WasmParser/WasmTypes.swift @@ -15,6 +15,10 @@ public struct Code { @usableFromInline internal let features: WasmFeatureSet +#if WasmDebuggingSupport + package var originalAddress: Int { self.offset } +#endif + @inlinable init(locals: [ValueType], expression: ArraySlice, offset: Int, features: WasmFeatureSet) { self.locals = locals diff --git a/Utilities/Sources/WasmGen.swift b/Utilities/Sources/WasmGen.swift index 541f409e..9ad32e9c 100644 --- a/Utilities/Sources/WasmGen.swift +++ b/Utilities/Sources/WasmGen.swift @@ -97,7 +97,8 @@ enum WasmGen { /// See the expression parsing method ``Code/parseExpression(visitor:)`` public protocol InstructionVisitor { /// Current offset in visitor's instruction stream. - var currentOffset: Int { get set } + var binaryOffset: Int { get set } + """ for instruction in instructions.categorized { @@ -533,6 +534,9 @@ enum WasmGen { @usableFromInline protocol BinaryInstructionDecoder { + /// Current offset in the decoded Wasm binary. + var offset: Int { get } + /// Claim the next byte to be decoded @inlinable func claimNextByte() throws -> UInt8 @@ -567,6 +571,7 @@ enum WasmGen { @inlinable func parseBinaryInstruction(visitor: inout some InstructionVisitor, decoder: inout some BinaryInstructionDecoder) throws -> Bool { + visitor.binaryOffset = decoder.offset """ func renderSwitchCase(_ root: Trie, depth: Int = 0) { From db1ec2decefd8c39ae8f5022738518c35cad96e3 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 20 Oct 2025 18:35:43 +0100 Subject: [PATCH 049/107] Fix formatting --- Sources/WasmKit/Execution/Debugger.swift | 16 ++++++++-------- Sources/WasmKit/Translator.swift | 8 ++++---- Sources/WasmParser/WasmTypes.swift | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 5caef354..3878b6f0 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -41,14 +41,14 @@ precondition(function.handle.isWasm) switch function.handle.wasm.code { - case .debuggable(let wasm, _): - return wasm.originalAddress - case .uncompiled: - try function.handle.wasm.ensureCompiled(store: StoreRef(self.store)) - return try self.originalAddress(function: function) - case .compiled: - print(function.handle.wasm.code) - fatalError() + case .debuggable(let wasm, _): + return wasm.originalAddress + case .uncompiled: + try function.handle.wasm.ensureCompiled(store: StoreRef(self.store)) + return try self.originalAddress(function: function) + case .compiled: + print(function.handle.wasm.code) + fatalError() } } diff --git a/Sources/WasmKit/Translator.swift b/Sources/WasmKit/Translator.swift index 60d44282..70c39219 100644 --- a/Sources/WasmKit/Translator.swift +++ b/Sources/WasmKit/Translator.swift @@ -1114,11 +1114,11 @@ struct InstructionTranslator: InstructionVisitor { private mutating func updateInstructionMapping() { // This is a hot path, so best to exclude the code altogether if the trait isn't enabled. -#if WasmDebuggingSupport - guard self.module.isDebuggable else { return } + #if WasmDebuggingSupport + guard self.module.isDebuggable else { return } - self.iSeqToWasmMapping[self.iseqBuilder.insertingPC.offsetFromHead] = self.binaryOffset -#endif + self.iSeqToWasmMapping[self.iseqBuilder.insertingPC.offsetFromHead] = self.binaryOffset + #endif } // MARK: Main entry point diff --git a/Sources/WasmParser/WasmTypes.swift b/Sources/WasmParser/WasmTypes.swift index 5eb8c3c6..153f8cb9 100644 --- a/Sources/WasmParser/WasmTypes.swift +++ b/Sources/WasmParser/WasmTypes.swift @@ -15,9 +15,9 @@ public struct Code { @usableFromInline internal let features: WasmFeatureSet -#if WasmDebuggingSupport - package var originalAddress: Int { self.offset } -#endif + #if WasmDebuggingSupport + package var originalAddress: Int { self.offset } + #endif @inlinable init(locals: [ValueType], expression: ArraySlice, offset: Int, features: WasmFeatureSet) { From 62592f1a7058e2be0cc1059b88aa33bb3793170c Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 20 Oct 2025 18:47:40 +0100 Subject: [PATCH 050/107] Add missing `mutating` to `Debugger.swift` --- Sources/WasmKit/Execution/Debugger.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 3878b6f0..48a60923 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -33,7 +33,7 @@ self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit)) } - package func stopAtEntrypoint() throws(Error) { + package mutating func stopAtEntrypoint() throws(Error) { try self.toggleBreakpoint(address: self.originalAddress(function: entrypointFunction)) } From bbfcb45de6df49e29891c27508b866fac94ce863 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 20 Oct 2025 20:31:56 +0100 Subject: [PATCH 051/107] Make `init` throw effect untyped in `Debugger.swift` --- Sources/WasmKit/Execution/Debugger.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 48a60923..f1a33696 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -16,7 +16,7 @@ let functionAddresses: [Int] let entrypointFunction: Function - package init(module: Module, store: Store, imports: Imports) throws(Error) { + package init(module: Module, store: Store, imports: Imports) throws { let limit = store.engine.configuration.stackSize / MemoryLayout.stride let instance = try module.instantiate(store: store, imports: imports, isDebuggable: true) From c6213eb21b4695f225f3f3bfe47713abb2a1b700 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 20 Oct 2025 20:35:26 +0100 Subject: [PATCH 052/107] Make throwing method effects untyped in `Debugger.swift` --- Sources/WasmKit/Execution/Debugger.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index f1a33696..b7a06d54 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -33,11 +33,11 @@ self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit)) } - package mutating func stopAtEntrypoint() throws(Error) { + package mutating func stopAtEntrypoint() throws { try self.toggleBreakpoint(address: self.originalAddress(function: entrypointFunction)) } - package func originalAddress(function: Function) throws(Error) -> Int { + package func originalAddress(function: Function) throws -> Int { precondition(function.handle.isWasm) switch function.handle.wasm.code { @@ -52,7 +52,7 @@ } } - package mutating func toggleBreakpoint(address: Int) throws(Error) { + package mutating func toggleBreakpoint(address: Int) throws { print("attempt to toggle a breakpoint at \(address)") } From a993b500e94a6bac922d1cfde318e60ba1af4ec0 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 20 Oct 2025 21:30:28 +0100 Subject: [PATCH 053/107] Make `debugger: Debugger` property mutable --- Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 4a3a7df2..fff3a9e5 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -30,7 +30,7 @@ private let wasmBinary: ByteBuffer private let moduleFilePath: FilePath private let logger: Logger - private let debugger: Debugger + private var debugger: Debugger private let functionsRLE: [(wasmAddress: Int, iSeqAddress: Int)] = [] package init(logger: Logger, moduleFilePath: FilePath) async throws { From 2d065e00ad0d7971e9c4721869648aa849c1d900 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 21 Oct 2025 09:22:24 +0100 Subject: [PATCH 054/107] Clean up `WasmGen` code --- Sources/WasmParser/BinaryInstructionDecoder.swift | 1 - Sources/WasmParser/InstructionVisitor.swift | 2 +- Utilities/Sources/WasmGen.swift | 3 --- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Sources/WasmParser/BinaryInstructionDecoder.swift b/Sources/WasmParser/BinaryInstructionDecoder.swift index 4e19c93f..a6a4ac6d 100644 --- a/Sources/WasmParser/BinaryInstructionDecoder.swift +++ b/Sources/WasmParser/BinaryInstructionDecoder.swift @@ -95,7 +95,6 @@ protocol BinaryInstructionDecoder { @inlinable func parseBinaryInstruction(visitor: inout some InstructionVisitor, decoder: inout some BinaryInstructionDecoder) throws -> Bool { visitor.binaryOffset = decoder.offset - let opcode0 = try decoder.claimNextByte() switch opcode0 { case 0x00: diff --git a/Sources/WasmParser/InstructionVisitor.swift b/Sources/WasmParser/InstructionVisitor.swift index 8c8facd3..b44d39a8 100644 --- a/Sources/WasmParser/InstructionVisitor.swift +++ b/Sources/WasmParser/InstructionVisitor.swift @@ -310,7 +310,7 @@ extension AnyInstructionVisitor { /// The visitor pattern is used while parsing WebAssembly expressions to allow for easy extensibility. /// See the expression parsing method ``Code/parseExpression(visitor:)`` public protocol InstructionVisitor { - /// Current offset in the original Wasm binary. + /// Current offset in visitor's instruction stream. var binaryOffset: Int { get set } /// Visiting `unreachable` instruction. diff --git a/Utilities/Sources/WasmGen.swift b/Utilities/Sources/WasmGen.swift index 9ad32e9c..1e217bda 100644 --- a/Utilities/Sources/WasmGen.swift +++ b/Utilities/Sources/WasmGen.swift @@ -540,9 +540,6 @@ enum WasmGen { /// Claim the next byte to be decoded @inlinable func claimNextByte() throws -> UInt8 - /// Current offset in decoder's instruction stream. - var currentOffset: Int { get } - /// Throw an error due to unknown opcode. func throwUnknown(_ opcode: [UInt8]) throws -> Never From fd0d1ee9a3db9b71e3bb1d52f8defcec810bba3d Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 21 Oct 2025 17:42:23 +0100 Subject: [PATCH 055/107] Add reverse wasm->iseq mapping, breakpoints toggling PoC --- Sources/WasmKit/Execution/Debugger.swift | 61 +++++++++++++++---- Sources/WasmKit/Execution/Instances.swift | 12 +++- .../WasmKit/Execution/StoreAllocator.swift | 3 +- Sources/WasmKit/Translator.swift | 19 ++++-- 4 files changed, 76 insertions(+), 19 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index b7a06d54..2a8efcc9 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -3,18 +3,29 @@ package struct Debugger: ~Copyable { enum Error: Swift.Error { case entrypointFunctionNotFound + case noInstructionMappingAvailable(Int) } - let valueStack: Sp - let execution: Execution - let store: Store + private let valueStack: Sp + private let execution: Execution + private let store: Store + /// Parsed in-memory representation of a Wasm module instantiated for debugging. private let module: Module + + /// Instance of parsed Wasm ``module``. private let instance: Instance /// Addresses of each function in the code section of ``module`` - let functionAddresses: [Int] - let entrypointFunction: Function + private let functionAddresses: [FunctionAddress] + + /// Reference to the entrypoint function of the currently debugged module, for use in ``stopAtEntrypoint``. + private let entrypointFunction: Function + + /// Threading model of the Wasm engine configuration cached for a potentially hot path. + private let threadingModel: EngineConfiguration.ThreadingModel + + private var breakpoints = [Int: CodeSlot]() package init(module: Module, store: Store, imports: Imports) throws { let limit = store.engine.configuration.stackSize / MemoryLayout.stride @@ -31,10 +42,11 @@ self.valueStack = UnsafeMutablePointer.allocate(capacity: limit) self.store = store self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit)) + self.threadingModel = store.engine.configuration.threadingModel } package mutating func stopAtEntrypoint() throws { - try self.toggleBreakpoint(address: self.originalAddress(function: entrypointFunction)) + try self.enableBreakpoint(address: self.originalAddress(function: entrypointFunction)) } package func originalAddress(function: Function) throws -> Int { @@ -47,23 +59,50 @@ try function.handle.wasm.ensureCompiled(store: StoreRef(self.store)) return try self.originalAddress(function: function) case .compiled: - print(function.handle.wasm.code) fatalError() } } - package mutating func toggleBreakpoint(address: Int) throws { + package mutating func enableBreakpoint(address: Int) throws { print("attempt to toggle a breakpoint at \(address)") + + guard self.breakpoints[address] == nil else { + print("breakpoint at \(address) already enabled") + return + } + + guard let iseq = self.instance.handle.wasmToIseqMapping[address] else { + throw Error.noInstructionMappingAvailable(address) + } + self.breakpoints[address] = iseq.pointee + iseq.pointee = Instruction.breakpoint.headSlot(threadingModel: self.threadingModel) + } + + package mutating func disableBreakpoint(address: Int) throws { + print("attempt to toggle a breakpoint at \(address)") + + guard let oldCodeSlot = self.breakpoints[address] else { + print("breakpoint at \(address) already disabled") + return + } + + guard let iseq = self.instance.handle.wasmToIseqMapping[address] else { + throw Error.noInstructionMappingAvailable(address) + } + iseq.pointee = oldCodeSlot + self.breakpoints[address] = nil + } + + mutating func enableBreakpoint(functionIndex: FunctionIndex, offset: Int) { } /// Array of addresses in the Wasm binary of executed instructions on the call stack. package var currentCallStack: [Int] { - guard let instance = self.valueStack.currentInstance else { return [] } - let isDebuggable = instance.isDebuggable + let isDebuggable = self.instance.handle.isDebuggable print("isDebuggable is \(isDebuggable)") return Execution.captureBacktrace(sp: self.valueStack, store: self.store).symbols.map { - instance.iSeqToWasmMapping[$0.address]! + self.instance.handle.iseqToWasmMapping[$0.address]! } } diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index 64fc71d9..6ecdc5f1 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -84,7 +84,14 @@ struct InstanceEntity /* : ~Copyable */ { var features: WasmFeatureSet var dataCount: UInt32? var isDebuggable: Bool - var iSeqToWasmMapping: [Pc: Int] + + /// Mapping from iSeq Pc to instruction addresses in the original binary. + /// Used for handling current call stack requests issued by a ``Debugger`` instance. + var iseqToWasmMapping: [Pc: Int] + + /// Mapping from Wasm instruction addresses in the original binary to iSeq instruction addresses. + /// Used for handling breakpoint requests issued by a ``Debugger`` instance. + var wasmToIseqMapping: [Int: Pc] static var empty: InstanceEntity { InstanceEntity( @@ -100,7 +107,8 @@ struct InstanceEntity /* : ~Copyable */ { features: [], dataCount: nil, isDebuggable: false, - iSeqToWasmMapping: [:] + iseqToWasmMapping: [:], + wasmToIseqMapping: [:] ) } diff --git a/Sources/WasmKit/Execution/StoreAllocator.swift b/Sources/WasmKit/Execution/StoreAllocator.swift index 73109c51..dff3da90 100644 --- a/Sources/WasmKit/Execution/StoreAllocator.swift +++ b/Sources/WasmKit/Execution/StoreAllocator.swift @@ -453,7 +453,8 @@ extension StoreAllocator { features: module.features, dataCount: module.dataCount, isDebuggable: isDebuggable, - iSeqToWasmMapping: [:] + iseqToWasmMapping: [:], + wasmToIseqMapping: [:] ) instancePointer.initialize(to: instanceEntity) instanceInitialized = true diff --git a/Sources/WasmKit/Translator.swift b/Sources/WasmKit/Translator.swift index 70c39219..1254df1a 100644 --- a/Sources/WasmKit/Translator.swift +++ b/Sources/WasmKit/Translator.swift @@ -830,8 +830,10 @@ struct InstructionTranslator: InstructionVisitor { /// Current offset to an instruction in the original Wasm binary processed by this translator. var binaryOffset: Int = 0 - /// Mapping from `self.iseqBuilder.instructions` to Wasm instructions - var iSeqToWasmMapping = [Int: Int]() + /// Mapping from `self.iseqBuilder.instructions` to Wasm instructions. + /// As mapping between iSeq to Wasm is many:many, but we only care about first mapping for overlapping address, + /// we need to iterate on it in the order the mappings were stored to ensure we don't overwrite the frist mapping. + var iseqToWasmMapping = [(iseq: Int, wasm: Int)]() init( allocator: ISeqAllocator, @@ -1098,9 +1100,16 @@ struct InstructionTranslator: InstructionVisitor { let initializedElementsIndex = buffer.initialize(fromContentsOf: instructions) assert(initializedElementsIndex == instructions.endIndex) - for (iseq, wasm) in self.iSeqToWasmMapping { + for (iseq, wasm) in self.iseqToWasmMapping { self.module.withValue { - $0.iSeqToWasmMapping[iseq + buffer.baseAddress.unsafelyUnwrapped] = wasm + let absoluteISeq = iseq + buffer.baseAddress.unsafelyUnwrapped + // Don't override the existing mapping, only store a new pair if there's no mapping for a given key. + if $0.iseqToWasmMapping[absoluteISeq] == nil { + $0.iseqToWasmMapping[absoluteISeq] = wasm + } + if $0.wasmToIseqMapping[wasm] == nil { + $0.wasmToIseqMapping[wasm] = absoluteISeq + } } } @@ -1117,7 +1126,7 @@ struct InstructionTranslator: InstructionVisitor { #if WasmDebuggingSupport guard self.module.isDebuggable else { return } - self.iSeqToWasmMapping[self.iseqBuilder.insertingPC.offsetFromHead] = self.binaryOffset + self.iseqToWasmMapping.append((self.iseqBuilder.insertingPC.offsetFromHead, self.binaryOffset)) #endif } From a35b73e875e6f401c219a31132d4edb70f2a0ced Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 21 Oct 2025 20:15:24 +0100 Subject: [PATCH 056/107] Remove unused `functionAddresses` property from `Debugger` --- Sources/WasmKit/Execution/Debugger.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 2a8efcc9..49e27ac3 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -16,9 +16,6 @@ /// Instance of parsed Wasm ``module``. private let instance: Instance - /// Addresses of each function in the code section of ``module`` - private let functionAddresses: [FunctionAddress] - /// Reference to the entrypoint function of the currently debugged module, for use in ``stopAtEntrypoint``. private let entrypointFunction: Function @@ -37,7 +34,6 @@ self.instance = instance self.module = module - self.functionAddresses = module.functions.map { $0.code.originalAddress } self.entrypointFunction = entrypointFunction self.valueStack = UnsafeMutablePointer.allocate(capacity: limit) self.store = store From 16ac0cf65a2fd046ecc6765c918f8c442435e594 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 09:45:02 +0100 Subject: [PATCH 057/107] Remove changes unrelated to the protocol --- Package@swift-6.1.swift | 13 --- .../GDBRemoteProtocol/GDBHostCommand.swift | 12 ++ .../GDBHostCommandDecoder.swift | 12 ++ Sources/GDBRemoteProtocol/GDBPacket.swift | 12 ++ .../GDBRemoteProtocol/GDBTargetResponse.swift | 12 ++ .../GDBTargetResponseEncoder.swift | 12 ++ Sources/WAT/Encoder.swift | 2 - Sources/WAT/Parser/WastParser.swift | 1 - Sources/WasmKit/Execution/Debugger.swift | 110 ------------------ Sources/WasmKit/Execution/Errors.swift | 12 +- Sources/WasmKit/Execution/Execution.swift | 22 ++-- Sources/WasmKit/Execution/Function.swift | 12 +- Sources/WasmKit/Execution/Instances.swift | 14 +-- .../WasmKit/Execution/StoreAllocator.swift | 8 +- Sources/WasmKit/Module.swift | 19 +-- Sources/WasmKit/Translator.swift | 63 ++-------- .../WasmKitGDBHandler/WasmKitGDBHandler.swift | 29 +++-- .../WasmParser/BinaryInstructionDecoder.swift | 4 - Sources/WasmParser/InstructionVisitor.swift | 3 - Sources/WasmParser/WasmParser.swift | 2 - Sources/WasmParser/WasmTypes.swift | 4 - Sources/wasmkit-gdb-tool/Entrypoint.swift | 110 ------------------ ...sts.swift => GDBRemoteProtocolTests.swift} | 14 ++- Tests/WasmKitTests/ExecutionTests.swift | 4 +- Utilities/Sources/WasmGen.swift | 7 -- 25 files changed, 121 insertions(+), 392 deletions(-) delete mode 100644 Sources/WasmKit/Execution/Debugger.swift delete mode 100644 Sources/wasmkit-gdb-tool/Entrypoint.swift rename Tests/GDBRemoteProtocolTests/{RemoteProtocolTests.swift => GDBRemoteProtocolTests.swift} (68%) diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index edafee37..d5b269ef 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -178,19 +178,6 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { exclude: ["Fixtures"] ), - .executableTarget( - name: "wasmkit-gdb-tool", - dependencies: [ - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "Logging", package: "swift-log"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), - .product(name: "SystemPackage", package: "swift-system"), - "GDBRemoteProtocol", - "WasmKitGDBHandler", - ] - ), - .target( name: "WasmKitGDBHandler", dependencies: [ diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index 278d07e5..d4f5023e 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + /// See GDB and LLDB remote protocol documentation for more details: /// * https://sourceware.org/gdb/current/onlinedocs/gdb.html/General-Query-Packets.html /// * https://lldb.llvm.org/resources/lldbgdbremote.html diff --git a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift index bd5650e8..77b78d54 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + import Logging import NIOCore diff --git a/Sources/GDBRemoteProtocol/GDBPacket.swift b/Sources/GDBRemoteProtocol/GDBPacket.swift index 483e9cb1..0cc5accb 100644 --- a/Sources/GDBRemoteProtocol/GDBPacket.swift +++ b/Sources/GDBRemoteProtocol/GDBPacket.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + package struct GDBPacket: Sendable { package let payload: Payload package let checksum: UInt8 diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift index d367cc29..002b632a 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + import NIOCore /// Actions supported in the `vCont` host command. diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index 7a889334..6a1f240b 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + import Foundation import NIOCore diff --git a/Sources/WAT/Encoder.swift b/Sources/WAT/Encoder.swift index baa28a3f..52b90740 100644 --- a/Sources/WAT/Encoder.swift +++ b/Sources/WAT/Encoder.swift @@ -158,7 +158,6 @@ extension TableType: WasmEncodable { struct ElementExprCollector: AnyInstructionVisitor { typealias Output = Void - var binaryOffset: Int = 0 var isAllRefFunc: Bool = true var instructions: [Instruction] = [] @@ -444,7 +443,6 @@ extension WatParser.DataSegmentDecl { } struct ExpressionEncoder: BinaryInstructionEncoder { - var binaryOffset: Int = 0 var encoder = Encoder() var hasDataSegmentInstruction: Bool = false diff --git a/Sources/WAT/Parser/WastParser.swift b/Sources/WAT/Parser/WastParser.swift index 70258283..cfc69fb1 100644 --- a/Sources/WAT/Parser/WastParser.swift +++ b/Sources/WAT/Parser/WastParser.swift @@ -54,7 +54,6 @@ struct WastParser { } struct ConstExpressionCollector: WastConstInstructionVisitor { - var binaryOffset: Int = 0 let addValue: (Value) -> Void mutating func visitI32Const(value: Int32) throws { addValue(.i32(UInt32(bitPattern: value))) } diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift deleted file mode 100644 index 49e27ac3..00000000 --- a/Sources/WasmKit/Execution/Debugger.swift +++ /dev/null @@ -1,110 +0,0 @@ -#if WasmDebuggingSupport - - package struct Debugger: ~Copyable { - enum Error: Swift.Error { - case entrypointFunctionNotFound - case noInstructionMappingAvailable(Int) - } - - private let valueStack: Sp - private let execution: Execution - private let store: Store - - /// Parsed in-memory representation of a Wasm module instantiated for debugging. - private let module: Module - - /// Instance of parsed Wasm ``module``. - private let instance: Instance - - /// Reference to the entrypoint function of the currently debugged module, for use in ``stopAtEntrypoint``. - private let entrypointFunction: Function - - /// Threading model of the Wasm engine configuration cached for a potentially hot path. - private let threadingModel: EngineConfiguration.ThreadingModel - - private var breakpoints = [Int: CodeSlot]() - - package init(module: Module, store: Store, imports: Imports) throws { - let limit = store.engine.configuration.stackSize / MemoryLayout.stride - let instance = try module.instantiate(store: store, imports: imports, isDebuggable: true) - - guard case .function(let entrypointFunction) = instance.exports["_start"] else { - throw Error.entrypointFunctionNotFound - } - - self.instance = instance - self.module = module - self.entrypointFunction = entrypointFunction - self.valueStack = UnsafeMutablePointer.allocate(capacity: limit) - self.store = store - self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit)) - self.threadingModel = store.engine.configuration.threadingModel - } - - package mutating func stopAtEntrypoint() throws { - try self.enableBreakpoint(address: self.originalAddress(function: entrypointFunction)) - } - - package func originalAddress(function: Function) throws -> Int { - precondition(function.handle.isWasm) - - switch function.handle.wasm.code { - case .debuggable(let wasm, _): - return wasm.originalAddress - case .uncompiled: - try function.handle.wasm.ensureCompiled(store: StoreRef(self.store)) - return try self.originalAddress(function: function) - case .compiled: - fatalError() - } - } - - package mutating func enableBreakpoint(address: Int) throws { - print("attempt to toggle a breakpoint at \(address)") - - guard self.breakpoints[address] == nil else { - print("breakpoint at \(address) already enabled") - return - } - - guard let iseq = self.instance.handle.wasmToIseqMapping[address] else { - throw Error.noInstructionMappingAvailable(address) - } - self.breakpoints[address] = iseq.pointee - iseq.pointee = Instruction.breakpoint.headSlot(threadingModel: self.threadingModel) - } - - package mutating func disableBreakpoint(address: Int) throws { - print("attempt to toggle a breakpoint at \(address)") - - guard let oldCodeSlot = self.breakpoints[address] else { - print("breakpoint at \(address) already disabled") - return - } - - guard let iseq = self.instance.handle.wasmToIseqMapping[address] else { - throw Error.noInstructionMappingAvailable(address) - } - iseq.pointee = oldCodeSlot - self.breakpoints[address] = nil - } - - mutating func enableBreakpoint(functionIndex: FunctionIndex, offset: Int) { - } - - /// Array of addresses in the Wasm binary of executed instructions on the call stack. - package var currentCallStack: [Int] { - let isDebuggable = self.instance.handle.isDebuggable - print("isDebuggable is \(isDebuggable)") - - return Execution.captureBacktrace(sp: self.valueStack, store: self.store).symbols.map { - self.instance.handle.iseqToWasmMapping[$0.address]! - } - } - - deinit { - valueStack.deallocate() - } - } - -#endif diff --git a/Sources/WasmKit/Execution/Errors.swift b/Sources/WasmKit/Execution/Errors.swift index 5d714d02..07c1880b 100644 --- a/Sources/WasmKit/Execution/Errors.swift +++ b/Sources/WasmKit/Execution/Errors.swift @@ -5,21 +5,19 @@ import struct WasmParser.Import /// The backtrace of the trap. struct Backtrace: CustomStringConvertible, Sendable { /// A symbol in the backtrace. - struct Symbol: @unchecked Sendable { + struct Symbol { /// The name of the symbol. let name: String? - let address: Pc } /// The symbols in the backtrace. - let symbols: [Symbol] + let symbols: [Symbol?] /// Textual description of the backtrace. var description: String { - print("backtrace contains \(symbols.count) symbols") - return symbols.enumerated().map { (index, symbol) in - let name = symbol.name ?? "unknown" - return " \(index): (\(symbol.address)) \(name)" + symbols.enumerated().map { (index, symbol) in + let name = symbol?.name ?? "unknown" + return " \(index): \(name)" }.joined(separator: "\n") } } diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index ab29e86c..b0d09dfa 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -4,7 +4,7 @@ import _CWasmKit /// /// Each new invocation through exported function has a separate ``Execution`` /// even though the invocation happens during another invocation. -struct Execution: ~Copyable { +struct Execution { /// The reference to the ``Store`` associated with the execution. let store: StoreRef /// The end of the VM stack space. @@ -14,13 +14,6 @@ struct Execution: ~Copyable { /// - Note: If the trap is set, it must be released manually. private var trap: (error: UnsafeRawPointer, sp: Sp)? = nil - #if WasmDebuggingSupport - package init(store: StoreRef, stackEnd: UnsafeMutablePointer) { - self.store = store - self.stackEnd = stackEnd - } - #endif - /// Executes the given closure with a new execution state associated with /// the given ``Store`` instance. static func with( @@ -68,15 +61,18 @@ struct Execution: ~Copyable { static func captureBacktrace(sp: Sp, store: Store) -> Backtrace { var frames = FrameIterator(sp: sp) - var symbols: [Backtrace.Symbol] = [] - + var symbols: [Backtrace.Symbol?] = [] while let frame = frames.next() { guard let function = frame.function else { - symbols.append(.init(name: nil, address: frame.pc)) + symbols.append(nil) continue } let symbolName = store.nameRegistry.symbolicate(.wasm(function)) - symbols.append(.init(name: symbolName, address: frame.pc)) + symbols.append( + Backtrace.Symbol( + name: symbolName + ) + ) } return Backtrace(symbols: symbols) } @@ -252,7 +248,7 @@ extension Sp { nonmutating set { self[-1] = UInt64(UInt(bitPattern: newValue)) } } - var currentInstance: InternalInstance? { + fileprivate var currentInstance: InternalInstance? { currentFunction?.instance } } diff --git a/Sources/WasmKit/Execution/Function.swift b/Sources/WasmKit/Execution/Function.swift index 4af8c740..3786cf16 100644 --- a/Sources/WasmKit/Execution/Function.swift +++ b/Sources/WasmKit/Execution/Function.swift @@ -243,7 +243,7 @@ struct WasmFunctionEntity { switch code { case .uncompiled(let code): return try compile(store: store, code: code) - case .compiled(let iseq), .debuggable(_, let iseq): + case .compiled(let iseq), .compiledAndPatchable(_, let iseq): return iseq } } @@ -280,14 +280,10 @@ extension EntityHandle { case .uncompiled(let code): return try self.withValue { let iseq = try $0.compile(store: store, code: code) - if $0.instance.isDebuggable { - $0.code = .debuggable(code, iseq) - } else { - $0.code = .compiled(iseq) - } + $0.code = .compiled(iseq) return iseq } - case .compiled(let iseq), .debuggable(_, let iseq): + case .compiled(let iseq), .compiledAndPatchable(_, let iseq): return iseq } } @@ -320,7 +316,7 @@ struct InstructionSequence { enum CodeBody { case uncompiled(InternalUncompiledCode) case compiled(InstructionSequence) - case debuggable(InternalUncompiledCode, InstructionSequence) + case compiledAndPatchable(InternalUncompiledCode, InstructionSequence) } extension Reference { diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index 6ecdc5f1..27a2452a 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -83,15 +83,6 @@ struct InstanceEntity /* : ~Copyable */ { var functionRefs: Set var features: WasmFeatureSet var dataCount: UInt32? - var isDebuggable: Bool - - /// Mapping from iSeq Pc to instruction addresses in the original binary. - /// Used for handling current call stack requests issued by a ``Debugger`` instance. - var iseqToWasmMapping: [Pc: Int] - - /// Mapping from Wasm instruction addresses in the original binary to iSeq instruction addresses. - /// Used for handling breakpoint requests issued by a ``Debugger`` instance. - var wasmToIseqMapping: [Int: Pc] static var empty: InstanceEntity { InstanceEntity( @@ -105,10 +96,7 @@ struct InstanceEntity /* : ~Copyable */ { exports: [:], functionRefs: [], features: [], - dataCount: nil, - isDebuggable: false, - iseqToWasmMapping: [:], - wasmToIseqMapping: [:] + dataCount: nil ) } diff --git a/Sources/WasmKit/Execution/StoreAllocator.swift b/Sources/WasmKit/Execution/StoreAllocator.swift index dff3da90..f060dc56 100644 --- a/Sources/WasmKit/Execution/StoreAllocator.swift +++ b/Sources/WasmKit/Execution/StoreAllocator.swift @@ -251,8 +251,7 @@ extension StoreAllocator { module: Module, engine: Engine, resourceLimiter: any ResourceLimiter, - imports: Imports, - isDebuggable: Bool + imports: Imports ) throws -> InternalInstance { // Step 1 of module allocation algorithm, according to Wasm 2.0 spec. @@ -451,10 +450,7 @@ extension StoreAllocator { exports: exports, functionRefs: functionRefs, features: module.features, - dataCount: module.dataCount, - isDebuggable: isDebuggable, - iseqToWasmMapping: [:], - wasmToIseqMapping: [:] + dataCount: module.dataCount ) instancePointer.initialize(to: instanceEntity) instanceInitialized = true diff --git a/Sources/WasmKit/Module.swift b/Sources/WasmKit/Module.swift index 499cb93b..30070d1a 100644 --- a/Sources/WasmKit/Module.swift +++ b/Sources/WasmKit/Module.swift @@ -138,23 +138,9 @@ public struct Module { Instance(handle: try self.instantiateHandle(store: store, imports: imports), store: store) } - #if WasmDebuggingSupport - /// Instantiate this module in the given imports. - /// - /// - Parameters: - /// - store: The ``Store`` to allocate the instance in. - /// - imports: The imports to use for instantiation. All imported entities - /// must be allocated in the given store. - /// - isDebuggable: Whether the module should support debugging actions - /// (breakpoints etc) after instantiation. - public func instantiate(store: Store, imports: Imports = [:], isDebuggable: Bool) throws -> Instance { - Instance(handle: try self.instantiateHandle(store: store, imports: imports, isDebuggable: isDebuggable), store: store) - } - #endif - /// > Note: /// - private func instantiateHandle(store: Store, imports: Imports, isDebuggable: Bool = false) throws -> InternalInstance { + private func instantiateHandle(store: Store, imports: Imports) throws -> InternalInstance { try ModuleValidator(module: self).validate() // Steps 5-8. @@ -166,8 +152,7 @@ public struct Module { let instance = try store.allocator.allocate( module: self, engine: store.engine, resourceLimiter: store.resourceLimiter, - imports: imports, - isDebuggable: isDebuggable + imports: imports ) if let nameSection = customSections.first(where: { $0.name == "name" }) { diff --git a/Sources/WasmKit/Translator.swift b/Sources/WasmKit/Translator.swift index 1254df1a..6f3e1116 100644 --- a/Sources/WasmKit/Translator.swift +++ b/Sources/WasmKit/Translator.swift @@ -811,7 +811,7 @@ struct InstructionTranslator: InstructionVisitor { let allocator: ISeqAllocator let funcTypeInterner: Interner - var module: InternalInstance + let module: InternalInstance private var iseqBuilder: ISeqBuilder var controlStack: ControlStack var valueStack: ValueStack @@ -822,19 +822,11 @@ struct InstructionTranslator: InstructionVisitor { let functionIndex: FunctionIndex /// Whether a call to this function should be intercepted let isIntercepting: Bool + /// Whether Wasm debugging facilities are currently enabled. + let isDebugging: Bool var constantSlots: ConstSlots let validator: InstructionValidator - // Wasm debugging support. - - /// Current offset to an instruction in the original Wasm binary processed by this translator. - var binaryOffset: Int = 0 - - /// Mapping from `self.iseqBuilder.instructions` to Wasm instructions. - /// As mapping between iSeq to Wasm is many:many, but we only care about first mapping for overlapping address, - /// we need to iterate on it in the order the mappings were stored to ensure we don't overwrite the frist mapping. - var iseqToWasmMapping = [(iseq: Int, wasm: Int)]() - init( allocator: ISeqAllocator, engineConfiguration: EngineConfiguration, @@ -844,7 +836,8 @@ struct InstructionTranslator: InstructionVisitor { locals: [WasmTypes.ValueType], functionIndex: FunctionIndex, codeSize: Int, - isIntercepting: Bool + isIntercepting: Bool, + isDebugging: Bool = false ) throws { self.allocator = allocator self.funcTypeInterner = funcTypeInterner @@ -861,6 +854,7 @@ struct InstructionTranslator: InstructionVisitor { self.locals = Locals(types: type.parameters + locals) self.functionIndex = functionIndex self.isIntercepting = isIntercepting + self.isDebugging = isDebugging self.constantSlots = ConstSlots(stackLayout: stackLayout) self.validator = InstructionValidator(context: module) @@ -884,14 +878,12 @@ struct InstructionTranslator: InstructionVisitor { } private mutating func emit(_ instruction: Instruction, resultRelink: ISeqBuilder.ResultRelink? = nil) { - self.updateInstructionMapping() iseqBuilder.emit(instruction, resultRelink: resultRelink) } @discardableResult private mutating func emitCopyStack(from source: VReg, to dest: VReg) -> Bool { guard source != dest else { return false } - self.updateInstructionMapping() emit(.copyStack(Instruction.CopyStackOperand(source: LVReg(source), dest: LVReg(dest)))) return true } @@ -1077,7 +1069,6 @@ struct InstructionTranslator: InstructionVisitor { emit(.onExit(functionIndex)) } try visitReturnLike() - self.updateInstructionMapping() iseqBuilder.emit(._return) } private mutating func markUnreachable() throws { @@ -1097,22 +1088,9 @@ struct InstructionTranslator: InstructionVisitor { let instructions = iseqBuilder.finalize() // TODO: Figure out a way to avoid the copy here while keeping the execution performance. let buffer = allocator.allocateInstructions(capacity: instructions.count) - let initializedElementsIndex = buffer.initialize(fromContentsOf: instructions) - assert(initializedElementsIndex == instructions.endIndex) - - for (iseq, wasm) in self.iseqToWasmMapping { - self.module.withValue { - let absoluteISeq = iseq + buffer.baseAddress.unsafelyUnwrapped - // Don't override the existing mapping, only store a new pair if there's no mapping for a given key. - if $0.iseqToWasmMapping[absoluteISeq] == nil { - $0.iseqToWasmMapping[absoluteISeq] = wasm - } - if $0.wasmToIseqMapping[wasm] == nil { - $0.wasmToIseqMapping[wasm] = absoluteISeq - } - } + for (idx, instruction) in instructions.enumerated() { + buffer[idx] = instruction } - let constants = allocator.allocateConstants(self.constantSlots.values) return InstructionSequence( instructions: buffer, @@ -1121,15 +1099,6 @@ struct InstructionTranslator: InstructionVisitor { ) } - private mutating func updateInstructionMapping() { - // This is a hot path, so best to exclude the code altogether if the trait isn't enabled. - #if WasmDebuggingSupport - guard self.module.isDebuggable else { return } - - self.iseqToWasmMapping.append((self.iseqBuilder.insertingPC.offsetFromHead, self.binaryOffset)) - #endif - } - // MARK: Main entry point /// Translate a Wasm expression into a sequence of instructions. @@ -1157,9 +1126,7 @@ struct InstructionTranslator: InstructionVisitor { emit(.unreachable) try markUnreachable() } - mutating func visitNop() -> Output { - emit(.nop) - } + mutating func visitNop() -> Output { emit(.nop) } mutating func visitBlock(blockType: WasmParser.BlockType) throws -> Output { let blockType = try module.resolveBlockType(blockType) @@ -1206,7 +1173,6 @@ struct InstructionTranslator: InstructionVisitor { ) ) guard let condition = condition else { return } - self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.brIfNot, endLabel) { iseqBuilder, selfPC, endPC in let targetPC: MetaProgramCounter if let elsePC = iseqBuilder.resolveLabel(elseLabel) { @@ -1227,8 +1193,6 @@ struct InstructionTranslator: InstructionVisitor { preserveOnStack(depth: valueStack.height - frame.stackHeight) try controlStack.resetReachability() iseqBuilder.resetLastEmission() - - self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.br, endLabel) { _, selfPC, endPC in let offset = endPC.offsetFromHead - selfPC.offsetFromHead return Int32(offset) @@ -1324,8 +1288,6 @@ struct InstructionTranslator: InstructionVisitor { currentFrame: try controlStack.currentFrame(), currentHeight: valueStack.height ) - - self.updateInstructionMapping() iseqBuilder.emitWithLabel(makeInstruction, frame.continuation) { _, selfPC, continuation in let relativeOffset = continuation.offsetFromHead - selfPC.offsetFromHead return make(Int32(relativeOffset), UInt32(copyCount), popCount) @@ -1359,7 +1321,6 @@ struct InstructionTranslator: InstructionVisitor { if frame.copyCount == 0 { guard let condition else { return } // Optimization where we don't need copying values when the branch taken - self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.brIf, frame.continuation) { _, selfPC, continuation in let relativeOffset = continuation.offsetFromHead - selfPC.offsetFromHead return Instruction.BrIfOperand( @@ -1392,13 +1353,11 @@ struct InstructionTranslator: InstructionVisitor { // [0x06] (local.get 1 reg:2) <----|---------+ // [0x07] ... <-------+ let onBranchNotTaken = iseqBuilder.allocLabel() - self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.brIfNot, onBranchNotTaken) { _, conditionCheckAt, continuation in let relativeOffset = continuation.offsetFromHead - conditionCheckAt.offsetFromHead return Instruction.BrIfOperand(condition: LVReg(condition), offset: Int32(relativeOffset)) } try copyOnBranch(targetFrame: frame) - self.updateInstructionMapping() try emitBranch(Instruction.br, relativeDepth: relativeDepth) { offset, copyCount, popCount in return offset } @@ -1425,7 +1384,6 @@ struct InstructionTranslator: InstructionVisitor { baseAddress: tableBuffer.baseAddress!, count: UInt16(tableBuffer.count), index: index ) - self.updateInstructionMapping() iseqBuilder.emit(.brTable(operand)) let brTableAt = iseqBuilder.insertingPC @@ -1475,7 +1433,6 @@ struct InstructionTranslator: InstructionVisitor { } let emittedCopy = try copyOnBranch(targetFrame: frame) if emittedCopy { - self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.br, frame.continuation) { _, brAt, continuation in let relativeOffset = continuation.offsetFromHead - brAt.offsetFromHead return Int32(relativeOffset) @@ -2305,7 +2262,7 @@ struct InstructionTranslator: InstructionVisitor { } mutating func visitUnknown(_ opcode: [UInt8]) throws -> Bool { - guard self.module.isDebuggable && opcode.count == 1 && opcode[0] == 0xFF else { + guard self.isDebugging && opcode.count == 1 && opcode[0] == 0xFF else { return false } diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index fff3a9e5..ee64f392 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -1,13 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + #if WasmDebuggingSupport import GDBRemoteProtocol import Logging import NIOCore import NIOFileSystem - import Synchronization import SystemPackage import WasmKit - import WasmKitWASI extension BinaryInteger { init?(hexEncoded: Substring) { @@ -30,7 +40,6 @@ private let wasmBinary: ByteBuffer private let moduleFilePath: FilePath private let logger: Logger - private var debugger: Debugger private let functionsRLE: [(wasmAddress: Int, iSeqAddress: Int)] = [] package init(logger: Logger, moduleFilePath: FilePath) async throws { @@ -41,14 +50,6 @@ } self.moduleFilePath = moduleFilePath - - let store = Store(engine: Engine()) - var imports = Imports() - let wasi = try WASIBridgeToHost() - wasi.link(to: &imports, store: store) - - self.debugger = try Debugger(module: parseWasm(bytes: .init(buffer: self.wasmBinary)), store: store, imports: imports) - try self.debugger.stopAtEntrypoint() } package func handle(command: GDBHostCommand) throws -> GDBTargetResponse { @@ -151,11 +152,7 @@ responseKind = .hexEncodedBinary(wasmBinary.readableBytesView[binaryOffset..<(binaryOffset + length)]) - case .wasmCallStack: - print(self.debugger.currentCallStack) - responseKind = .empty - - case .generalRegisters: + case .wasmCallStack, .generalRegisters: fatalError() } diff --git a/Sources/WasmParser/BinaryInstructionDecoder.swift b/Sources/WasmParser/BinaryInstructionDecoder.swift index a6a4ac6d..5cc84908 100644 --- a/Sources/WasmParser/BinaryInstructionDecoder.swift +++ b/Sources/WasmParser/BinaryInstructionDecoder.swift @@ -6,9 +6,6 @@ import WasmTypes @usableFromInline protocol BinaryInstructionDecoder { - /// Current offset in the decoded Wasm binary. - var offset: Int { get } - /// Claim the next byte to be decoded @inlinable func claimNextByte() throws -> UInt8 @@ -94,7 +91,6 @@ protocol BinaryInstructionDecoder { @inlinable func parseBinaryInstruction(visitor: inout some InstructionVisitor, decoder: inout some BinaryInstructionDecoder) throws -> Bool { - visitor.binaryOffset = decoder.offset let opcode0 = try decoder.claimNextByte() switch opcode0 { case 0x00: diff --git a/Sources/WasmParser/InstructionVisitor.swift b/Sources/WasmParser/InstructionVisitor.swift index b44d39a8..2a6b0271 100644 --- a/Sources/WasmParser/InstructionVisitor.swift +++ b/Sources/WasmParser/InstructionVisitor.swift @@ -310,9 +310,6 @@ extension AnyInstructionVisitor { /// The visitor pattern is used while parsing WebAssembly expressions to allow for easy extensibility. /// See the expression parsing method ``Code/parseExpression(visitor:)`` public protocol InstructionVisitor { - /// Current offset in visitor's instruction stream. - var binaryOffset: Int { get set } - /// Visiting `unreachable` instruction. mutating func visitUnreachable() throws /// Visiting `nop` instruction. diff --git a/Sources/WasmParser/WasmParser.swift b/Sources/WasmParser/WasmParser.swift index 0b7c7199..0bca6049 100644 --- a/Sources/WasmParser/WasmParser.swift +++ b/Sources/WasmParser/WasmParser.swift @@ -757,8 +757,6 @@ extension Parser: BinaryInstructionDecoder { @usableFromInline struct InstructionFactory: AnyInstructionVisitor { - @usableFromInline var binaryOffset: Int = 0 - @usableFromInline var insts: [Instruction] = [] @inlinable init() {} diff --git a/Sources/WasmParser/WasmTypes.swift b/Sources/WasmParser/WasmTypes.swift index 153f8cb9..df95b6c2 100644 --- a/Sources/WasmParser/WasmTypes.swift +++ b/Sources/WasmParser/WasmTypes.swift @@ -15,10 +15,6 @@ public struct Code { @usableFromInline internal let features: WasmFeatureSet - #if WasmDebuggingSupport - package var originalAddress: Int { self.offset } - #endif - @inlinable init(locals: [ValueType], expression: ArraySlice, offset: Int, features: WasmFeatureSet) { self.locals = locals diff --git a/Sources/wasmkit-gdb-tool/Entrypoint.swift b/Sources/wasmkit-gdb-tool/Entrypoint.swift deleted file mode 100644 index ffe793ec..00000000 --- a/Sources/wasmkit-gdb-tool/Entrypoint.swift +++ /dev/null @@ -1,110 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftNIO open source project -// -// Copyright (c) 2017-2025 Apple Inc. and the SwiftNIO project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftNIO project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import ArgumentParser -import GDBRemoteProtocol -import Logging -import NIOCore -import NIOPosix -import SystemPackage -import WasmKitGDBHandler - -@main -struct Entrypoint: AsyncParsableCommand { - @Option(help: "TCP port that a debugger can connect to") - var port = 8080 - - @Option( - name: .shortAndLong, - transform: { stringValue in - guard let logLevel = Logger.Level(rawValue: stringValue.lowercased()) else { - throw ValidationError("not a valid log level: \(stringValue)") - } - return logLevel - } - ) - var logLevel = Logger.Level.info - - @Argument(transform: { FilePath($0) }) - var wasmModulePath: FilePath - - func run() async throws { - #if WasmDebuggingSupport - let logger = { - var result = Logger(label: "org.swiftwasm.WasmKit") - result.logLevel = self.logLevel - return result - }() - - try await MultiThreadedEventLoopGroup.withEventLoopGroup(numberOfThreads: System.coreCount) { group in - let bootstrap = ServerBootstrap(group: group) - // Specify backlog and enable SO_REUSEADDR for the server itself - .serverChannelOption(.backlog, value: 256) - .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) - - // Set the handlers that are applied to the accepted child `Channel`s. - .childChannelInitializer { channel in - // Ensure we don't read faster then we can write by adding the BackPressureHandler into the pipeline. - channel.eventLoop.makeCompletedFuture { - try channel.pipeline.syncOperations.addHandler(BackPressureHandler()) - // make sure to instantiate your `ChannelHandlers` inside of - // the closure as it will be invoked once per connection. - try channel.pipeline.syncOperations.addHandlers([ - ByteToMessageHandler(GDBHostCommandDecoder(logger: logger)), - MessageToByteHandler(GDBTargetResponseEncoder()), - ]) - } - } - - // Enable SO_REUSEADDR for the accepted Channels - .childChannelOption(.socketOption(.so_reuseaddr), value: 1) - .childChannelOption(.maxMessagesPerRead, value: 16) - .childChannelOption(.recvAllocator, value: AdaptiveRecvByteBufferAllocator()) - - let serverChannel = try await bootstrap.bind(host: "127.0.0.1", port: port) { childChannel in - childChannel.eventLoop.makeCompletedFuture { - try NIOAsyncChannel, GDBTargetResponse>( - wrappingChannelSynchronously: childChannel - ) - } - } - /* the server will now be accepting connections */ - logger.info("listening on port \(port)") - - let debugger = try await WasmKitGDBHandler(logger: logger, moduleFilePath: self.wasmModulePath) - - try await withThrowingDiscardingTaskGroup { group in - try await serverChannel.executeThenClose { serverChannelInbound in - for try await connectionChannel in serverChannelInbound { - group.addTask { - do { - try await connectionChannel.executeThenClose { connectionChannelInbound, connectionChannelOutbound in - for try await inboundData in connectionChannelInbound { - // Let's echo back all inbound data - try await connectionChannelOutbound.write(debugger.handle(command: inboundData.payload)) - } - } - } catch { - logger.error("Error in GDB remote protocol connection channel", metadata: ["error": "\(error)"]) - } - } - } - } - } - } - #else - fatalError("Build WasmKit with `WasmDebuggingSupport` trait") - #endif - } -} diff --git a/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift b/Tests/GDBRemoteProtocolTests/GDBRemoteProtocolTests.swift similarity index 68% rename from Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift rename to Tests/GDBRemoteProtocolTests/GDBRemoteProtocolTests.swift index 4a30f0e1..c01db004 100644 --- a/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift +++ b/Tests/GDBRemoteProtocolTests/GDBRemoteProtocolTests.swift @@ -1,10 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + import GDBRemoteProtocol import Logging import NIOCore import Testing @Suite -struct LLDBRemoteProtocolTests { +struct GDBRemoteProtocolTests { @Test func decoding() throws { var logger = Logger(label: "com.swiftwasm.WasmKit.tests") diff --git a/Tests/WasmKitTests/ExecutionTests.swift b/Tests/WasmKitTests/ExecutionTests.swift index 42c8759d..e5b61c87 100644 --- a/Tests/WasmKitTests/ExecutionTests.swift +++ b/Tests/WasmKitTests/ExecutionTests.swift @@ -111,7 +111,7 @@ """ ) { trap in #expect( - trap.backtrace?.symbols.compactMap(\.name) == [ + trap.backtrace?.symbols.compactMap(\.?.name) == [ "foo", "bar", "_start", @@ -138,7 +138,7 @@ """ ) { trap in #expect( - trap.backtrace?.symbols.compactMap(\.name) == [ + trap.backtrace?.symbols.compactMap(\.?.name) == [ "wasm function[1]", "bar", "_start", diff --git a/Utilities/Sources/WasmGen.swift b/Utilities/Sources/WasmGen.swift index 1e217bda..af354614 100644 --- a/Utilities/Sources/WasmGen.swift +++ b/Utilities/Sources/WasmGen.swift @@ -96,9 +96,6 @@ enum WasmGen { /// The visitor pattern is used while parsing WebAssembly expressions to allow for easy extensibility. /// See the expression parsing method ``Code/parseExpression(visitor:)`` public protocol InstructionVisitor { - /// Current offset in visitor's instruction stream. - var binaryOffset: Int { get set } - """ for instruction in instructions.categorized { @@ -534,9 +531,6 @@ enum WasmGen { @usableFromInline protocol BinaryInstructionDecoder { - /// Current offset in the decoded Wasm binary. - var offset: Int { get } - /// Claim the next byte to be decoded @inlinable func claimNextByte() throws -> UInt8 @@ -568,7 +562,6 @@ enum WasmGen { @inlinable func parseBinaryInstruction(visitor: inout some InstructionVisitor, decoder: inout some BinaryInstructionDecoder) throws -> Bool { - visitor.binaryOffset = decoder.offset """ func renderSwitchCase(_ root: Trie, depth: Int = 0) { From 410c47025b450dd2e07af105a9592b94fb4d0b78 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 10:00:54 +0100 Subject: [PATCH 058/107] Clarify licensing in `README.md` --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 5768d2ae..82ee24f5 100644 --- a/README.md +++ b/README.md @@ -93,3 +93,14 @@ $ swift test This project was originally developed by [@akkyie](https://github.com/akkyie), and is now maintained by the community. [^1]: On a 2020 Mac mini (M1, 16GB RAM) with Swift 5.10. Measured by `swift package resolve && swift package clean && time swift build --product PrintAdd`. +License + +## License + +WasmKit runtime modules are licensed under MIT License. See [LICENSE](https://raw.githubusercontent.com/swiftwasm/WasmKit/refs/heads/main/LICENSE) file for license information. + +GDB Remote Protocol support (`GDBRemoteProtocol` and `WasmKitGDBHandler` modules) is licensed separately under Apache License v2.0 with Runtime Library Exception, Copyright 2025 Apple Inc. and the Swift project authors. + +See https://swift.org/LICENSE.txt for license information. + +See https://swift.org/CONTRIBUTORS.txt for Swift project authors. From 977b93930615102e9b7fb55fcedbbb65dfe312d3 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 10:02:44 +0100 Subject: [PATCH 059/107] Add license files for separate modules --- Sources/GDBRemoteProtocol/LICENSE.txt | 211 +++++++++++++++++++++++ Sources/WasmKitGDBHandler/LICENSE.txt | 1 + Tests/GDBRemoteProtocolTests/LICENSE.txt | 1 + 3 files changed, 213 insertions(+) create mode 100644 Sources/GDBRemoteProtocol/LICENSE.txt create mode 120000 Sources/WasmKitGDBHandler/LICENSE.txt create mode 120000 Tests/GDBRemoteProtocolTests/LICENSE.txt diff --git a/Sources/GDBRemoteProtocol/LICENSE.txt b/Sources/GDBRemoteProtocol/LICENSE.txt new file mode 100644 index 00000000..61b0c781 --- /dev/null +++ b/Sources/GDBRemoteProtocol/LICENSE.txt @@ -0,0 +1,211 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +## Runtime Library Exception to the Apache 2.0 License: ## + + + As an exception, if you use this Software to compile your source code and + portions of this Software are embedded into the binary product as a result, + you may redistribute such product without providing attribution as would + otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. diff --git a/Sources/WasmKitGDBHandler/LICENSE.txt b/Sources/WasmKitGDBHandler/LICENSE.txt new file mode 120000 index 00000000..b25e6fe6 --- /dev/null +++ b/Sources/WasmKitGDBHandler/LICENSE.txt @@ -0,0 +1 @@ +../GDBRemoteProtocol/LICENSE.txt \ No newline at end of file diff --git a/Tests/GDBRemoteProtocolTests/LICENSE.txt b/Tests/GDBRemoteProtocolTests/LICENSE.txt new file mode 120000 index 00000000..9617e0aa --- /dev/null +++ b/Tests/GDBRemoteProtocolTests/LICENSE.txt @@ -0,0 +1 @@ +../../Sources/GDBRemoteProtocol/LICENSE.txt \ No newline at end of file From 2acd4f3321c40fc839223837ceddee2dc8e1f7e5 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 10:14:17 +0100 Subject: [PATCH 060/107] Basic doc comments for `GDBHostCommand` --- Sources/GDBRemoteProtocol/GDBHostCommand.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index d4f5023e..04af72f2 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -10,6 +10,8 @@ // //===----------------------------------------------------------------------===// +/// A command sent from a debugger host (GDB or LLDB) to a debugger target (a device +/// or a virtual machine being debugged). /// See GDB and LLDB remote protocol documentation for more details: /// * https://sourceware.org/gdb/current/onlinedocs/gdb.html/General-Query-Packets.html /// * https://lldb.llvm.org/resources/lldbgdbremote.html @@ -43,6 +45,7 @@ package struct GDBHostCommand: Equatable { case generalRegisters + /// Decodes kind of a command from a raw string sent from a host. package init?(rawValue: String) { switch rawValue { case "g": @@ -86,8 +89,10 @@ package struct GDBHostCommand: Equatable { } } + /// The kind of a host command for the target to act upon. package let kind: Kind + /// Arguments supplied with a host command. package let arguments: String package init(kindString: String, arguments: String) throws { @@ -118,6 +123,7 @@ package struct GDBHostCommand: Equatable { self.arguments = arguments } + /// Memberwise initializer of `GDBHostCommand` type. package init(kind: Kind, arguments: String) { self.kind = kind self.arguments = arguments From 3b2e30fe3eaaf6a9cdbea6446479efc56fcdbf50 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 10:40:43 +0100 Subject: [PATCH 061/107] Add doc comments for `GDBHostCommandDecoder` --- .../GDBRemoteProtocol/GDBHostCommand.swift | 19 +++---- .../GDBHostCommandDecoder.swift | 55 +++++++++++++++---- Sources/GDBRemoteProtocol/GDBPacket.swift | 5 ++ 3 files changed, 59 insertions(+), 20 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index 04af72f2..3c3d4e11 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -16,11 +16,6 @@ /// * https://sourceware.org/gdb/current/onlinedocs/gdb.html/General-Query-Packets.html /// * https://lldb.llvm.org/resources/lldbgdbremote.html package struct GDBHostCommand: Equatable { - enum Error: Swift.Error { - case unexpectedArgumentsValue - case unknownCommand(kind: String, arguments: String) - } - package enum Kind: String, Equatable { // Currently listed in the order that LLDB sends them in. case startNoAckMode @@ -94,8 +89,12 @@ package struct GDBHostCommand: Equatable { /// Arguments supplied with a host command. package let arguments: String - - package init(kindString: String, arguments: String) throws { + + /// Initialize a host command from raw strings sent from a host. + /// - Parameters: + /// - kindString: raw ``String`` that denotes kind of the command. + /// - arguments: raw arguments that immediately follow kind of the command. + package init(kindString: String, arguments: String) throws(GDBHostCommandDecoder.Error) { let registerInfoPrefix = "qRegisterInfo" if kindString.starts(with: "x") { @@ -110,20 +109,20 @@ package struct GDBHostCommand: Equatable { self.kind = .registerInfo guard arguments.isEmpty else { - throw Error.unexpectedArgumentsValue + throw GDBHostCommandDecoder.Error.unexpectedArgumentsValue } self.arguments = String(kindString.dropFirst(registerInfoPrefix.count)) return } else if let kind = Kind(rawValue: kindString) { self.kind = kind } else { - throw Error.unknownCommand(kind: kindString, arguments: arguments) + throw GDBHostCommandDecoder.Error.unknownCommand(kind: kindString, arguments: arguments) } self.arguments = arguments } - /// Memberwise initializer of `GDBHostCommand` type. + /// Member-wise initializer of `GDBHostCommand` type. package init(kind: Kind, arguments: String) { self.kind = kind self.arguments = arguments diff --git a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift index 77b78d54..3596319c 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -14,36 +14,69 @@ import Logging import NIOCore extension ByteBuffer { + /// Returns `true` if byte to be read immediately is a GDB RP checksum + /// delimiter. Returns `false` otherwise. var isChecksumDelimiterAtReader: Bool { self.peekInteger(as: UInt8.self) == UInt8(ascii: "#") } + /// Returns `true` if byte to be read immediately is a GDB RP command arguments + /// delimiter. Returns `false` otherwise. var isArgumentsDelimiterAtReader: Bool { self.peekInteger(as: UInt8.self) == UInt8(ascii: ":") } } +/// Decoder of GDB RP host commands, that takes raw `ByteBuffer` as an input encoded +/// per https://sourceware.org/gdb/current/onlinedocs/gdb.html/Overview.html#Overview +/// and produces a `GDBPacket` value as output. This decoder is +/// compatible with NIO channel pipelines, making it easy to integrate with different +/// I/O configurations. package struct GDBHostCommandDecoder: ByteToMessageDecoder { - enum Error: Swift.Error { + /// Errors that can be thrown during host command decoding. + package enum Error: Swift.Error { + /// Expected `+` acknowledgement character to be included in the packet, when + /// ``GDBHostCommandDecoder/isNoAckModeActive`` is set to `false`. case expectedAck + + /// Expected command to start with `$` character`. case expectedCommandStart - case unknownCommandKind(String) + + /// Expected checksum to be included with the packet was not found. case expectedChecksum + + /// Expected checksum included with the packet did not match the expected value. case checksumIncorrect(expectedChecksum: Int, receivedChecksum: UInt8) + + /// Unexpected arguments value supplied for a given command. + case unexpectedArgumentsValue + + /// Host command kind could not be parsed. See `GDBHostCommand.Kind` for the + /// list of supported commands. + case unknownCommand(kind: String, arguments: String) } + /// Type of the output value produced by this decoder. package typealias InboundOut = GDBPacket private var accumulatedDelimiter: UInt8? private var accummulatedKind = [UInt8]() private var accummulatedArguments = [UInt8]() - + + /// Logger instance used by this decoder. private let logger: Logger - + + /// Initializes a new decoder. + /// - Parameter logger: logger instance that consumes messages from the newly + /// initialized decoder. package init(logger: Logger) { self.logger = logger } - + + /// Sum of the raw character values consumed in the current command so far, + /// used in checksum computation. private var accummulatedSum = 0 + + /// Computed checksum for the values consumed in the current command so far. package var accummulatedChecksum: UInt8 { UInt8(self.accummulatedSum % 256) } @@ -51,13 +84,15 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { private var isNoAckModeRequested = false private var isNoAckModeActive = false - mutating package func decode(buffer: inout ByteBuffer) throws -> GDBPacket? { + package mutating func decode( + buffer: inout ByteBuffer + ) throws(Error) -> GDBPacket? { guard var startDelimiter = self.accumulatedDelimiter ?? buffer.readInteger(as: UInt8.self) else { // Not enough data to parse. return nil } - if !isNoAckModeActive { + if !self.isNoAckModeActive { let firstStartDelimiter = startDelimiter guard firstStartDelimiter == UInt8(ascii: "+") else { @@ -65,7 +100,7 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { throw Error.expectedAck } - if isNoAckModeRequested { + if self.isNoAckModeRequested { self.isNoAckModeActive = true } @@ -82,7 +117,7 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { // Command start delimiters. guard startDelimiter == UInt8(ascii: "$") else { - logger.error("unexpected delimiter: \(Character(UnicodeScalar(startDelimiter)))") + self.logger.error("unexpected delimiter: \(Character(UnicodeScalar(startDelimiter)))") throw Error.expectedCommandStart } @@ -151,7 +186,7 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { mutating package func decode( context: ChannelHandlerContext, buffer: inout ByteBuffer - ) throws -> DecodingState { + ) throws(Error) -> DecodingState { logger.trace(.init(stringLiteral: buffer.peekString(length: buffer.readableBytes)!)) guard let command = try self.decode(buffer: &buffer) else { diff --git a/Sources/GDBRemoteProtocol/GDBPacket.swift b/Sources/GDBRemoteProtocol/GDBPacket.swift index 0cc5accb..1a1a65d7 100644 --- a/Sources/GDBRemoteProtocol/GDBPacket.swift +++ b/Sources/GDBRemoteProtocol/GDBPacket.swift @@ -10,6 +10,11 @@ // //===----------------------------------------------------------------------===// +/// GDB host commands and target responses are wrapped with delimiters followed +/// by a single byte checksum value. This type denotes such a packet by attaching +/// a checksum value to the contained payload. +/// See GDB remote protocol overview for more details: +/// https://sourceware.org/gdb/current/onlinedocs/gdb.html/Overview.html#Overview package struct GDBPacket: Sendable { package let payload: Payload package let checksum: UInt8 From fc740e52d5d6183ce2d0f25478f29b1839251e55 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 10:57:13 +0100 Subject: [PATCH 062/107] Add remaining doc comments --- .../GDBRemoteProtocol/GDBHostCommand.swift | 1 + .../GDBHostCommandDecoder.swift | 11 +++++++- .../GDBRemoteProtocol/GDBTargetResponse.swift | 28 +++++++++++++++++-- .../GDBTargetResponseEncoder.swift | 7 ++++- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index 3c3d4e11..5c4ff619 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -16,6 +16,7 @@ /// * https://sourceware.org/gdb/current/onlinedocs/gdb.html/General-Query-Packets.html /// * https://lldb.llvm.org/resources/lldbgdbremote.html package struct GDBHostCommand: Equatable { + /// Kind of the command sent from the debugger host to the debugger target. package enum Kind: String, Equatable { // Currently listed in the order that LLDB sends them in. case startNoAckMode diff --git a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift index 3596319c..143be79b 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -80,8 +80,17 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { package var accummulatedChecksum: UInt8 { UInt8(self.accummulatedSum % 256) } - + + /// Whether `QStartNoAckMode` command was sent. Note that this is separate + /// from ``isNoAckModeActive``. This mode is "activated" for the subsequent + /// host command, which is when `isNoAckModeActive` is set by the decoder to + /// `false`, but not for the immediate response. + /// See https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packet-Acknowledgment.html#Packet-Acknowledgment private var isNoAckModeRequested = false + + /// Whether `QStartNoAckMode` command was sent and this mode has been + /// subsequently activated. + /// See https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packet-Acknowledgment.html#Packet-Acknowledgment private var isNoAckModeActive = false package mutating func decode( diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift index 002b632a..4ced3929 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift @@ -22,21 +22,43 @@ package enum VContActions: String { case stepInRange = "r" } +/// A response sent from a debugger target (a device +/// or a virtual machine being debugged) to a debugger host (GDB or LLDB). +/// See GDB remote protocol documentation for more details: +/// * https://sourceware.org/gdb/current/onlinedocs/gdb.html/General-Query-Packets.html package struct GDBTargetResponse { + /// Kind of the response sent from the debugger target to the debugger host. package enum Kind { + /// Standard `OK` response. case ok + + /// A list of key-value pairs, with keys delimited from values by a colon `:` + /// character, and pairs in the list delimited by the semicolon `;` character. case keyValuePairs(KeyValuePairs) + + /// List of ``VContActions`` values delimited by the semicolon `;` character. case vContSupportedActions([VContActions]) + + /// Raw string included as is in the response. case string(String) + + /// Binary buffer hex-encoded in the response. case hexEncodedBinary(ByteBufferView) + + /// Standard empty response (no content is sent). case empty } package let kind: Kind - package let isNoAckModeActivated: Bool - package init(kind: Kind, isNoAckModeActivated: Bool) { + /// Whether `QStartNoAckMode` is activated and no ack `+` symbol should be sent + /// before encoding this response. + /// See https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packet-Acknowledgment.html#Packet-Acknowledgment + package let isNoAckModeActivate: Bool + + /// Member-wise initializer for the debugger response. + package init(kind: Kind, isNoAckModeActivate: Bool) { self.kind = kind - self.isNoAckModeActivated = isNoAckModeActivated + self.isNoAckModeActivate = isNoAckModeActivate } } diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index 6a1f240b..3b97ecea 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -14,11 +14,16 @@ import Foundation import NIOCore extension String { + /// Computes a GDB RP checksum of characters in a given string. fileprivate var appendedChecksum: String { "\(self)#\(String(format:"%02X", self.utf8.reduce(0, { $0 + Int($1) }) % 256))" } } +/// Encoder of GDB RP target responses, that takes ``GDBTargetResponse`` as an input +/// and encodes it per https://sourceware.org/gdb/current/onlinedocs/gdb.html/Overview.html#Overview +/// format in a `ByteBuffer` value as output. This encoder is compatible with NIO channel pipelines, +/// making it easy to integrate with different I/O configurations. package class GDBTargetResponseEncoder: MessageToByteEncoder { private var isNoAckModeActive = false @@ -27,7 +32,7 @@ package class GDBTargetResponseEncoder: MessageToByteEncoder { if !isNoAckModeActive { out.writeInteger(UInt8(ascii: "+")) } - if data.isNoAckModeActivated { + if data.isNoAckModeActivate { self.isNoAckModeActive = true } out.writeInteger(UInt8(ascii: "$")) From a297f2da8748ce11d592b0f03739bf5cbbeb34fe Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 10:59:22 +0100 Subject: [PATCH 063/107] Fix formatting --- Sources/GDBRemoteProtocol/GDBHostCommand.swift | 2 +- Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift | 8 ++++---- Sources/GDBRemoteProtocol/GDBTargetResponse.swift | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index 5c4ff619..cf0276dc 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -90,7 +90,7 @@ package struct GDBHostCommand: Equatable { /// Arguments supplied with a host command. package let arguments: String - + /// Initialize a host command from raw strings sent from a host. /// - Parameters: /// - kindString: raw ``String`` that denotes kind of the command. diff --git a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift index 143be79b..7e6c665f 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -63,15 +63,15 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { private var accummulatedKind = [UInt8]() private var accummulatedArguments = [UInt8]() - + /// Logger instance used by this decoder. private let logger: Logger - + /// Initializes a new decoder. /// - Parameter logger: logger instance that consumes messages from the newly /// initialized decoder. package init(logger: Logger) { self.logger = logger } - + /// Sum of the raw character values consumed in the current command so far, /// used in checksum computation. private var accummulatedSum = 0 @@ -80,7 +80,7 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { package var accummulatedChecksum: UInt8 { UInt8(self.accummulatedSum % 256) } - + /// Whether `QStartNoAckMode` command was sent. Note that this is separate /// from ``isNoAckModeActive``. This mode is "activated" for the subsequent /// host command, which is when `isNoAckModeActive` is set by the decoder to diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift index 4ced3929..d8ffc34f 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift @@ -55,7 +55,7 @@ package struct GDBTargetResponse { /// before encoding this response. /// See https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packet-Acknowledgment.html#Packet-Acknowledgment package let isNoAckModeActivate: Bool - + /// Member-wise initializer for the debugger response. package init(kind: Kind, isNoAckModeActivate: Bool) { self.kind = kind From ff2e9408626387009f295d0bc3ec997cd703ffd7 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 11:06:39 +0100 Subject: [PATCH 064/107] Fix var naming typo --- Sources/GDBRemoteProtocol/GDBTargetResponse.swift | 6 +++--- Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift | 2 +- Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift | 7 +++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift index d8ffc34f..77391d73 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift @@ -54,11 +54,11 @@ package struct GDBTargetResponse { /// Whether `QStartNoAckMode` is activated and no ack `+` symbol should be sent /// before encoding this response. /// See https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packet-Acknowledgment.html#Packet-Acknowledgment - package let isNoAckModeActivate: Bool + package let isNoAckModeActive: Bool /// Member-wise initializer for the debugger response. - package init(kind: Kind, isNoAckModeActivate: Bool) { + package init(kind: Kind, isNoAckModeActive: Bool) { self.kind = kind - self.isNoAckModeActivate = isNoAckModeActivate + self.isNoAckModeActive = isNoAckModeActive } } diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index 3b97ecea..00da50e7 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -32,7 +32,7 @@ package class GDBTargetResponseEncoder: MessageToByteEncoder { if !isNoAckModeActive { out.writeInteger(UInt8(ascii: "+")) } - if data.isNoAckModeActivate { + if data.isNoAckModeActive { self.isNoAckModeActive = true } out.writeInteger(UInt8(ascii: "$")) diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index ee64f392..5313bdd3 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -56,10 +56,10 @@ let responseKind: GDBTargetResponse.Kind logger.trace("handling GDB host command", metadata: ["GDBHostCommand": .string(command.kind.rawValue)]) - var isNoAckModeActivated = false + var isNoAckModeActive = false switch command.kind { case .startNoAckMode: - isNoAckModeActivated = true + isNoAckModeActive = true fallthrough case .isThreadSuffixSupported, .listThreadsInStopReply: @@ -158,9 +158,8 @@ logger.trace("handler produced a response", metadata: ["GDBTargetResponse": .string("\(responseKind)")]) - return .init(kind: responseKind, isNoAckModeActivated: isNoAckModeActivated) + return .init(kind: responseKind, isNoAckModeActive: isNoAckModeActive) } - } #endif From 5f7fbb560393d682c4be37532977e84d572f74ad Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 10:00:54 +0100 Subject: [PATCH 065/107] Clarify licensing in `README.md` --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 5768d2ae..82ee24f5 100644 --- a/README.md +++ b/README.md @@ -93,3 +93,14 @@ $ swift test This project was originally developed by [@akkyie](https://github.com/akkyie), and is now maintained by the community. [^1]: On a 2020 Mac mini (M1, 16GB RAM) with Swift 5.10. Measured by `swift package resolve && swift package clean && time swift build --product PrintAdd`. +License + +## License + +WasmKit runtime modules are licensed under MIT License. See [LICENSE](https://raw.githubusercontent.com/swiftwasm/WasmKit/refs/heads/main/LICENSE) file for license information. + +GDB Remote Protocol support (`GDBRemoteProtocol` and `WasmKitGDBHandler` modules) is licensed separately under Apache License v2.0 with Runtime Library Exception, Copyright 2025 Apple Inc. and the Swift project authors. + +See https://swift.org/LICENSE.txt for license information. + +See https://swift.org/CONTRIBUTORS.txt for Swift project authors. From d100d1ee65c7e715bf1866d0883f835210bfbc59 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 10:02:44 +0100 Subject: [PATCH 066/107] Add license files for separate modules --- Sources/GDBRemoteProtocol/LICENSE.txt | 211 +++++++++++++++++++++++ Sources/WasmKitGDBHandler/LICENSE.txt | 1 + Tests/GDBRemoteProtocolTests/LICENSE.txt | 1 + 3 files changed, 213 insertions(+) create mode 100644 Sources/GDBRemoteProtocol/LICENSE.txt create mode 120000 Sources/WasmKitGDBHandler/LICENSE.txt create mode 120000 Tests/GDBRemoteProtocolTests/LICENSE.txt diff --git a/Sources/GDBRemoteProtocol/LICENSE.txt b/Sources/GDBRemoteProtocol/LICENSE.txt new file mode 100644 index 00000000..61b0c781 --- /dev/null +++ b/Sources/GDBRemoteProtocol/LICENSE.txt @@ -0,0 +1,211 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +## Runtime Library Exception to the Apache 2.0 License: ## + + + As an exception, if you use this Software to compile your source code and + portions of this Software are embedded into the binary product as a result, + you may redistribute such product without providing attribution as would + otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. diff --git a/Sources/WasmKitGDBHandler/LICENSE.txt b/Sources/WasmKitGDBHandler/LICENSE.txt new file mode 120000 index 00000000..b25e6fe6 --- /dev/null +++ b/Sources/WasmKitGDBHandler/LICENSE.txt @@ -0,0 +1 @@ +../GDBRemoteProtocol/LICENSE.txt \ No newline at end of file diff --git a/Tests/GDBRemoteProtocolTests/LICENSE.txt b/Tests/GDBRemoteProtocolTests/LICENSE.txt new file mode 120000 index 00000000..9617e0aa --- /dev/null +++ b/Tests/GDBRemoteProtocolTests/LICENSE.txt @@ -0,0 +1 @@ +../../Sources/GDBRemoteProtocol/LICENSE.txt \ No newline at end of file From 2407284ec96aaca6c32087c7fd77f8327bc076b7 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 10:14:17 +0100 Subject: [PATCH 067/107] Basic doc comments for `GDBHostCommand` # Conflicts: # Sources/GDBRemoteProtocol/GDBHostCommand.swift --- Sources/GDBRemoteProtocol/GDBHostCommand.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index 278d07e5..04af72f2 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// A command sent from a debugger host (GDB or LLDB) to a debugger target (a device +/// or a virtual machine being debugged). /// See GDB and LLDB remote protocol documentation for more details: /// * https://sourceware.org/gdb/current/onlinedocs/gdb.html/General-Query-Packets.html /// * https://lldb.llvm.org/resources/lldbgdbremote.html @@ -31,6 +45,7 @@ package struct GDBHostCommand: Equatable { case generalRegisters + /// Decodes kind of a command from a raw string sent from a host. package init?(rawValue: String) { switch rawValue { case "g": @@ -74,8 +89,10 @@ package struct GDBHostCommand: Equatable { } } + /// The kind of a host command for the target to act upon. package let kind: Kind + /// Arguments supplied with a host command. package let arguments: String package init(kindString: String, arguments: String) throws { @@ -106,6 +123,7 @@ package struct GDBHostCommand: Equatable { self.arguments = arguments } + /// Memberwise initializer of `GDBHostCommand` type. package init(kind: Kind, arguments: String) { self.kind = kind self.arguments = arguments From f6282196c206f12f2f562859261f59b7b9d15f6b Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 10:40:43 +0100 Subject: [PATCH 068/107] Add doc comments for `GDBHostCommandDecoder` # Conflicts: # Sources/GDBRemoteProtocol/GDBPacket.swift --- .../GDBRemoteProtocol/GDBHostCommand.swift | 19 +++---- .../GDBHostCommandDecoder.swift | 55 +++++++++++++++---- Sources/GDBRemoteProtocol/GDBPacket.swift | 17 ++++++ 3 files changed, 71 insertions(+), 20 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index 04af72f2..3c3d4e11 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -16,11 +16,6 @@ /// * https://sourceware.org/gdb/current/onlinedocs/gdb.html/General-Query-Packets.html /// * https://lldb.llvm.org/resources/lldbgdbremote.html package struct GDBHostCommand: Equatable { - enum Error: Swift.Error { - case unexpectedArgumentsValue - case unknownCommand(kind: String, arguments: String) - } - package enum Kind: String, Equatable { // Currently listed in the order that LLDB sends them in. case startNoAckMode @@ -94,8 +89,12 @@ package struct GDBHostCommand: Equatable { /// Arguments supplied with a host command. package let arguments: String - - package init(kindString: String, arguments: String) throws { + + /// Initialize a host command from raw strings sent from a host. + /// - Parameters: + /// - kindString: raw ``String`` that denotes kind of the command. + /// - arguments: raw arguments that immediately follow kind of the command. + package init(kindString: String, arguments: String) throws(GDBHostCommandDecoder.Error) { let registerInfoPrefix = "qRegisterInfo" if kindString.starts(with: "x") { @@ -110,20 +109,20 @@ package struct GDBHostCommand: Equatable { self.kind = .registerInfo guard arguments.isEmpty else { - throw Error.unexpectedArgumentsValue + throw GDBHostCommandDecoder.Error.unexpectedArgumentsValue } self.arguments = String(kindString.dropFirst(registerInfoPrefix.count)) return } else if let kind = Kind(rawValue: kindString) { self.kind = kind } else { - throw Error.unknownCommand(kind: kindString, arguments: arguments) + throw GDBHostCommandDecoder.Error.unknownCommand(kind: kindString, arguments: arguments) } self.arguments = arguments } - /// Memberwise initializer of `GDBHostCommand` type. + /// Member-wise initializer of `GDBHostCommand` type. package init(kind: Kind, arguments: String) { self.kind = kind self.arguments = arguments diff --git a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift index bd5650e8..893d2ec2 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -2,36 +2,69 @@ import Logging import NIOCore extension ByteBuffer { + /// Returns `true` if byte to be read immediately is a GDB RP checksum + /// delimiter. Returns `false` otherwise. var isChecksumDelimiterAtReader: Bool { self.peekInteger(as: UInt8.self) == UInt8(ascii: "#") } + /// Returns `true` if byte to be read immediately is a GDB RP command arguments + /// delimiter. Returns `false` otherwise. var isArgumentsDelimiterAtReader: Bool { self.peekInteger(as: UInt8.self) == UInt8(ascii: ":") } } +/// Decoder of GDB RP host commands, that takes raw `ByteBuffer` as an input encoded +/// per https://sourceware.org/gdb/current/onlinedocs/gdb.html/Overview.html#Overview +/// and produces a `GDBPacket` value as output. This decoder is +/// compatible with NIO channel pipelines, making it easy to integrate with different +/// I/O configurations. package struct GDBHostCommandDecoder: ByteToMessageDecoder { - enum Error: Swift.Error { + /// Errors that can be thrown during host command decoding. + package enum Error: Swift.Error { + /// Expected `+` acknowledgement character to be included in the packet, when + /// ``GDBHostCommandDecoder/isNoAckModeActive`` is set to `false`. case expectedAck + + /// Expected command to start with `$` character`. case expectedCommandStart - case unknownCommandKind(String) + + /// Expected checksum to be included with the packet was not found. case expectedChecksum + + /// Expected checksum included with the packet did not match the expected value. case checksumIncorrect(expectedChecksum: Int, receivedChecksum: UInt8) + + /// Unexpected arguments value supplied for a given command. + case unexpectedArgumentsValue + + /// Host command kind could not be parsed. See `GDBHostCommand.Kind` for the + /// list of supported commands. + case unknownCommand(kind: String, arguments: String) } + /// Type of the output value produced by this decoder. package typealias InboundOut = GDBPacket private var accumulatedDelimiter: UInt8? private var accummulatedKind = [UInt8]() private var accummulatedArguments = [UInt8]() - + + /// Logger instance used by this decoder. private let logger: Logger - + + /// Initializes a new decoder. + /// - Parameter logger: logger instance that consumes messages from the newly + /// initialized decoder. package init(logger: Logger) { self.logger = logger } - + + /// Sum of the raw character values consumed in the current command so far, + /// used in checksum computation. private var accummulatedSum = 0 + + /// Computed checksum for the values consumed in the current command so far. package var accummulatedChecksum: UInt8 { UInt8(self.accummulatedSum % 256) } @@ -39,13 +72,15 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { private var isNoAckModeRequested = false private var isNoAckModeActive = false - mutating package func decode(buffer: inout ByteBuffer) throws -> GDBPacket? { + package mutating func decode( + buffer: inout ByteBuffer + ) throws(Error) -> GDBPacket? { guard var startDelimiter = self.accumulatedDelimiter ?? buffer.readInteger(as: UInt8.self) else { // Not enough data to parse. return nil } - if !isNoAckModeActive { + if !self.isNoAckModeActive { let firstStartDelimiter = startDelimiter guard firstStartDelimiter == UInt8(ascii: "+") else { @@ -53,7 +88,7 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { throw Error.expectedAck } - if isNoAckModeRequested { + if self.isNoAckModeRequested { self.isNoAckModeActive = true } @@ -70,7 +105,7 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { // Command start delimiters. guard startDelimiter == UInt8(ascii: "$") else { - logger.error("unexpected delimiter: \(Character(UnicodeScalar(startDelimiter)))") + self.logger.error("unexpected delimiter: \(Character(UnicodeScalar(startDelimiter)))") throw Error.expectedCommandStart } @@ -139,7 +174,7 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { mutating package func decode( context: ChannelHandlerContext, buffer: inout ByteBuffer - ) throws -> DecodingState { + ) throws(Error) -> DecodingState { logger.trace(.init(stringLiteral: buffer.peekString(length: buffer.readableBytes)!)) guard let command = try self.decode(buffer: &buffer) else { diff --git a/Sources/GDBRemoteProtocol/GDBPacket.swift b/Sources/GDBRemoteProtocol/GDBPacket.swift index 483e9cb1..1a1a65d7 100644 --- a/Sources/GDBRemoteProtocol/GDBPacket.swift +++ b/Sources/GDBRemoteProtocol/GDBPacket.swift @@ -1,3 +1,20 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// GDB host commands and target responses are wrapped with delimiters followed +/// by a single byte checksum value. This type denotes such a packet by attaching +/// a checksum value to the contained payload. +/// See GDB remote protocol overview for more details: +/// https://sourceware.org/gdb/current/onlinedocs/gdb.html/Overview.html#Overview package struct GDBPacket: Sendable { package let payload: Payload package let checksum: UInt8 From ab295db72d1235df30773d80060aecb16f3211d9 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 10:57:13 +0100 Subject: [PATCH 069/107] Add remaining doc comments --- .../GDBRemoteProtocol/GDBHostCommand.swift | 1 + .../GDBHostCommandDecoder.swift | 11 +++++++- .../GDBRemoteProtocol/GDBTargetResponse.swift | 28 +++++++++++++++++-- .../GDBTargetResponseEncoder.swift | 7 ++++- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index 3c3d4e11..5c4ff619 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -16,6 +16,7 @@ /// * https://sourceware.org/gdb/current/onlinedocs/gdb.html/General-Query-Packets.html /// * https://lldb.llvm.org/resources/lldbgdbremote.html package struct GDBHostCommand: Equatable { + /// Kind of the command sent from the debugger host to the debugger target. package enum Kind: String, Equatable { // Currently listed in the order that LLDB sends them in. case startNoAckMode diff --git a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift index 893d2ec2..0f6cc595 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -68,8 +68,17 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { package var accummulatedChecksum: UInt8 { UInt8(self.accummulatedSum % 256) } - + + /// Whether `QStartNoAckMode` command was sent. Note that this is separate + /// from ``isNoAckModeActive``. This mode is "activated" for the subsequent + /// host command, which is when `isNoAckModeActive` is set by the decoder to + /// `false`, but not for the immediate response. + /// See https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packet-Acknowledgment.html#Packet-Acknowledgment private var isNoAckModeRequested = false + + /// Whether `QStartNoAckMode` command was sent and this mode has been + /// subsequently activated. + /// See https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packet-Acknowledgment.html#Packet-Acknowledgment private var isNoAckModeActive = false package mutating func decode( diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift index d367cc29..36b44254 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift @@ -10,21 +10,43 @@ package enum VContActions: String { case stepInRange = "r" } +/// A response sent from a debugger target (a device +/// or a virtual machine being debugged) to a debugger host (GDB or LLDB). +/// See GDB remote protocol documentation for more details: +/// * https://sourceware.org/gdb/current/onlinedocs/gdb.html/General-Query-Packets.html package struct GDBTargetResponse { + /// Kind of the response sent from the debugger target to the debugger host. package enum Kind { + /// Standard `OK` response. case ok + + /// A list of key-value pairs, with keys delimited from values by a colon `:` + /// character, and pairs in the list delimited by the semicolon `;` character. case keyValuePairs(KeyValuePairs) + + /// List of ``VContActions`` values delimited by the semicolon `;` character. case vContSupportedActions([VContActions]) + + /// Raw string included as is in the response. case string(String) + + /// Binary buffer hex-encoded in the response. case hexEncodedBinary(ByteBufferView) + + /// Standard empty response (no content is sent). case empty } package let kind: Kind - package let isNoAckModeActivated: Bool - package init(kind: Kind, isNoAckModeActivated: Bool) { + /// Whether `QStartNoAckMode` is activated and no ack `+` symbol should be sent + /// before encoding this response. + /// See https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packet-Acknowledgment.html#Packet-Acknowledgment + package let isNoAckModeActivate: Bool + + /// Member-wise initializer for the debugger response. + package init(kind: Kind, isNoAckModeActivate: Bool) { self.kind = kind - self.isNoAckModeActivated = isNoAckModeActivated + self.isNoAckModeActivate = isNoAckModeActivate } } diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index 7a889334..a83c73a2 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -2,11 +2,16 @@ import Foundation import NIOCore extension String { + /// Computes a GDB RP checksum of characters in a given string. fileprivate var appendedChecksum: String { "\(self)#\(String(format:"%02X", self.utf8.reduce(0, { $0 + Int($1) }) % 256))" } } +/// Encoder of GDB RP target responses, that takes ``GDBTargetResponse`` as an input +/// and encodes it per https://sourceware.org/gdb/current/onlinedocs/gdb.html/Overview.html#Overview +/// format in a `ByteBuffer` value as output. This encoder is compatible with NIO channel pipelines, +/// making it easy to integrate with different I/O configurations. package class GDBTargetResponseEncoder: MessageToByteEncoder { private var isNoAckModeActive = false @@ -15,7 +20,7 @@ package class GDBTargetResponseEncoder: MessageToByteEncoder { if !isNoAckModeActive { out.writeInteger(UInt8(ascii: "+")) } - if data.isNoAckModeActivated { + if data.isNoAckModeActivate { self.isNoAckModeActive = true } out.writeInteger(UInt8(ascii: "$")) From e3505bc5e8d79b2e70cb7221b79bcb9f31019752 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 10:59:22 +0100 Subject: [PATCH 070/107] Fix formatting --- Sources/GDBRemoteProtocol/GDBHostCommand.swift | 2 +- Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift | 8 ++++---- Sources/GDBRemoteProtocol/GDBTargetResponse.swift | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index 5c4ff619..cf0276dc 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -90,7 +90,7 @@ package struct GDBHostCommand: Equatable { /// Arguments supplied with a host command. package let arguments: String - + /// Initialize a host command from raw strings sent from a host. /// - Parameters: /// - kindString: raw ``String`` that denotes kind of the command. diff --git a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift index 0f6cc595..06165d51 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -51,15 +51,15 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { private var accummulatedKind = [UInt8]() private var accummulatedArguments = [UInt8]() - + /// Logger instance used by this decoder. private let logger: Logger - + /// Initializes a new decoder. /// - Parameter logger: logger instance that consumes messages from the newly /// initialized decoder. package init(logger: Logger) { self.logger = logger } - + /// Sum of the raw character values consumed in the current command so far, /// used in checksum computation. private var accummulatedSum = 0 @@ -68,7 +68,7 @@ package struct GDBHostCommandDecoder: ByteToMessageDecoder { package var accummulatedChecksum: UInt8 { UInt8(self.accummulatedSum % 256) } - + /// Whether `QStartNoAckMode` command was sent. Note that this is separate /// from ``isNoAckModeActive``. This mode is "activated" for the subsequent /// host command, which is when `isNoAckModeActive` is set by the decoder to diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift index 36b44254..42c7639b 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift @@ -43,7 +43,7 @@ package struct GDBTargetResponse { /// before encoding this response. /// See https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packet-Acknowledgment.html#Packet-Acknowledgment package let isNoAckModeActivate: Bool - + /// Member-wise initializer for the debugger response. package init(kind: Kind, isNoAckModeActivate: Bool) { self.kind = kind From c4f9b3f46777a1bd8cff6da603e4c067af7a0478 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 11:06:39 +0100 Subject: [PATCH 071/107] Fix var naming typo --- Sources/GDBRemoteProtocol/GDBTargetResponse.swift | 6 +++--- Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift | 2 +- Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift | 7 +++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift index 42c7639b..d294b95b 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift @@ -42,11 +42,11 @@ package struct GDBTargetResponse { /// Whether `QStartNoAckMode` is activated and no ack `+` symbol should be sent /// before encoding this response. /// See https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packet-Acknowledgment.html#Packet-Acknowledgment - package let isNoAckModeActivate: Bool + package let isNoAckModeActive: Bool /// Member-wise initializer for the debugger response. - package init(kind: Kind, isNoAckModeActivate: Bool) { + package init(kind: Kind, isNoAckModeActive: Bool) { self.kind = kind - self.isNoAckModeActivate = isNoAckModeActivate + self.isNoAckModeActive = isNoAckModeActive } } diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index a83c73a2..36b3d734 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -20,7 +20,7 @@ package class GDBTargetResponseEncoder: MessageToByteEncoder { if !isNoAckModeActive { out.writeInteger(UInt8(ascii: "+")) } - if data.isNoAckModeActivate { + if data.isNoAckModeActive { self.isNoAckModeActive = true } out.writeInteger(UInt8(ascii: "$")) diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index fff3a9e5..55ff986f 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -55,10 +55,10 @@ let responseKind: GDBTargetResponse.Kind logger.trace("handling GDB host command", metadata: ["GDBHostCommand": .string(command.kind.rawValue)]) - var isNoAckModeActivated = false + var isNoAckModeActive = false switch command.kind { case .startNoAckMode: - isNoAckModeActivated = true + isNoAckModeActive = true fallthrough case .isThreadSuffixSupported, .listThreadsInStopReply: @@ -161,9 +161,8 @@ logger.trace("handler produced a response", metadata: ["GDBTargetResponse": .string("\(responseKind)")]) - return .init(kind: responseKind, isNoAckModeActivated: isNoAckModeActivated) + return .init(kind: responseKind, isNoAckModeActive: isNoAckModeActive) } - } #endif From f6bab201d96279a2dd0195e11d956b08a4b81f94 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 12:18:10 +0100 Subject: [PATCH 072/107] Remove unused `throws` in `GDBTargetResponseEncoder.swift` --- Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index 00da50e7..37b76dc3 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -28,7 +28,7 @@ package class GDBTargetResponseEncoder: MessageToByteEncoder { private var isNoAckModeActive = false package init() {} - package func encode(data: GDBTargetResponse, out: inout ByteBuffer) throws { + package func encode(data: GDBTargetResponse, out: inout ByteBuffer) { if !isNoAckModeActive { out.writeInteger(UInt8(ascii: "+")) } From f33dc82be64663b4ebe50cb0b96f565a67a48f13 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 12:18:10 +0100 Subject: [PATCH 073/107] Remove unused `throws` in `GDBTargetResponseEncoder.swift` --- Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index 36b3d734..a2987d15 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -16,7 +16,7 @@ package class GDBTargetResponseEncoder: MessageToByteEncoder { private var isNoAckModeActive = false package init() {} - package func encode(data: GDBTargetResponse, out: inout ByteBuffer) throws { + package func encode(data: GDBTargetResponse, out: inout ByteBuffer) { if !isNoAckModeActive { out.writeInteger(UInt8(ascii: "+")) } From dccc6af9422105bbb64b778086d49a31080c5196 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 16:28:03 +0100 Subject: [PATCH 074/107] Revert "Remove changes unrelated to the protocol" This reverts commit 16ac0cf65a2fd046ecc6765c918f8c442435e594. # Conflicts: # Sources/GDBRemoteProtocol/GDBHostCommand.swift # Sources/GDBRemoteProtocol/GDBPacket.swift --- Package@swift-6.1.swift | 13 +++ .../GDBHostCommandDecoder.swift | 12 -- .../GDBRemoteProtocol/GDBTargetResponse.swift | 12 -- .../GDBTargetResponseEncoder.swift | 12 -- Sources/WAT/Encoder.swift | 2 + Sources/WAT/Parser/WastParser.swift | 1 + Sources/WasmKit/Execution/Debugger.swift | 110 ++++++++++++++++++ Sources/WasmKit/Execution/Errors.swift | 12 +- Sources/WasmKit/Execution/Execution.swift | 22 ++-- Sources/WasmKit/Execution/Function.swift | 12 +- Sources/WasmKit/Execution/Instances.swift | 14 ++- .../WasmKit/Execution/StoreAllocator.swift | 8 +- Sources/WasmKit/Module.swift | 19 ++- Sources/WasmKit/Translator.swift | 63 ++++++++-- .../WasmKitGDBHandler/WasmKitGDBHandler.swift | 29 ++--- .../WasmParser/BinaryInstructionDecoder.swift | 4 + Sources/WasmParser/InstructionVisitor.swift | 3 + Sources/WasmParser/WasmParser.swift | 2 + Sources/WasmParser/WasmTypes.swift | 4 + Sources/wasmkit-gdb-tool/Entrypoint.swift | 110 ++++++++++++++++++ ...lTests.swift => RemoteProtocolTests.swift} | 14 +-- Tests/WasmKitTests/ExecutionTests.swift | 4 +- Utilities/Sources/WasmGen.swift | 7 ++ 23 files changed, 392 insertions(+), 97 deletions(-) create mode 100644 Sources/WasmKit/Execution/Debugger.swift create mode 100644 Sources/wasmkit-gdb-tool/Entrypoint.swift rename Tests/GDBRemoteProtocolTests/{GDBRemoteProtocolTests.swift => RemoteProtocolTests.swift} (68%) diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index d5b269ef..edafee37 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -178,6 +178,19 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { exclude: ["Fixtures"] ), + .executableTarget( + name: "wasmkit-gdb-tool", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Logging", package: "swift-log"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "SystemPackage", package: "swift-system"), + "GDBRemoteProtocol", + "WasmKitGDBHandler", + ] + ), + .target( name: "WasmKitGDBHandler", dependencies: [ diff --git a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift index 7e6c665f..06165d51 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -1,15 +1,3 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - import Logging import NIOCore diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift index 77391d73..d294b95b 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift @@ -1,15 +1,3 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - import NIOCore /// Actions supported in the `vCont` host command. diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index 37b76dc3..a2987d15 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -1,15 +1,3 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - import Foundation import NIOCore diff --git a/Sources/WAT/Encoder.swift b/Sources/WAT/Encoder.swift index 52b90740..baa28a3f 100644 --- a/Sources/WAT/Encoder.swift +++ b/Sources/WAT/Encoder.swift @@ -158,6 +158,7 @@ extension TableType: WasmEncodable { struct ElementExprCollector: AnyInstructionVisitor { typealias Output = Void + var binaryOffset: Int = 0 var isAllRefFunc: Bool = true var instructions: [Instruction] = [] @@ -443,6 +444,7 @@ extension WatParser.DataSegmentDecl { } struct ExpressionEncoder: BinaryInstructionEncoder { + var binaryOffset: Int = 0 var encoder = Encoder() var hasDataSegmentInstruction: Bool = false diff --git a/Sources/WAT/Parser/WastParser.swift b/Sources/WAT/Parser/WastParser.swift index cfc69fb1..70258283 100644 --- a/Sources/WAT/Parser/WastParser.swift +++ b/Sources/WAT/Parser/WastParser.swift @@ -54,6 +54,7 @@ struct WastParser { } struct ConstExpressionCollector: WastConstInstructionVisitor { + var binaryOffset: Int = 0 let addValue: (Value) -> Void mutating func visitI32Const(value: Int32) throws { addValue(.i32(UInt32(bitPattern: value))) } diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift new file mode 100644 index 00000000..49e27ac3 --- /dev/null +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -0,0 +1,110 @@ +#if WasmDebuggingSupport + + package struct Debugger: ~Copyable { + enum Error: Swift.Error { + case entrypointFunctionNotFound + case noInstructionMappingAvailable(Int) + } + + private let valueStack: Sp + private let execution: Execution + private let store: Store + + /// Parsed in-memory representation of a Wasm module instantiated for debugging. + private let module: Module + + /// Instance of parsed Wasm ``module``. + private let instance: Instance + + /// Reference to the entrypoint function of the currently debugged module, for use in ``stopAtEntrypoint``. + private let entrypointFunction: Function + + /// Threading model of the Wasm engine configuration cached for a potentially hot path. + private let threadingModel: EngineConfiguration.ThreadingModel + + private var breakpoints = [Int: CodeSlot]() + + package init(module: Module, store: Store, imports: Imports) throws { + let limit = store.engine.configuration.stackSize / MemoryLayout.stride + let instance = try module.instantiate(store: store, imports: imports, isDebuggable: true) + + guard case .function(let entrypointFunction) = instance.exports["_start"] else { + throw Error.entrypointFunctionNotFound + } + + self.instance = instance + self.module = module + self.entrypointFunction = entrypointFunction + self.valueStack = UnsafeMutablePointer.allocate(capacity: limit) + self.store = store + self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit)) + self.threadingModel = store.engine.configuration.threadingModel + } + + package mutating func stopAtEntrypoint() throws { + try self.enableBreakpoint(address: self.originalAddress(function: entrypointFunction)) + } + + package func originalAddress(function: Function) throws -> Int { + precondition(function.handle.isWasm) + + switch function.handle.wasm.code { + case .debuggable(let wasm, _): + return wasm.originalAddress + case .uncompiled: + try function.handle.wasm.ensureCompiled(store: StoreRef(self.store)) + return try self.originalAddress(function: function) + case .compiled: + fatalError() + } + } + + package mutating func enableBreakpoint(address: Int) throws { + print("attempt to toggle a breakpoint at \(address)") + + guard self.breakpoints[address] == nil else { + print("breakpoint at \(address) already enabled") + return + } + + guard let iseq = self.instance.handle.wasmToIseqMapping[address] else { + throw Error.noInstructionMappingAvailable(address) + } + self.breakpoints[address] = iseq.pointee + iseq.pointee = Instruction.breakpoint.headSlot(threadingModel: self.threadingModel) + } + + package mutating func disableBreakpoint(address: Int) throws { + print("attempt to toggle a breakpoint at \(address)") + + guard let oldCodeSlot = self.breakpoints[address] else { + print("breakpoint at \(address) already disabled") + return + } + + guard let iseq = self.instance.handle.wasmToIseqMapping[address] else { + throw Error.noInstructionMappingAvailable(address) + } + iseq.pointee = oldCodeSlot + self.breakpoints[address] = nil + } + + mutating func enableBreakpoint(functionIndex: FunctionIndex, offset: Int) { + } + + /// Array of addresses in the Wasm binary of executed instructions on the call stack. + package var currentCallStack: [Int] { + let isDebuggable = self.instance.handle.isDebuggable + print("isDebuggable is \(isDebuggable)") + + return Execution.captureBacktrace(sp: self.valueStack, store: self.store).symbols.map { + self.instance.handle.iseqToWasmMapping[$0.address]! + } + } + + deinit { + valueStack.deallocate() + } + } + +#endif diff --git a/Sources/WasmKit/Execution/Errors.swift b/Sources/WasmKit/Execution/Errors.swift index 07c1880b..5d714d02 100644 --- a/Sources/WasmKit/Execution/Errors.swift +++ b/Sources/WasmKit/Execution/Errors.swift @@ -5,19 +5,21 @@ import struct WasmParser.Import /// The backtrace of the trap. struct Backtrace: CustomStringConvertible, Sendable { /// A symbol in the backtrace. - struct Symbol { + struct Symbol: @unchecked Sendable { /// The name of the symbol. let name: String? + let address: Pc } /// The symbols in the backtrace. - let symbols: [Symbol?] + let symbols: [Symbol] /// Textual description of the backtrace. var description: String { - symbols.enumerated().map { (index, symbol) in - let name = symbol?.name ?? "unknown" - return " \(index): \(name)" + print("backtrace contains \(symbols.count) symbols") + return symbols.enumerated().map { (index, symbol) in + let name = symbol.name ?? "unknown" + return " \(index): (\(symbol.address)) \(name)" }.joined(separator: "\n") } } diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index b0d09dfa..ab29e86c 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -4,7 +4,7 @@ import _CWasmKit /// /// Each new invocation through exported function has a separate ``Execution`` /// even though the invocation happens during another invocation. -struct Execution { +struct Execution: ~Copyable { /// The reference to the ``Store`` associated with the execution. let store: StoreRef /// The end of the VM stack space. @@ -14,6 +14,13 @@ struct Execution { /// - Note: If the trap is set, it must be released manually. private var trap: (error: UnsafeRawPointer, sp: Sp)? = nil + #if WasmDebuggingSupport + package init(store: StoreRef, stackEnd: UnsafeMutablePointer) { + self.store = store + self.stackEnd = stackEnd + } + #endif + /// Executes the given closure with a new execution state associated with /// the given ``Store`` instance. static func with( @@ -61,18 +68,15 @@ struct Execution { static func captureBacktrace(sp: Sp, store: Store) -> Backtrace { var frames = FrameIterator(sp: sp) - var symbols: [Backtrace.Symbol?] = [] + var symbols: [Backtrace.Symbol] = [] + while let frame = frames.next() { guard let function = frame.function else { - symbols.append(nil) + symbols.append(.init(name: nil, address: frame.pc)) continue } let symbolName = store.nameRegistry.symbolicate(.wasm(function)) - symbols.append( - Backtrace.Symbol( - name: symbolName - ) - ) + symbols.append(.init(name: symbolName, address: frame.pc)) } return Backtrace(symbols: symbols) } @@ -248,7 +252,7 @@ extension Sp { nonmutating set { self[-1] = UInt64(UInt(bitPattern: newValue)) } } - fileprivate var currentInstance: InternalInstance? { + var currentInstance: InternalInstance? { currentFunction?.instance } } diff --git a/Sources/WasmKit/Execution/Function.swift b/Sources/WasmKit/Execution/Function.swift index 3786cf16..4af8c740 100644 --- a/Sources/WasmKit/Execution/Function.swift +++ b/Sources/WasmKit/Execution/Function.swift @@ -243,7 +243,7 @@ struct WasmFunctionEntity { switch code { case .uncompiled(let code): return try compile(store: store, code: code) - case .compiled(let iseq), .compiledAndPatchable(_, let iseq): + case .compiled(let iseq), .debuggable(_, let iseq): return iseq } } @@ -280,10 +280,14 @@ extension EntityHandle { case .uncompiled(let code): return try self.withValue { let iseq = try $0.compile(store: store, code: code) - $0.code = .compiled(iseq) + if $0.instance.isDebuggable { + $0.code = .debuggable(code, iseq) + } else { + $0.code = .compiled(iseq) + } return iseq } - case .compiled(let iseq), .compiledAndPatchable(_, let iseq): + case .compiled(let iseq), .debuggable(_, let iseq): return iseq } } @@ -316,7 +320,7 @@ struct InstructionSequence { enum CodeBody { case uncompiled(InternalUncompiledCode) case compiled(InstructionSequence) - case compiledAndPatchable(InternalUncompiledCode, InstructionSequence) + case debuggable(InternalUncompiledCode, InstructionSequence) } extension Reference { diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index 27a2452a..6ecdc5f1 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -83,6 +83,15 @@ struct InstanceEntity /* : ~Copyable */ { var functionRefs: Set var features: WasmFeatureSet var dataCount: UInt32? + var isDebuggable: Bool + + /// Mapping from iSeq Pc to instruction addresses in the original binary. + /// Used for handling current call stack requests issued by a ``Debugger`` instance. + var iseqToWasmMapping: [Pc: Int] + + /// Mapping from Wasm instruction addresses in the original binary to iSeq instruction addresses. + /// Used for handling breakpoint requests issued by a ``Debugger`` instance. + var wasmToIseqMapping: [Int: Pc] static var empty: InstanceEntity { InstanceEntity( @@ -96,7 +105,10 @@ struct InstanceEntity /* : ~Copyable */ { exports: [:], functionRefs: [], features: [], - dataCount: nil + dataCount: nil, + isDebuggable: false, + iseqToWasmMapping: [:], + wasmToIseqMapping: [:] ) } diff --git a/Sources/WasmKit/Execution/StoreAllocator.swift b/Sources/WasmKit/Execution/StoreAllocator.swift index f060dc56..dff3da90 100644 --- a/Sources/WasmKit/Execution/StoreAllocator.swift +++ b/Sources/WasmKit/Execution/StoreAllocator.swift @@ -251,7 +251,8 @@ extension StoreAllocator { module: Module, engine: Engine, resourceLimiter: any ResourceLimiter, - imports: Imports + imports: Imports, + isDebuggable: Bool ) throws -> InternalInstance { // Step 1 of module allocation algorithm, according to Wasm 2.0 spec. @@ -450,7 +451,10 @@ extension StoreAllocator { exports: exports, functionRefs: functionRefs, features: module.features, - dataCount: module.dataCount + dataCount: module.dataCount, + isDebuggable: isDebuggable, + iseqToWasmMapping: [:], + wasmToIseqMapping: [:] ) instancePointer.initialize(to: instanceEntity) instanceInitialized = true diff --git a/Sources/WasmKit/Module.swift b/Sources/WasmKit/Module.swift index 30070d1a..499cb93b 100644 --- a/Sources/WasmKit/Module.swift +++ b/Sources/WasmKit/Module.swift @@ -138,9 +138,23 @@ public struct Module { Instance(handle: try self.instantiateHandle(store: store, imports: imports), store: store) } + #if WasmDebuggingSupport + /// Instantiate this module in the given imports. + /// + /// - Parameters: + /// - store: The ``Store`` to allocate the instance in. + /// - imports: The imports to use for instantiation. All imported entities + /// must be allocated in the given store. + /// - isDebuggable: Whether the module should support debugging actions + /// (breakpoints etc) after instantiation. + public func instantiate(store: Store, imports: Imports = [:], isDebuggable: Bool) throws -> Instance { + Instance(handle: try self.instantiateHandle(store: store, imports: imports, isDebuggable: isDebuggable), store: store) + } + #endif + /// > Note: /// - private func instantiateHandle(store: Store, imports: Imports) throws -> InternalInstance { + private func instantiateHandle(store: Store, imports: Imports, isDebuggable: Bool = false) throws -> InternalInstance { try ModuleValidator(module: self).validate() // Steps 5-8. @@ -152,7 +166,8 @@ public struct Module { let instance = try store.allocator.allocate( module: self, engine: store.engine, resourceLimiter: store.resourceLimiter, - imports: imports + imports: imports, + isDebuggable: isDebuggable ) if let nameSection = customSections.first(where: { $0.name == "name" }) { diff --git a/Sources/WasmKit/Translator.swift b/Sources/WasmKit/Translator.swift index 6f3e1116..1254df1a 100644 --- a/Sources/WasmKit/Translator.swift +++ b/Sources/WasmKit/Translator.swift @@ -811,7 +811,7 @@ struct InstructionTranslator: InstructionVisitor { let allocator: ISeqAllocator let funcTypeInterner: Interner - let module: InternalInstance + var module: InternalInstance private var iseqBuilder: ISeqBuilder var controlStack: ControlStack var valueStack: ValueStack @@ -822,11 +822,19 @@ struct InstructionTranslator: InstructionVisitor { let functionIndex: FunctionIndex /// Whether a call to this function should be intercepted let isIntercepting: Bool - /// Whether Wasm debugging facilities are currently enabled. - let isDebugging: Bool var constantSlots: ConstSlots let validator: InstructionValidator + // Wasm debugging support. + + /// Current offset to an instruction in the original Wasm binary processed by this translator. + var binaryOffset: Int = 0 + + /// Mapping from `self.iseqBuilder.instructions` to Wasm instructions. + /// As mapping between iSeq to Wasm is many:many, but we only care about first mapping for overlapping address, + /// we need to iterate on it in the order the mappings were stored to ensure we don't overwrite the frist mapping. + var iseqToWasmMapping = [(iseq: Int, wasm: Int)]() + init( allocator: ISeqAllocator, engineConfiguration: EngineConfiguration, @@ -836,8 +844,7 @@ struct InstructionTranslator: InstructionVisitor { locals: [WasmTypes.ValueType], functionIndex: FunctionIndex, codeSize: Int, - isIntercepting: Bool, - isDebugging: Bool = false + isIntercepting: Bool ) throws { self.allocator = allocator self.funcTypeInterner = funcTypeInterner @@ -854,7 +861,6 @@ struct InstructionTranslator: InstructionVisitor { self.locals = Locals(types: type.parameters + locals) self.functionIndex = functionIndex self.isIntercepting = isIntercepting - self.isDebugging = isDebugging self.constantSlots = ConstSlots(stackLayout: stackLayout) self.validator = InstructionValidator(context: module) @@ -878,12 +884,14 @@ struct InstructionTranslator: InstructionVisitor { } private mutating func emit(_ instruction: Instruction, resultRelink: ISeqBuilder.ResultRelink? = nil) { + self.updateInstructionMapping() iseqBuilder.emit(instruction, resultRelink: resultRelink) } @discardableResult private mutating func emitCopyStack(from source: VReg, to dest: VReg) -> Bool { guard source != dest else { return false } + self.updateInstructionMapping() emit(.copyStack(Instruction.CopyStackOperand(source: LVReg(source), dest: LVReg(dest)))) return true } @@ -1069,6 +1077,7 @@ struct InstructionTranslator: InstructionVisitor { emit(.onExit(functionIndex)) } try visitReturnLike() + self.updateInstructionMapping() iseqBuilder.emit(._return) } private mutating func markUnreachable() throws { @@ -1088,9 +1097,22 @@ struct InstructionTranslator: InstructionVisitor { let instructions = iseqBuilder.finalize() // TODO: Figure out a way to avoid the copy here while keeping the execution performance. let buffer = allocator.allocateInstructions(capacity: instructions.count) - for (idx, instruction) in instructions.enumerated() { - buffer[idx] = instruction + let initializedElementsIndex = buffer.initialize(fromContentsOf: instructions) + assert(initializedElementsIndex == instructions.endIndex) + + for (iseq, wasm) in self.iseqToWasmMapping { + self.module.withValue { + let absoluteISeq = iseq + buffer.baseAddress.unsafelyUnwrapped + // Don't override the existing mapping, only store a new pair if there's no mapping for a given key. + if $0.iseqToWasmMapping[absoluteISeq] == nil { + $0.iseqToWasmMapping[absoluteISeq] = wasm + } + if $0.wasmToIseqMapping[wasm] == nil { + $0.wasmToIseqMapping[wasm] = absoluteISeq + } + } } + let constants = allocator.allocateConstants(self.constantSlots.values) return InstructionSequence( instructions: buffer, @@ -1099,6 +1121,15 @@ struct InstructionTranslator: InstructionVisitor { ) } + private mutating func updateInstructionMapping() { + // This is a hot path, so best to exclude the code altogether if the trait isn't enabled. + #if WasmDebuggingSupport + guard self.module.isDebuggable else { return } + + self.iseqToWasmMapping.append((self.iseqBuilder.insertingPC.offsetFromHead, self.binaryOffset)) + #endif + } + // MARK: Main entry point /// Translate a Wasm expression into a sequence of instructions. @@ -1126,7 +1157,9 @@ struct InstructionTranslator: InstructionVisitor { emit(.unreachable) try markUnreachable() } - mutating func visitNop() -> Output { emit(.nop) } + mutating func visitNop() -> Output { + emit(.nop) + } mutating func visitBlock(blockType: WasmParser.BlockType) throws -> Output { let blockType = try module.resolveBlockType(blockType) @@ -1173,6 +1206,7 @@ struct InstructionTranslator: InstructionVisitor { ) ) guard let condition = condition else { return } + self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.brIfNot, endLabel) { iseqBuilder, selfPC, endPC in let targetPC: MetaProgramCounter if let elsePC = iseqBuilder.resolveLabel(elseLabel) { @@ -1193,6 +1227,8 @@ struct InstructionTranslator: InstructionVisitor { preserveOnStack(depth: valueStack.height - frame.stackHeight) try controlStack.resetReachability() iseqBuilder.resetLastEmission() + + self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.br, endLabel) { _, selfPC, endPC in let offset = endPC.offsetFromHead - selfPC.offsetFromHead return Int32(offset) @@ -1288,6 +1324,8 @@ struct InstructionTranslator: InstructionVisitor { currentFrame: try controlStack.currentFrame(), currentHeight: valueStack.height ) + + self.updateInstructionMapping() iseqBuilder.emitWithLabel(makeInstruction, frame.continuation) { _, selfPC, continuation in let relativeOffset = continuation.offsetFromHead - selfPC.offsetFromHead return make(Int32(relativeOffset), UInt32(copyCount), popCount) @@ -1321,6 +1359,7 @@ struct InstructionTranslator: InstructionVisitor { if frame.copyCount == 0 { guard let condition else { return } // Optimization where we don't need copying values when the branch taken + self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.brIf, frame.continuation) { _, selfPC, continuation in let relativeOffset = continuation.offsetFromHead - selfPC.offsetFromHead return Instruction.BrIfOperand( @@ -1353,11 +1392,13 @@ struct InstructionTranslator: InstructionVisitor { // [0x06] (local.get 1 reg:2) <----|---------+ // [0x07] ... <-------+ let onBranchNotTaken = iseqBuilder.allocLabel() + self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.brIfNot, onBranchNotTaken) { _, conditionCheckAt, continuation in let relativeOffset = continuation.offsetFromHead - conditionCheckAt.offsetFromHead return Instruction.BrIfOperand(condition: LVReg(condition), offset: Int32(relativeOffset)) } try copyOnBranch(targetFrame: frame) + self.updateInstructionMapping() try emitBranch(Instruction.br, relativeDepth: relativeDepth) { offset, copyCount, popCount in return offset } @@ -1384,6 +1425,7 @@ struct InstructionTranslator: InstructionVisitor { baseAddress: tableBuffer.baseAddress!, count: UInt16(tableBuffer.count), index: index ) + self.updateInstructionMapping() iseqBuilder.emit(.brTable(operand)) let brTableAt = iseqBuilder.insertingPC @@ -1433,6 +1475,7 @@ struct InstructionTranslator: InstructionVisitor { } let emittedCopy = try copyOnBranch(targetFrame: frame) if emittedCopy { + self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.br, frame.continuation) { _, brAt, continuation in let relativeOffset = continuation.offsetFromHead - brAt.offsetFromHead return Int32(relativeOffset) @@ -2262,7 +2305,7 @@ struct InstructionTranslator: InstructionVisitor { } mutating func visitUnknown(_ opcode: [UInt8]) throws -> Bool { - guard self.isDebugging && opcode.count == 1 && opcode[0] == 0xFF else { + guard self.module.isDebuggable && opcode.count == 1 && opcode[0] == 0xFF else { return false } diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 5313bdd3..55ff986f 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -1,23 +1,13 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - #if WasmDebuggingSupport import GDBRemoteProtocol import Logging import NIOCore import NIOFileSystem + import Synchronization import SystemPackage import WasmKit + import WasmKitWASI extension BinaryInteger { init?(hexEncoded: Substring) { @@ -40,6 +30,7 @@ private let wasmBinary: ByteBuffer private let moduleFilePath: FilePath private let logger: Logger + private var debugger: Debugger private let functionsRLE: [(wasmAddress: Int, iSeqAddress: Int)] = [] package init(logger: Logger, moduleFilePath: FilePath) async throws { @@ -50,6 +41,14 @@ } self.moduleFilePath = moduleFilePath + + let store = Store(engine: Engine()) + var imports = Imports() + let wasi = try WASIBridgeToHost() + wasi.link(to: &imports, store: store) + + self.debugger = try Debugger(module: parseWasm(bytes: .init(buffer: self.wasmBinary)), store: store, imports: imports) + try self.debugger.stopAtEntrypoint() } package func handle(command: GDBHostCommand) throws -> GDBTargetResponse { @@ -152,7 +151,11 @@ responseKind = .hexEncodedBinary(wasmBinary.readableBytesView[binaryOffset..<(binaryOffset + length)]) - case .wasmCallStack, .generalRegisters: + case .wasmCallStack: + print(self.debugger.currentCallStack) + responseKind = .empty + + case .generalRegisters: fatalError() } diff --git a/Sources/WasmParser/BinaryInstructionDecoder.swift b/Sources/WasmParser/BinaryInstructionDecoder.swift index 5cc84908..a6a4ac6d 100644 --- a/Sources/WasmParser/BinaryInstructionDecoder.swift +++ b/Sources/WasmParser/BinaryInstructionDecoder.swift @@ -6,6 +6,9 @@ import WasmTypes @usableFromInline protocol BinaryInstructionDecoder { + /// Current offset in the decoded Wasm binary. + var offset: Int { get } + /// Claim the next byte to be decoded @inlinable func claimNextByte() throws -> UInt8 @@ -91,6 +94,7 @@ protocol BinaryInstructionDecoder { @inlinable func parseBinaryInstruction(visitor: inout some InstructionVisitor, decoder: inout some BinaryInstructionDecoder) throws -> Bool { + visitor.binaryOffset = decoder.offset let opcode0 = try decoder.claimNextByte() switch opcode0 { case 0x00: diff --git a/Sources/WasmParser/InstructionVisitor.swift b/Sources/WasmParser/InstructionVisitor.swift index 2a6b0271..b44d39a8 100644 --- a/Sources/WasmParser/InstructionVisitor.swift +++ b/Sources/WasmParser/InstructionVisitor.swift @@ -310,6 +310,9 @@ extension AnyInstructionVisitor { /// The visitor pattern is used while parsing WebAssembly expressions to allow for easy extensibility. /// See the expression parsing method ``Code/parseExpression(visitor:)`` public protocol InstructionVisitor { + /// Current offset in visitor's instruction stream. + var binaryOffset: Int { get set } + /// Visiting `unreachable` instruction. mutating func visitUnreachable() throws /// Visiting `nop` instruction. diff --git a/Sources/WasmParser/WasmParser.swift b/Sources/WasmParser/WasmParser.swift index 0bca6049..0b7c7199 100644 --- a/Sources/WasmParser/WasmParser.swift +++ b/Sources/WasmParser/WasmParser.swift @@ -757,6 +757,8 @@ extension Parser: BinaryInstructionDecoder { @usableFromInline struct InstructionFactory: AnyInstructionVisitor { + @usableFromInline var binaryOffset: Int = 0 + @usableFromInline var insts: [Instruction] = [] @inlinable init() {} diff --git a/Sources/WasmParser/WasmTypes.swift b/Sources/WasmParser/WasmTypes.swift index df95b6c2..153f8cb9 100644 --- a/Sources/WasmParser/WasmTypes.swift +++ b/Sources/WasmParser/WasmTypes.swift @@ -15,6 +15,10 @@ public struct Code { @usableFromInline internal let features: WasmFeatureSet + #if WasmDebuggingSupport + package var originalAddress: Int { self.offset } + #endif + @inlinable init(locals: [ValueType], expression: ArraySlice, offset: Int, features: WasmFeatureSet) { self.locals = locals diff --git a/Sources/wasmkit-gdb-tool/Entrypoint.swift b/Sources/wasmkit-gdb-tool/Entrypoint.swift new file mode 100644 index 00000000..ffe793ec --- /dev/null +++ b/Sources/wasmkit-gdb-tool/Entrypoint.swift @@ -0,0 +1,110 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2017-2025 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import GDBRemoteProtocol +import Logging +import NIOCore +import NIOPosix +import SystemPackage +import WasmKitGDBHandler + +@main +struct Entrypoint: AsyncParsableCommand { + @Option(help: "TCP port that a debugger can connect to") + var port = 8080 + + @Option( + name: .shortAndLong, + transform: { stringValue in + guard let logLevel = Logger.Level(rawValue: stringValue.lowercased()) else { + throw ValidationError("not a valid log level: \(stringValue)") + } + return logLevel + } + ) + var logLevel = Logger.Level.info + + @Argument(transform: { FilePath($0) }) + var wasmModulePath: FilePath + + func run() async throws { + #if WasmDebuggingSupport + let logger = { + var result = Logger(label: "org.swiftwasm.WasmKit") + result.logLevel = self.logLevel + return result + }() + + try await MultiThreadedEventLoopGroup.withEventLoopGroup(numberOfThreads: System.coreCount) { group in + let bootstrap = ServerBootstrap(group: group) + // Specify backlog and enable SO_REUSEADDR for the server itself + .serverChannelOption(.backlog, value: 256) + .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) + + // Set the handlers that are applied to the accepted child `Channel`s. + .childChannelInitializer { channel in + // Ensure we don't read faster then we can write by adding the BackPressureHandler into the pipeline. + channel.eventLoop.makeCompletedFuture { + try channel.pipeline.syncOperations.addHandler(BackPressureHandler()) + // make sure to instantiate your `ChannelHandlers` inside of + // the closure as it will be invoked once per connection. + try channel.pipeline.syncOperations.addHandlers([ + ByteToMessageHandler(GDBHostCommandDecoder(logger: logger)), + MessageToByteHandler(GDBTargetResponseEncoder()), + ]) + } + } + + // Enable SO_REUSEADDR for the accepted Channels + .childChannelOption(.socketOption(.so_reuseaddr), value: 1) + .childChannelOption(.maxMessagesPerRead, value: 16) + .childChannelOption(.recvAllocator, value: AdaptiveRecvByteBufferAllocator()) + + let serverChannel = try await bootstrap.bind(host: "127.0.0.1", port: port) { childChannel in + childChannel.eventLoop.makeCompletedFuture { + try NIOAsyncChannel, GDBTargetResponse>( + wrappingChannelSynchronously: childChannel + ) + } + } + /* the server will now be accepting connections */ + logger.info("listening on port \(port)") + + let debugger = try await WasmKitGDBHandler(logger: logger, moduleFilePath: self.wasmModulePath) + + try await withThrowingDiscardingTaskGroup { group in + try await serverChannel.executeThenClose { serverChannelInbound in + for try await connectionChannel in serverChannelInbound { + group.addTask { + do { + try await connectionChannel.executeThenClose { connectionChannelInbound, connectionChannelOutbound in + for try await inboundData in connectionChannelInbound { + // Let's echo back all inbound data + try await connectionChannelOutbound.write(debugger.handle(command: inboundData.payload)) + } + } + } catch { + logger.error("Error in GDB remote protocol connection channel", metadata: ["error": "\(error)"]) + } + } + } + } + } + } + #else + fatalError("Build WasmKit with `WasmDebuggingSupport` trait") + #endif + } +} diff --git a/Tests/GDBRemoteProtocolTests/GDBRemoteProtocolTests.swift b/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift similarity index 68% rename from Tests/GDBRemoteProtocolTests/GDBRemoteProtocolTests.swift rename to Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift index c01db004..4a30f0e1 100644 --- a/Tests/GDBRemoteProtocolTests/GDBRemoteProtocolTests.swift +++ b/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift @@ -1,22 +1,10 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - import GDBRemoteProtocol import Logging import NIOCore import Testing @Suite -struct GDBRemoteProtocolTests { +struct LLDBRemoteProtocolTests { @Test func decoding() throws { var logger = Logger(label: "com.swiftwasm.WasmKit.tests") diff --git a/Tests/WasmKitTests/ExecutionTests.swift b/Tests/WasmKitTests/ExecutionTests.swift index e5b61c87..42c8759d 100644 --- a/Tests/WasmKitTests/ExecutionTests.swift +++ b/Tests/WasmKitTests/ExecutionTests.swift @@ -111,7 +111,7 @@ """ ) { trap in #expect( - trap.backtrace?.symbols.compactMap(\.?.name) == [ + trap.backtrace?.symbols.compactMap(\.name) == [ "foo", "bar", "_start", @@ -138,7 +138,7 @@ """ ) { trap in #expect( - trap.backtrace?.symbols.compactMap(\.?.name) == [ + trap.backtrace?.symbols.compactMap(\.name) == [ "wasm function[1]", "bar", "_start", diff --git a/Utilities/Sources/WasmGen.swift b/Utilities/Sources/WasmGen.swift index af354614..1e217bda 100644 --- a/Utilities/Sources/WasmGen.swift +++ b/Utilities/Sources/WasmGen.swift @@ -96,6 +96,9 @@ enum WasmGen { /// The visitor pattern is used while parsing WebAssembly expressions to allow for easy extensibility. /// See the expression parsing method ``Code/parseExpression(visitor:)`` public protocol InstructionVisitor { + /// Current offset in visitor's instruction stream. + var binaryOffset: Int { get set } + """ for instruction in instructions.categorized { @@ -531,6 +534,9 @@ enum WasmGen { @usableFromInline protocol BinaryInstructionDecoder { + /// Current offset in the decoded Wasm binary. + var offset: Int { get } + /// Claim the next byte to be decoded @inlinable func claimNextByte() throws -> UInt8 @@ -562,6 +568,7 @@ enum WasmGen { @inlinable func parseBinaryInstruction(visitor: inout some InstructionVisitor, decoder: inout some BinaryInstructionDecoder) throws -> Bool { + visitor.binaryOffset = decoder.offset """ func renderSwitchCase(_ root: Trie, depth: Int = 0) { From a25399db5d15142aac3cf83c4339f0401acce6ed Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 22 Oct 2025 16:35:32 +0100 Subject: [PATCH 075/107] Restore base branch state --- .../GDBRemoteProtocol/GDBHostCommandDecoder.swift | 12 ++++++++++++ Sources/GDBRemoteProtocol/GDBTargetResponse.swift | 12 ++++++++++++ .../GDBTargetResponseEncoder.swift | 12 ++++++++++++ ...colTests.swift => GDBRemoteProtocolTests.swift} | 14 +++++++++++++- 4 files changed, 49 insertions(+), 1 deletion(-) rename Tests/GDBRemoteProtocolTests/{RemoteProtocolTests.swift => GDBRemoteProtocolTests.swift} (68%) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift index 06165d51..7e6c665f 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + import Logging import NIOCore diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift index d294b95b..77391d73 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + import NIOCore /// Actions supported in the `vCont` host command. diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index a2987d15..37b76dc3 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + import Foundation import NIOCore diff --git a/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift b/Tests/GDBRemoteProtocolTests/GDBRemoteProtocolTests.swift similarity index 68% rename from Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift rename to Tests/GDBRemoteProtocolTests/GDBRemoteProtocolTests.swift index 4a30f0e1..c01db004 100644 --- a/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift +++ b/Tests/GDBRemoteProtocolTests/GDBRemoteProtocolTests.swift @@ -1,10 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + import GDBRemoteProtocol import Logging import NIOCore import Testing @Suite -struct LLDBRemoteProtocolTests { +struct GDBRemoteProtocolTests { @Test func decoding() throws { var logger = Logger(label: "com.swiftwasm.WasmKit.tests") From ae91e24901e56f3a0a68093027960e5b9a5e33cb Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 23 Oct 2025 00:12:57 +0100 Subject: [PATCH 076/107] Add `DebuggerTests` test suite --- Sources/WasmKit/Execution/Debugger.swift | 12 ++++---- Sources/WasmKit/Execution/Errors.swift | 3 +- Tests/WasmKitTests/DebuggerTests.swift | 38 ++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 Tests/WasmKitTests/DebuggerTests.swift diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 49e27ac3..60c2f399 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -1,7 +1,7 @@ #if WasmDebuggingSupport package struct Debugger: ~Copyable { - enum Error: Swift.Error { + package enum Error: Swift.Error { case entrypointFunctionNotFound case noInstructionMappingAvailable(Int) } @@ -59,8 +59,9 @@ } } - package mutating func enableBreakpoint(address: Int) throws { + package mutating func enableBreakpoint(address: Int) throws(Error) { print("attempt to toggle a breakpoint at \(address)") + print("available mapping: \(self.instance.handle.wasmToIseqMapping)") guard self.breakpoints[address] == nil else { print("breakpoint at \(address) already enabled") @@ -74,7 +75,7 @@ iseq.pointee = Instruction.breakpoint.headSlot(threadingModel: self.threadingModel) } - package mutating func disableBreakpoint(address: Int) throws { + package mutating func disableBreakpoint(address: Int) throws(Error) { print("attempt to toggle a breakpoint at \(address)") guard let oldCodeSlot = self.breakpoints[address] else { @@ -85,11 +86,12 @@ guard let iseq = self.instance.handle.wasmToIseqMapping[address] else { throw Error.noInstructionMappingAvailable(address) } - iseq.pointee = oldCodeSlot self.breakpoints[address] = nil + iseq.pointee = oldCodeSlot } - mutating func enableBreakpoint(functionIndex: FunctionIndex, offset: Int) { + package func run() throws { + try self.entrypointFunction() } /// Array of addresses in the Wasm binary of executed instructions on the call stack. diff --git a/Sources/WasmKit/Execution/Errors.swift b/Sources/WasmKit/Execution/Errors.swift index 5d714d02..3d3996ad 100644 --- a/Sources/WasmKit/Execution/Errors.swift +++ b/Sources/WasmKit/Execution/Errors.swift @@ -16,8 +16,7 @@ struct Backtrace: CustomStringConvertible, Sendable { /// Textual description of the backtrace. var description: String { - print("backtrace contains \(symbols.count) symbols") - return symbols.enumerated().map { (index, symbol) in + symbols.enumerated().map { (index, symbol) in let name = symbol.name ?? "unknown" return " \(index): (\(symbol.address)) \(name)" }.joined(separator: "\n") diff --git a/Tests/WasmKitTests/DebuggerTests.swift b/Tests/WasmKitTests/DebuggerTests.swift new file mode 100644 index 00000000..321314c7 --- /dev/null +++ b/Tests/WasmKitTests/DebuggerTests.swift @@ -0,0 +1,38 @@ +#if WasmDebuggingSupport + +import Testing +import WAT +@testable import WasmKit + +private let trivialModuleWAT = """ +(module + (func (export "_start") (result i32) (local $x i32) + (i32.const 42) + (i32.const 0) + (i32.eqz) + (drop) + (local.set $x) + (local.get $x) + ) +) +""" + +@Suite +struct DebuggerTests { + @Test + func stopAtEntrypoint() throws { + let store = Store(engine: Engine()) + let bytes = try wat2wasm(trivialModuleWAT) + print(bytes.count) + let module = try parseWasm(bytes: bytes) + var debugger = try Debugger(module: module, store: store, imports: [:]) + + try debugger.stopAtEntrypoint() + + #expect(throws: Execution.Breakpoint.self) { + try debugger.run() + } + } +} + +#endif From d5d16009bc870811d19db0730baa0ea89c6a2ab7 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 23 Oct 2025 09:18:38 +0100 Subject: [PATCH 077/107] Fix build error, add debug logging --- Sources/WasmKit/Translator.swift | 5 +++++ Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift | 1 + 2 files changed, 6 insertions(+) diff --git a/Sources/WasmKit/Translator.swift b/Sources/WasmKit/Translator.swift index 1254df1a..a1d622e2 100644 --- a/Sources/WasmKit/Translator.swift +++ b/Sources/WasmKit/Translator.swift @@ -1127,6 +1127,7 @@ struct InstructionTranslator: InstructionVisitor { guard self.module.isDebuggable else { return } self.iseqToWasmMapping.append((self.iseqBuilder.insertingPC.offsetFromHead, self.binaryOffset)) + print("added iSeqToWasmMapping[\(self.iseqBuilder.insertingPC.offsetFromHead)] = \(self.binaryOffset)") #endif } @@ -1901,6 +1902,9 @@ struct InstructionTranslator: InstructionVisitor { } private mutating func visitConst(_ type: ValueType, _ value: Value) { + print("in \(#function) self.binaryOffset is \(self.binaryOffset)") + + // TODO: document this behavior if let constSlotIndex = constantSlots.allocate(value) { valueStack.pushConst(constSlotIndex, type: type) iseqBuilder.resetLastEmission() @@ -1908,6 +1912,7 @@ struct InstructionTranslator: InstructionVisitor { } let value = UntypedValue(value) let is32Bit = type == .i32 || type == .f32 + print("emitting const \(value)") if is32Bit { pushEmit( type, diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 5fb8c5b7..a737c6cd 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -6,6 +6,7 @@ import NIOFileSystem import SystemPackage import WasmKit + import WasmKitWASI extension BinaryInteger { init?(hexEncoded: Substring) { From 9d79c83ec7ec1e59086dbaa43d65a81e752dd264 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 23 Oct 2025 15:50:08 +0100 Subject: [PATCH 078/107] Fix `DebuggerTests` --- Sources/WasmKit/Execution/Debugger.swift | 54 +++++++++++++--- Sources/WasmKit/Execution/Instances.swift | 8 ++- Sources/WasmKit/Translator.swift | 5 +- Tests/WasmKitTests/DebuggerTests.swift | 78 +++++++++++++---------- 4 files changed, 97 insertions(+), 48 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 60c2f399..7f679b3b 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -1,5 +1,45 @@ #if WasmDebuggingSupport + extension [Int] { + func binarySearch(nextClosestTo value: Int) -> Int? { + switch self.count { + case 0: + return nil + default: + var slice = self[0.. 1 { + let middle = (slice.endIndex - slice.startIndex) / 2 + if slice[middle] < value { + // Not found anything in the lower half, assigning higher half to `slice`. + slice = slice[(middle + 1).. Pc { + // Look in the main mapping first + guard + let iseq = handle.wasmToIseqMapping[address] + // If nothing found, find the closest Wasm address using binary search + ?? handle.wasmMappings.binarySearch(nextClosestTo: address) + // Look in the main mapping again with the next closest address if binary search produced anything + .flatMap({ handle.wasmToIseqMapping[$0] }) + else { + throw Debugger.Error.noInstructionMappingAvailable(address) + } + + return iseq + } + } + package struct Debugger: ~Copyable { package enum Error: Swift.Error { case entrypointFunctionNotFound @@ -60,17 +100,12 @@ } package mutating func enableBreakpoint(address: Int) throws(Error) { - print("attempt to toggle a breakpoint at \(address)") - print("available mapping: \(self.instance.handle.wasmToIseqMapping)") - guard self.breakpoints[address] == nil else { - print("breakpoint at \(address) already enabled") return } - guard let iseq = self.instance.handle.wasmToIseqMapping[address] else { - throw Error.noInstructionMappingAvailable(address) - } + let iseq = try self.instance.findIseq(forWasmAddress: address) + self.breakpoints[address] = iseq.pointee iseq.pointee = Instruction.breakpoint.headSlot(threadingModel: self.threadingModel) } @@ -83,9 +118,8 @@ return } - guard let iseq = self.instance.handle.wasmToIseqMapping[address] else { - throw Error.noInstructionMappingAvailable(address) - } + let iseq = try self.instance.findIseq(forWasmAddress: address) + self.breakpoints[address] = nil iseq.pointee = oldCodeSlot } diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index 6ecdc5f1..e20a35a3 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -87,11 +87,15 @@ struct InstanceEntity /* : ~Copyable */ { /// Mapping from iSeq Pc to instruction addresses in the original binary. /// Used for handling current call stack requests issued by a ``Debugger`` instance. - var iseqToWasmMapping: [Pc: Int] + var iseqToWasmMapping = [Pc: Int]() /// Mapping from Wasm instruction addresses in the original binary to iSeq instruction addresses. /// Used for handling breakpoint requests issued by a ``Debugger`` instance. - var wasmToIseqMapping: [Int: Pc] + var wasmToIseqMapping = [Int: Pc]() + + /// Wasm addresses sorted in ascending order for binary search when of the next closest mapped + /// instruction, when no key is found in `wasmToIseqMapping`. + var wasmMappings = [Int]() static var empty: InstanceEntity { InstanceEntity( diff --git a/Sources/WasmKit/Translator.swift b/Sources/WasmKit/Translator.swift index a1d622e2..22b42c9a 100644 --- a/Sources/WasmKit/Translator.swift +++ b/Sources/WasmKit/Translator.swift @@ -1110,6 +1110,7 @@ struct InstructionTranslator: InstructionVisitor { if $0.wasmToIseqMapping[wasm] == nil { $0.wasmToIseqMapping[wasm] = absoluteISeq } + $0.wasmMappings.append(wasm) } } @@ -1127,7 +1128,6 @@ struct InstructionTranslator: InstructionVisitor { guard self.module.isDebuggable else { return } self.iseqToWasmMapping.append((self.iseqBuilder.insertingPC.offsetFromHead, self.binaryOffset)) - print("added iSeqToWasmMapping[\(self.iseqBuilder.insertingPC.offsetFromHead)] = \(self.binaryOffset)") #endif } @@ -1902,8 +1902,6 @@ struct InstructionTranslator: InstructionVisitor { } private mutating func visitConst(_ type: ValueType, _ value: Value) { - print("in \(#function) self.binaryOffset is \(self.binaryOffset)") - // TODO: document this behavior if let constSlotIndex = constantSlots.allocate(value) { valueStack.pushConst(constSlotIndex, type: type) @@ -1912,7 +1910,6 @@ struct InstructionTranslator: InstructionVisitor { } let value = UntypedValue(value) let is32Bit = type == .i32 || type == .f32 - print("emitting const \(value)") if is32Bit { pushEmit( type, diff --git a/Tests/WasmKitTests/DebuggerTests.swift b/Tests/WasmKitTests/DebuggerTests.swift index 321314c7..9caaf9a4 100644 --- a/Tests/WasmKitTests/DebuggerTests.swift +++ b/Tests/WasmKitTests/DebuggerTests.swift @@ -1,38 +1,52 @@ #if WasmDebuggingSupport -import Testing -import WAT -@testable import WasmKit - -private let trivialModuleWAT = """ -(module - (func (export "_start") (result i32) (local $x i32) - (i32.const 42) - (i32.const 0) - (i32.eqz) - (drop) - (local.set $x) - (local.get $x) - ) -) -""" - -@Suite -struct DebuggerTests { - @Test - func stopAtEntrypoint() throws { - let store = Store(engine: Engine()) - let bytes = try wat2wasm(trivialModuleWAT) - print(bytes.count) - let module = try parseWasm(bytes: bytes) - var debugger = try Debugger(module: module, store: store, imports: [:]) - - try debugger.stopAtEntrypoint() - - #expect(throws: Execution.Breakpoint.self) { - try debugger.run() + import Testing + import WAT + @testable import WasmKit + + private let trivialModuleWAT = """ + (module + (func (export "_start") (result i32) (local $x i32) + (i32.const 42) + (i32.const 0) + (i32.eqz) + (drop) + (local.set $x) + (local.get $x) + ) + ) + """ + + @Suite + struct DebuggerTests { + @Test + func stopAtEntrypoint() throws { + let store = Store(engine: Engine()) + let bytes = try wat2wasm(trivialModuleWAT) + print(bytes.count) + let module = try parseWasm(bytes: bytes) + var debugger = try Debugger(module: module, store: store, imports: [:]) + + try debugger.stopAtEntrypoint() + + #expect(throws: Execution.Breakpoint.self) { + try debugger.run() + } + } + + @Test + func binarySearch() throws { + #expect([Int]().binarySearch(nextClosestTo: 42) == nil) + + var result = try #require([1].binarySearch(nextClosestTo: 8)) + #expect(result == 1) + + result = try #require([9, 15, 37].binarySearch(nextClosestTo: 28)) + #expect(result == 37) + + result = try #require([9, 15, 37].binarySearch(nextClosestTo: 0)) + #expect(result == 9) } } -} #endif From f64bf23fa02590fa4db98e35cdd154d125744c63 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 23 Oct 2025 15:52:01 +0100 Subject: [PATCH 079/107] Remove `print` debugging --- Sources/WasmKit/Execution/Debugger.swift | 4 ---- Tests/WasmKitTests/DebuggerTests.swift | 1 - 2 files changed, 5 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 7f679b3b..f6744909 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -111,10 +111,7 @@ } package mutating func disableBreakpoint(address: Int) throws(Error) { - print("attempt to toggle a breakpoint at \(address)") - guard let oldCodeSlot = self.breakpoints[address] else { - print("breakpoint at \(address) already disabled") return } @@ -131,7 +128,6 @@ /// Array of addresses in the Wasm binary of executed instructions on the call stack. package var currentCallStack: [Int] { let isDebuggable = self.instance.handle.isDebuggable - print("isDebuggable is \(isDebuggable)") return Execution.captureBacktrace(sp: self.valueStack, store: self.store).symbols.map { self.instance.handle.iseqToWasmMapping[$0.address]! diff --git a/Tests/WasmKitTests/DebuggerTests.swift b/Tests/WasmKitTests/DebuggerTests.swift index 9caaf9a4..ac4b2f73 100644 --- a/Tests/WasmKitTests/DebuggerTests.swift +++ b/Tests/WasmKitTests/DebuggerTests.swift @@ -23,7 +23,6 @@ func stopAtEntrypoint() throws { let store = Store(engine: Engine()) let bytes = try wat2wasm(trivialModuleWAT) - print(bytes.count) let module = try parseWasm(bytes: bytes) var debugger = try Debugger(module: module, store: store, imports: [:]) From 8bf6849b4449d70d3e48305af82bbe83bd9a7a49 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 23 Oct 2025 18:33:03 +0100 Subject: [PATCH 080/107] Allocate and modify `Pc` directly as `Debugger` property --- Sources/WasmKit/Execution/Debugger.swift | 35 ++++++++++---- Sources/WasmKit/Execution/Execution.swift | 56 +++++++++++++++++++---- Tests/WasmKitTests/DebuggerTests.swift | 4 ++ 3 files changed, 78 insertions(+), 17 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index f6744909..2157d71f 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -46,8 +46,8 @@ case noInstructionMappingAvailable(Int) } - private let valueStack: Sp - private let execution: Execution + private var valueStack: Sp + private var execution: Execution private let store: Store /// Parsed in-memory representation of a Wasm module instantiated for debugging. @@ -62,7 +62,8 @@ /// Threading model of the Wasm engine configuration cached for a potentially hot path. private let threadingModel: EngineConfiguration.ThreadingModel - private var breakpoints = [Int: CodeSlot]() + private(set) var breakpoints = [Int: CodeSlot]() + private var pc: Pc package init(module: Module, store: Store, imports: Imports) throws { let limit = store.engine.configuration.stackSize / MemoryLayout.stride @@ -79,6 +80,13 @@ self.store = store self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit)) self.threadingModel = store.engine.configuration.threadingModel + let endOfExecution = Instruction.endOfExecution.headSlot( + threadingModel: threadingModel + ) + // TODO: clarify why `func executeWasm` allocates 2 Pc slots on the native stack + self.pc = Pc.allocate(capacity: 2) + self.pc[0] = endOfExecution + } package mutating func stopAtEntrypoint() throws { @@ -121,21 +129,32 @@ iseq.pointee = oldCodeSlot } - package func run() throws { - try self.entrypointFunction() + package mutating func run() throws { + try self.execution.executeWasm( + threadingModel: self.threadingModel, + function: self.entrypointFunction.handle, + type: self.entrypointFunction.type, + arguments: [], + sp: &self.valueStack, + pc: &self.pc + ) } /// Array of addresses in the Wasm binary of executed instructions on the call stack. package var currentCallStack: [Int] { let isDebuggable = self.instance.handle.isDebuggable - return Execution.captureBacktrace(sp: self.valueStack, store: self.store).symbols.map { - self.instance.handle.iseqToWasmMapping[$0.address]! + print(self.pc) + return Execution.captureBacktrace(sp: self.valueStack, store: self.store).symbols.compactMap { + print(self.instance.handle.iseqToWasmMapping) + print(self.instance.handle.wasmToIseqMapping) + return self.instance.handle.iseqToWasmMapping[$0.address] } } deinit { - valueStack.deallocate() + self.valueStack.deallocate() + self.pc.deallocate() } } diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index ab29e86c..669f5e0b 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -293,7 +293,7 @@ func executeWasm( return try Execution.with(store: store) { (stack, sp) in // Advance the stack pointer to be able to reference negative indices // for saving slots. - let sp = sp.advanced(by: FrameHeaderLayout.numberOfSavingSlots) + var sp = sp.advanced(by: FrameHeaderLayout.numberOfSavingSlots) // Mark root stack pointer and current function as nil. sp.previousSP = nil sp.currentFunction = nil @@ -305,9 +305,10 @@ func executeWasm( rootISeq[0] = Instruction.endOfExecution.headSlot( threadingModel: store.value.engine.configuration.threadingModel ) + var pc = rootISeq.baseAddress! try stack.execute( - sp: sp, - pc: rootISeq.baseAddress!, + sp: &sp, + pc: &pc, handle: handle, type: type ) @@ -318,6 +319,46 @@ func executeWasm( } } +#if WasmDebuggingSupport + extension Execution { + @inline(never) + mutating func executeWasm( + threadingModel: EngineConfiguration.ThreadingModel, + function handle: InternalFunction, + type: FunctionType, + arguments: [Value], + sp: inout Sp, + pc: inout Pc + ) throws -> [Value] { + // Advance the stack pointer to be able to reference negative indices + // for saving slots. + sp = sp.advanced(by: FrameHeaderLayout.numberOfSavingSlots) + // Mark root stack pointer and current function as nil. + sp.previousSP = nil + sp.currentFunction = nil + for (index, argument) in arguments.enumerated() { + sp[VReg(index)] = UntypedValue(argument) + } + + try withUnsafeTemporaryAllocation(of: CodeSlot.self, capacity: 2) { rootISeq in + rootISeq[0] = Instruction.endOfExecution.headSlot( + threadingModel: threadingModel + ) + pc = rootISeq.baseAddress! + try self.execute( + sp: &sp, + pc: &pc, + handle: handle, + type: type + ) + } + return type.results.enumerated().map { (i, type) in + sp[VReg(i)].cast(to: type) + } + } + } +#endif + extension Execution { /// A namespace for the "current memory" (Md and Ms) management. enum CurrentMemory { @@ -373,14 +414,12 @@ extension Execution { /// The entry point for the execution of the WebAssembly function. @inline(never) mutating func execute( - sp: Sp, pc: Pc, + sp: inout Sp, pc: inout Pc, handle: InternalFunction, type: FunctionType ) throws { - var sp: Sp = sp var md: Md = nil var ms: Ms = 0 - var pc = pc (pc, sp) = try invoke( function: handle, callerInstance: nil, @@ -390,7 +429,7 @@ extension Execution { do { switch self.store.value.engine.configuration.threadingModel { case .direct: - try runDirectThreaded(sp: sp, pc: pc, md: md, ms: ms) + try runDirectThreaded(sp: sp, pc: &pc, md: md, ms: ms) case .token: try runTokenThreaded(sp: &sp, pc: &pc, md: &md, ms: &ms) } @@ -402,12 +441,11 @@ extension Execution { /// Starts the main execution loop using the direct threading model. @inline(never) mutating func runDirectThreaded( - sp: Sp, pc: Pc, md: Md, ms: Ms + sp: Sp, pc: inout Pc, md: Md, ms: Ms ) throws { #if os(WASI) fatalError("Direct threading is not supported on WASI") #else - var pc = pc let handler = pc.read(wasmkit_tc_exec.self) wasmkit_tc_start(handler, sp, pc, md, ms, &self) if let (rawError, trappingSp) = self.trap { diff --git a/Tests/WasmKitTests/DebuggerTests.swift b/Tests/WasmKitTests/DebuggerTests.swift index ac4b2f73..627fae4b 100644 --- a/Tests/WasmKitTests/DebuggerTests.swift +++ b/Tests/WasmKitTests/DebuggerTests.swift @@ -31,6 +31,10 @@ #expect(throws: Execution.Breakpoint.self) { try debugger.run() } + + let callStack = debugger.currentCallStack + #expect(callStack.count == 1) + #expect(callStack.first == debugger.breakpoints.keys.first) } @Test From ea04cc3292483d52cb8f0b178d88dea50bdf64b4 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 23 Oct 2025 22:46:12 +0100 Subject: [PATCH 081/107] Rewind Pc back by 1 word after catching `Breakpoint` --- Sources/WasmKit/Execution/Debugger.swift | 67 ++++++++++++++---------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 2157d71f..8ceeb13e 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -24,19 +24,21 @@ } extension Instance { - func findIseq(forWasmAddress address: Int) throws(Debugger.Error) -> Pc { - // Look in the main mapping first - guard - let iseq = handle.wasmToIseqMapping[address] - // If nothing found, find the closest Wasm address using binary search - ?? handle.wasmMappings.binarySearch(nextClosestTo: address) - // Look in the main mapping again with the next closest address if binary search produced anything - .flatMap({ handle.wasmToIseqMapping[$0] }) + func findIseq(forWasmAddress address: Int) throws(Debugger.Error) -> (iseq: Pc, wasm: Int) { + // Look in the main mapping + if let iseq = handle.wasmToIseqMapping[address] { + return (iseq, address) + } + + // If nothing found, find the closest Wasm address using binary search + guard let nextAddress = handle.wasmMappings.binarySearch(nextClosestTo: address), + // Look in the main mapping again with the next closest address if binary search produced anything + let iseq = handle.wasmToIseqMapping[nextAddress] else { throw Debugger.Error.noInstructionMappingAvailable(address) } - return iseq + return (iseq, nextAddress) } } @@ -112,10 +114,12 @@ return } - let iseq = try self.instance.findIseq(forWasmAddress: address) + let (iseq, wasm) = try self.instance.findIseq(forWasmAddress: address) - self.breakpoints[address] = iseq.pointee + self.breakpoints[wasm] = iseq.pointee iseq.pointee = Instruction.breakpoint.headSlot(threadingModel: self.threadingModel) + + print("breakpoint enabled at \(iseq)") } package mutating func disableBreakpoint(address: Int) throws(Error) { @@ -123,38 +127,47 @@ return } - let iseq = try self.instance.findIseq(forWasmAddress: address) + let (iseq, wasm) = try self.instance.findIseq(forWasmAddress: address) - self.breakpoints[address] = nil + self.breakpoints[wasm] = nil iseq.pointee = oldCodeSlot } package mutating func run() throws { - try self.execution.executeWasm( - threadingModel: self.threadingModel, - function: self.entrypointFunction.handle, - type: self.entrypointFunction.type, - arguments: [], - sp: &self.valueStack, - pc: &self.pc - ) + do { + try self.execution.executeWasm( + threadingModel: self.threadingModel, + function: self.entrypointFunction.handle, + type: self.entrypointFunction.type, + arguments: [], + sp: &self.valueStack, + pc: &self.pc + ) + } catch _ as Execution.Breakpoint { + pc -= 1 + throw Execution.Breakpoint() + } } /// Array of addresses in the Wasm binary of executed instructions on the call stack. package var currentCallStack: [Int] { let isDebuggable = self.instance.handle.isDebuggable - print(self.pc) - return Execution.captureBacktrace(sp: self.valueStack, store: self.store).symbols.compactMap { - print(self.instance.handle.iseqToWasmMapping) - print(self.instance.handle.wasmToIseqMapping) + var result = Execution.captureBacktrace(sp: self.valueStack, store: self.store).symbols.compactMap { return self.instance.handle.iseqToWasmMapping[$0.address] } + if let wasmPc = self.instance.handle.iseqToWasmMapping[self.pc] { + result.append(wasmPc) + } + + print(result) + return result } deinit { - self.valueStack.deallocate() - self.pc.deallocate() + print("Debugger.deinit") + // self.valueStack.deallocate() + // self.pc.deallocate() } } From c4d3ac9eb7370b76731608567eca23f5448aeb49 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 24 Oct 2025 11:54:34 +0100 Subject: [PATCH 082/107] Revert "Allocate and modify `Pc` directly as `Debugger` property" This reverts commit 8bf6849b4449d70d3e48305af82bbe83bd9a7a49. # Conflicts: # Sources/WasmKit/Execution/Debugger.swift --- Sources/WasmKit/Execution/Debugger.swift | 42 ++++--------- Sources/WasmKit/Execution/Execution.swift | 60 ++++--------------- .../Execution/Instructions/Control.swift | 5 +- Tests/WasmKitTests/DebuggerTests.swift | 10 ++-- 4 files changed, 33 insertions(+), 84 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 8ceeb13e..db81d435 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -48,8 +48,8 @@ case noInstructionMappingAvailable(Int) } - private var valueStack: Sp - private var execution: Execution + private let valueStack: Sp + private let execution: Execution private let store: Store /// Parsed in-memory representation of a Wasm module instantiated for debugging. @@ -65,7 +65,8 @@ private let threadingModel: EngineConfiguration.ThreadingModel private(set) var breakpoints = [Int: CodeSlot]() - private var pc: Pc + + private var iseqPc: Pc? package init(module: Module, store: Store, imports: Imports) throws { let limit = store.engine.configuration.stackSize / MemoryLayout.stride @@ -82,13 +83,6 @@ self.store = store self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit)) self.threadingModel = store.engine.configuration.threadingModel - let endOfExecution = Instruction.endOfExecution.headSlot( - threadingModel: threadingModel - ) - // TODO: clarify why `func executeWasm` allocates 2 Pc slots on the native stack - self.pc = Pc.allocate(capacity: 2) - self.pc[0] = endOfExecution - } package mutating func stopAtEntrypoint() throws { @@ -118,8 +112,6 @@ self.breakpoints[wasm] = iseq.pointee iseq.pointee = Instruction.breakpoint.headSlot(threadingModel: self.threadingModel) - - print("breakpoint enabled at \(iseq)") } package mutating func disableBreakpoint(address: Int) throws(Error) { @@ -133,19 +125,14 @@ iseq.pointee = oldCodeSlot } - package mutating func run() throws { + /// Returns: `true` if current instance ran to completion, `false` if it stopped at a breakpoint. + package mutating func run() throws -> Bool { do { - try self.execution.executeWasm( - threadingModel: self.threadingModel, - function: self.entrypointFunction.handle, - type: self.entrypointFunction.type, - arguments: [], - sp: &self.valueStack, - pc: &self.pc - ) - } catch _ as Execution.Breakpoint { - pc -= 1 - throw Execution.Breakpoint() + try self.entrypointFunction() + return true + } catch let breakpoint as Execution.Breakpoint { + self.iseqPc = breakpoint.pc + return false } } @@ -156,18 +143,15 @@ var result = Execution.captureBacktrace(sp: self.valueStack, store: self.store).symbols.compactMap { return self.instance.handle.iseqToWasmMapping[$0.address] } - if let wasmPc = self.instance.handle.iseqToWasmMapping[self.pc] { + if let iseqPc, let wasmPc = self.instance.handle.iseqToWasmMapping[iseqPc] { result.append(wasmPc) } - print(result) return result } deinit { - print("Debugger.deinit") - // self.valueStack.deallocate() - // self.pc.deallocate() + valueStack.deallocate() } } diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index 669f5e0b..fdb9ea4c 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -293,7 +293,7 @@ func executeWasm( return try Execution.with(store: store) { (stack, sp) in // Advance the stack pointer to be able to reference negative indices // for saving slots. - var sp = sp.advanced(by: FrameHeaderLayout.numberOfSavingSlots) + let sp = sp.advanced(by: FrameHeaderLayout.numberOfSavingSlots) // Mark root stack pointer and current function as nil. sp.previousSP = nil sp.currentFunction = nil @@ -305,10 +305,9 @@ func executeWasm( rootISeq[0] = Instruction.endOfExecution.headSlot( threadingModel: store.value.engine.configuration.threadingModel ) - var pc = rootISeq.baseAddress! try stack.execute( - sp: &sp, - pc: &pc, + sp: sp, + pc: rootISeq.baseAddress!, handle: handle, type: type ) @@ -319,46 +318,6 @@ func executeWasm( } } -#if WasmDebuggingSupport - extension Execution { - @inline(never) - mutating func executeWasm( - threadingModel: EngineConfiguration.ThreadingModel, - function handle: InternalFunction, - type: FunctionType, - arguments: [Value], - sp: inout Sp, - pc: inout Pc - ) throws -> [Value] { - // Advance the stack pointer to be able to reference negative indices - // for saving slots. - sp = sp.advanced(by: FrameHeaderLayout.numberOfSavingSlots) - // Mark root stack pointer and current function as nil. - sp.previousSP = nil - sp.currentFunction = nil - for (index, argument) in arguments.enumerated() { - sp[VReg(index)] = UntypedValue(argument) - } - - try withUnsafeTemporaryAllocation(of: CodeSlot.self, capacity: 2) { rootISeq in - rootISeq[0] = Instruction.endOfExecution.headSlot( - threadingModel: threadingModel - ) - pc = rootISeq.baseAddress! - try self.execute( - sp: &sp, - pc: &pc, - handle: handle, - type: type - ) - } - return type.results.enumerated().map { (i, type) in - sp[VReg(i)].cast(to: type) - } - } - } -#endif - extension Execution { /// A namespace for the "current memory" (Md and Ms) management. enum CurrentMemory { @@ -409,17 +368,21 @@ extension Execution { struct EndOfExecution: Error {} /// An ``Error`` thrown when a breakpoint is triggered. - struct Breakpoint: Error {} + struct Breakpoint: Error, @unchecked Sendable { + let pc: Pc + } /// The entry point for the execution of the WebAssembly function. @inline(never) mutating func execute( - sp: inout Sp, pc: inout Pc, + sp: Sp, pc: Pc, handle: InternalFunction, type: FunctionType ) throws { + var sp: Sp = sp var md: Md = nil var ms: Ms = 0 + var pc = pc (pc, sp) = try invoke( function: handle, callerInstance: nil, @@ -429,7 +392,7 @@ extension Execution { do { switch self.store.value.engine.configuration.threadingModel { case .direct: - try runDirectThreaded(sp: sp, pc: &pc, md: md, ms: ms) + try runDirectThreaded(sp: sp, pc: pc, md: md, ms: ms) case .token: try runTokenThreaded(sp: &sp, pc: &pc, md: &md, ms: &ms) } @@ -441,11 +404,12 @@ extension Execution { /// Starts the main execution loop using the direct threading model. @inline(never) mutating func runDirectThreaded( - sp: Sp, pc: inout Pc, md: Md, ms: Ms + sp: Sp, pc: Pc, md: Md, ms: Ms ) throws { #if os(WASI) fatalError("Direct threading is not supported on WASI") #else + var pc = pc let handler = pc.read(wasmkit_tc_exec.self) wasmkit_tc_start(handler, sp, pc, md, ms, &self) if let (rawError, trappingSp) = self.trap { diff --git a/Sources/WasmKit/Execution/Instructions/Control.swift b/Sources/WasmKit/Execution/Instructions/Control.swift index a3cdedba..d4a51abc 100644 --- a/Sources/WasmKit/Execution/Instructions/Control.swift +++ b/Sources/WasmKit/Execution/Instructions/Control.swift @@ -225,6 +225,9 @@ extension Execution { } mutating func breakpoint(sp: inout Sp, pc: Pc) throws -> (Pc, CodeSlot) { - throw Breakpoint() + throw Breakpoint( + // Throw `pc` value before the breakpoint was triggered to allow resumption in same place + pc: pc - 1 + ) } } diff --git a/Tests/WasmKitTests/DebuggerTests.swift b/Tests/WasmKitTests/DebuggerTests.swift index 627fae4b..eaf512f2 100644 --- a/Tests/WasmKitTests/DebuggerTests.swift +++ b/Tests/WasmKitTests/DebuggerTests.swift @@ -27,14 +27,12 @@ var debugger = try Debugger(module: module, store: store, imports: [:]) try debugger.stopAtEntrypoint() + #expect(debugger.breakpoints.count == 1) - #expect(throws: Execution.Breakpoint.self) { - try debugger.run() - } + #expect(try debugger.run() == false) - let callStack = debugger.currentCallStack - #expect(callStack.count == 1) - #expect(callStack.first == debugger.breakpoints.keys.first) + let expectedPc = try #require(debugger.breakpoints.keys.first) + #expect(debugger.currentCallStack == [expectedPc]) } @Test From 6246506d24b4ac95949145fb5ddf55d8a8a76ca3 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 24 Oct 2025 13:28:09 +0100 Subject: [PATCH 083/107] Fully implement `qWasmCallStack` --- .../WasmKitGDBHandler/WasmKitGDBHandler.swift | 17 ++++++++++++----- Sources/wasmkit-gdb-tool/Entrypoint.swift | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index a737c6cd..410e71f6 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -20,6 +20,8 @@ } } + private let codeOffset = UInt64(0x4000_0000_0000_0000) + package actor WasmKitGDBHandler { enum Error: Swift.Error { case unknownTransferArguments @@ -29,11 +31,12 @@ private let wasmBinary: ByteBuffer private let moduleFilePath: FilePath private let logger: Logger + private let allocator: ByteBufferAllocator private var debugger: Debugger - private let functionsRLE: [(wasmAddress: Int, iSeqAddress: Int)] = [] - package init(logger: Logger, moduleFilePath: FilePath) async throws { + package init(moduleFilePath: FilePath, logger: Logger, allocator: ByteBufferAllocator) async throws { self.logger = logger + self.allocator = allocator self.wasmBinary = try await FileSystem.shared.withFileHandle(forReadingAt: moduleFilePath) { try await $0.readToEnd(maximumSizeAllowed: .unlimited) @@ -142,7 +145,7 @@ var length = Int(hexEncoded: argumentsArray[1]) else { throw Error.unknownReadMemoryArguments } - let binaryOffset = Int(address - 0x4000_0000_0000_0000) + let binaryOffset = Int(address - codeOffset) if binaryOffset + length > wasmBinary.readableBytes { length = wasmBinary.readableBytes - binaryOffset @@ -151,8 +154,12 @@ responseKind = .hexEncodedBinary(wasmBinary.readableBytesView[binaryOffset..<(binaryOffset + length)]) case .wasmCallStack: - print(self.debugger.currentCallStack) - responseKind = .empty + let callStack = self.debugger.currentCallStack + var buffer = self.allocator.buffer(capacity: callStack.count * 8) + for pc in callStack { + buffer.writeInteger(UInt64(pc) + codeOffset, endianness: .little) + } + responseKind = .hexEncodedBinary(buffer.readableBytesView) case .generalRegisters: fatalError() diff --git a/Sources/wasmkit-gdb-tool/Entrypoint.swift b/Sources/wasmkit-gdb-tool/Entrypoint.swift index ffe793ec..ca482933 100644 --- a/Sources/wasmkit-gdb-tool/Entrypoint.swift +++ b/Sources/wasmkit-gdb-tool/Entrypoint.swift @@ -82,7 +82,7 @@ struct Entrypoint: AsyncParsableCommand { /* the server will now be accepting connections */ logger.info("listening on port \(port)") - let debugger = try await WasmKitGDBHandler(logger: logger, moduleFilePath: self.wasmModulePath) + let debugger = try await WasmKitGDBHandler(moduleFilePath: self.wasmModulePath, logger: logger, allocator: serverChannel.channel.allocator) try await withThrowingDiscardingTaskGroup { group in try await serverChannel.executeThenClose { serverChannelInbound in From 27dbbc72ee53f2ca4bb0576084f435b6a2326d29 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 24 Oct 2025 14:58:26 +0100 Subject: [PATCH 084/107] Pass `Sp` in `Breakpoint`, add `executeWasm` instance method --- Sources/WasmKit/Execution/Debugger.swift | 36 ++++++++++++------ Sources/WasmKit/Execution/Execution.swift | 37 ++++++++++++++++++- Sources/WasmKit/Execution/Function.swift | 3 +- .../Execution/Instructions/Control.swift | 1 + Tests/WasmKitTests/DebuggerTests.swift | 2 +- 5 files changed, 63 insertions(+), 16 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index db81d435..84ad8205 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -49,7 +49,7 @@ } private let valueStack: Sp - private let execution: Execution + private var execution: Execution private let store: Store /// Parsed in-memory representation of a Wasm module instantiated for debugging. @@ -66,7 +66,9 @@ private(set) var breakpoints = [Int: CodeSlot]() - private var iseqPc: Pc? + private var currentBreakpoint: Execution.Breakpoint? + + private var pc = Pc.allocate(capacity: 1) package init(module: Module, store: Store, imports: Imports) throws { let limit = store.engine.configuration.stackSize / MemoryLayout.stride @@ -83,6 +85,7 @@ self.store = store self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit)) self.threadingModel = store.engine.configuration.threadingModel + self.pc.pointee = Instruction.endOfExecution.headSlot(threadingModel: threadingModel) } package mutating func stopAtEntrypoint() throws { @@ -125,25 +128,33 @@ iseq.pointee = oldCodeSlot } - /// Returns: `true` if current instance ran to completion, `false` if it stopped at a breakpoint. - package mutating func run() throws -> Bool { + /// Returns: `[Value]` result of `entrypointFunction` if current instance ran to completion, `nil` if it stopped at a breakpoint. + package mutating func run() throws -> [Value]? { do { - try self.entrypointFunction() - return true + return try self.execution.executeWasm( + threadingModel: self.threadingModel, + function: self.entrypointFunction.handle, + type: self.entrypointFunction.type, + arguments: [], + sp: self.valueStack, + pc: self.pc + ) } catch let breakpoint as Execution.Breakpoint { - self.iseqPc = breakpoint.pc - return false + self.currentBreakpoint = breakpoint + return nil } } /// Array of addresses in the Wasm binary of executed instructions on the call stack. package var currentCallStack: [Int] { - let isDebuggable = self.instance.handle.isDebuggable + guard let currentBreakpoint else { + return [] + } - var result = Execution.captureBacktrace(sp: self.valueStack, store: self.store).symbols.compactMap { + var result = Execution.captureBacktrace(sp: currentBreakpoint.sp, store: self.store).symbols.compactMap { return self.instance.handle.iseqToWasmMapping[$0.address] } - if let iseqPc, let wasmPc = self.instance.handle.iseqToWasmMapping[iseqPc] { + if let wasmPc = self.instance.handle.iseqToWasmMapping[currentBreakpoint.pc] { result.append(wasmPc) } @@ -151,7 +162,8 @@ } deinit { - valueStack.deallocate() + self.valueStack.deallocate() + self.pc.deallocate() } } diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index fdb9ea4c..c100c334 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -286,7 +286,6 @@ func executeWasm( function handle: InternalFunction, type: FunctionType, arguments: [Value], - callerInstance: InternalInstance ) throws -> [Value] { // NOTE: `store` variable must not outlive this function let store = StoreRef(store) @@ -319,6 +318,41 @@ func executeWasm( } extension Execution { + + #if WasmDebuggingSupport + + mutating func executeWasm( + threadingModel: EngineConfiguration.ThreadingModel, + function handle: InternalFunction, + type: FunctionType, + arguments: [Value], + sp: Sp, + pc: Pc + ) throws -> [Value] { + // Advance the stack pointer to be able to reference negative indices + // for saving slots. + let sp = sp.advanced(by: FrameHeaderLayout.numberOfSavingSlots) + // Mark root stack pointer and current function as nil. + sp.previousSP = nil + sp.currentFunction = nil + for (index, argument) in arguments.enumerated() { + sp[VReg(index)] = UntypedValue(argument) + } + + try self.execute( + sp: sp, + pc: pc, + handle: handle, + type: type + ) + + return type.results.enumerated().map { (i, type) in + sp[VReg(i)].cast(to: type) + } + } + + #endif + /// A namespace for the "current memory" (Md and Ms) management. enum CurrentMemory { /// Assigns the current memory to the given internal memory. @@ -369,6 +403,7 @@ extension Execution { /// An ``Error`` thrown when a breakpoint is triggered. struct Breakpoint: Error, @unchecked Sendable { + let sp: Sp let pc: Pc } diff --git a/Sources/WasmKit/Execution/Function.swift b/Sources/WasmKit/Execution/Function.swift index 4af8c740..93aa8bde 100644 --- a/Sources/WasmKit/Execution/Function.swift +++ b/Sources/WasmKit/Execution/Function.swift @@ -172,8 +172,7 @@ extension InternalFunction { store: store, function: self, type: resolvedType, - arguments: arguments, - callerInstance: entity.instance + arguments: arguments ) } else { let entity = host diff --git a/Sources/WasmKit/Execution/Instructions/Control.swift b/Sources/WasmKit/Execution/Instructions/Control.swift index d4a51abc..94f12535 100644 --- a/Sources/WasmKit/Execution/Instructions/Control.swift +++ b/Sources/WasmKit/Execution/Instructions/Control.swift @@ -226,6 +226,7 @@ extension Execution { mutating func breakpoint(sp: inout Sp, pc: Pc) throws -> (Pc, CodeSlot) { throw Breakpoint( + sp: sp, // Throw `pc` value before the breakpoint was triggered to allow resumption in same place pc: pc - 1 ) diff --git a/Tests/WasmKitTests/DebuggerTests.swift b/Tests/WasmKitTests/DebuggerTests.swift index eaf512f2..da9ebd2a 100644 --- a/Tests/WasmKitTests/DebuggerTests.swift +++ b/Tests/WasmKitTests/DebuggerTests.swift @@ -29,7 +29,7 @@ try debugger.stopAtEntrypoint() #expect(debugger.breakpoints.count == 1) - #expect(try debugger.run() == false) + #expect(try debugger.run() == nil) let expectedPc = try #require(debugger.breakpoints.keys.first) #expect(debugger.currentCallStack == [expectedPc]) From 036fcb865626b00f794251d6bacc9dc1f871cc0f Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 24 Oct 2025 14:59:49 +0100 Subject: [PATCH 085/107] Fix license header diff --- Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 410e71f6..d7feffda 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + #if WasmDebuggingSupport import GDBRemoteProtocol From a57ec75f3a29f0b4cede9cfa92cb13bc03e0bdb0 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 24 Oct 2025 15:14:39 +0100 Subject: [PATCH 086/107] Fix trailing comma compatibility with Swift 6.0 --- Sources/WasmKit/Execution/Execution.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index c100c334..f400fdda 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -285,7 +285,7 @@ func executeWasm( store: Store, function handle: InternalFunction, type: FunctionType, - arguments: [Value], + arguments: [Value] ) throws -> [Value] { // NOTE: `store` variable must not outlive this function let store = StoreRef(store) From 935f877f9abcaa876d1850f303eaa74501cd949b Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 24 Oct 2025 16:35:43 +0100 Subject: [PATCH 087/107] Fix breakpoint resumption test expectation --- Sources/WasmKit/Execution/Debugger.swift | 70 ++++++++++++++++++----- Sources/WasmKit/Execution/Execution.swift | 5 ++ Tests/WasmKitTests/DebuggerTests.swift | 2 + 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 84ad8205..11dd7def 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -45,7 +45,9 @@ package struct Debugger: ~Copyable { package enum Error: Swift.Error { case entrypointFunctionNotFound + case unknownCurrentFunctionForResumedBreakpoint(UnsafeMutablePointer) case noInstructionMappingAvailable(Int) + case noReverseInstructionMappingAvailable(UnsafeMutablePointer) } private let valueStack: Sp @@ -66,7 +68,7 @@ private(set) var breakpoints = [Int: CodeSlot]() - private var currentBreakpoint: Execution.Breakpoint? + private var currentBreakpoint: (iseq: Execution.Breakpoint, wasmPc: Int)? private var pc = Pc.allocate(capacity: 1) @@ -131,16 +133,58 @@ /// Returns: `[Value]` result of `entrypointFunction` if current instance ran to completion, `nil` if it stopped at a breakpoint. package mutating func run() throws -> [Value]? { do { - return try self.execution.executeWasm( - threadingModel: self.threadingModel, - function: self.entrypointFunction.handle, - type: self.entrypointFunction.type, - arguments: [], - sp: self.valueStack, - pc: self.pc - ) + if let currentBreakpoint { + // Remove the breakpoint before resuming + try self.disableBreakpoint(address: currentBreakpoint.wasmPc) + self.execution.resetError() + + var sp = currentBreakpoint.iseq.sp + var pc = currentBreakpoint.iseq.pc + var md: Md = nil + var ms: Ms = 0 + + guard let currentFunction = sp.currentFunction else { + throw Error.unknownCurrentFunctionForResumedBreakpoint(sp) + } + + Execution.CurrentMemory.mayUpdateCurrentInstance( + instance: currentFunction.instance, + from: self.instance.handle, + md: &md, + ms: &ms + ) + + do { + switch self.threadingModel { + case .direct: + try self.execution.runDirectThreaded(sp: sp, pc: pc, md: md, ms: ms) + case .token: + try self.execution.runTokenThreaded(sp: &sp, pc: &pc, md: &md, ms: &ms) + } + } catch is Execution.EndOfExecution { + } + + let type = self.store.engine.funcTypeInterner.resolve(currentFunction.type) + return type.results.enumerated().map { (i, type) in + sp[VReg(i)].cast(to: type) + } + } else { + return try self.execution.executeWasm( + threadingModel: self.threadingModel, + function: self.entrypointFunction.handle, + type: self.entrypointFunction.type, + arguments: [], + sp: self.valueStack, + pc: self.pc + ) + } } catch let breakpoint as Execution.Breakpoint { - self.currentBreakpoint = breakpoint + let pc = breakpoint.pc + guard let wasmPc = self.instance.handle.iseqToWasmMapping[pc] else { + throw Error.noReverseInstructionMappingAvailable(pc) + } + + self.currentBreakpoint = (breakpoint, wasmPc) return nil } } @@ -151,12 +195,10 @@ return [] } - var result = Execution.captureBacktrace(sp: currentBreakpoint.sp, store: self.store).symbols.compactMap { + var result = Execution.captureBacktrace(sp: currentBreakpoint.iseq.sp, store: self.store).symbols.compactMap { return self.instance.handle.iseqToWasmMapping[$0.address] } - if let wasmPc = self.instance.handle.iseqToWasmMapping[currentBreakpoint.pc] { - result.append(wasmPc) - } + result.append(currentBreakpoint.wasmPc) return result } diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index f400fdda..7e8fcd30 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -557,6 +557,11 @@ extension Execution { self.trap = (rawError, sp) } + /// Used by the debugger to resume execution after breakpoints. + mutating func resetError() { + self.trap = nil + } + @inline(__always) func checkStackBoundary(_ sp: Sp) throws { guard sp < stackEnd else { throw Trap(.callStackExhausted) } diff --git a/Tests/WasmKitTests/DebuggerTests.swift b/Tests/WasmKitTests/DebuggerTests.swift index da9ebd2a..267e2ea1 100644 --- a/Tests/WasmKitTests/DebuggerTests.swift +++ b/Tests/WasmKitTests/DebuggerTests.swift @@ -33,6 +33,8 @@ let expectedPc = try #require(debugger.breakpoints.keys.first) #expect(debugger.currentCallStack == [expectedPc]) + + #expect(try debugger.run() == [.i32(42)]) } @Test From a7fac8ee333aef201937fb0b2586595c5d220045 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 24 Oct 2025 16:39:00 +0100 Subject: [PATCH 088/107] Mark `Sp.currentFunction` as `internal` --- Sources/WasmKit/Execution/Execution.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index 7e8fcd30..3a92c55e 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -235,7 +235,7 @@ extension Sp { // MARK: - Special slots /// The current executing function. - fileprivate var currentFunction: EntityHandle? { + var currentFunction: EntityHandle? { get { return EntityHandle(bitPattern: UInt(self[-3].i64)) } nonmutating set { self[-3] = UInt64(UInt(bitPattern: newValue?.bitPattern ?? 0)) } } From 4e8b062c9d46651869297ad4716d593581b8e537 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 24 Oct 2025 17:06:25 +0100 Subject: [PATCH 089/107] Mark `Debugger.Error` as `@unchecked Sendable` --- Sources/WasmKit/Execution/Debugger.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 11dd7def..2bfd0e05 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -43,7 +43,7 @@ } package struct Debugger: ~Copyable { - package enum Error: Swift.Error { + package enum Error: Swift.Error, @unchecked Sendable { case entrypointFunctionNotFound case unknownCurrentFunctionForResumedBreakpoint(UnsafeMutablePointer) case noInstructionMappingAvailable(Int) @@ -130,7 +130,8 @@ iseq.pointee = oldCodeSlot } - /// Returns: `[Value]` result of `entrypointFunction` if current instance ran to completion, `nil` if it stopped at a breakpoint. + /// - Returns: `[Value]` result of `entrypointFunction` if current instance ran to completion, + /// `nil` if it stopped at a breakpoint. package mutating func run() throws -> [Value]? { do { if let currentBreakpoint { From 969b9922eea07b7d17150782b4530bbc0c312b23 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 24 Oct 2025 17:30:33 +0100 Subject: [PATCH 090/107] Handle `qThreadStopInfo`, run up to breakpoint in handler --- Sources/GDBRemoteProtocol/GDBHostCommand.swift | 10 ++++++++++ .../GDBTargetResponseEncoder.swift | 13 ++++++++++--- Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift | 6 +++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index cf0276dc..ff8cc6b7 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -38,6 +38,7 @@ package struct GDBHostCommand: Equatable { case readMemoryBinaryData case readMemory case wasmCallStack + case threadStopInfo case generalRegisters @@ -97,6 +98,7 @@ package struct GDBHostCommand: Equatable { /// - arguments: raw arguments that immediately follow kind of the command. package init(kindString: String, arguments: String) throws(GDBHostCommandDecoder.Error) { let registerInfoPrefix = "qRegisterInfo" + let threadStopInfoPrefix = "qThreadStopInfo" if kindString.starts(with: "x") { self.kind = .readMemoryBinaryData @@ -109,6 +111,14 @@ package struct GDBHostCommand: Equatable { } else if kindString.starts(with: registerInfoPrefix) { self.kind = .registerInfo + guard arguments.isEmpty else { + throw GDBHostCommandDecoder.Error.unexpectedArgumentsValue + } + self.arguments = String(kindString.dropFirst(registerInfoPrefix.count)) + return + } else if kindString.starts(with: threadStopInfoPrefix) { + self.kind = .threadStopInfo + guard arguments.isEmpty else { throw GDBHostCommandDecoder.Error.unexpectedArgumentsValue } diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index 37b76dc3..ca10e6ee 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import Foundation +import Logging import NIOCore extension String { @@ -27,7 +28,12 @@ extension String { package class GDBTargetResponseEncoder: MessageToByteEncoder { private var isNoAckModeActive = false - package init() {} + private let logger: Logger + + package init(logger: Logger) { + self.logger = logger + } + package func encode(data: GDBTargetResponse, out: inout ByteBuffer) { if !isNoAckModeActive { out.writeInteger(UInt8(ascii: "+")) @@ -51,8 +57,9 @@ package class GDBTargetResponseEncoder: MessageToByteEncoder { out.writeString(str.appendedChecksum) case .hexEncodedBinary(let binary): - let hexDump = ByteBuffer(bytes: binary).hexDump(format: .compact) - out.writeString(hexDump.appendedChecksum) + let hexDumpResponse = ByteBuffer(bytes: binary).hexDump(format: .compact).appendedChecksum + self.logger.trace("GDBTargetResponseEncoder encoded a response", metadata: ["RawResponse": .string(hexDumpResponse)]) + out.writeString(hexDumpResponse) case .empty: out.writeString("".appendedChecksum) diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index d7feffda..35fd0adc 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -38,6 +38,7 @@ enum Error: Swift.Error { case unknownTransferArguments case unknownReadMemoryArguments + case stoppingAtEntrypointFailed } private let wasmBinary: ByteBuffer @@ -63,6 +64,9 @@ self.debugger = try Debugger(module: parseWasm(bytes: .init(buffer: self.wasmBinary)), store: store, imports: imports) try self.debugger.stopAtEntrypoint() + guard try self.debugger.run() == nil else { + throw Error.stoppingAtEntrypointFailed + } } package func handle(command: GDBHostCommand) throws -> GDBTargetResponse { @@ -114,7 +118,7 @@ case .subsequentThreadInfo: responseKind = .string("l") - case .targetStatus: + case .targetStatus, .threadStopInfo: responseKind = .keyValuePairs([ "T05thread": "1", "reason": "trace", From 2486ec8c1b17639db0dde4e9272fb27672ca3fcb Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 24 Oct 2025 17:34:05 +0100 Subject: [PATCH 091/107] Add missing `logger` argument in `Entrypoint.swift` --- Sources/wasmkit-gdb-tool/Entrypoint.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/wasmkit-gdb-tool/Entrypoint.swift b/Sources/wasmkit-gdb-tool/Entrypoint.swift index ca482933..432ddc05 100644 --- a/Sources/wasmkit-gdb-tool/Entrypoint.swift +++ b/Sources/wasmkit-gdb-tool/Entrypoint.swift @@ -62,7 +62,7 @@ struct Entrypoint: AsyncParsableCommand { // the closure as it will be invoked once per connection. try channel.pipeline.syncOperations.addHandlers([ ByteToMessageHandler(GDBHostCommandDecoder(logger: logger)), - MessageToByteHandler(GDBTargetResponseEncoder()), + MessageToByteHandler(GDBTargetResponseEncoder(logger: logger)), ]) } } From 2134b94c7779d2a57f17edc9635ae642b05c5a9a Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 27 Oct 2025 15:05:04 +0000 Subject: [PATCH 092/107] Make some functions `fileprivate` --- Sources/WasmKit/Execution/Debugger.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 2bfd0e05..031dd5ef 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -1,7 +1,7 @@ #if WasmDebuggingSupport extension [Int] { - func binarySearch(nextClosestTo value: Int) -> Int? { + fileprivate func binarySearch(nextClosestTo value: Int) -> Int? { switch self.count { case 0: return nil @@ -24,7 +24,7 @@ } extension Instance { - func findIseq(forWasmAddress address: Int) throws(Debugger.Error) -> (iseq: Pc, wasm: Int) { + fileprivate func findIseq(forWasmAddress address: Int) throws(Debugger.Error) -> (iseq: Pc, wasm: Int) { // Look in the main mapping if let iseq = handle.wasmToIseqMapping[address] { return (iseq, address) From 0945a36942b1d734fb2945aa4662a8b86b2a3f3e Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 27 Oct 2025 15:31:26 +0000 Subject: [PATCH 093/107] Add doc comments for the debugger type --- Sources/WasmKit/Execution/Debugger.swift | 48 ++++++++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 031dd5ef..e6dc6b99 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -1,6 +1,9 @@ #if WasmDebuggingSupport - extension [Int] { +extension [Int] { + /// Uses search to find for an element in an array that's the next closest to a given value. + /// - Parameter value: the array element to search for or to use as a baseline when searching. + /// - Returns: array element `result`, where `result - value` is the smallest possible, and `result > value`. fileprivate func binarySearch(nextClosestTo value: Int) -> Int? { switch self.count { case 0: @@ -23,7 +26,11 @@ } } - extension Instance { +extension Instance { + /// Return an address of WasmKit's iseq bytecode instruction that matches a given Wasm instruction address. + /// - Parameter address: the Wasm instruction to find a mapping for. + /// - Returns: A tuple with an address of found iseq instruction and the closests matching Wasm instruction + /// if no direct match was found. fileprivate func findIseq(forWasmAddress address: Int) throws(Debugger.Error) -> (iseq: Pc, wasm: Int) { // Look in the main mapping if let iseq = handle.wasmToIseqMapping[address] { @@ -42,6 +49,8 @@ } } +/// User-facing debugger state that driven by a debugger host. This implementation has no knowledge of the exact +/// debugger protocol, which allows any protocol implementation or direct API users to be layered on top if needed. package struct Debugger: ~Copyable { package enum Error: Swift.Error, @unchecked Sendable { case entrypointFunctionNotFound @@ -71,7 +80,12 @@ private var currentBreakpoint: (iseq: Execution.Breakpoint, wasmPc: Int)? private var pc = Pc.allocate(capacity: 1) - + + /// Initializes a new debugger state instance. + /// - Parameters: + /// - module: Wasm module to instantiate. + /// - store: Store that instantiates the module. + /// - imports: Imports required by `module` for instantiation. package init(module: Module, store: Store, imports: Imports) throws { let limit = store.engine.configuration.stackSize / MemoryLayout.stride let instance = try module.instantiate(store: store, imports: imports, isDebuggable: true) @@ -89,12 +103,17 @@ self.threadingModel = store.engine.configuration.threadingModel self.pc.pointee = Instruction.endOfExecution.headSlot(threadingModel: threadingModel) } - + + /// Sets a breakpoint at the first instruction in the entrypoint function of the module instantiated by + /// this debugger. package mutating func stopAtEntrypoint() throws { try self.enableBreakpoint(address: self.originalAddress(function: entrypointFunction)) } - - package func originalAddress(function: Function) throws -> Int { + + /// Finds a Wasm address for the first instruction in a given function. + /// - Parameter function: the Wasm function to find a Wasm address for. + /// - Returns: byte offset of the first Wasm instruction of given function in the module it was parsed from. + private func originalAddress(function: Function) throws -> Int { precondition(function.handle.isWasm) switch function.handle.wasm.code { @@ -107,7 +126,12 @@ fatalError() } } - + + /// Enable a breakpoint at a given Wasm address. + /// - Parameter address: byte offset of the Wasm instruction that will be replaced with a breakpoint. If no + /// direct internal bytecode matching instruction is found, the next closest internal bytecode instruction + /// is replaced with a breakpoint. The original instruction to be restored is preserved in debugger state. + /// See also ``Debugger/disableBreakpoint(address:)``. package mutating func enableBreakpoint(address: Int) throws(Error) { guard self.breakpoints[address] == nil else { return @@ -118,7 +142,12 @@ self.breakpoints[wasm] = iseq.pointee iseq.pointee = Instruction.breakpoint.headSlot(threadingModel: self.threadingModel) } - + + /// Disables a breakpoint at a given Wasm address. If no breakpoint at a given address was previously set with + /// `self.enableBreakpoint(address:), this function immediately returns. + /// - Parameter address: byte offset of the Wasm instruction that was replaced with a breakpoint. The original + /// instruction is restored from debugger state and replaces the breakpoint instruction. + /// See also ``Debugger/enableBreakpoint(address:)``. package mutating func disableBreakpoint(address: Int) throws(Error) { guard let oldCodeSlot = self.breakpoints[address] else { return @@ -130,6 +159,9 @@ iseq.pointee = oldCodeSlot } + /// If the module instantiated by the debugger is stopped at a breakpoint, the breakpoint is disabled + /// and execution is resumed until the next breakpoint is triggered or all remaining instructions are + /// executed. If the module is not stopped at a breakpoint, this function retusn immediately. /// - Returns: `[Value]` result of `entrypointFunction` if current instance ran to completion, /// `nil` if it stopped at a breakpoint. package mutating func run() throws -> [Value]? { From 553dd7d48bc59ca363b650683327f81f530ae68a Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 27 Oct 2025 15:35:22 +0000 Subject: [PATCH 094/107] Refine doc comment Sources/WasmKit/Execution/Debugger.swift # Conflicts: # Sources/WasmKit/Execution/Debugger.swift --- Sources/WasmKit/Execution/Debugger.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index e6dc6b99..6cb656de 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -1,9 +1,9 @@ #if WasmDebuggingSupport -extension [Int] { - /// Uses search to find for an element in an array that's the next closest to a given value. - /// - Parameter value: the array element to search for or to use as a baseline when searching. - /// - Returns: array element `result`, where `result - value` is the smallest possible, and `result > value`. + extension [Int] { + /// Uses binary search to find an element in `self` that's next closest to a given value. + /// - Parameter value: the array element to search for or to use as a baseline when searching. + /// - Returns: array element `result`, where `result - value` is the smallest possible, and `result > value`. fileprivate func binarySearch(nextClosestTo value: Int) -> Int? { switch self.count { case 0: From 01d8e4412d5db54e143886612c2c35de980fc704 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 27 Oct 2025 15:37:46 +0000 Subject: [PATCH 095/107] Refine doc comment in `Debugger.swift` --- Sources/WasmKit/Execution/Debugger.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 6cb656de..be2da560 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -3,7 +3,8 @@ extension [Int] { /// Uses binary search to find an element in `self` that's next closest to a given value. /// - Parameter value: the array element to search for or to use as a baseline when searching. - /// - Returns: array element `result`, where `result - value` is the smallest possible, and `result > value`. + /// - Returns: array element `result`, where `result - value` is the smallest possible, while + /// `result > value` also holds. fileprivate func binarySearch(nextClosestTo value: Int) -> Int? { switch self.count { case 0: From d3ee7f1ea999c09293c9864ad93f25a5326446f9 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 27 Oct 2025 15:46:14 +0000 Subject: [PATCH 096/107] Refine doc comments wording # Conflicts: # Sources/WasmKit/Execution/Debugger.swift --- Sources/WasmKit/Execution/Debugger.swift | 26 ++++++++++++----------- Sources/WasmKit/Execution/Execution.swift | 2 ++ Sources/WasmKit/Execution/Instances.swift | 4 ++-- Sources/WasmKit/Module.swift | 2 +- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index be2da560..cfefee98 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -27,11 +27,11 @@ } } -extension Instance { - /// Return an address of WasmKit's iseq bytecode instruction that matches a given Wasm instruction address. - /// - Parameter address: the Wasm instruction to find a mapping for. - /// - Returns: A tuple with an address of found iseq instruction and the closests matching Wasm instruction - /// if no direct match was found. + extension Instance { + /// Return an address of WasmKit's iseq bytecode instruction that matches a given Wasm instruction address. + /// - Parameter address: the Wasm instruction to find a mapping for. + /// - Returns: A tuple with an address of found iseq instruction and the original Wasm instruction or next + /// closest match if no direct match was found. fileprivate func findIseq(forWasmAddress address: Int) throws(Debugger.Error) -> (iseq: Pc, wasm: Int) { // Look in the main mapping if let iseq = handle.wasmToIseqMapping[address] { @@ -50,8 +50,8 @@ extension Instance { } } -/// User-facing debugger state that driven by a debugger host. This implementation has no knowledge of the exact -/// debugger protocol, which allows any protocol implementation or direct API users to be layered on top if needed. + /// User-facing debugger state driven by a debugger host. This implementation has no knowledge of the exact + /// debugger protocol, which allows any protocol implementation or direct API users to be layered on top if needed. package struct Debugger: ~Copyable { package enum Error: Swift.Error, @unchecked Sendable { case entrypointFunctionNotFound @@ -71,9 +71,10 @@ extension Instance { private let instance: Instance /// Reference to the entrypoint function of the currently debugged module, for use in ``stopAtEntrypoint``. + /// Currently assumed to be the WASI command `_start` entrypoint. private let entrypointFunction: Function - /// Threading model of the Wasm engine configuration cached for a potentially hot path. + /// Threading model of the Wasm engine configuration, cached for a potentially hot path. private let threadingModel: EngineConfiguration.ThreadingModel private(set) var breakpoints = [Int: CodeSlot]() @@ -112,7 +113,7 @@ extension Instance { } /// Finds a Wasm address for the first instruction in a given function. - /// - Parameter function: the Wasm function to find a Wasm address for. + /// - Parameter function: the Wasm function to find the first Wasm instruction address for. /// - Returns: byte offset of the first Wasm instruction of given function in the module it was parsed from. private func originalAddress(function: Function) throws -> Int { precondition(function.handle.isWasm) @@ -127,11 +128,12 @@ extension Instance { fatalError() } } - - /// Enable a breakpoint at a given Wasm address. + + /// Enables a breakpoint at a given Wasm address. /// - Parameter address: byte offset of the Wasm instruction that will be replaced with a breakpoint. If no /// direct internal bytecode matching instruction is found, the next closest internal bytecode instruction - /// is replaced with a breakpoint. The original instruction to be restored is preserved in debugger state. + /// is replaced with a breakpoint. The original instruction to be restored is preserved in debugger state + /// represented by `self`. /// See also ``Debugger/disableBreakpoint(address:)``. package mutating func enableBreakpoint(address: Int) throws(Error) { guard self.breakpoints[address] == nil else { diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index 3a92c55e..a66dfb92 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -321,6 +321,8 @@ extension Execution { #if WasmDebuggingSupport + /// Counterpart to the free `executeWasm` function but implemented as a method of `Execution`, + /// Useful for representation of debugger state that needs to own `Execution`'s memory. mutating func executeWasm( threadingModel: EngineConfiguration.ThreadingModel, function handle: InternalFunction, diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index e20a35a3..d7dd20e8 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -85,11 +85,11 @@ struct InstanceEntity /* : ~Copyable */ { var dataCount: UInt32? var isDebuggable: Bool - /// Mapping from iSeq Pc to instruction addresses in the original binary. + /// Mapping from iseq Pc to instruction addresses in the original binary. /// Used for handling current call stack requests issued by a ``Debugger`` instance. var iseqToWasmMapping = [Pc: Int]() - /// Mapping from Wasm instruction addresses in the original binary to iSeq instruction addresses. + /// Mapping from Wasm instruction addresses in the original binary to iseq instruction addresses. /// Used for handling breakpoint requests issued by a ``Debugger`` instance. var wasmToIseqMapping = [Int: Pc]() diff --git a/Sources/WasmKit/Module.swift b/Sources/WasmKit/Module.swift index 499cb93b..61ad28c8 100644 --- a/Sources/WasmKit/Module.swift +++ b/Sources/WasmKit/Module.swift @@ -139,7 +139,7 @@ public struct Module { } #if WasmDebuggingSupport - /// Instantiate this module in the given imports. + /// Instantiate this module with the given imports. /// /// - Parameters: /// - store: The ``Store`` to allocate the instance in. From d82df5fd004490d93df294e6dc0e6d725d9e9b2a Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 27 Oct 2025 16:00:14 +0000 Subject: [PATCH 097/107] Fix build error # Conflicts: # Tests/WasmKitTests/ExecutionTests.swift --- Sources/WasmKit/Execution/Debugger.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index cfefee98..170f120b 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -5,7 +5,7 @@ /// - Parameter value: the array element to search for or to use as a baseline when searching. /// - Returns: array element `result`, where `result - value` is the smallest possible, while /// `result > value` also holds. - fileprivate func binarySearch(nextClosestTo value: Int) -> Int? { + package func binarySearch(nextClosestTo value: Int) -> Int? { switch self.count { case 0: return nil From 87d22150d84d8ac38b4f45ff212e52be67591427 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 27 Oct 2025 16:01:38 +0000 Subject: [PATCH 098/107] Fix formatting --- Sources/WasmKit/Execution/Debugger.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 170f120b..158fd24e 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -30,7 +30,7 @@ extension Instance { /// Return an address of WasmKit's iseq bytecode instruction that matches a given Wasm instruction address. /// - Parameter address: the Wasm instruction to find a mapping for. - /// - Returns: A tuple with an address of found iseq instruction and the original Wasm instruction or next + /// - Returns: A tuple with an address of found iseq instruction and the original Wasm instruction or next /// closest match if no direct match was found. fileprivate func findIseq(forWasmAddress address: Int) throws(Debugger.Error) -> (iseq: Pc, wasm: Int) { // Look in the main mapping From a0f646405d45e5cac685973a6c0fb79039d1a8b2 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 27 Oct 2025 16:04:40 +0000 Subject: [PATCH 099/107] Apply formatting to `Package@swift-6.1.swift` --- Package@swift-6.1.swift | 5 +++-- Utilities/format.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index edafee37..11d9ae69 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -21,7 +21,7 @@ let package = Package( ], traits: [ .default(enabledTraits: []), - "WasmDebuggingSupport" + "WasmDebuggingSupport", ], targets: [ .executableTarget( @@ -123,7 +123,8 @@ let package = Package( .target(name: "WITExtractor"), .testTarget(name: "WITExtractorTests", dependencies: ["WITExtractor", "WIT"]), - .target(name: "GDBRemoteProtocol", + .target( + name: "GDBRemoteProtocol", dependencies: [ .product(name: "Logging", package: "swift-log"), .product(name: "NIOCore", package: "swift-nio"), diff --git a/Utilities/format.py b/Utilities/format.py index eb5b9c4e..70d93143 100755 --- a/Utilities/format.py +++ b/Utilities/format.py @@ -58,6 +58,7 @@ def main(): for target in targets: arguments.append(os.path.join(SOURCE_ROOT, target)) arguments.append(os.path.join(SOURCE_ROOT, "Package.swift")) + arguments.append(os.path.join(SOURCE_ROOT, "Package@swift-6.1.swift")) run(arguments) From 6bc4eca48f2f41d402491596386d3e1660ac27a4 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 27 Oct 2025 16:22:38 +0000 Subject: [PATCH 100/107] Disable nightly toolchain in CI configuration Until https://github.com/swiftlang/swift/commit/b219d4089c922ceb8b700424236ca97f6087a9a1 is tagged, CI with nightly snapshots off `main` keeps crashing. --- .github/workflows/main.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 660d68dc..19791fd2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -137,12 +137,14 @@ jobs: wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" test-args: "--traits WasmDebuggingSupport --enable-code-coverage" build-dev-dashboard: true - - swift: "swiftlang/swift:nightly-main-noble" - development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz" - wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" - wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm - wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" - test-args: "--traits WasmDebuggingSupport -Xswiftc -DWASMKIT_CI_TOOLCHAIN_NIGHTLY" + # Disabled until a toolchain containing https://github.com/swiftlang/swift/commit/b219d4089c922ceb8b700424236ca97f6087a9a1 + # is tagged. + # - swift: "swiftlang/swift:nightly-main-noble" + # development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz" + # wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" + # wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm + # wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" + # test-args: "--traits WasmDebuggingSupport -Xswiftc -DWASMKIT_CI_TOOLCHAIN_NIGHTLY" runs-on: ubuntu-24.04 name: "build-linux (${{ matrix.swift }})" From d09be4f8d26ed31a1af24f41addb2e9337a5e370 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 27 Oct 2025 16:32:38 +0000 Subject: [PATCH 101/107] Pin `build-android` job to v2.6.4 of `swift-android-action` For some reason `link-secret-dir-b/secret-c.txt` started throwing `.ELOOP` instead of `.ENOTDIR` in https://github.com/swiftwasm/WasmKit/actions/runs/18847983546/job/53777014067 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 19791fd2..ef25a106 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -235,7 +235,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Run Tests on Android emulator - uses: skiptools/swift-android-action@v2 + uses: skiptools/swift-android-action@v2.6.4 build-windows: runs-on: windows-latest From 420288c99957d9b2c9901264b975348873d87568 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 27 Oct 2025 17:00:17 +0000 Subject: [PATCH 102/107] Refine doc comments wording --- Sources/WasmKit/Execution/Debugger.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 158fd24e..9affe410 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -28,7 +28,7 @@ } extension Instance { - /// Return an address of WasmKit's iseq bytecode instruction that matches a given Wasm instruction address. + /// Computes an address of WasmKit's iseq bytecode instruction that matches a given Wasm instruction address. /// - Parameter address: the Wasm instruction to find a mapping for. /// - Returns: A tuple with an address of found iseq instruction and the original Wasm instruction or next /// closest match if no direct match was found. @@ -162,9 +162,9 @@ iseq.pointee = oldCodeSlot } - /// If the module instantiated by the debugger is stopped at a breakpoint, the breakpoint is disabled + /// Resumes the module instantiated by the debugger stopped at a breakpoint. The breakpoint is disabled /// and execution is resumed until the next breakpoint is triggered or all remaining instructions are - /// executed. If the module is not stopped at a breakpoint, this function retusn immediately. + /// executed. If the module is not stopped at a breakpoint, this function returns immediately. /// - Returns: `[Value]` result of `entrypointFunction` if current instance ran to completion, /// `nil` if it stopped at a breakpoint. package mutating func run() throws -> [Value]? { From 395f9072e95f04bf3c0e5cc96b96e8c0199a17d8 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 28 Oct 2025 16:01:34 +0000 Subject: [PATCH 103/107] Move mapping dictionaries to `DebuggerInstructionMapping` --- .../WasmKit/DebuggerInstructionMapping.swift | 86 +++++++++++++++++++ Sources/WasmKit/Execution/Debugger.swift | 63 ++------------ Sources/WasmKit/Execution/Instances.swift | 15 +--- .../WasmKit/Execution/StoreAllocator.swift | 3 +- Sources/WasmKit/Translator.swift | 23 +---- 5 files changed, 101 insertions(+), 89 deletions(-) create mode 100644 Sources/WasmKit/DebuggerInstructionMapping.swift diff --git a/Sources/WasmKit/DebuggerInstructionMapping.swift b/Sources/WasmKit/DebuggerInstructionMapping.swift new file mode 100644 index 00000000..e9487dc2 --- /dev/null +++ b/Sources/WasmKit/DebuggerInstructionMapping.swift @@ -0,0 +1,86 @@ + +/// Two-way mapping between Wasm and internal iseq bytecode instructions. The implementation of the mapping +/// is private and is empty when `WasmDebuggingSupport` package trait is disabled. +struct DebuggerInstructionMapping { +#if WasmDebuggingSupport + + /// Mapping from iseq Pc to instruction addresses in the original binary. + /// Used for handling current call stack requests issued by a ``Debugger`` instance. + private var iseqToWasm = [Pc: Int]() + + /// Mapping from Wasm instruction addresses in the original binary to iseq instruction addresses. + /// Used for handling breakpoint requests issued by a ``Debugger`` instance. + private var wasmToIseq = [Int: Pc]() + + /// Wasm addresses sorted in ascending order for binary search when of the next closest mapped + /// instruction, when no key is found in `wasmToIseqMapping`. + private var wasmMappings = [Int]() + + mutating func add(wasm: Int, iseq: Pc) { + // Don't override the existing mapping, only store a new pair if there's no mapping for a given key. + if self.iseqToWasm[iseq] == nil { + self.iseqToWasm[iseq] = wasm + } + if self.wasmToIseq[wasm] == nil { + self.wasmToIseq[wasm] = iseq + } + self.wasmMappings.append(wasm) + } + + /// Computes an address of WasmKit's iseq bytecode instruction that matches a given Wasm instruction address. + /// - Parameter address: the Wasm instruction to find a mapping for. + /// - Returns: A tuple with an address of found iseq instruction and the original Wasm instruction or next + /// closest match if no direct match was found. + func findIseq(forWasmAddress address: Int) -> (iseq: Pc, wasm: Int)? { + // Look in the main mapping + if let iseq = self.wasmToIseq[address] { + return (iseq, address) + } + + // If nothing found, find the closest Wasm address using binary search + guard let nextAddress = self.wasmMappings.binarySearch(nextClosestTo: address), + // Look in the main mapping again with the next closest address if binary search produced anything + let iseq = self.wasmToIseq[nextAddress] + else { + return nil + } + + return (iseq, nextAddress) + } + + func findWasm(forIseqAddress pc: Pc) -> Int? { + self.iseqToWasm[pc] + } +#endif +} + + +#if WasmDebuggingSupport + extension [Int] { + /// Uses binary search to find an element in `self` that's next closest to a given value. + /// - Parameter value: the array element to search for or to use as a baseline when searching. + /// - Returns: array element `result`, where `result - value` is the smallest possible, while + /// `result > value` also holds. + package func binarySearch(nextClosestTo value: Int) -> Int? { + switch self.count { + case 0: + return nil + default: + var slice = self[0.. 1 { + let middle = (slice.endIndex - slice.startIndex) / 2 + if slice[middle] < value { + // Not found anything in the lower half, assigning higher half to `slice`. + slice = slice[(middle + 1).. value` also holds. - package func binarySearch(nextClosestTo value: Int) -> Int? { - switch self.count { - case 0: - return nil - default: - var slice = self[0.. 1 { - let middle = (slice.endIndex - slice.startIndex) / 2 - if slice[middle] < value { - // Not found anything in the lower half, assigning higher half to `slice`. - slice = slice[(middle + 1).. (iseq: Pc, wasm: Int) { - // Look in the main mapping - if let iseq = handle.wasmToIseqMapping[address] { - return (iseq, address) - } - - // If nothing found, find the closest Wasm address using binary search - guard let nextAddress = handle.wasmMappings.binarySearch(nextClosestTo: address), - // Look in the main mapping again with the next closest address if binary search produced anything - let iseq = handle.wasmToIseqMapping[nextAddress] - else { - throw Debugger.Error.noInstructionMappingAvailable(address) - } - - return (iseq, nextAddress) - } - } - /// User-facing debugger state driven by a debugger host. This implementation has no knowledge of the exact /// debugger protocol, which allows any protocol implementation or direct API users to be layered on top if needed. package struct Debugger: ~Copyable { @@ -140,7 +89,9 @@ return } - let (iseq, wasm) = try self.instance.findIseq(forWasmAddress: address) + guard let (iseq, wasm) = try self.instance.handle.instructionMapping.findIseq(forWasmAddress: address) else { + throw Error.noInstructionMappingAvailable(address) + } self.breakpoints[wasm] = iseq.pointee iseq.pointee = Instruction.breakpoint.headSlot(threadingModel: self.threadingModel) @@ -156,7 +107,9 @@ return } - let (iseq, wasm) = try self.instance.findIseq(forWasmAddress: address) + guard let (iseq, wasm) = try self.instance.handle.instructionMapping.findIseq(forWasmAddress: address) else { + throw Error.noInstructionMappingAvailable(address) + } self.breakpoints[wasm] = nil iseq.pointee = oldCodeSlot @@ -216,7 +169,7 @@ } } catch let breakpoint as Execution.Breakpoint { let pc = breakpoint.pc - guard let wasmPc = self.instance.handle.iseqToWasmMapping[pc] else { + guard let wasmPc = self.instance.handle.instructionMapping.findWasm(forIseqAddress: pc) else { throw Error.noReverseInstructionMappingAvailable(pc) } @@ -232,7 +185,7 @@ } var result = Execution.captureBacktrace(sp: currentBreakpoint.iseq.sp, store: self.store).symbols.compactMap { - return self.instance.handle.iseqToWasmMapping[$0.address] + return self.instance.handle.instructionMapping.findWasm(forIseqAddress: $0.address) } result.append(currentBreakpoint.wasmPc) diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index d7dd20e8..f5234b5d 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -85,17 +85,7 @@ struct InstanceEntity /* : ~Copyable */ { var dataCount: UInt32? var isDebuggable: Bool - /// Mapping from iseq Pc to instruction addresses in the original binary. - /// Used for handling current call stack requests issued by a ``Debugger`` instance. - var iseqToWasmMapping = [Pc: Int]() - - /// Mapping from Wasm instruction addresses in the original binary to iseq instruction addresses. - /// Used for handling breakpoint requests issued by a ``Debugger`` instance. - var wasmToIseqMapping = [Int: Pc]() - - /// Wasm addresses sorted in ascending order for binary search when of the next closest mapped - /// instruction, when no key is found in `wasmToIseqMapping`. - var wasmMappings = [Int]() + var instructionMapping: DebuggerInstructionMapping static var empty: InstanceEntity { InstanceEntity( @@ -111,8 +101,7 @@ struct InstanceEntity /* : ~Copyable */ { features: [], dataCount: nil, isDebuggable: false, - iseqToWasmMapping: [:], - wasmToIseqMapping: [:] + instructionMapping: .init() ) } diff --git a/Sources/WasmKit/Execution/StoreAllocator.swift b/Sources/WasmKit/Execution/StoreAllocator.swift index dff3da90..8ca44f07 100644 --- a/Sources/WasmKit/Execution/StoreAllocator.swift +++ b/Sources/WasmKit/Execution/StoreAllocator.swift @@ -453,8 +453,7 @@ extension StoreAllocator { features: module.features, dataCount: module.dataCount, isDebuggable: isDebuggable, - iseqToWasmMapping: [:], - wasmToIseqMapping: [:] + instructionMapping: .init() ) instancePointer.initialize(to: instanceEntity) instanceInitialized = true diff --git a/Sources/WasmKit/Translator.swift b/Sources/WasmKit/Translator.swift index 22b42c9a..b1f30c45 100644 --- a/Sources/WasmKit/Translator.swift +++ b/Sources/WasmKit/Translator.swift @@ -1100,19 +1100,14 @@ struct InstructionTranslator: InstructionVisitor { let initializedElementsIndex = buffer.initialize(fromContentsOf: instructions) assert(initializedElementsIndex == instructions.endIndex) + #if WasmDebuggingSupport for (iseq, wasm) in self.iseqToWasmMapping { self.module.withValue { - let absoluteISeq = iseq + buffer.baseAddress.unsafelyUnwrapped - // Don't override the existing mapping, only store a new pair if there's no mapping for a given key. - if $0.iseqToWasmMapping[absoluteISeq] == nil { - $0.iseqToWasmMapping[absoluteISeq] = wasm - } - if $0.wasmToIseqMapping[wasm] == nil { - $0.wasmToIseqMapping[wasm] = absoluteISeq - } - $0.wasmMappings.append(wasm) + let absoluteIseq = iseq + buffer.baseAddress.unsafelyUnwrapped + $0.instructionMapping.add(wasm: wasm, iseq: absoluteIseq) } } +#endif let constants = allocator.allocateConstants(self.constantSlots.values) return InstructionSequence( @@ -2305,16 +2300,6 @@ struct InstructionTranslator: InstructionVisitor { return .tableSize(Instruction.TableSizeOperand(tableIndex: table, result: LVReg(result))) } } - - mutating func visitUnknown(_ opcode: [UInt8]) throws -> Bool { - guard self.module.isDebuggable && opcode.count == 1 && opcode[0] == 0xFF else { - return false - } - - emit(.breakpoint) - - return true - } } struct TranslationError: Error, CustomStringConvertible { From e4bf52957a0ab5f468a63459943f87d2840fd37a Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 28 Oct 2025 16:08:54 +0000 Subject: [PATCH 104/107] Fix formatting and CMake --- Sources/WasmKit/CMakeLists.txt | 1 + .../WasmKit/DebuggerInstructionMapping.swift | 86 ------------------- .../DebuggerInstructionMapping.swift | 84 ++++++++++++++++++ Sources/WasmKit/Translator.swift | 12 +-- 4 files changed, 91 insertions(+), 92 deletions(-) delete mode 100644 Sources/WasmKit/DebuggerInstructionMapping.swift create mode 100644 Sources/WasmKit/Execution/DebuggerInstructionMapping.swift diff --git a/Sources/WasmKit/CMakeLists.txt b/Sources/WasmKit/CMakeLists.txt index 256aed0c..e77ea72a 100644 --- a/Sources/WasmKit/CMakeLists.txt +++ b/Sources/WasmKit/CMakeLists.txt @@ -11,6 +11,7 @@ add_wasmkit_library(WasmKit Component/CanonicalCall.swift Component/CanonicalOptions.swift Component/ComponentTypes.swift + Execution/DebuggerInstructionMapping.swift Execution/Instructions/Control.swift Execution/Instructions/Instruction.swift Execution/Instructions/Table.swift diff --git a/Sources/WasmKit/DebuggerInstructionMapping.swift b/Sources/WasmKit/DebuggerInstructionMapping.swift deleted file mode 100644 index e9487dc2..00000000 --- a/Sources/WasmKit/DebuggerInstructionMapping.swift +++ /dev/null @@ -1,86 +0,0 @@ - -/// Two-way mapping between Wasm and internal iseq bytecode instructions. The implementation of the mapping -/// is private and is empty when `WasmDebuggingSupport` package trait is disabled. -struct DebuggerInstructionMapping { -#if WasmDebuggingSupport - - /// Mapping from iseq Pc to instruction addresses in the original binary. - /// Used for handling current call stack requests issued by a ``Debugger`` instance. - private var iseqToWasm = [Pc: Int]() - - /// Mapping from Wasm instruction addresses in the original binary to iseq instruction addresses. - /// Used for handling breakpoint requests issued by a ``Debugger`` instance. - private var wasmToIseq = [Int: Pc]() - - /// Wasm addresses sorted in ascending order for binary search when of the next closest mapped - /// instruction, when no key is found in `wasmToIseqMapping`. - private var wasmMappings = [Int]() - - mutating func add(wasm: Int, iseq: Pc) { - // Don't override the existing mapping, only store a new pair if there's no mapping for a given key. - if self.iseqToWasm[iseq] == nil { - self.iseqToWasm[iseq] = wasm - } - if self.wasmToIseq[wasm] == nil { - self.wasmToIseq[wasm] = iseq - } - self.wasmMappings.append(wasm) - } - - /// Computes an address of WasmKit's iseq bytecode instruction that matches a given Wasm instruction address. - /// - Parameter address: the Wasm instruction to find a mapping for. - /// - Returns: A tuple with an address of found iseq instruction and the original Wasm instruction or next - /// closest match if no direct match was found. - func findIseq(forWasmAddress address: Int) -> (iseq: Pc, wasm: Int)? { - // Look in the main mapping - if let iseq = self.wasmToIseq[address] { - return (iseq, address) - } - - // If nothing found, find the closest Wasm address using binary search - guard let nextAddress = self.wasmMappings.binarySearch(nextClosestTo: address), - // Look in the main mapping again with the next closest address if binary search produced anything - let iseq = self.wasmToIseq[nextAddress] - else { - return nil - } - - return (iseq, nextAddress) - } - - func findWasm(forIseqAddress pc: Pc) -> Int? { - self.iseqToWasm[pc] - } -#endif -} - - -#if WasmDebuggingSupport - extension [Int] { - /// Uses binary search to find an element in `self` that's next closest to a given value. - /// - Parameter value: the array element to search for or to use as a baseline when searching. - /// - Returns: array element `result`, where `result - value` is the smallest possible, while - /// `result > value` also holds. - package func binarySearch(nextClosestTo value: Int) -> Int? { - switch self.count { - case 0: - return nil - default: - var slice = self[0.. 1 { - let middle = (slice.endIndex - slice.startIndex) / 2 - if slice[middle] < value { - // Not found anything in the lower half, assigning higher half to `slice`. - slice = slice[(middle + 1).. (iseq: Pc, wasm: Int)? { + // Look in the main mapping + if let iseq = self.wasmToIseq[address] { + return (iseq, address) + } + + // If nothing found, find the closest Wasm address using binary search + guard let nextAddress = self.wasmMappings.binarySearch(nextClosestTo: address), + // Look in the main mapping again with the next closest address if binary search produced anything + let iseq = self.wasmToIseq[nextAddress] + else { + return nil + } + + return (iseq, nextAddress) + } + + func findWasm(forIseqAddress pc: Pc) -> Int? { + self.iseqToWasm[pc] + } + #endif +} + +#if WasmDebuggingSupport + extension [Int] { + /// Uses binary search to find an element in `self` that's next closest to a given value. + /// - Parameter value: the array element to search for or to use as a baseline when searching. + /// - Returns: array element `result`, where `result - value` is the smallest possible, while + /// `result > value` also holds. + package func binarySearch(nextClosestTo value: Int) -> Int? { + switch self.count { + case 0: + return nil + default: + var slice = self[0.. 1 { + let middle = (slice.endIndex - slice.startIndex) / 2 + if slice[middle] < value { + // Not found anything in the lower half, assigning higher half to `slice`. + slice = slice[(middle + 1).. Date: Tue, 28 Oct 2025 16:12:34 +0000 Subject: [PATCH 105/107] Fix formatting --- Sources/WasmKit/Execution/Debugger.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index a2d2b828..01a22983 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -31,7 +31,7 @@ private var currentBreakpoint: (iseq: Execution.Breakpoint, wasmPc: Int)? private var pc = Pc.allocate(capacity: 1) - + /// Initializes a new debugger state instance. /// - Parameters: /// - module: Wasm module to instantiate. @@ -54,13 +54,13 @@ self.threadingModel = store.engine.configuration.threadingModel self.pc.pointee = Instruction.endOfExecution.headSlot(threadingModel: threadingModel) } - + /// Sets a breakpoint at the first instruction in the entrypoint function of the module instantiated by /// this debugger. package mutating func stopAtEntrypoint() throws { try self.enableBreakpoint(address: self.originalAddress(function: entrypointFunction)) } - + /// Finds a Wasm address for the first instruction in a given function. /// - Parameter function: the Wasm function to find the first Wasm instruction address for. /// - Returns: byte offset of the first Wasm instruction of given function in the module it was parsed from. @@ -96,7 +96,7 @@ self.breakpoints[wasm] = iseq.pointee iseq.pointee = Instruction.breakpoint.headSlot(threadingModel: self.threadingModel) } - + /// Disables a breakpoint at a given Wasm address. If no breakpoint at a given address was previously set with /// `self.enableBreakpoint(address:), this function immediately returns. /// - Parameter address: byte offset of the Wasm instruction that was replaced with a breakpoint. The original From e52abb1356c4e6c07c830d0add1f22b58881598e Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 28 Oct 2025 16:20:48 +0000 Subject: [PATCH 106/107] Disable `main` development snapshots for now --- .github/workflows/main.yml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e0a38b3a..5ce1b07b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -143,13 +143,21 @@ jobs: wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" test-args: "--traits WasmDebuggingSupport -Xswiftc -DWASMKIT_CI_TOOLCHAIN_NIGHTLY" - - swift: "swiftlang/swift:nightly-main-noble" - development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz" - wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" - wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm - wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" - test-args: "--traits WasmDebuggingSupport -Xswiftc -DWASMKIT_CI_TOOLCHAIN_NIGHTLY --build-system swiftbuild" - label: " --build-system swiftbuild" + # Disabled until a toolchain containing https://github.com/swiftlang/swift/commit/b219d4089c922ceb8b700424236ca97f6087a9a1 + # is tagged. + # - swift: "swiftlang/swift:nightly-main-noble" + # development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz" + # wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" + # wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm + # wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" + # test-args: "--traits WasmDebuggingSupport -Xswiftc -DWASMKIT_CI_TOOLCHAIN_NIGHTLY" +# - swift: "swiftlang/swift:nightly-main-noble" +# development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz" +# wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" +# wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm +# wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" +# test-args: "--traits WasmDebuggingSupport -Xswiftc -DWASMKIT_CI_TOOLCHAIN_NIGHTLY --build-system swiftbuild" +# label: " --build-system swiftbuild" runs-on: ubuntu-24.04 name: "build-linux (${{ matrix.swift }}${{ matrix.label }})" From a595446354e97a18193f06baf3c83e127f7b31a1 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 28 Oct 2025 16:27:15 +0000 Subject: [PATCH 107/107] Disable remaining `main` development snapshot job --- .github/workflows/main.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5ce1b07b..4bf26ea2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -137,12 +137,6 @@ jobs: wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" test-args: "--traits WasmDebuggingSupport --enable-code-coverage" build-dev-dashboard: true - - swift: "swiftlang/swift:nightly-main-noble" - development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz" - wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" - wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm - wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" - test-args: "--traits WasmDebuggingSupport -Xswiftc -DWASMKIT_CI_TOOLCHAIN_NIGHTLY" # Disabled until a toolchain containing https://github.com/swiftlang/swift/commit/b219d4089c922ceb8b700424236ca97f6087a9a1 # is tagged. # - swift: "swiftlang/swift:nightly-main-noble"