Chasm WebAssembly Runtime

repository·main·Indexed 42 days ago

https://github.com/charlietap/chasm

A WebAssembly (Wasm) runtime built on Kotlin Multiplatform targeting the Wasm 3.0 specification. It supports Wasm GC, Exception Handling, and Tail Calls across JVM, Native, and JS platforms. Includes a Gradle plugin for generating typesafe Kotlin interfaces from .wasm binaries, a low-level API for manual module instantiation, and WASI Preview 1 support.

Tokens
6.5K
Snippets
14
Records
28
Agent score
50%

What's inside Chasm

  1. Overview of the Chasm example application

    main
    The Chasm example project demonstrates how to integrate Chasm with a barebones Android application. It serves as both a functional demonstration and an integration test for Chasm and its plugin. The project is structured into a host Android application and several consumer modules that inject WebAssembly (Wasm) services into the dependency injection (DI) graph.
  2. Understand Sweet output directories and path preservation

    main

    Sweet organizes outputs by source to prevent collisions. Each source has its own dedicated directories:

    • Repositories: build/sweet/repositories/<source>/
    • Intermediates: build/sweet/intermediates/<source>/<relative-wast-path>/
    • Generated Tests: build/generated/sweet/<source>/

    Sweet preserves the complete file path found below the testDirectory. This ensures that files with the same name in different subdirectories (e.g., wasm-tools/resources.wast and wasmtime/resources.wast) do not overwrite each other.

  3. Use the Arena collection strategy (default)

    main
    The Arena strategy is the default collection mode in Chasm. It minimizes overhead by avoiding per-allocation policy checks. Instead, Chasm checks the managed heap against RuntimeConfig.gcThreshold at the end of every top-level WebAssembly invocation and performs collection only if the threshold has been reached.
  4. Use the Manual collection strategy

    main

    When using the Manual strategy, Chasm performs no automatic garbage collection. The embedding environment is responsible for triggering collection manually by calling the public gc function.

    /* Trigger collection manually via the public gc function */
  5. How BenchmarkStabilizer ensures consistent CPU core placement

    main

    On Apple silicon, macOS frequently moves threads between different core classes (Efficiency, Performance, and Super cores), which introduces noise into JVM benchmarks.

    BenchmarkStabilizer mitigates this by implementing a preference and verification strategy:

    1. It requests macOS to prefer the fastest available core class.
    2. It waits for the benchmark thread to reach that class.
    3. It verifies the thread's core class before and after the measured work.
    4. It rejects results if the core class changed during the measurement.

    This approach catches bad placement without adding sampling overhead inside the timed region. On unsupported platforms (Linux, Windows, or Macs where core classes cannot be detected), the API acts as a no-op, allowing benchmarks to run normally without stabilization.

  6. Use the Traditional collection strategy

    main

    The Traditional strategy provides immediate collection when needed. Every allocation of a struct, array, or exception performs a capacity and threshold preflight. If a collection is required, the currently executing value stack is scanned directly before the allocation proceeds.

    After a collection, the next threshold is adaptively calculated as: max(configured threshold, 2 * live words, 2 * pending allocation words)

    If the configured heap is exhausted, Chasm will attempt one collection before reporting a guest heap exhaustion error.

  7. Use the Chasm Gradle plugin for typesafe Kotlin interfaces

    main

    The Chasm Gradle plugin automates the creation of a typesafe Kotlin interface from your WebAssembly module. When applied to a Kotlin JVM, Kotlin Multiplatform, or Android module, it reads your .wasm binary and generates a class and interface pair containing Kotlin functions for every exported function in the Wasm binary.

    On JS targets, the plugin leverages the embedded Wasm VM (like V8 or JavaScriptCore), while on JVM and native targets, it uses Chasm.

    plugins {
        ...
        id("io.github.charlietap.chasm.gradle") version "3.0.0-2.0.0"
    }
    
    chasm {
        modules {
            create("ExampleService") {
                binary = layout.projectDirectory.file("src/main/resources/example.wasm")
                packageName = "com.example"
            }
        }
    }
  8. Manage Corpus baselines and cleanup

    main

    The plugin provides specific tasks for managing the lifecycle of corpus data and baselines:

    • updateCorpusBaseline: Performs a fresh measurement (bypassing build cache) and copies the resulting report to baselines/corpus/baseline.json. CLI phase overrides apply here.
    • cleanCorpusTests: Removes generated fixture index, test sources, packaged resources, raw fixture results, and the aggregate report. It leaves the synced corpus checkout in place to avoid re-cloning.
    • corpusResultsDirectory: An extension property that defines where raw fixture results are stored (defaults to build/wasm-corpus-results).
  9. Invoke WebAssembly functions using the low-level API

    main

    If you do not want to use the Gradle plugin, you can use Chasm's low-level API to decode, instantiate, and invoke Wasm modules manually.

    Basic Invocation

    1. Decode: Convert a .wasm byte array into a Module using module().
    2. Instantiate: Create an instance by providing a store and the module.
    3. Invoke: Call an exported function by name using invoke(store, instance, "function_name").

    Optimized Invocation with prepareFunction

    For performance-critical code where the same function is called repeatedly, use prepareFunction. This resolves the named export and its runtime function upfront, amortizing the resolution cost. The returned handle can be called directly with a list of arguments.

    // 1. Decode
    val wasmFileAsByteArray = ...
    val module = module(wasmFileAsByteArray)
    
    // 2. Instantiate
    val store = store()
    val instance = instance(store, module)
    
    // 3. Invoke (Dynamic)
    val result = invoke(store, instance, "fibonacci")
    
    // OR: Optimized (Prepared)
    val fibonacci = prepareFunction(store, instance, "fibonacci")
        .expect("Failed to prepare fibonacci")
    
    val result = fibonacci(listOf(NumberValue.I32(10)))
  10. Configure Sweet for Kotlin Multiplatform test generation

    main

    Sweet is used to generate Kotlin Multiplatform (KMP) tests from WebAssembly (WAST) repositories. Configuration is performed via the sweet extension in your Gradle build script.

    Key configuration concepts:

    • Sources: Each source represents a repository with its own Git URL, revision, and file selection rules.
    • Phase Support: You can define the maximum SemanticPhase supported by a source (e.g., DECODING, VALIDATION, EXECUTION).
    • Phase Limits: Use PhaseLimit to restrict specific file patterns to a lower phase than the source's default.
    • Line Excludes: Use LineExclude to skip specific one-based lines in a file during a specific phase. This is passed to the ScriptRunner and does not modify the original file.
    • Path Matching: includes, excludes, and PhaseLimit.patterns use normalized forward-slash paths relative to the testDirectory.

    Important: Always use full Git commit hashes for revision to ensure reproducible suites. Ensure wasmToolsVersion and the repository revision are upgraded in tandem to avoid syntax compatibility issues.

    import io.github.charlietap.sweet.lib.SemanticPhase
    import io.github.charlietap.sweet.plugin.LineExclude
    import io.github.charlietap.sweet.plugin.PhaseLimit
    
    sweet {
        wasmToolsVersion = "1.253.0"
        scriptRunner = "example.runtime.ExampleScriptRunner"
        testPackageName = "example.runtime.testsuite"
    
        sources {
            register("core") {
                repositoryUrl = "https://github.com/WebAssembly/testsuite.git"
                revision = "<full commit>"
                testDirectory = "."
                includes = listOf(
                    "*.wast",
                    "proposals/threads/*.wast",
                )
                phaseSupport = SemanticPhase.EXECUTION
                phaseLimits = listOf(
                    PhaseLimit(
                        patterns = setOf("proposals/threads/**"),
                        phaseSupport = SemanticPhase.VALIDATION,
                    ),
                )
                lineExcludes = listOf(
                    LineExclude(
                        filePath = "proposals/threads/imports.wast",
                        lines = setOf(310, 314),
                        phase = SemanticPhase.VALIDATION,
                    ),
                )
            }
    
            register("componentModel") {
                repositoryUrl = "https://github.com/WebAssembly/component-model.git"
                revision = "<full commit>"
                testDirectory = "test"
                includes = listOf("**/*.wast")
                phaseSupport = SemanticPhase.DECODING
            }
        }
    }
  11. Use the Wasm Corpus Plugin to generate Kotlin tests

    main

    The corpus plugin is a Gradle build that generates Kotlin tests from wasm-corpus fixtures. These tests run against the local Chasm source tree using a runtime-owned CorpusRunner.

    To use the plugin, you can configure a corpus {} block in your chasm/build.gradle.kts file. This block allows you to filter the fixtures used for testing based on:

    • Wasm versions
    • Source languages
    • Required or excluded features
    • Included or excluded canonical tags
    • Maximum binary size
    • Maximum test duration
    • Target names
    // Example configuration in chasm/build.gradle.kts
    corpus {
        // Configuration options like versions, features, and targets go here
    }