Introduction

Welcome to the SwiftWasm Documentation!

SwiftWasm is an open source project to support the WebAssembly target for Swift.

The goal of this project is to fully support the WebAssembly target for Swift and to be merged into the upstream repository.

WebAssembly is described on its home page as:

WebAssembly (abbreviated as Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.

We use LLVM as a compiler backend to produce WebAssembly binaries. Our resulting binaries also depend on WASI, which is a modular system interface for WebAssembly. WASI is mainly required to compile Swift Standard Library.

Getting Started

This is a getting started guide section to use SwiftWasm.

You can learn about:

  • How to set up a Swift toolchain for compiling to WebAssembly
  • How to compile a simple Swift code and Swift Package into WebAssembly
  • How to interoperate with JavaScript

Installation

To install Swift for WebAssembly toolchain, download one of the packages below and follow the instructions for your operating system.

Releases

SwiftWasm 5.3

Tag: swift-wasm-5.3.1-RELEASE

DownloadDocker Tag
macOSUnavailable
Ubuntu 18.045.3, 5.3-bionic, bionic, latest
Ubuntu 20.045.3-focal, focal

You can download the latest development snapshot from the Releases page

Using Downloads

macOS

An Xcode toolchain (.xctoolchain) includes a copy of the compiler, linker, and other related tools needed to provide a cohesive development experience for working in a specific version of Swift.

Requirements

  • macOS 10.15 or later

Installation

  1. Download the latest package release.
  2. Run the package installer, which will install an Xcode toolchain into /Library/Developer/Toolchains/.
  3. To use the Swift toolchain with command-line tools, use xcrun --toolchain swiftwasm or add the Swift toolchain to your path as follows:
export PATH=/Library/Developer/Toolchains/swift-latest.xctoolchain/usr/bin:"${PATH}"
  1. Run swift --version. If you installed the toolchain successfully, you can get the following message.
$ swift --version
SwiftWasm Swift version 5.3 (swiftlang-5.3.1)
Target: x86_64-apple-darwin19.6.0

Warning: xcrun finds executable binary based on --toolchain option or TOOLCHAINS environment variable, but it also sets SDKROOT as host target SDK (e.g. MacOSX.sdk). So you need to specify -sdk option as /Library/Developer/Toolchains/swift-wasm-5.3.1-RELEASE.xctoolchain/usr/share/wasi-sysroot when launching swiftc from xcrun. swift build or other SwiftPM commands automatically find SDK path based on target triple, so they don't require to specify it.

Linux

Packages for Linux are tar archives including a copy of the Swift compiler, linker, and related tools. You can install them anywhere as long as the extracted tools are in your PATH.

Requirements

  • Ubuntu 18.04 or 20.04 (64-bit)

Installation

  1. Install required dependencies:
# Ubuntu 18.04
apt-get install \
          binutils \
          git \
          libc6-dev \
          libcurl4 \
          libedit2 \
          libgcc-5-dev \
          libpython2.7 \
          libsqlite3-0 \
          libstdc++-5-dev \
          libxml2 \
          pkg-config \
          tzdata \
          zlib1g-dev
# Ubuntu 20.04
apt-get install \
          binutils \
          git \
          gnupg2 \
          libc6-dev \
          libcurl4 \
          libedit2 \
          libgcc-9-dev \
          libpython2.7 \
          libsqlite3-0 \
          libstdc++-9-dev \
          libxml2 \
          libz3-dev \
          pkg-config \
          tzdata \
          zlib1g-dev
  1. Download the latest binary release above.

The swift-wasm-<VERSION>-<PLATFORM>.tar.gz file is the toolchain itself.

  1. Extract the archive with the following command:
tar xzf swift-wasm-<VERSION>-<PLATFORM>.tar.gz

This creates a usr/ directory in the location of the archive.

  1. Add the Swift toolchain to your path as follows:
export PATH=$(pwd)/usr/bin:"${PATH}"

You can now execute the swiftc command to build Swift projects.

Docker

SwiftWasm offical Docker images are hosted on GitHub Container Registry.

SwiftWasm Dockerfiles are located on swiftwasm-docker repository.

Supported Platforms

  • Ubuntu 18.04
  • Ubuntu 20.04

Using Docker Images

  1. Pull the Docker image from GitHub Container Registry:
docker pull ghcr.io/swiftwasm/swift:latest
  1. Create a container using tag latest and attach it to the container:
docker run --rm -it ghcr.io/swiftwasm/swift:latest /bin/bash

Hello, World!

This section will show you how to compile a simple Swift code into WebAssembly and run the produced binary on WASI supported WebAssembly runtime.

1. Create a Swift file

$ echo 'print("Hello, world!")' > hello.swift

2. Compile Swift code into WebAssembly with WASI

$ swiftc -target wasm32-unknown-wasi hello.swift -o hello.wasm

3. Run the produced binary on WebAssembly runtime

You can the run the produced binary with wasmer (or other WebAssembly runtime):

$ wasmer hello.wasm

The produced binary depends on WASI which is an interface of system call for WebAssembly. So you need to use WASI supported runtime and when you run the binary on browser, you need WASI polyfill library like @wasmer/wasi.

Compile a SwiftPM package to WebAssembly

You can also use SwiftPM for managing packages in the same way as other platforms.

1. Create a package from template

$ swift package init --type executable
Creating executable package: hello
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/hello/main.swift
Creating Tests/
Creating Tests/LinuxMain.swift
Creating Tests/helloTests/
Creating Tests/helloTests/helloTests.swift
Creating Tests/helloTests/XCTestManifests.swift

2. Build the Project into a WebAssembly binary

You need to pass --triple option, which indicates that you are building for the target.

$ swift build --triple wasm32-unknown-wasi

3. Run the produced binary

Just as in the previous section, you can run the produced binary with the wasmer WebAssembly runtime.

$ wasmer ./.build/debug/hello-swiftwasm.wasm
Hello, world!

Creating a Browser App

Currently, the Tokamak UI framework is one of the easiest ways to build a browser app with SwiftWasm. It tries to be compatible with the SwiftUI API as much as possible, which potentially allows you to share most if not all code between SwiftWasm and other platforms.

Requirements

Tokamak relies on the carton development tool for development and testing. While you can build Tokamak apps without carton, that would require a lot of manual steps that are already automated with carton.

System Requirements

  • macOS 10.15 and Xcode 11.4 or later.
  • Swift 5.2 or later and Ubuntu 18.04 for Linux users.

Installation

On macOS carton can be installed with Homebrew. Make sure you have Homebrew installed and then run:

brew install swiftwasm/tap/carton

You'll have to build carton from sources on Linux. Clone the repository and run swift build -c release, the carton binary will be located in the .build/release directory after that. Assuming you already have Homebrew installed, you can create a new Tokamak app by following these steps:

  1. Install carton:
brew install swiftwasm/tap/carton

If you had carton installed before this, make sure you have version 0.6.1 or greater:

carton --version
  1. Create a directory for your project and make it current:
mkdir TokamakApp && cd TokamakApp
  1. Initialize the project from a template with carton:
carton init --template tokamak
  1. Build the project and start the development server, carton dev can be kept running during development:
carton dev
  1. Open http://127.0.0.1:8080/ in your browser to see the app running. You can edit the app source code in your favorite editor and save it, carton will immediately rebuild the app and reload all browser tabs that have the app open.

You can also clone the Tokamak repository and run carton dev in its root directory. This will build the demo app that shows almost all of the currently implemented APIs.

JavaScript interoperation

JavaScriptKit is a Swift framework to interact with JavaScript through WebAssembly.

You can use any JavaScript API from Swift with this library. Here's a quick example of JavaScriptKit usage in a browser app:

import JavaScriptKit

let document = JSObject.global.document

var divElement = document.createElement("div")
divElement.innerText = "Hello, world"
_ = document.body.appendChild(divElement)

You can also use JavaScriptKit in SwiftWasm apps integrated with Node.js, as there no assumptions that any browser API is present in the library.

JavaScriptKit consists of a runtime library package hosted on npm, and a SwiftPM package for the API on the Swift side. To integrate the JavaScript runtime automatically into your app, we recommend following the corresponding guide for browser apps in our book.

You can get more detailed JavaScriptKit documentation here.

Use Swift Foundation in your code

The Foundation core library is available in SwiftWasm, but in a limited capacity. The main reason is that the Dispatch core library is unavailable due to the lack of standardized multi-threading support in WebAssembly and SwiftWasm itself. Many Foundation APIs rely on the presence of Dispatch under the hood, specifically file system access and threading helpers. A few other types are unavailable in browsers or aren't standardized in WASI hosts, such as support for sockets and low-level networking, or support for time zone files, and they had to be disabled. These types are therefore absent in SwiftWasm Foundation:

  • FoundationNetworking types, such as URLSession and related APIs
  • FileManager
  • Host
  • Notification
  • NotificationQueue
  • NSKeyedArchiver
  • NSKeyedArchiverHelpers
  • NSKeyedCoderOldStyleArray
  • NSKeyedUnarchiver
  • NSNotification
  • NSSpecialValue
  • Port
  • PortMessage
  • Process
  • ProcessInfo
  • PropertyListEncoder
  • RunLoop
  • Stream
  • Thread
  • Timer
  • UserDefaults

Related functions and properties on other types are also absent or disabled. We would like to make them available in the future as soon as possible, and we invite you to contribute and help us in achieving this goal!

Testing your app

You can write a test suite for your SwiftWasm app or library, or run an existing test suite written for XCTest if you port existing code to SwiftWasm. Your project has to have a Package.swift package manifest for this to work. We assume that you use SwiftPM to build your project and that you have a working package manifest. Please follow our SwiftPM guide for new projects.

A simple test case

Let's assume you have a SwiftWasmLibrary target in your project that you'd like to test. Your Package.swift should also have a test suite target with a dependency on the library target. It would probably look like this:

// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
  name: "HelloSwiftWasm",
  products: [
    .executable(name: "SwiftWasmApp", targets: ["SwiftWasmApp"]),
  ],
  targets: [
    // Targets are the basic building blocks of a package. A target can define a module or a test
    // suite. Targets can depend on other targets in this package, and on products in packages which
    // this package depends on.
    .target(
      name: "SwiftWasmApp",
      dependencies: ["SwiftWasmLibrary"],
    ),
    .target(name: "SwiftWasmLibrary"),
    .testTarget(name: "SwiftWasmTests", dependencies: ["SwiftWasmLibrary"]),
  ]
)

