Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,24 @@ jobs:
wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9"
test-args: "--traits WasmDebuggingSupport --enable-code-coverage"
build-dev-dashboard: true
- swift: "swiftlang/swift:nightly-main-noble"
development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz"
wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz"
wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm
wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9"
test-args: "--traits WasmDebuggingSupport -Xswiftc -DWASMKIT_CI_TOOLCHAIN_NIGHTLY"
# Disabled until a toolchain containing https://github.com/swiftlang/swift/commit/b219d4089c922ceb8b700424236ca97f6087a9a1
# is tagged.
# - swift: "swiftlang/swift:nightly-main-noble"
# development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz"
# wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz"
# wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm
# wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9"
# test-args: "--traits WasmDebuggingSupport -Xswiftc -DWASMKIT_CI_TOOLCHAIN_NIGHTLY"
# - swift: "swiftlang/swift:nightly-main-noble"
# development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz"
# wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz"
# wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm
# wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9"
# test-args: "--traits WasmDebuggingSupport -Xswiftc -DWASMKIT_CI_TOOLCHAIN_NIGHTLY --build-system swiftbuild"
# label: " --build-system swiftbuild"

runs-on: ubuntu-24.04
name: "build-linux (${{ matrix.swift }})"
name: "build-linux (${{ matrix.swift }}${{ matrix.label }})"

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -186,7 +195,7 @@ jobs:
run: sudo chown -R $USER .build/html
- if: matrix.build-dev-dashboard
id: deployment
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v4
with:
path: .build/html

Expand Down Expand Up @@ -230,10 +239,19 @@ jobs:

build-android:
runs-on: ubuntu-24.04
strategy:
matrix:
include:
- swift-version: "6.2"
- swift-version: nightly-main

steps:
- uses: actions/checkout@v4
- name: Run Tests on Android emulator
uses: skiptools/swift-android-action@v2
with:
android-api-level: 30
swift-version: "${{ matrix.swift-version }}"

build-windows:
runs-on: windows-latest
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ jobs:
- uses: actions/checkout@v4
- run: ./Utilities/build-release.py -o wasmkit-x86_64-apple-macos.tar.gz -- --triple x86_64-apple-macos
- run: ./Utilities/build-release.py -o wasmkit-arm64-apple-macos.tar.gz -- --triple arm64-apple-macos
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: release-wasmkit-x86_64-apple-macos
path: wasmkit-x86_64-apple-macos.tar.gz
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: release-wasmkit-arm64-apple-macos
path: wasmkit-arm64-apple-macos.tar.gz
Expand All @@ -43,11 +43,11 @@ jobs:

- run: ./build-exec ./Utilities/build-release.py -o wasmkit-x86_64-swift-linux-musl.tar.gz -- --swift-sdk x86_64-swift-linux-musl
- run: ./build-exec ./Utilities/build-release.py -o wasmkit-aarch64-swift-linux-musl.tar.gz -- --swift-sdk aarch64-swift-linux-musl
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: release-wasmkit-x86_64-swift-linux-musl
path: wasmkit-x86_64-swift-linux-musl.tar.gz
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: release-wasmkit-aarch64-swift-linux-musl
path: wasmkit-aarch64-swift-linux-musl.tar.gz
Expand All @@ -59,7 +59,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v6
with:
pattern: release-wasmkit-*
path: ./release/
Expand Down
5 changes: 3 additions & 2 deletions Package@swift-6.1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let package = Package(
],
traits: [
.default(enabledTraits: []),
"WasmDebuggingSupport"
"WasmDebuggingSupport",
],
targets: [
.executableTarget(
Expand Down Expand Up @@ -123,7 +123,8 @@ let package = Package(
.target(name: "WITExtractor"),
.testTarget(name: "WITExtractorTests", dependencies: ["WITExtractor", "WIT"]),

.target(name: "GDBRemoteProtocol",
.target(
name: "GDBRemoteProtocol",
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "NIOCore", package: "swift-nio"),
Expand Down
1 change: 1 addition & 0 deletions Sources/WasmKit/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ add_wasmkit_library(WasmKit
Component/CanonicalCall.swift
Component/CanonicalOptions.swift
Component/ComponentTypes.swift
Execution/DebuggerInstructionMapping.swift
Execution/Instructions/Control.swift
Execution/Instructions/Instruction.swift
Execution/Instructions/Table.swift
Expand Down
86 changes: 37 additions & 49 deletions Sources/WasmKit/Execution/Debugger.swift
Original file line number Diff line number Diff line change
@@ -1,47 +1,6 @@
#if WasmDebuggingSupport

extension [Int] {
func binarySearch(nextClosestTo value: Int) -> Int? {
switch self.count {
case 0:
return nil
default:
var slice = self[0..<self.count]
while slice.count > 1 {
let middle = (slice.endIndex - slice.startIndex) / 2
if slice[middle] < value {
// Not found anything in the lower half, assigning higher half to `slice`.
slice = slice[(middle + 1)..<slice.endIndex]
} else {
// Not found anything in the higher half, assigning lower half to `slice`.
slice = slice[slice.startIndex..<middle]
}
}

return self[slice.startIndex]
}
}
}

extension Instance {
func findIseq(forWasmAddress address: Int) throws(Debugger.Error) -> (iseq: Pc, wasm: Int) {
// Look in the main mapping
if let iseq = handle.wasmToIseqMapping[address] {
return (iseq, address)
}

// If nothing found, find the closest Wasm address using binary search
guard let nextAddress = handle.wasmMappings.binarySearch(nextClosestTo: address),
// Look in the main mapping again with the next closest address if binary search produced anything
let iseq = handle.wasmToIseqMapping[nextAddress]
else {
throw Debugger.Error.noInstructionMappingAvailable(address)
}

return (iseq, nextAddress)
}
}

/// User-facing debugger state driven by a debugger host. This implementation has no knowledge of the exact
/// debugger protocol, which allows any protocol implementation or direct API users to be layered on top if needed.
package struct Debugger: ~Copyable {
package enum Error: Swift.Error, @unchecked Sendable {
case entrypointFunctionNotFound
Expand All @@ -61,9 +20,10 @@
private let instance: Instance

/// Reference to the entrypoint function of the currently debugged module, for use in ``stopAtEntrypoint``.
/// Currently assumed to be the WASI command `_start` entrypoint.
private let entrypointFunction: Function

/// Threading model of the Wasm engine configuration cached for a potentially hot path.
/// Threading model of the Wasm engine configuration, cached for a potentially hot path.
private let threadingModel: EngineConfiguration.ThreadingModel

private(set) var breakpoints = [Int: CodeSlot]()
Expand All @@ -72,6 +32,11 @@

private var pc = Pc.allocate(capacity: 1)

/// Initializes a new debugger state instance.
/// - Parameters:
/// - module: Wasm module to instantiate.
/// - store: Store that instantiates the module.
/// - imports: Imports required by `module` for instantiation.
package init(module: Module, store: Store, imports: Imports) throws {
let limit = store.engine.configuration.stackSize / MemoryLayout<StackSlot>.stride
let instance = try module.instantiate(store: store, imports: imports, isDebuggable: true)
Expand All @@ -90,11 +55,16 @@
self.pc.pointee = Instruction.endOfExecution.headSlot(threadingModel: threadingModel)
}

/// Sets a breakpoint at the first instruction in the entrypoint function of the module instantiated by
/// this debugger.
package mutating func stopAtEntrypoint() throws {
try self.enableBreakpoint(address: self.originalAddress(function: entrypointFunction))
}

package func originalAddress(function: Function) throws -> Int {
/// Finds a Wasm address for the first instruction in a given function.
/// - Parameter function: the Wasm function to find the first Wasm instruction address for.
/// - Returns: byte offset of the first Wasm instruction of given function in the module it was parsed from.
private func originalAddress(function: Function) throws -> Int {
precondition(function.handle.isWasm)

switch function.handle.wasm.code {
Expand All @@ -108,28 +78,46 @@
}
}

/// Enables a breakpoint at a given Wasm address.
/// - Parameter address: byte offset of the Wasm instruction that will be replaced with a breakpoint. If no
/// direct internal bytecode matching instruction is found, the next closest internal bytecode instruction
/// is replaced with a breakpoint. The original instruction to be restored is preserved in debugger state
/// represented by `self`.
/// See also ``Debugger/disableBreakpoint(address:)``.
package mutating func enableBreakpoint(address: Int) throws(Error) {
guard self.breakpoints[address] == nil else {
return
}

let (iseq, wasm) = try self.instance.findIseq(forWasmAddress: address)
guard let (iseq, wasm) = try self.instance.handle.instructionMapping.findIseq(forWasmAddress: address) else {
throw Error.noInstructionMappingAvailable(address)
}

self.breakpoints[wasm] = iseq.pointee
iseq.pointee = Instruction.breakpoint.headSlot(threadingModel: self.threadingModel)
}

/// Disables a breakpoint at a given Wasm address. If no breakpoint at a given address was previously set with
/// `self.enableBreakpoint(address:), this function immediately returns.
/// - Parameter address: byte offset of the Wasm instruction that was replaced with a breakpoint. The original
/// instruction is restored from debugger state and replaces the breakpoint instruction.
/// See also ``Debugger/enableBreakpoint(address:)``.
package mutating func disableBreakpoint(address: Int) throws(Error) {
guard let oldCodeSlot = self.breakpoints[address] else {
return
}

let (iseq, wasm) = try self.instance.findIseq(forWasmAddress: address)
guard let (iseq, wasm) = try self.instance.handle.instructionMapping.findIseq(forWasmAddress: address) else {
throw Error.noInstructionMappingAvailable(address)
}

self.breakpoints[wasm] = nil
iseq.pointee = oldCodeSlot
}

/// Resumes the module instantiated by the debugger stopped at a breakpoint. The breakpoint is disabled
/// and execution is resumed until the next breakpoint is triggered or all remaining instructions are
/// executed. If the module is not stopped at a breakpoint, this function returns immediately.
/// - Returns: `[Value]` result of `entrypointFunction` if current instance ran to completion,
/// `nil` if it stopped at a breakpoint.
package mutating func run() throws -> [Value]? {
Expand Down Expand Up @@ -181,7 +169,7 @@
}
} catch let breakpoint as Execution.Breakpoint {
let pc = breakpoint.pc
guard let wasmPc = self.instance.handle.iseqToWasmMapping[pc] else {
guard let wasmPc = self.instance.handle.instructionMapping.findWasm(forIseqAddress: pc) else {
throw Error.noReverseInstructionMappingAvailable(pc)
}

Expand All @@ -197,7 +185,7 @@
}

var result = Execution.captureBacktrace(sp: currentBreakpoint.iseq.sp, store: self.store).symbols.compactMap {
return self.instance.handle.iseqToWasmMapping[$0.address]
return self.instance.handle.instructionMapping.findWasm(forIseqAddress: $0.address)
}
result.append(currentBreakpoint.wasmPc)

Expand Down
84 changes: 84 additions & 0 deletions Sources/WasmKit/Execution/DebuggerInstructionMapping.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/// Two-way mapping between Wasm and internal iseq bytecode instructions. The implementation of the mapping
/// is private and is empty when `WasmDebuggingSupport` package trait is disabled.
struct DebuggerInstructionMapping {
#if WasmDebuggingSupport

/// Mapping from iseq Pc to instruction addresses in the original binary.
/// Used for handling current call stack requests issued by a ``Debugger`` instance.
private var iseqToWasm = [Pc: Int]()

/// Mapping from Wasm instruction addresses in the original binary to iseq instruction addresses.
/// Used for handling breakpoint requests issued by a ``Debugger`` instance.
private var wasmToIseq = [Int: Pc]()

/// Wasm addresses sorted in ascending order for binary search when of the next closest mapped
/// instruction, when no key is found in `wasmToIseqMapping`.
private var wasmMappings = [Int]()

mutating func add(wasm: Int, iseq: Pc) {
// Don't override the existing mapping, only store a new pair if there's no mapping for a given key.
if self.iseqToWasm[iseq] == nil {
self.iseqToWasm[iseq] = wasm
}
if self.wasmToIseq[wasm] == nil {
self.wasmToIseq[wasm] = iseq
}
self.wasmMappings.append(wasm)
}

/// Computes an address of WasmKit's iseq bytecode instruction that matches a given Wasm instruction address.
/// - Parameter address: the Wasm instruction to find a mapping for.
/// - Returns: A tuple with an address of found iseq instruction and the original Wasm instruction or next
/// closest match if no direct match was found.
func findIseq(forWasmAddress address: Int) -> (iseq: Pc, wasm: Int)? {
// Look in the main mapping
if let iseq = self.wasmToIseq[address] {
return (iseq, address)
}

// If nothing found, find the closest Wasm address using binary search
guard let nextAddress = self.wasmMappings.binarySearch(nextClosestTo: address),
// Look in the main mapping again with the next closest address if binary search produced anything
let iseq = self.wasmToIseq[nextAddress]
else {
return nil
}

return (iseq, nextAddress)
}

func findWasm(forIseqAddress pc: Pc) -> Int? {
self.iseqToWasm[pc]
}
#endif
}

#if WasmDebuggingSupport
extension [Int] {
/// Uses binary search to find an element in `self` that's next closest to a given value.
/// - Parameter value: the array element to search for or to use as a baseline when searching.
/// - Returns: array element `result`, where `result - value` is the smallest possible, while
/// `result > value` also holds.
package func binarySearch(nextClosestTo value: Int) -> Int? {
switch self.count {
case 0:
return nil
default:
var slice = self[0..<self.count]
while slice.count > 1 {
let middle = (slice.endIndex - slice.startIndex) / 2
if slice[middle] < value {
// Not found anything in the lower half, assigning higher half to `slice`.
slice = slice[(middle + 1)..<slice.endIndex]
} else {
// Not found anything in the higher half, assigning lower half to `slice`.
slice = slice[slice.startIndex..<middle]
}
}

return self[slice.startIndex]
}
}
}

#endif
2 changes: 2 additions & 0 deletions Sources/WasmKit/Execution/Execution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ extension Execution {

#if WasmDebuggingSupport

/// Counterpart to the free `executeWasm` function but implemented as a method of `Execution`,
/// Useful for representation of debugger state that needs to own `Execution`'s memory.
mutating func executeWasm(
threadingModel: EngineConfiguration.ThreadingModel,
function handle: InternalFunction,
Expand Down
Loading