Building WASM Applications with Swift

Published on

With the official release of Swift 6.1, SwiftWasm has also undergone a major upgrade. This milestone update marks the first time SwiftWasm has achieved a build entirely based on the official Swift open-source toolchain—leaving behind the era of custom patches. This change not only significantly simplifies the installation process for developers and greatly reduces the consumption of system storage, but more importantly, the genuine build method drastically lowers platform maintenance costs and injects new vitality into the Swift ecosystem. In this article, we will explore how to build WebAssembly applications using Swift, showcasing the endless possibilities of Swift’s cross-platform development.

Understanding WebAssembly (WASM)

WebAssembly, commonly abbreviated as WASM, is a revolutionary lightweight binary instruction format that enables secure and efficient execution in modern browsers and other compatible environments. It was designed to provide a truly cross-platform, high-performance execution environment, effectively addressing JavaScript’s performance shortcomings in handling compute-intensive tasks.

As an intermediate code format, WASM breaks through the limitations imposed by programming languages, allowing developers to write code in languages such as C/C++, Rust, Go, and Swift, and compile it into a unified WASM bytecode. This feature greatly expands the technical boundaries of web development and provides developers with unprecedented freedom of choice.

The Swift community began exploring the integration of Swift and WASM as early as 2020. However, a real breakthrough occurred after the release of Swift 6.1—Swift’s support for WASM finally reached maturity as the official toolchain and SwiftWasm became integrated, leading to a qualitative leap in the development experience (including enhancements such as VSCode integration and swift-testing support). Today, for developers familiar with Swift, the technical threshold for building WASM applications has been significantly lowered, making it an ideal time to experience the charm of Swift’s cross-platform development.

Installing the Required Tools

Since SwiftWasm does not support the Swift toolchain integrated within Xcode, developers need to install the open-source Swift toolchain provided officially. The most convenient method currently is to manage Swift versions with swiftly.

Installing swiftly

swiftly is a convenient version management tool for the Swift toolchain, supporting rapid installation, switching, and updating of Swift toolchains. It is compatible with macOS and major Linux distributions, and allows a team to unify the Swift version via a .swift-version file.

On macOS, it can be installed with the following commands:

Bash
curl -O https://download.swift.org/swiftly/darwin/swiftly.pkg && \
installer -pkg swiftly.pkg -target CurrentUserHomeDirectory && \
~/.swiftly/bin/swiftly init --quiet-shell-followup && \
. ~/.swiftly/env.sh && \
hash -r

For Linux installation instructions, please refer to the official installation guide.

After installation, swiftly will automatically download the latest Swift toolchain. You can check the installed toolchain versions with the following command:

Bash
% swiftly list
Installed release toolchains
----------------------------
Swift 6.1.0 (in use) (default)

Installed snapshot toolchains
-----------------------------

Note: If Xcode is also installed on macOS, you must verify the order of the PATH environment variable to ensure that the swift command calls the toolchain installed by swiftly.

Installing the WebAssembly Swift SDK

Starting from SwiftWasm 6.1, it is only necessary to install the SDK for compiling WASM on top of the existing Swift toolchain.

After confirming that the Swift toolchain is installed, execute the following command to install the WebAssembly Swift SDK:

Bash
swift sdk install "https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.1-RELEASE/swift-wasm-6.1-RELEASE-wasm32-unknown-wasi.artifactbundle.zip" --checksum "7550b4c77a55f4b637c376f5d192f297fe185607003a6212ad608276928db992"

It is especially important to note that from version 6.1 onward, the Swift toolchain and WASM SDK versions must correspond exactly. If Swift is upgraded, you must install the corresponding version of the SDK simultaneously.

After installation, verify that the SDK is successfully installed using the following command:

Swift
% swift sdk list
6.1-RELEASE-wasm32-unknown-wasi

Installing Wasmtime

Although most WASM code runs in browsers, we can also run WASM code in non-browser environments using compatible platforms like Wasmtime.

Wasmtime provides a rich set of APIs that enable WASM code to interact with the host environment via the WASI standard—somewhat similar to the relationship between Node and JavaScript.

It can be installed as follows:

Bash
curl https://wasmtime.dev/install.sh -sSf | bash

Configuring VSCode

The latest vscode-swift extension now fully supports WASM. Once installed, it allows you to easily write and test Swift code in VSCode and related editors (such as Cursor, WindSurf, Trae, etc.).

For a better development and debugging experience, it is recommended to also install plugins such as SwiftFormat, SwiftLint, and CodeLLDB.

At this point, all preparations for building WASM applications with Swift have been completed, and you can officially begin your development journey.

Writing Your First WASM Project

Creating the Hello Wasm Project