Now you should make sure there's Tests/SwiftWasmTests subdirectory in your project. If you don't have any files in it yet, create SwiftWasmTests.swift in it:

import SwiftWasmLibrary
import XCTest

final class SwiftWasmTests: XCTestCase {
  func testTrivial() {
    XCTAssertEqual(text, "Hello, world")
  }
}

This code assumes that your SwiftWasmLibrary defines some text with "Hello, world" value for this test to pass. Your test functions should all start with test, please see XCTest documentation for more details.

XCTest limitations in the SwiftWasm toolchain

As was mentioned in our section about Swift Foundation, multi-threading and file system APIs are currently not available in SwiftWasm. This means that XCTestExpectation and test hooks related to Bundle (such as testBundleWillStart(_:) and testBundleDidFinish(_:)) are not available in test suites compiled with SwiftWasm. If you have an existing test suite you're porting to WebAssembly, you should use #if os(WASI) directives to exclude places where you use these APIs from compilation.

Building and running the test suite with carton

If you use carton to develop and build your app, as described in our guide for browser apps, just run carton test in the root directory of your package. This will automatically build the test suite and run it with Wasmer for you.

Building and running the test suite with SwiftPM

If you manage your SwiftWasm toolchain without carton (as shown in the "Setup" section), you can build your test suite by running this command in your terminal:

