diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift new file mode 100644 index 00000000..316ebdac --- /dev/null +++ b/Package@swift-6.1.swift @@ -0,0 +1,201 @@ +// 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(.v14), .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") + ], + 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: "GDBRemoteProtocol", + dependencies: [ + .product(name: "Logging", package: "swift-log"), + .product(name: "NIOCore", package: "swift-nio"), + ] + ), + .testTarget(name: "GDBRemoteProtocolTests", dependencies: ["GDBRemoteProtocol"]), + + .target( + name: "WasmKitGDBHandler", + dependencies: [ + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "SystemPackage", package: "swift-system"), + "WasmKit", + "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", + ] + ), + ], +) + +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"] + ), + ]) +#endif diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift new file mode 100644 index 00000000..e83e2eea --- /dev/null +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -0,0 +1,64 @@ +/// 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: 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 generalRegisters + + 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 + 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/GDBRemoteProtocol/GDBHostCommandDecoder.swift b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift new file mode 100644 index 00000000..46b696c0 --- /dev/null +++ b/Sources/GDBRemoteProtocol/GDBHostCommandDecoder.swift @@ -0,0 +1,135 @@ +import Logging +import NIOCore + +extension ByteBuffer { + var isChecksumDelimiterAtReader: Bool { + self.peekInteger(as: UInt8.self) == UInt8(ascii: "#") + } + + var isArgumentsDelimiterAtReader: Bool { + self.peekInteger(as: UInt8.self) == UInt8(ascii: ":") + } +} + +package struct GDBHostCommandDecoder: ByteToMessageDecoder { + enum Error: Swift.Error { + case expectedCommandStart + case unknownCommandKind(String) + case expectedChecksum + case checksumIncorrect + } + + package typealias InboundOut = GDBPacket + + private var accumulatedDelimiter: UInt8? + + private var accummulatedKind = [UInt8]() + private var accummulatedArguments = [UInt8]() + + private let logger: Logger + + package init(logger: Logger) { self.logger = logger } + + private var accummulatedSum = 0 + package var accummulatedChecksum: UInt8 { + UInt8(self.accummulatedSum % 256) + } + + 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. + guard + firstStartDelimiter == UInt8(ascii: "+") + && secondStartDelimiter == UInt8(ascii: "$") + else { + logger.error("unexpected delimiter: \(Character(UnicodeScalar(firstStartDelimiter)))\(Character(UnicodeScalar(secondStartDelimiter)))") + 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 + } + + let kindString = String(decoding: self.accummulatedKind, as: UTF8.self) + + 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 (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 { + 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..10a24fa6 --- /dev/null +++ b/Sources/GDBRemoteProtocol/GDBPacket.swift @@ -0,0 +1,12 @@ +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..5f55430d --- /dev/null +++ b/Sources/GDBRemoteProtocol/GDBTargetResponse.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 GDBTargetResponse { + package enum Kind { + case ok + case keyValuePairs(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/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift new file mode 100644 index 00000000..20ad3928 --- /dev/null +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.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: GDBTargetResponse, 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 .keyValuePairs(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/WasmKit/Execution/Errors.swift b/Sources/WasmKit/Execution/Errors.swift index 07c1880b..6bfab3ac 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 4f86cb75..ee866a60 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..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 4af369c4..e8dfb62c 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) @@ -2247,7 +2243,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/WasmKitDebugger.swift b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift new file mode 100644 index 00000000..53043486 --- /dev/null +++ b/Sources/WasmKitGDBHandler/WasmKitDebugger.swift @@ -0,0 +1,69 @@ +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/wasmkit-gdb-tool/Entrypoint.swift b/Sources/wasmkit-gdb-tool/Entrypoint.swift new file mode 100644 index 00000000..5ca8adf5 --- /dev/null +++ b/Sources/wasmkit-gdb-tool/Entrypoint.swift @@ -0,0 +1,115 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +#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") + var port = 8080 + + @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 + .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 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 await group.shutdownGracefully() + } +} diff --git a/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift b/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift new file mode 100644 index 00000000..4a30f0e1 --- /dev/null +++ b/Tests/GDBRemoteProtocolTests/RemoteProtocolTests.swift @@ -0,0 +1,36 @@ +import GDBRemoteProtocol +import Logging +import NIOCore +import Testing + +@Suite +struct LLDBRemoteProtocolTests { + @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/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