用 Swift 构建 WASM 应用

发表于

随着 Swift 6.1 版本的正式发布,SwiftWasm 也迎来了重大升级。这一里程碑式的更新标志着 SwiftWasm 首次实现了完全基于官方 Swift 开源工具链的构建——告别了自定义补丁的时代,不仅显著简化了开发者的安装流程,大幅节省了系统存储空间,更为重要的是,这种纯正构建方式极大降低了平台的维护成本,为 Swift 生态系统注入了新的活力。在本文中,我们将探索如何利用 Swift 构建 WebAssembly 应用,带你领略 Swift 跨平台开发的无限可能。

解读 WebAssembly (WASM)

WebAssembly,通常简称为 WASM,是一种革命性的轻量级二进制指令格式,能够在现代浏览器和其他兼容环境中安全高效地执行。它的诞生旨在提供一个真正跨平台、高性能的执行环境,有效弥补了 JavaScript 在处理计算密集型任务时的性能短板。

作为一种中间代码格式,WASM 突破了编程语言的限制,让开发者能够使用 C/C++、Rust、Go 以及 Swift 等多种语言编写代码,并将其编译为统一的 WASM 字节码。这一特性极大地拓展了 Web 开发的技术边界,为开发者提供了前所未有的选择自由。

Swift 社区早在 2020 年就开始了将 Swift 与 WASM 结合的探索之旅。然而,真正的突破发生在 Swift 6.1 的发布之后——Swift 对 WASM 的支持终于臻于成熟,官方工具链与 SwiftWasm 实现了整合,开发体验也得到了质的飞跃(包括 VSCode 集成、swift-testing 支持等增强功能)。如今,对于熟悉 Swift 的开发者而言,构建 WASM 应用的技术门槛已显著降低,这无疑是体验 Swift 跨平台开发魅力的绝佳时机。

安装所需工具

由于 SwiftWasm 并不支持 Xcode 中集成的 Swift 工具链,开发者需要安装官方提供的开源 Swift 工具链。目前最便捷的方式是通过 swiftly 来管理 Swift 版本。

安装 swiftly

swiftly 是一款便捷的 Swift 工具链版本管理工具,支持快速安装、切换及更新 Swift 工具链,兼容 macOS 和主要 Linux 发行版,并可通过 .swift-version 文件统一团队所用的 Swift 版本。

在 macOS 上,可通过以下命令进行安装:

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

Linux 系统的安装方法请参阅 官方安装指南

安装完成后,swiftly 会自动下载最新的 Swift 工具链。你可以使用以下命令查看已安装的工具链版本:

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

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

注意:若 macOS 上同时安装了 Xcode,需确认 PATH 环境变量顺序,确保 swift 命令调用的是 swiftly 安装的工具链。

安装 WebAssembly Swift SDK

从 SwiftWasm 6.1 开始,仅需在现有 Swift 工具链基础上安装用于编译 WASM 的 SDK。

确认 Swift 工具链安装完毕后,执行以下命令安装 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"

需特别注意:从 6.1 版本起,Swift 工具链与 WASM SDK 版本必须严格对应。若 Swift 升级,需同步安装对应版本的 SDK。

安装完成后,使用以下命令确认 SDK 已成功安装:

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

安装 Wasmtime

尽管多数情况下 WASM 代码运行在浏览器中,但我们也可以使用 Wasmtime 等兼容环境,将 WASM 代码运行在非浏览器环境。

Wasmtime 提供丰富的 API,支持 WASM 代码通过 WASI 标准与主机环境交互,有些类似 Node 与 JavaScript 的关系。

安装方式如下:

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

配置 VSCode

最新的 vscode-swift 插件已完整支持 WASM,安装后即可在 VSCode 及相关编辑器(如 Cursor、WindSurf、Trae 等)中轻松编写与测试 Swift 代码。

为获得更好的开发和调试体验,建议同时安装 SwiftFormatSwiftLintCodeLLDB 等插件。

至此,通过 Swift 构建 WASM 应用的准备工作已全部完成,可以正式开始你的开发之旅了。

编写第一个 WASM 项目

创建 Hello Wasm 项目

首先创建一个项目目录并进入:

Swift
mkdir HelloWASM && cd HelloWASM

创建一个新的 Swift 可执行软件包:

Swift
swift package init --type executable

查看已安装的 WASM SDK 名称:

Swift
swift sdk list
# 输出示例
6.1-RELEASE-wasm32-unknown-wasi

使用上述 SDK 名称编译项目:

Swift
swift build --swift-sdk 6.1-RELEASE-wasm32-unknown-wasi
# 编译输出示例
Building for debugging...
[8/8] Linking HelloWASM.wasm
Build complete! (1.78s)

通过 Wasmtime 运行 WebAssembly 项目:

Swift
wasmtime .build/wasm32-unknown-wasi/debug/HelloWASM.wasm
# 输出示例
Hello, world!

配置 VSCode 项目

实际开发中通常会使用 IDE 进行编译、运行和调试。使用 VSCode 打开项目目录后,vscode-swift 插件会自动在 .vscode 目录下生成 launch.json 文件。