$ swift build --build-tests --triple wasm32-unknown-wasi

If you're used to running swift test to run test suites for other Swift platforms, we have to warn you that this won't work. swift test doesn't know what WebAssembly environment you'd like to use to run your tests. Because of this building tests and running them are two separate steps when using SwiftPM. After your tests are built, you can use a WASI-compatible host such as Wasmer to run the test bundle:

$ wasmer .build/debug/HelloSwiftWasmPackageTests.xctest

As you can see, the produced test binary starts with the name of your package followed by PackageTests.xctest. It is located in the .build/debug subdirectory, or in the .build/release subdirectory when you build in release mode.

WASILibc module

When porting existing projects from other platforms to SwiftWasm you might stumble upon code that relies on importing a platform-specific C library module directly. It looks like import Glibc on Linux, or import Darwin on Apple platforms. Fortunately, most of the standard C library APIs are available when using SwiftWasm, you just need to use import WASILibc to get access to it. Most probably you want to preserve compatibility with other platforms, thus your imports would look like this:

#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(WASILibc)
import WASILibc
#endif

Limitations

WebAssembly and WASI provide a constrained environment, which currently does not directly support multi-threading, or filesystem access in the browser. Thus, you should not expect these APIs to work when importing WASILibc. Please be aware of these limitations when porting your code, which also has an impact on what can be supported in the Swift Foundation at the moment.