First, create a project directory and enter it:

Swift
mkdir HelloWASM && cd HelloWASM

Create a new Swift executable package:

Swift
swift package init --type executable

Check the name of the installed WASM SDK:

Swift
swift sdk list
# Expected output:
6.1-RELEASE-wasm32-unknown-wasi

Compile the project using the SDK name mentioned above:

Swift
swift build --swift-sdk 6.1-RELEASE-wasm32-unknown-wasi
# Expected compilation output:
Building for debugging...
[8/8] Linking HelloWASM.wasm
Build complete! (1.78s)

Run the WebAssembly project using Wasmtime:

Swift
wasmtime .build/wasm32-unknown-wasi/debug/HelloWASM.wasm
# Expected output:
Hello, world!

Configuring the VSCode Project

In practice, an IDE is typically used for compiling, running, and debugging. After opening the project directory in VSCode, the vscode-swift extension will automatically generate a launch.json file in the .vscode directory.

image-20250408104947126

Within the .vscode directory, create a settings.json file to specify the path to the swiftly toolchain:

JSON
{
    "swift.path": "/Users/yangxu/.swiftly/bin/"
}

Next, create a tasks.json file to define a custom build task:

JSON
{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "swift: Build HelloWASM",
            "type": "shell",
            "command": "swift",
            "args": [
                "build",
                "--swift-sdk",
                "6.1-RELEASE-wasm32-unknown-wasi",
                "--product",
                "HelloWASM"
            ],
            "group": "build",
            "problemMatcher": [],
            "detail": "Custom build for WASM target"
        }
    ]
}

Modify the launch.json file to ensure that the debugging program path points to the WASM executable and that the pre-launch tasks are set correctly:

JSON
{
    "configurations": [
        {
            "type": "swift-lldb",
            "request": "launch",
            "args": [],
            "cwd": "${workspaceFolder:HelloWASM}",
            "name": "Debug HelloWASM",
            "program": "${workspaceFolder:HelloWASM}/.build/debug/HelloWASM",
            "preLaunchTask": "swift: Build Debug HelloWASM"
        },
        {
            "type": "swift-lldb",
            "request": "launch",
            "args": [],
            "cwd": "${workspaceFolder:HelloWASM}",
            "name": "Release HelloWASM",
            "program": "${workspaceFolder:HelloWASM}/.build/release/HelloWASM",
            "preLaunchTask": "swift: Build Release HelloWASM"
        }
    ]
}

Finally, press Cmd + Shift + P and select Reload Window to reload the VSCode window.

Now you can take advantage of VSCode’s built-in debugging features to develop and debug your Swift WASM application.

Creating a Browser-Based WASM Project

In most cases, WASM projects run in browser environments and interact with web page content. However, manually writing all the interaction and communication code can be both technically challenging and labor-intensive. Fortunately, the Swift community offers the JavaScriptKit library, which significantly simplifies such tasks.

JavaScriptKit provides a convenient way for Swift to interact with JavaScript. It allows you to access JavaScript objects and functions, create callable closures in JavaScript, perform data type conversions, and even implement multithreading support.

Below, we demonstrate how to create a simple webpage project using WASM for performing addition: the user can input two numbers into the webpage, click a button, and Swift code will calculate the sum and display the result.

Create and enter the project directory:

Bash
mkdir SumCalculator && cd SumCalculator

Initialize a Swift executable package:

Bash
swift package init --type executable

Add the JavaScriptKit dependency to your Package.swift:

Swift
let package = Package(
    name: "SumCalculator",
    dependencies: [
        .package(url: "https://github.com/swiftwasm/JavaScriptKit.git", from: "0.20.0"),
    ],
    targets: [
        .executableTarget(
            name: "SumCalculator",
            dependencies: [
                .product(name: "JavaScriptKit", package: "JavaScriptKit"),
            ]),
    ])

Modify the code in Sources/main.swift to implement the addition functionality and interact with the webpage:

Swift
import JavaScriptKit

// Define a function to calculate the sum of two Int32 numbers
func sum(_ first: Int32, _ second: Int32) -> Int32 {
    first + second
}

// Get the button element with the id "calculateSum"
let calculateSumElement = JSObject.global.document.getElementById("calculateSum").object!

// Add a click event listener to the button element
_ = calculateSumElement.addEventListener!(
    "click",
    JSClosure { _ in
        // Get the value of the input element with id "firstNumber" and convert it to Int32
        let firstNumber = Int32(
            JSObject.global.document.getElementById("firstNumber").object!.value
                .string!)
        // Get the value of the input element with id "secondNumber" and convert it to Int32
        let secondNumber = Int32(
            JSObject.global.document.getElementById("secondNumber").object!
                .value.string!)
        guard let firstNumber, let secondNumber else {
            _ = JSObject.global.alert!("Input Number")
            return JSValue.undefined
        }
        // Call the sum function to calculate the sum and alert the result
        let sum = sum(firstNumber, secondNumber)
        _ = JSObject.global.alert!("\(firstNumber) + \(secondNumber) = \(sum)")
        return JSValue.undefined
    })