image-20250408104947126

.vscode 目录中,创建 settings.json,并指定 swiftly 工具链的路径:

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

创建 tasks.json,定义自定义编译任务:

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"
        }
    ]
}

修改 launch.json,确保调试程序路径指向 WASM 可执行文件和预启动任务正确:

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"
        }
    ]
}

最后,按下 Cmd + Shift + P 并选择 Reload Window 来重新加载 VSCode 窗口。

现在你就可以使用 VSCode 内置的调试功能,对 Swift WASM 应用进行开发和调试了。

创建浏览器中的 WASM 项目

大多数情况下,WASM 项目是在浏览器环境中运行,并与网页内容进行交互。不过,如果开发者手动编写所有交互和通信代码,不仅技术门槛较高,工作量也不小。好在 Swift 社区提供了 JavaScriptKit 库,可帮助开发者大幅简化此类操作。

JavaScriptKit 提供了 Swift 和 JavaScript 间便捷的交互方式,包括访问 JavaScript 对象和函数、创建 JavaScript 可调用的闭包、进行数据类型转换以及实现多线程支持。

下面我们将演示如何创建一个通过 WASM 实现简单求和计算的网页项目:用户可在网页输入两个数字,点击按钮后调用 Swift 代码完成计算并显示结果。

创建并进入项目目录:

Bash
mkdir SumCalculator && cd SumCalculator

初始化 Swift 可执行软件包:

Bash
swift package init --type executable

添加 JavaScriptKit 依赖至项目的 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"),
            ]),
    ])

修改 Sources/main.swift 代码,实现求和功能并与网页交互:

Swift
import JavaScriptKit

// 定义一个函数来计算两个 Int32 数字的和
func sum(_ first: Int32, _ second: Int32) -> Int32 {
    first + second
}

// 获取 id 为 calculateSum 的按钮元素
let calculateSumElement = JSObject.global.document.getElementById("calculateSum").object!

// 为按钮元素添加点击事件监听器
_ = calculateSumElement.addEventListener!(
    "click",
    JSClosure { _ in
        // 获取输入框 firstNumber 的值并转换为 Int32
        let firstNumber = Int32(
            JSObject.global.document.getElementById("firstNumber").object!.value
                .string!)
        // 获取输入框的 SecondNumber 值并转换为 Int32
        let secondNumber = Int32(
            JSObject.global.document.getElementById("secondNumber").object!
                .value.string!)
        guard let firstNumber, let secondNumber else {
            _ = JSObject.global.alert!("请输入数字")
            return JSValue.undefined
        }
        // 调用 sum 函数计算和,并弹出结果
        let sum = sum(firstNumber, secondNumber)
        _ = JSObject.global.alert!("\(firstNumber) + \(secondNumber) = \(sum)")
        return JSValue.undefined
    })

// 获取全局的document对象
let document = JSObject.global.document

// 创建一个div元素
var divElement = document.createElement("div")
// 设置div元素的文本内容
divElement.innerText = "Welcome to WASM Calculator"
// 将div元素添加到 body
_ = document.body.appendChild(divElement)

在项目根目录创建 index.html 文件:

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">
        // 引入 WASM 模块,这里的路径需要根据实际情况调整
        import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js";
        // 页面加载时初始化 WASM
        init();
    </script>
</body>
</html> 

编译项目为 JavaScript/WebAssembly

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

# 输出示例
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:指定构建目标为适用于 JavaScript 环境的 WebAssembly。
  • —use-cdn:构建时自动从 CDN 加载必要的 JavaScript 依赖。

在项目根目录启动简单的 Web 服务器:

Bash
python -m http.server

# 输出示例
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 -

打开浏览器访问 http://localhost:8000,即可体验 Swift 和 JavaScript 无缝交互的 WASM 应用。

尽管 JavaScriptKit 已经大幅降低了 Swift 与 JavaScript 间通信的复杂度,但对部分 Swift 开发者而言仍存在一定的学习曲线。

目前,TokamakUI 是使用 SwiftWasm 构建浏览器应用程序的最简单方法之一。它提供了与 SwiftUI 高度相似的语法和 API,显著降低了使用 Swift 构建 Web 应用的门槛。然而,由于 TokamakUI 尚未适配 Swift 6.x,仍需搭配 Swift 5.9.x 来构建,因此本文暂不涉及。待 TokamakUI 兼容 Swift 6 后,将再补充相关内容。

总结

随着 Swift 生态系统对 WebAssembly 支持的持续完善,我们有理由相信,在不远的将来,Swift 开发者将能以更加自然、流畅的方式构建丰富多彩、性能卓越的跨平台 Web 应用。TokamakUI 等框架的成熟也将进一步降低入门门槛,让更多 Swift 开发者能够无缝地将其技能扩展到 Web 领域。

"加入我们的 Discord 社区,与超过 2000 名苹果生态的中文开发者一起交流!"

每周精选 Swift 与 SwiftUI 精华!