Examples

This section shows you example usage of our toolchain.

Importing a function from host environments

You can import a function from your host environment using the integration of Swift Package Manager with C targets. Firstly, you should declare a signature for your function in a C header with an appropriate __import_name__ attribute:

__attribute__((__import_name__("add")))
extern int add(int lhs, int rhs);

Here __import_name__ specifies the name under which this function will be exposed to Swift code. Move this C header to a separate target, we'll call it HostFunction in this example. Your Package.swift manifest for your WebAssembly app would look like this:

// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription

let package = Package(
    name: "SwiftWasmApp",
    targets: [
      .target(name: "HostFunction", dependencies: []),
      .target(name: "SwiftWasmApp", dependencies: ["HostFunction"]),
    ]
)

Place this header into the include subdirectory of your HostFunction target directory. You can then import this host function module into Swift code just as any other module:

import HostFunction

print(add(2, 2))

Then, you can inject a host function into the produced WebAssembly binary.

Note that we use env as default import module name. You can specify the module name as __import_module__ in your C header. The full list of attributes in the header could look like __attribute__((__import_module__("env"),__import_name__("add"))).

const WASI = require("@wasmer/wasi").WASI;
const WasmFs = require("@wasmer/wasmfs").WasmFs;

const promisify = require("util").promisify;
const fs = require("fs");
const readFile = promisify(fs.readFile);

const main = async () => {
  const wasmFs = new WasmFs();
  // Output stdout and stderr to console
  const originalWriteSync = wasmFs.fs.writeSync;
  wasmFs.fs.writeSync = (fd, buffer, offset, length, position) => {
    const text = new TextDecoder("utf-8").decode(buffer);
    switch (fd) {
      case 1:
        console.log(text);
        break;
      case 2:
        console.error(text);
        break;
    }
    return originalWriteSync(fd, buffer, offset, length, position);
  };

  // Instantiate a new WASI Instance
  let wasi = new WASI({
    args: [],
    env: {},
    bindings: {
      ...WASI.defaultBindings,
      fs: wasmFs.fs,
    },
  });

  const wasmBinary = await readFile("lib.wasm");

  // Instantiate the WebAssembly file
  let { instance } = await WebAssembly.instantiate(wasmBinary, {
    wasi_snapshot_preview1: wasi.wasiImport,
    env: {
      add: (lhs, rhs) => (lhs + rhs),
    }
  });

  wasi.start(instance);
};

main()

If you use Go bindings for Wasmer as your host environment, you should check an example repository from one of our contributors that shows an integration with an imported host function.

A more streamlined way to import host functions will be implemented in the future version of the SwiftWasm toolchain.

Exporting function for host environment

You can expose a Swift function for host environment using special attribute and linker option.

@_cdecl("add")
func add(_ lhs: Int, _ rhs: Int) -> Int {
    return lhs + rhs
}

You need to compile the Swift code with linker option --export.

$ swiftc \
    -target wasm32-unknown-wasi \
    lib.swift -o lib.wasm \
    -Xlinker --export=add

Then, you can use the exported function from host environment.

const WASI = require("@wasmer/wasi").WASI;
const WasmFs = require("@wasmer/wasmfs").WasmFs;

const promisify = require("util").promisify;
const fs = require("fs");
const readFile = promisify(fs.readFile);

const main = async () => {
  // Instantiate a new WASI Instance
  const wasmFs = new WasmFs();
  let wasi = new WASI({
    args: [],
    env: {},
    bindings: {
      ...WASI.defaultBindings,
      fs: wasmFs.fs,
    },
  });

  const wasmBinary = await readFile("lib.wasm");

  // Instantiate the WebAssembly file
  let { instance } = await WebAssembly.instantiate(wasmBinary, {
    wasi_snapshot_preview1: wasi.wasiImport,
  });
  // Get the exported function
  const addFn = instance.exports.add;
  console.log("2 + 3 = " + addFn(2, 3))

};