// Get the global document object
let document = JSObject.global.document

// Create a div element
var divElement = document.createElement("div")
// Set the text content of the div element
divElement.innerText = "Welcome to WASM Calculator"
// Append the div element to the body
_ = document.body.appendChild(divElement)

Create an index.html file in the project root:

HTML
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Swift WASM Demo</title>
</head>
<body>
    <div id="calculator">
    <input type="number" id="firstNumber" placeholder="First number" value="5">
    <input type="number" id="secondNumber" placeholder="Second number" value="3">
    <button id="calculateSum">Calculate Sum</button>
    </div>
    <script type="module">
        // Import the WASM module, adjust the path as necessary
        import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js";
        // Initialize WASM when the page loads
        init();
    </script>
</body>
</html> 

Compile the project for a JavaScript/WebAssembly target:

Bash
swift package --swift-sdk 6.1-RELEASE-wasm32-unknown-wasi js --use-cdn

# Expected output:
Building for debugging...
[0/2] Write swift-version--47A8281FCF6E46BB.txt
[1/4] Write sources
[3/6] Compiling SumCalculator main.swift
[4/6] Emitting module SumCalculator
[5/7] Wrapping AST for SumCalculator for debugging
[6/8] Write Objects.LinkFileList
[7/8] Linking SumCalculator.wasm
Build of product 'SumCalculator' complete! (0.67s)
Packaging...
[1/14] .build/plugins/PackageToJS/outputs/Package/SumCalculator.wasm: building
[2/14] .build/plugins/PackageToJS/outputs/Package.tmp/wasm-imports.json: building
[3/14] .build/plugins/PackageToJS/outputs/Package/index.d.ts: building
[4/14] .build/plugins/PackageToJS/outputs/Package/index.js: building
[5/14] .build/plugins/PackageToJS/outputs/Package/instantiate.d.ts: building
[6/14] .build/plugins/PackageToJS/outputs/Package/instantiate.js: building
[7/14] .build/plugins/PackageToJS/outputs/Package/package.json: skipped
[8/14] .build/plugins/PackageToJS/outputs/Package/platforms/browser.d.ts: building
[9/14] .build/plugins/PackageToJS/outputs/Package/platforms/browser.js: building
[10/14] .build/plugins/PackageToJS/outputs/Package/platforms/browser.worker.js: building
[11/14] .build/plugins/PackageToJS/outputs/Package/platforms/node.d.ts: building
[12/14] .build/plugins/PackageToJS/outputs/Package/platforms/node.js: building
[13/14] .build/plugins/PackageToJS/outputs/Package/runtime.d.ts: building
[14/14] .build/plugins/PackageToJS/outputs/Package/runtime.js: building
Packaging finished
  • js: Specifies that the build target is for a JavaScript environment using WebAssembly.
  • —use-cdn: Automatically loads the required JavaScript dependencies from a CDN during the build.

Start a simple web server in the project root:

Bash
python -m http.server

# Expected output:
Serving HTTP on :: port 8000 (http://[::]:8000/) ...
::1 - - [08/Apr/2025 13:26:06] "GET / HTTP/1.1" 200 -
::1 - - [08/Apr/2025 13:26:06] "GET /.build/plugins/PackageToJS/outputs/Package/index.js HTTP/1.1" 200 -

Open your browser and visit http://localhost:8000 to experience a WASM application where Swift and JavaScript interact seamlessly.

Although JavaScriptKit has significantly reduced the complexity of communication between Swift and JavaScript, there remains a learning curve for some Swift developers.

Currently, TokamakUI is one of the simplest methods for building browser applications with SwiftWasm. It offers syntax and APIs very similar to SwiftUI, which greatly lowers the barrier to building web applications with Swift. However, since TokamakUI has not yet been adapted for Swift 6.x and still requires Swift 5.9.x, it is not discussed in this article. Additional content will be provided once TokamakUI becomes compatible with Swift 6.

Conclusion

With the continuous improvements in WebAssembly support within the Swift ecosystem, there is every reason to believe that, in the near future, Swift developers will be able to build vibrant, high-performance, cross-platform web applications in a more natural and seamless manner. As frameworks like TokamakUI mature, the entry barrier will be further lowered, allowing more Swift developers to expand their skills into the web domain.

Weekly Swift & SwiftUI highlights!