diff --git a/Sources/WasmKit/Execution/DispatchInstruction.swift b/Sources/WasmKit/Execution/DispatchInstruction.swift index 4d79e822..741a1eae 100644 --- a/Sources/WasmKit/Execution/DispatchInstruction.swift +++ b/Sources/WasmKit/Execution/DispatchInstruction.swift @@ -212,6 +212,7 @@ extension Execution { case 196: return self.execute_tableElementDrop(sp: &sp, pc: &pc, md: &md, ms: &ms) case 197: return self.execute_onEnter(sp: &sp, pc: &pc, md: &md, ms: &ms) case 198: return self.execute_onExit(sp: &sp, pc: &pc, md: &md, ms: &ms) + case 199: return try self.execute_breakpoint(sp: &sp, pc: &pc, md: &md, ms: &ms) default: preconditionFailure("Unknown instruction!?") } @@ -1795,6 +1796,12 @@ extension Execution { pc.pointee = pc.pointee.advanced(by: 1) return next } + @_silgen_name("wasmkit_execute_breakpoint") @inline(__always) + mutating func execute_breakpoint(sp: UnsafeMutablePointer, pc: UnsafeMutablePointer, md: UnsafeMutablePointer, ms: UnsafeMutablePointer) throws -> CodeSlot { + let next: CodeSlot + (pc.pointee, next) = try self.breakpoint(sp: &sp.pointee, pc: pc.pointee) + return next + } } extension Instruction { diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index 7431d20f..3702ba72 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -361,9 +361,12 @@ extension Execution { } } - /// A ``Error`` thrown when the execution normally ends. + /// An ``Error`` thrown when the execution normally ends. struct EndOfExecution: Error {} + /// An ``Error`` thrown when a breakpoint is triggered. + struct Breakpoint: Error {} + /// The entry point for the execution of the WebAssembly function. @inline(never) mutating func execute( diff --git a/Sources/WasmKit/Execution/Function.swift b/Sources/WasmKit/Execution/Function.swift index 52fc06f2..d79eb1db 100644 --- a/Sources/WasmKit/Execution/Function.swift +++ b/Sources/WasmKit/Execution/Function.swift @@ -124,6 +124,8 @@ struct InternalFunction: Equatable, Hashable { _storage = bitPattern } + /// Returns `true` if the function is defined as a Wasm function in its original module. + /// Returns `false` if the function is implemented by the host. var isWasm: Bool { _storage & 0b1 == 0 } @@ -241,7 +243,7 @@ struct WasmFunctionEntity { switch code { case .uncompiled(let code): return try compile(store: store, code: code) - case .compiled(let iseq): + case .compiled(let iseq), .compiledAndPatchable(_, let iseq): return iseq } } @@ -260,7 +262,7 @@ struct WasmFunctionEntity { locals: code.locals, functionIndex: index, codeSize: code.expression.count, - intercepting: engine.interceptor != nil + isIntercepting: engine.interceptor != nil ) let iseq = try code.withValue { code in try translator.translate(code: code) @@ -281,7 +283,8 @@ extension EntityHandle { $0.code = .compiled(iseq) return iseq } - case .compiled(let iseq): return iseq + case .compiled(let iseq), .compiledAndPatchable(_, let iseq): + return iseq } } } @@ -313,6 +316,7 @@ struct InstructionSequence { enum CodeBody { case uncompiled(InternalUncompiledCode) case compiled(InstructionSequence) + case compiledAndPatchable(InternalUncompiledCode, InstructionSequence) } extension Reference { diff --git a/Sources/WasmKit/Execution/Instructions/Control.swift b/Sources/WasmKit/Execution/Instructions/Control.swift index 47dbbb49..93291b7b 100644 --- a/Sources/WasmKit/Execution/Instructions/Control.swift +++ b/Sources/WasmKit/Execution/Instructions/Control.swift @@ -223,4 +223,8 @@ extension Execution { Function(handle: function, store: store.value) ) } + + mutating func breakpoint(sp: inout Sp, pc: Pc) throws -> (Pc, CodeSlot) { + throw Breakpoint() + } } diff --git a/Sources/WasmKit/Execution/Instructions/Instruction.swift b/Sources/WasmKit/Execution/Instructions/Instruction.swift index 65b4028c..4c4dd969 100644 --- a/Sources/WasmKit/Execution/Instructions/Instruction.swift +++ b/Sources/WasmKit/Execution/Instructions/Instruction.swift @@ -413,6 +413,10 @@ enum Instruction: Equatable { case onEnter(Instruction.OnEnterOperand) /// Intercept the exit of a function case onExit(Instruction.OnExitOperand) + /// Stop the VM on this instruction as a breakpoint + /// + /// This instruction is used in debugging scenarios. + case breakpoint } extension Instruction { @@ -1271,6 +1275,7 @@ extension Instruction { case .tableElementDrop: return 196 case .onEnter: return 197 case .onExit: return 198 + case .breakpoint: return 199 } } } @@ -1481,6 +1486,7 @@ extension Instruction { case 196: return .tableElementDrop(Instruction.TableElementDropOperand.load(from: &pc)) case 197: return .onEnter(Instruction.OnEnterOperand.load(from: &pc)) case 198: return .onExit(Instruction.OnExitOperand.load(from: &pc)) + case 199: return .breakpoint default: fatalError("Unknown instruction opcode: \(opcode)") } } @@ -1694,6 +1700,7 @@ extension Instruction { case 196: return "tableElementDrop" case 197: return "onEnter" case 198: return "onExit" + case 199: return "breakpoint" default: fatalError("Unknown instruction index: \(opcode)") } } diff --git a/Sources/WasmKit/Translator.swift b/Sources/WasmKit/Translator.swift index 7e6ed95a..8b8c6188 100644 --- a/Sources/WasmKit/Translator.swift +++ b/Sources/WasmKit/Translator.swift @@ -820,7 +820,9 @@ struct InstructionTranslator: InstructionVisitor { /// The index of the function in the module let functionIndex: FunctionIndex /// Whether a call to this function should be intercepted - let intercepting: Bool + let isIntercepting: Bool + /// Whether Wasm debugging facilities are currently enabled. + let isDebugging: Bool var constantSlots: ConstSlots let validator: InstructionValidator @@ -833,7 +835,8 @@ struct InstructionTranslator: InstructionVisitor { locals: [WasmTypes.ValueType], functionIndex: FunctionIndex, codeSize: Int, - intercepting: Bool + isIntercepting: Bool, + isDebugging: Bool = false ) throws { self.allocator = allocator self.funcTypeInterner = funcTypeInterner @@ -849,7 +852,8 @@ struct InstructionTranslator: InstructionVisitor { self.valueStack = ValueStack(stackLayout: stackLayout) self.locals = Locals(types: type.parameters + locals) self.functionIndex = functionIndex - self.intercepting = intercepting + self.isIntercepting = isIntercepting + self.isDebugging = isDebugging self.constantSlots = ConstSlots(stackLayout: stackLayout) self.validator = InstructionValidator(context: module) @@ -1059,7 +1063,7 @@ struct InstructionTranslator: InstructionVisitor { return emittedCopy } private mutating func translateReturn() throws { - if intercepting { + if isIntercepting { // Emit `onExit` instruction before every `return` instruction emit(.onExit(functionIndex)) } @@ -1098,7 +1102,7 @@ struct InstructionTranslator: InstructionVisitor { /// Translate a Wasm expression into a sequence of instructions. mutating func translate(code: Code) throws -> InstructionSequence { - if intercepting { + if isIntercepting { // Emit `onEnter` instruction at the beginning of the function emit(.onEnter(functionIndex)) } @@ -2239,6 +2243,16 @@ struct InstructionTranslator: InstructionVisitor { return .tableSize(Instruction.TableSizeOperand(tableIndex: table, result: LVReg(result))) } } + + mutating func visitUnknown(_ opcode: [UInt8]) throws -> Bool { + guard self.isDebugging && opcode.count == 1 && opcode[0] == 0xFF else { + return false + } + + emit(.breakpoint) + + return true + } } struct TranslationError: Error, CustomStringConvertible { diff --git a/Sources/WasmParser/BinaryInstructionDecoder.swift b/Sources/WasmParser/BinaryInstructionDecoder.swift index a89443c2..1111b67d 100644 --- a/Sources/WasmParser/BinaryInstructionDecoder.swift +++ b/Sources/WasmParser/BinaryInstructionDecoder.swift @@ -8,8 +8,8 @@ import WasmTypes protocol BinaryInstructionDecoder { /// Claim the next byte to be decoded @inlinable func claimNextByte() throws -> UInt8 - /// Visit unknown instruction - @inlinable func visitUnknown(_ opcode: [UInt8]) throws + + func throwUnknown(_ opcode: [UInt8]) throws -> Never /// Decode `block` immediates @inlinable mutating func visitBlock() throws -> BlockType /// Decode `loop` immediates @@ -87,8 +87,9 @@ protocol BinaryInstructionDecoder { /// Decode `table.size` immediates @inlinable mutating func visitTableSize() throws -> UInt32 } + @inlinable -func parseBinaryInstruction(visitor: inout V, decoder: inout D) throws -> Bool { +func parseBinaryInstruction(visitor: inout some InstructionVisitor, decoder: inout some BinaryInstructionDecoder) throws -> Bool { let opcode0 = try decoder.claimNextByte() switch opcode0 { case 0x00: @@ -562,10 +563,10 @@ func parseBinaryInstruction( let (table) = try decoder.visitTableFill() try visitor.visitTableFill(table: table) default: - try decoder.visitUnknown([opcode0, opcode1]) + if try !visitor.visitUnknown([opcode0, opcode1]) { try decoder.throwUnknown([opcode0, opcode1]) } } default: - try decoder.visitUnknown([opcode0]) + if try !visitor.visitUnknown([opcode0]) { try decoder.throwUnknown([opcode0]) } } return false } diff --git a/Sources/WasmParser/InstructionVisitor.swift b/Sources/WasmParser/InstructionVisitor.swift index 230ba132..2b4f3c7f 100644 --- a/Sources/WasmParser/InstructionVisitor.swift +++ b/Sources/WasmParser/InstructionVisitor.swift @@ -398,6 +398,8 @@ public protocol InstructionVisitor { mutating func visitTableGrow(table: UInt32) throws /// Visiting `table.size` instruction. mutating func visitTableSize(table: UInt32) throws + /// Returns: `true` if the parser should silently proceed parsing. + mutating func visitUnknown(_ opcode: [UInt8]) throws -> Bool } extension InstructionVisitor { @@ -514,5 +516,6 @@ extension InstructionVisitor { public mutating func visitTableSet(table: UInt32) throws {} public mutating func visitTableGrow(table: UInt32) throws {} public mutating func visitTableSize(table: UInt32) throws {} + public mutating func visitUnknown(_ opcode: [UInt8]) throws -> Bool { false } } diff --git a/Sources/WasmParser/WasmParser.swift b/Sources/WasmParser/WasmParser.swift index 2f8374b2..4b653337 100644 --- a/Sources/WasmParser/WasmParser.swift +++ b/Sources/WasmParser/WasmParser.swift @@ -606,10 +606,14 @@ extension Parser: BinaryInstructionDecoder { return 0 } - @inlinable func visitUnknown(_ opcode: [UInt8]) throws { + @inlinable func throwUnknown(_ opcode: [UInt8]) throws -> Never { throw makeError(.illegalOpcode(opcode)) } + @inlinable func visitUnknown(_ opcode: [UInt8]) throws -> Bool { + try throwUnknown(opcode) + } + @inlinable mutating func visitBlock() throws -> BlockType { try parseResultType() } @inlinable mutating func visitLoop() throws -> BlockType { try parseResultType() } @inlinable mutating func visitIf() throws -> BlockType { try parseResultType() } @@ -744,6 +748,7 @@ extension Parser: BinaryInstructionDecoder { return try stream.consumeAny() } + /// Returns: `true` if the parsed instruction is the block end instruction. @inline(__always) @inlinable mutating func parseInstruction(visitor v: inout V) throws -> Bool { diff --git a/Sources/_CWasmKit/include/DirectThreadedCode.inc b/Sources/_CWasmKit/include/DirectThreadedCode.inc index e52f81c1..71eb00a6 100644 --- a/Sources/_CWasmKit/include/DirectThreadedCode.inc +++ b/Sources/_CWasmKit/include/DirectThreadedCode.inc @@ -1262,6 +1262,13 @@ SWIFT_CC(swiftasync) static inline void wasmkit_tc_onExit(Sp sp, Pc pc, Md md, M INLINE_CALL next = wasmkit_execute_onExit(&sp, &pc, &md, &ms, state, &error); return ((wasmkit_tc_exec)next)(sp, pc, md, ms, state); } +SWIFT_CC(swiftasync) static inline void wasmkit_tc_breakpoint(Sp sp, Pc pc, Md md, Ms ms, SWIFT_CONTEXT void *state) { + SWIFT_CC(swift) uint64_t wasmkit_execute_breakpoint(Sp *sp, Pc *pc, Md *md, Ms *ms, SWIFT_CONTEXT void *state, SWIFT_ERROR_RESULT void **error); + void * _Nullable error = NULL; uint64_t next; + INLINE_CALL next = wasmkit_execute_breakpoint(&sp, &pc, &md, &ms, state, &error); + if (error) return wasmkit_execution_state_set_error(error, sp, state); + return ((wasmkit_tc_exec)next)(sp, pc, md, ms, state); +} static const uintptr_t wasmkit_tc_exec_handlers[] = { (uintptr_t)((wasmkit_tc_exec)&wasmkit_tc_copyStack), (uintptr_t)((wasmkit_tc_exec)&wasmkit_tc_globalGet), @@ -1462,4 +1469,5 @@ static const uintptr_t wasmkit_tc_exec_handlers[] = { (uintptr_t)((wasmkit_tc_exec)&wasmkit_tc_tableElementDrop), (uintptr_t)((wasmkit_tc_exec)&wasmkit_tc_onEnter), (uintptr_t)((wasmkit_tc_exec)&wasmkit_tc_onExit), + (uintptr_t)((wasmkit_tc_exec)&wasmkit_tc_breakpoint), }; diff --git a/Utilities/Sources/VMSpec/Instruction.swift b/Utilities/Sources/VMSpec/Instruction.swift index 0243ed79..dcff3f95 100644 --- a/Utilities/Sources/VMSpec/Instruction.swift +++ b/Utilities/Sources/VMSpec/Instruction.swift @@ -502,6 +502,16 @@ extension VMGen { // Profiling Instruction(name: "onEnter", documentation: "Intercept the entry of a function", immediate: "OnEnterOperand"), Instruction(name: "onExit", documentation: "Intercept the exit of a function", immediate: "OnExitOperand"), + + // Debugging + Instruction(name: "breakpoint", + documentation: """ + Stop the VM on this instruction as a breakpoint + + This instruction is used in debugging scenarios. + """, + isControl: true, mayThrow: true, mayUpdateFrame: true + ), ] // MARK: - Instruction generation diff --git a/Utilities/Sources/WasmGen.swift b/Utilities/Sources/WasmGen.swift index 60b5275f..af354614 100644 --- a/Utilities/Sources/WasmGen.swift +++ b/Utilities/Sources/WasmGen.swift @@ -110,6 +110,8 @@ enum WasmGen { code += """ + /// Returns: `true` if the parser should silently proceed parsing. + mutating func visitUnknown(_ opcode: [UInt8]) throws -> Bool } """ @@ -162,7 +164,11 @@ enum WasmGen { }.joined(separator: ", ") code += ") throws {}\n" } - code += "}\n" + code += """ + public mutating func visitUnknown(_ opcode: [UInt8]) throws -> Bool { false } + } + + """ return code } @@ -527,8 +533,9 @@ enum WasmGen { protocol BinaryInstructionDecoder { /// Claim the next byte to be decoded @inlinable func claimNextByte() throws -> UInt8 - /// Visit unknown instruction - @inlinable func visitUnknown(_ opcode: [UInt8]) throws + + /// Throw an error due to unknown opcode. + func throwUnknown(_ opcode: [UInt8]) throws -> Never """ for instruction in instructions.categorized { @@ -552,8 +559,9 @@ enum WasmGen { """ code += """ + @inlinable - func parseBinaryInstruction(visitor: inout V, decoder: inout D) throws -> Bool { + func parseBinaryInstruction(visitor: inout some InstructionVisitor, decoder: inout some BinaryInstructionDecoder) throws -> Bool { """ func renderSwitchCase(_ root: Trie, depth: Int = 0) { @@ -603,9 +611,10 @@ enum WasmGen { } } code += "\(indent)default:\n" - code += "\(indent) try decoder.visitUnknown(" - code += "[" + (0...depth).map { opcodeByteName($0) }.joined(separator: ", ") + "]" - code += ")\n" + code += "\(indent) if try !visitor.visitUnknown(" + let opcode = "[" + (0...depth).map { opcodeByteName($0) }.joined(separator: ", ") + "]" + code += opcode + code += ") { try decoder.throwUnknown(\(opcode)) }\n" code += "\(indent)}\n" }