diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f88eacb8..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 }})" @@ -107,24 +107,26 @@ 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.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 +208,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 @@ -266,5 +268,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 + 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/Package@swift-6.1.swift b/Package@swift-6.1.swift new file mode 100644 index 00000000..d5b269ef --- /dev/null +++ b/Package@swift-6.1.swift @@ -0,0 +1,193 @@ +// 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(.v15), .iOS(.v17)], + 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"), + .target(name: "CSystemExtras", condition: .when(platforms: [.wasi])), + ], + exclude: ["CMakeLists.txt"], + swiftSettings: [ + .define("SYSTEM_PACKAGE_DARWIN", .when(platforms: DarwinPlatforms)) + ] + ), + + .target(name: "CSystemExtras"), + + .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: "GDBRemoteProtocol", + dependencies: [ + .product(name: "Logging", package: "swift-log"), + .product(name: "NIOCore", package: "swift-nio"), + ] + ), + .testTarget(name: "GDBRemoteProtocolTests", dependencies: ["GDBRemoteProtocol"]), + ], +) + +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"), + .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"), + ] +} + +#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"] + ), + + .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 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. diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift new file mode 100644 index 00000000..cf0276dc --- /dev/null +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -0,0 +1,131 @@ +//===----------------------------------------------------------------------===// +// +// 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 +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 + case supportedFeatures + case isThreadSuffixSupported + case listThreadsInStopReply + case hostInfo + case vContSupportedActions + case isVAttachOrWaitSupported + case enableErrorStrings + case processInfo + case currentThreadID + case firstThreadInfo + case subsequentThreadInfo + case targetStatus + case registerInfo + case structuredDataPlugins + case transfer + case readMemoryBinaryData + case readMemory + case wasmCallStack + + case generalRegisters + + /// Decodes kind of a command from a raw string sent from a host. + 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 + case "vCont?": + self = .vContSupportedActions + case "qVAttachOrWaitSupported": + self = .isVAttachOrWaitSupported + case "QEnableErrorStrings": + self = .enableErrorStrings + case "qProcessInfo": + self = .processInfo + case "qC": + self = .currentThreadID + case "qfThreadInfo": + self = .firstThreadInfo + case "qsThreadInfo": + self = .subsequentThreadInfo + case "?": + self = .targetStatus + case "qStructuredDataPlugins": + self = .structuredDataPlugins + case "qXfer": + self = .transfer + case "qWasmCallStack": + self = .wasmCallStack + + default: + return nil + } + } + } + + /// 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 + + /// 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") { + 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 { + throw GDBHostCommandDecoder.Error.unexpectedArgumentsValue + } + self.arguments = String(kindString.dropFirst(registerInfoPrefix.count)) + return + } else if let kind = Kind(rawValue: kindString) { + self.kind = kind + } else { + throw GDBHostCommandDecoder.Error.unknownCommand(kind: kindString, arguments: arguments) + } + + self.arguments = arguments + } + + /// 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 new file mode 100644 index 00000000..7e6c665f --- /dev/null +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -0,0 +1,209 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +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 { + /// 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 + + /// 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) + } + + /// 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( + 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 !self.isNoAckModeActive { + let firstStartDelimiter = startDelimiter + + guard firstStartDelimiter == UInt8(ascii: "+") else { + logger.error("unexpected ack character: \(Character(UnicodeScalar(startDelimiter)))") + throw Error.expectedAck + } + + if self.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 startDelimiter == UInt8(ascii: "$") else { + self.logger.error("unexpected delimiter: \(Character(UnicodeScalar(startDelimiter)))") + 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.accumulatedDelimiter = nil + self.accummulatedKind = [] + self.accummulatedArguments = [] + self.accummulatedSum = 0 + } + + 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 + } + + let expectedChecksum = (first * 16) + last + + guard expectedChecksum == self.accummulatedChecksum else { + throw Error.checksumIncorrect( + expectedChecksum: expectedChecksum, + receivedChecksum: self.accummulatedChecksum + ) + } + + let payload = try GDBHostCommand( + kindString: String(decoding: self.accummulatedKind, as: UTF8.self), + arguments: String(decoding: self.accummulatedArguments, as: UTF8.self) + ) + + if payload.kind == .startNoAckMode { + self.isNoAckModeRequested = true + } + + return .init(payload: payload, checksum: accummulatedChecksum) + } + + mutating package func decode( + context: ChannelHandlerContext, + buffer: inout ByteBuffer + ) throws(Error) -> DecodingState { + logger.trace(.init(stringLiteral: 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 + } +} diff --git a/Sources/GDBRemoteProtocol/GDBPacket.swift b/Sources/GDBRemoteProtocol/GDBPacket.swift new file mode 100644 index 00000000..1a1a65d7 --- /dev/null +++ b/Sources/GDBRemoteProtocol/GDBPacket.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// 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 + + package init(payload: Payload, checksum: UInt8) { + self.payload = payload + self.checksum = checksum + } +} + +extension GDBPacket: Equatable where Payload: Equatable {} diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift new file mode 100644 index 00000000..77391d73 --- /dev/null +++ b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// 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. +package enum VContActions: String { + case `continue` = "c" + case continueWithSignal = "C" + case step = "s" + case stepWithSignal = "S" + case stop = "t" + 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 + + /// 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 isNoAckModeActive: Bool + + /// Member-wise initializer for the debugger response. + package init(kind: Kind, isNoAckModeActive: Bool) { + self.kind = kind + self.isNoAckModeActive = isNoAckModeActive + } +} diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift new file mode 100644 index 00000000..37b76dc3 --- /dev/null +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +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 + + package init() {} + package func encode(data: GDBTargetResponse, out: inout ByteBuffer) { + if !isNoAckModeActive { + out.writeInteger(UInt8(ascii: "+")) + } + if data.isNoAckModeActive { + self.isNoAckModeActive = true + } + out.writeInteger(UInt8(ascii: "$")) + + switch data.kind { + case .ok: + out.writeString("OK#9a") + + case .keyValuePairs(let info): + out.writeString(info.map { (key, value) in "\(key):\(value);" }.joined().appendedChecksum) + + case .vContSupportedActions(let actions): + out.writeString("vCont;\(actions.map { "\($0.rawValue);" }.joined())".appendedChecksum) + + 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/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/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift new file mode 100644 index 00000000..5313bdd3 --- /dev/null +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -0,0 +1,165 @@ +//===----------------------------------------------------------------------===// +// +// 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 SystemPackage + import WasmKit + + 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 + } + + private let wasmBinary: ByteBuffer + private let moduleFilePath: FilePath + private let logger: Logger + private let functionsRLE: [(wasmAddress: Int, iSeqAddress: Int)] = [] + + 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.moduleFilePath = moduleFilePath + } + + package func handle(command: GDBHostCommand) throws -> GDBTargetResponse { + let responseKind: GDBTargetResponse.Kind + logger.trace("handling GDB host command", metadata: ["GDBHostCommand": .string(command.kind.rawValue)]) + + var isNoAckModeActive = false + switch command.kind { + case .startNoAckMode: + isNoAckModeActive = 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" { + 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, .generalRegisters: + fatalError() + } + + logger.trace("handler produced a response", metadata: ["GDBTargetResponse": .string("\(responseKind)")]) + + return .init(kind: responseKind, isNoAckModeActive: isNoAckModeActive) + } + } + +#endif diff --git a/Tests/GDBRemoteProtocolTests/GDBRemoteProtocolTests.swift b/Tests/GDBRemoteProtocolTests/GDBRemoteProtocolTests.swift new file mode 100644 index 00000000..c01db004 --- /dev/null +++ b/Tests/GDBRemoteProtocolTests/GDBRemoteProtocolTests.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// 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 { + @Test + func decoding() throws { + 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) + #expect(packet == GDBPacket(payload: GDBHostCommand(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 = GDBPacket( + payload: GDBHostCommand( + kind: .supportedFeatures, + arguments: "xmlRegisters=i386,arm,mips,arc;multiprocess+;fork-events+;vfork-events+" + ), + checksum: 0x2e, + ) + #expect(packet == expectedPacket) + #expect(decoder.accummulatedChecksum == 0) + } +} 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