main()

If you use SwiftPM package, you can omit linker flag using clang's __atribute__. Please see swiftwasm/JavaScriptKit#91 for more detail info

Example Projects

You can learn more practical usage of our toolchain in swiftwasm/awesome-swiftwasm

Contribution Guide

Forum posts

Repositories

swiftwasm/swift

The main repository of this project. Forked from apple/swift. We are tracking upstream changes using pull

Branching scheme

  • swiftwasm is the main development branch.
  • main is a mirror of the main branch of the upstream apple/swift repository. This branch is necessary to avoid some issues.
  • swiftwasm-release/5.3 is the branch where 5.3 release of SwiftWasm is prepared.
  • release/5.3 is a mirror of the upstream release/5.3 branch.

swiftwasm/llvm-project

This repository is a fork of apple/llvm-project.

swiftwasm branch is based on swift/main branch of apple/llvm-project.

Please see the AppleBranchingScheme.md document in the upstream repository for more details.

swiftwasm/icu4c-wasi

Build script and patches for building ICU project for WebAssembly. The required changes to build it were merged to the upstream repository.

swiftwasm/wasi-sdk and swiftwasm/wasi-libc

Forked from WebAssembly/wasi-sdk and WebAssembly/wasi-libc.

We fork them to build wasi-sysroot with pthread header. There aren't so many diff from upstream.

How to build toolchain

1. Checkout the project source code.

$ mkdir swiftwasm-source
$ cd swiftwasm-source
$ git clone https://github.com/swiftwasm/swift.git
$ ./swift/utils/update-checkout --scheme wasm --clone

2. Install required dependencies

Before building Swift, please install required dependencies.

# On macOS
$ brew install cmake ninja llvm sccache wasmer
$ ./utils/webassembly/macos/install-dependencies.sh
# On Linux
$ ./utils/webassembly/linux/install-dependencies.sh

3. Build using custom preset options

We support both Linux and macOS to build Swift. You need to select the preset name, sccache path and LLVM tools directory.

# On macOS
$ ./utils/build-script \
        --preset=webassembly-macos-target \
        --preset-file ./utils/webassembly/build-presets.ini  \
        SOURCE_PATH=$(dirname $(pwd)) \
        LLVM_BIN_DIR=/usr/local/opt/llvm/bin \
        C_CXX_LAUNCHER=$(which sccache)
# On Linux
$ ./utils/build-script \
        --preset=webassembly-linux-target \
        --preset-file ./utils/webassembly/build-presets.ini  \
        SOURCE_PATH=$(dirname $(pwd)) \
        LLVM_BIN_DIR=/usr/local/opt/llvm/bin \
        C_CXX_LAUNCHER=$(which sccache)

Or if you want to build whole toolchain, please use ./utils/webassembly/build-toolchain.sh. This script builds compiler, Swift Standard Library for host environment (e.g. macOS or Linux) and target environment (wasm32-unknown-wasi), Foundation and other packages. So it takes longer time than the above script.

$ ./utils/webassembly/build-toolchain.sh

If you want to get more information about build system, please feel free to ask @kateinoigakukun on Twitter or GitHub.

Continuous Integration

We use GitHub Actions to build and run tests continuously. (But jobs without cache takes 2~3 hours to be completed)

Currently (2020/10/4), we only run stdlib tests on Linux CI because the job execution time and storage reached GitHub Actions limits.

References:

Nightly distribution

We distribute latest swiftwasm branch and swiftwasm-release/5.3 branch toolchains at UTC 00:00 everyday. You can check the latest toolchain at GitHub Release Page

Cache

Compilation time of LLVM and the Swift toolchain is very long, so we recommend to cache the artifacts using sccache. The "Getting started" guide from the upstream toolchain provides more details about the use of sccache.

References

Debugging

When you want to debug a WebAssembly binary, wasminspect can help in the investigation if the debugged binary does not rely on integration with JavaScript. We recommend splitting your packages into separate executable targets, most of which shouldn't assume the availability of JavaScript to make debugging easier.

demo