WasmKit Documentation

repository·main·Indexed 19 days ago

https://github.com/swiftwasm/wasmkit

A standalone, embeddable WebAssembly runtime and tooling implementation written in Swift. Designed to be lightweight with minimal dependencies, WasmKit supports WASI and can be embedded in Swift applications across various platforms, including embedded targets like ESP32-C6. The project includes a CLI for executing WASI-compatible binaries, a suite of benchmarks, and comprehensive fuzzing and differential testing tools.

Tokens
10.1K
Snippets
23
Records
48
Agent score
67%

What's inside WasmKit

  1. Understand the Stack Frame Layout

    main

    The stack frame is inspired by the stitch WebAssembly interpreter and consists of four main parts:

    1. Frame Header: Contains the saved stack pointer, return address, current instance, and value slots for parameters and return values.
    2. Locals: Contains the local variables of the current function.
    3. Constant Pool: Contains constant values. Its size is determined during translation based on Wasm-level code size.
    4. Dynamic Stack: Contains intermediate values produced by instructions. Its size is fixed at the end of translation based on the maximum stack height.

    Slots vs Values

    All values are indexed in 64-bit slots (StackSlot == UInt64).

    • i32, i64, f32, f64, and ref occupy 1 slot.
    • v128 occupies 2 consecutive slots (lo then hi).

    Note: Register indices always refer to the first slot of a value. For a v128 value, the register index points to the lo slot, and reg + 1 points to the hi slot.

  2. Understand the relationship between Worlds and Components

    main

    In the WasmKit Swift toolchain, a World corresponds to a Component. A component is represented as a linked WebAssembly binary after the wasm-ld stage.

    Key rules:

    • A component contains exactly one World.
    • A World can include other Worlds. When this happens, the items from the included Worlds are flattened into the including World, which does not violate the single-world rule.
  3. Understand identifier transformations for Swift

    main

    When working with WIT (WebAssembly Interface Type) definitions, identifiers often use kebab-case, :, or /. Because Swift does not support these characters in identifiers, WasmKit transforms them into PascalCase.

    Specifically, -, :, and / are replaced, and the first letter following these symbols is upcased.

    Warning: Because of this transformation, names that are intended to be distinct in WIT might conflict in Swift. For example, a WIT interface named ns:pkg/iface and one named ns-pkg-iface will both transform to the same identifier in Swift, causing a conflict. Name escaping is not currently implemented to avoid complex transformation issues.

    package ns:pkg
    interface iface {
      type my-type = u8
    }
    world w {
      interface ns-pkg-iface {
        type my-type = u32
      }
    }
  4. Understand the Register-based Interpreter Design

    main

    WasmKit uses a register-based interpreter where most VM instructions correspond to a single WebAssembly instruction but encode their operand and result registers directly into the instruction.

    Key concepts:

    • Registers: A register is a 64-bit slot in the stack frame that can hold any WebAssembly value type (i32, i64, f32, f64, ref). Registers are identified by a 16-bit index.
    • Provider Instructions: Instructions like local.get or {i32,i64,f32,f64}.const are treated as no-ops at runtime and are encoded as registers in operands rather than having their own VM instruction.
    • Translation: The Translator.swift pass converts WebAssembly instructions into a sequence of VM instructions by tracking stack value sources (constants, locals, etc.). This process can fuse instructions, such as embedding a local.set or a constant directly into an arithmetic instruction.
  5. How the Canonical ABI works in the Component Model

    main

    The Canonical ABI is the mapping layer that translates high-level WebAssembly Interface Type (WIT) values into low-level WebAssembly core values and memory operations. It enables communication between components by defining how complex types (like strings or records) are converted into primitive types (like i32, i64) that the WebAssembly core engine understands.

    Every WIT type utilizes two fundamental operations:

    • Lift: Translates core values into a WIT value. This is used when a core-typed function calls a WIT-typed function, or when a core-typed function returns a WIT value.
    • Lower: Translates a WIT value into core values. This is used when a WIT-typed function calls a core-typed function, or when a WIT-typed function returns a core value.
  6. Understanding Lifting and Lowering operations

    main

    Lifting and Lowering are the two primary mechanisms for type conversion in the Canonical ABI. They are often split into two stages depending on whether the data fits in function arguments or must be handled via memory.

    Lifting (Core $\rightarrow$ WIT)

    Used to bring core values into the WIT domain.

    1. Flat Lifting: Translates a list of core values directly into a WIT value.
    2. Loading: Reads a WIT value from memory. This is used when the value is too large to be passed directly as a function argument or return value.

    Lowering (WIT $\rightarrow$ Core)

    Used to move WIT values into the core domain.

    1. Flat Lowering: Translates a WIT value into a list of core values.
    2. Storing: Writes a WIT value to memory. This is used when the value is too large to be passed directly as a function argument or return value.

    Note on implementation: To support environments without the multi-value proposal, the number of return values (MAX_FLAT_RESULTS) is currently limited to 1 in the core-level signature.

  7. Implement UTF-8 string interop via StringPassing pattern

    main

    The StringPassing example demonstrates a common ABI (Application Binary Interface) for passing UTF-8 strings between a WebAssembly host and a guest module. This pattern involves two primary directions of data flow:

    Guest to Host (Printing Strings)

    To pass a string from the guest to the host:

    1. The WebAssembly guest calls an imported host function (e.g., printer.print_str).
    2. The guest passes a pair of i32 values: a pointer (ptr) and a length (len).
    3. The host reads the UTF-8 bytes from the module's exported memory starting at ptr for len bytes.

    Host to Guest (Allocating and Writing Strings)

    To pass a string from the host to the guest:

    1. The host calls an exported guest function alloc(size: i32) -> i32 to reserve space in the guest's memory.
    2. The host writes the UTF-8 bytes into the memory region returned by alloc.
    3. The host calls a guest function (e.g., checksum(ptr: i32, len: i32) -> i32) using the allocated pointer and the string length to process the data.
  8. How WasmKit implements the Canonical ABI

    main

    WasmKit implements the Canonical ABI using a unified modeling approach located in Sources/WIT/CanonicalABI/. This allows the same logic to be used across three different implementation contexts:

    1. Code Generator: Statically generates Swift-level lifting and lowering code for guest components and host runtimes.
    2. Host Runtime: Dynamically exchanges WIT values between guest components at runtime based on WIT definitions.
    3. AOT/JIT Compiler: Statically generates lifting and lowering code at runtime based on the provided WIT definition.

    In static contexts, operations are performed at the meta-level to construct instruction sequences without executing them. In dynamic contexts, the operations are executed directly to perform the value exchange.

  9. Understand the Instruction Dispatch Threading Models

    main

    WasmKit uses "threaded code" techniques to minimize instruction dispatch overhead. It supports two models:

    1. Direct-threaded (Default): Uses a guaranteed tail call approach (swiftasync calling convention) to dispatch instructions. This is the preferred model on most platforms.
    2. Token-threaded: A fallback option for platforms that do not support guaranteed tail calls.

    In the direct-threaded model, instruction handler functions are defined in C headers and call Swift functions for the actual semantics. The C handlers then tail-call the next instruction handler to maintain efficiency.

  10. Understanding the WasmKit Register-based Interpreter Design

    main

    WasmKit uses a register-based interpreter design to achieve high performance and memory efficiency. This design is an evolution from previous generations:

    1. First Generation: Interpreted structured WebAssembly instructions directly using a switch-case dispatch. Every block, loop, or if instruction required its own stack frame, making it slow and memory-inefficient.
    2. Second Generation: Introduced a translation pass that converted WebAssembly instructions into a stack-based linear intermediate representation. It pre-computed stack-height information and branch offsets to improve branching performance (achieving ~5x speedup).
    3. Current (Register-based) Generation: Designed to address performance bottlenecks in non-optimized WebAssembly code (like Swift Standard Library tests). It removes 'provider' instructions (e.g., local.get, i32.const) during translation and embeds register information directly into 'consumer' instructions (e.g., i32.add, call). This reduces the total number of instructions executed.
  11. Understand Resource method implementation responsibilities

    main

    When using resource types in WIT, the component that exposes an interface containing that resource type is expected to provide the implementation for its methods.

    If multiple components (e.g., a service and a middleware) both import and export the same interface containing a resource, both components are responsible for providing implementations for the resource's lifecycle and methods. This includes:

    • Constructors (#[constructor])
    • Destructors (#[dtor])
    • Methods (#[method])

    Type Sharing Constraints:

    • Plain types (like record or enum) can be shared between export and import interfaces.
    • Resource types (and types that transitively use them) cannot be shared in the same way. Even if they have the same raw representation, each resource instance in an import or export has its own unique constructor, destructor, and method implementations. Data passing between these boundaries must call the appropriate imported/exported implementations.
    package example:http
    interface handler {
      record header-entry {
        key: string,
        value: string,
      }
      resource blob {
        constructor(bytes: list<u8>)
        size: func() -> u32
      }
      record message {
        body: own<blob>,
        headers: list<header-entry>,
      }
      handle: func(request: message) -> message
    }
    world service {
      export handler
    }
    world middleware {
      import handler
      export handler
    }
  12. Understand the mprotect linear memory layout

    main

    When using the .mprotect strategy for wasm32 memories, WasmKit reserves a 4 GiB address space plus an internal guard tail (memoryOffsetGuardSize).

    Memory is laid out as follows:

    • [base, base + committed): Accessible with PROT_READ | PROT_WRITE.
    • [base + committed, base + reservation): Inaccessible (PROT_NONE).

    When memory grows, WasmKit commits more of the reserved range using mprotect without relocating existing contents.