Max Desiatov
Max DesiatovI'm Max Desiatov, a software consultant building mobile and backend apps. I curate @ServerSideSwift feed, and co-maintain WebAssembly support for Swift and a framework called Tokamak compatible with SwiftUI.

The state of Swift for WebAssembly in 2020 (and earlier)

16 September, 2020

It probably all started with an innocently-looking issue in the emscripten repository back in 2014:

A screenshot of a GitHub issue with title

For context, back in 2014 the Emscripten project was built as a backend for LLVM-based compilers. While initially it produced JavaScript and later its low-level subset asm.js, Emscripten now mainly targets WebAssembly.

As I wrote in my previous article, the availability of a virtual machine in all relatively recent major browsers makes a lot of code much more portable. It easily follows that Swift targeting WebAssembly would essentially make Swift available in some form on all platforms that have browsers with WebAssembly support.

I started thinking seriously about it in 2018, but I remember having some passing interest in Emscripten and potential Swift support probably much earlier. By 2018 Swift was available on Linux for two years already, people were actively trying to make it work well on Android, and WebAssembly support in upstream LLVM became more or less stable, especially with WebAssembly support in the Rust compiler maturing to some point of usability. In September I started playing with forks and patches that were shared in the original Emscripten thread, and posted a quick update:

A screenshot of my comment for the same issue:

The positive reaction surprised me to say the least, I knew the thread was long-running, but I didn’t expect people to actually pay attention to it, and I still thought that WebAssembly was perceived as a fringe technology at the time.

Unfortunately, both LLVM and the Swift toolchain are not documented really well. Additionally, as I learned from my own mistakes, to comfortably work with the codebase you have to be proficient not just in C++ and compiler internals, but also in CMake, Python and Shell to be able to wrangle the huge build system of the toolchain. Powerful hardware is also required, as clean builds can take hours, and incremental builds are not very reliable. I didn’t know what I was doing, and even now I’m just winging it. 🙈 I was just trying to resolve conflicts in patches from other people (mainly Patrick Cheng) and to make at least some code to build without errors. A book that I read many years ago and blog posts by Brian Gesiak and Matt Diephouse gave me some understanding of what things looked like, and confidence that people outside Apple can do some work on it too.

The cover of

This book, a couple of blog posts from other people, my rusty (no pun intended!) knowledge of C++, Python and Shell scripting, and my MacBook Pro 2018 (I still hate that keyboard) were probably the only weapons I had when I started fighting the dragon.

I was trying to keep up with upstream Swift and LLVM changes, working on it on and off, with some things building, but nothing still worked end-to-end. By February 2019 I was blocked by Swift’s reliance on the pthreads API and atomics, none of which were available in the initial version of WebAssembly (and still aren’t enabled in all browsers by default). I was only able to work on it nights and weekends, while still doing software consulting work during weekdays, which was my main source of income at the time. I still didn’t have enough knowledge of the toolchain to make all necessary changes, and just resolving the conflicts and waiting for multi-hour builds to complete consumed all the efforts I could spend on it.

The most significant move forward happened when Zhuowei Zhang made it all work with the freshly available WASI API and fixed most blocking runtime, metadata and linking issues. By April-May 2019 the SwiftWasm organization was set up with first toolchain builds uploaded and working with basic example code. He also deployed the SwiftWasm website that hosts a simple demo, allowing you to download the compiled WebAssembly binary.

A tweet from @zhuowei:

The fact that Chris Lattner himself is interested in WebAssembly support is incredibly motivating!

Unfortunately, neither Zhuowei, nor I could put much time into making this work for something more complex than a simple example. Specifically, on a lower level the Swift compiler makes a distinction between “thin” and “thick” closures. Thin closures are trivial, they don’t capture any surrounding vairables in its context (you can read more details about closure environment in one of my previous articles). Also, Swift error handling is implemented on a lower level as an additional pointer argument passed to throwing functions, but thin closures can’t be throwing as they don’t take that argument. Thick closures expect both context and error pointer arguments to be passed to them. These closure types might seem to be incompatible, but on a high level Swift allows you to pass non-throwing closures where throwing ones are expected:

// Expects a non-throwing closure
func f(_ a: (Int) -> Void) {
  // Passes a non-throwing closure where a throwing one is expected.  
  g(a)
}

// Expects a throwing closure
func g(_ b: (Int) throws -> Void) {
    try! b(1)
}

f { _ in }

This works on a lower level, because none of the existing hardware architectures care if you cast a function with a specific number of arguments to a function with less arguments and ignore the rest of them. You can compile the corresponding C code to x86 or ARM64 and it will work just fine:

#include <stdio.h>

void closure(int argument, void *context, void *error) {
  printf("Argument is %d\n", argument);
}

int main(void) {
  void (*dropArguments)(int) = &closure;
  dropArguments(42);
}

Swift relies on the same trick where thin closures are cast as thick, and this was never a problem that the Swift compiler previously encountered. WebAssembly is much stricter though when casting function pointers. This C code will just crash when compiled to WebAssembly, and quite possibly you won’t be able to launch it at all. Wasm hosts are required to run validation on supplied binaries before execution. One of the validation checks includes verification that caller and callee function signatures match exactly to each other in the number of arguments.

This caused crashes in almost all non-trivial Swift code, as a lot of the standard library functions (map being the most widely used example) can take a throwing closure as an argument, but also accept a non-throwing one. Fortunately, we’ve reached yet another significant milestone when Yuta Saito submitted a PR that worked around this problem in October 2019. The whole issue of Swift relying on the function arguments mismatch trick still bites us here and there, but at least we’re now aware of it, and all known instances of it are fixed in our fork.

What’s going on with SwiftWasm in 2020?

By end of January 2020 Yuta set up automated upstream tracking with the wonderful pull GitHub app that automatically submits PRs with changes from Apple’s repository to our fork. Thanks to this we no longer needed to do it manually to keep up with upstream.

In February-March I no longer had any full-time consulting engagements and decided that it was a good opportunity to focus on open-source work full-time instead. The COVID thing has just started, but it was also clear that it wouldn’t be easy to find consulting clients anyway in this economic environment. In the meantime I wanted to polish some of the other open-source libraries I maintain, and to focus on SwiftWasm as the next big thing in Swift for me personally. I’ve been doing it since then, funding the efforts with my own savings and partially with the help from amazing GitHub sponsors.

There’s a big dilemma here in my SwiftWasm development strategy. I can spend my time either on submitting the changes from our SwiftWasm fork to the upstream Swift repository, or on developing user libraries, improving documentation and writing example code. Kickstarting the ecosystem allows people to build apps and lowers the barrier for contributions from the community, even without WebAssembly becoming a platform officially supported in upstream distributions of Swift.

Submitting PRs upstream on the other hand is very time consuming. Not only the changes need to be polished to a high standard, but also, due to the reasons I’ve mentioned above, working on the toolchain requires a ton of effort. On top of that, due to the limited amount of people available to review PRs to the official Swift repository, sometimes you need to wait weeks for a review. Then if any changes were requested, you apply the changes and wait for weeks again for a re-review. Obviously, we can’t submit one giant PR for everything. So with about 150 files changed in our fork just in the main swift toolchain repository this will be multiplied by dozens if not a hundred PRs. There’s also a comparable amount of changes to the Foundation library.

At the same time, making all the changes available upstream would be a great boost to the overall visibility of the project. But it doesn’t necessarily mean that the WebAssembly SDK will be distributed with Xcode or with any other official Swift distribution. Most probably, Swift developers targeting Wasm would need to download the toolchain and SDK separately just as they do now. This download and installation process is already automated with a developer tool we had to build for SwiftWasm anyway, so I don’t think that official support would have a big impact on the actual developer experience. Given that I’m not employed by any company funding this work, and that my savings and sponsorship revenue are limited, I decided I’d rather spend the next months building libraries and writing documentation for our users rather than drown in the upstreaming process, even though some PRs have already been submitted and a subset of those have been merged. This doesn’t mean that upstreaming work will never be completed. It just won’t happen as soon as some would expect, at least without more people joining the project and helping us out with this.

In April I was sure that we were still pretty far from SwiftWasm becoming usable in complex projects, but Yuta managed to make JavaScript bindings work, which were demoed through the wonderful Game of Life demo built with Swift and running in the browser. This was the first example known to me of SwiftWasm interacting with browser DOM. I consider this to be the most significant milestone this year. I also didn’t expect that SwiftPM would work so well for this project, but it did, and with comparably minor changes.

A short video of how the Game of Life built in Swift was developed, showing Xcode and hot reload
of the browser window when code changes are detected.

Inspired by this, I decided to revive my old framework called Tokamak. It was initially conceived in 2018 as a port of React to Swift and iOS, but that aspect of it obviously became irrelevant after the release of SwiftUI. When SwiftUI was released, it really surprised me how similar SwiftUI was to React after all, and it’s somewhat ironic, given how much aversion I saw to React from an average Swift developer before SwiftUI have been introduced. SwiftUI, while built with the same fundamental principles as React (and many other similar frameworks), is augmented with language features that make it a lot more elegant with property wrappers and function builders.

I’m going to publish a detailed overview of Tokamak in the future, but for the purpose of this article, I’ll just mention that in July of 2020 the first release of Tokamak compatible with WebAssembly and SwiftUI was tagged with incredible amount of help from Carson Katri, Jed Fox, Helge Heß, my sponsors, and many other people who reported bugs and provided early feedback. There’s still enough of the SwiftUI API left to implement, but I’m convinced that the general approach is viable, and more releases are coming soon.

Future goals for SwiftWasm

Despite the fact that 2020 has been a horrible year so far from many perspectives, I’m deeply grateful that I have an opportunity to work on a project such as this. We’re just getting started, and there’s much more to do for the ecosystem to grow sustainably. I’ve prepared a list of some of the issues that I’d like to see resolved sooner rather than later.

Short term

  • Integrate binary size improvements developed by Yuta into the 5.3 branch of our SwiftWasm fork.
  • Enable SourceKit-LSP support in our SwiftWasm distributions.
  • You currently can get access to the standard C library by using an import Glibc statement in your code. As the API of the WASI C library is substantially different from the actual Glibc module, it should be exposed under a different name. SwiftWasm users will have to use import WASI instead of import Glibc in later SwiftWasm snapshots, but this will make the API differences explicit.
  • Release SwiftWasm 5.3.0 after the official Swift 5.3.0 is available for other platforms.

Medium term

  • Continue our upstreaming work to LLVM, Swift toolchain and SDK repositories.

  • Set up a community CI node integrated with the official CI Jenkins server.

  • As Swift support for Windows hosts is going to be available in Swift 5.3, we need to make sure that cross-compiling from Windows to WebAssembly works well. We also need to provide Windows builds of the SwiftWasm toolchain and SDK. It’s not clear to me if Windows support for 5.3 also implies SwiftPM support, as right now it’s only included in builds off the master branch. If it doesn’t land in 5.3, SwiftWasm snapshots for Windows probably will only be available off master and in some future SwiftWasm version.

  • Similarly, we need to make sure that ARM64 builds of SwiftWasm are available for macOS Big Sur, but as far as I know, none of SwiftWasm maintainers have access to the Apple Silicon DTK, and I personally don’t have any budget for new Apple Silicon production hardware to be released later this year. Sponsoring SwiftWasm maintainers so that we have access to hardware for testing would be greatly appreciated. 🙏

  • Research more opportunities for performance improvements and binary size reductions. We already have a few in mind:

    1. JavaScript strings use UTF-16, while Swift strings use UTF-8 encoding under the hood since Swift 5. This means that every call into JavaScript, especially for modifying DOM, incurs a re-encoding overhead. As Swift String API does not expose the underlying encoding directly, maybe we could go back to UTF-16 in builds that target WebAssembly.
    2. One of the biggest dependencies in Swift binaries is the ICU library, which is used for locale-aware string conversions. One way or another, the browser already has access to this code and exposes it to JavaScript. I wonder if we could make Swift strings delegate this corresponding ICU-specific code to the JavaScript API, meaning we’d no longer need to bundle ICU in every binary produced by SwiftWasm.
    3. While WebAssembly is a “bare metal” platform with no basic facilities for memory allocation and other basic APIs one would expect, WASI is a compatibility layer that gives you that. It is a big dependency too, so we should explore if it could be stripped down just to memory allocators and whatever other basic code is required to make things work. I’m sure it can be stripped down substantially as the full AssemblyScript runtime is only 2KB, while the WASI JavaScript polyfill required for things to work in browsers, even without counting WASI itself, is more than 200KB.
  • WebAssembly and web APIs is a collection of different features and not all of them are always implemented by all browsers. We should figure out a way to communicate it in our Swift code, and I don’t think that making the Swift toolchain aware of every feature is a reasonable way forward. That is, I’d prefer (versions are arbitrary) @available(Chrome 242, Firefox 303, Safari 24) attributes on functions to @available(WebAssembly([.mvp, .multiThreading])). The problem with the latter approach is that the number of features will grow very fast, and one would want to encode not only WebAssembly features, but DOM APIs too. For example, if your code uses the Web Animations API and the Intersection Observer API, would you want to specify both of those in the @available attribute? Making the Swift compiler aware of each feature is an unreasonable expectation, while the number of WebAssembly hosts is limited and can be reasonably updated with time if new significantly popular hosts appear.

  • Speaking of web APIs, JavaScriptKit gives us mostly untyped API bindings, which is not ideal. There are some basic classes in JavaScript, such as Date, Error, Promise, and a few more, but the browser DOM API is much more complex and is huge, and new features are constantly added. It is untenable to create and maintain bindings for DOM manually with such a small team as ours. Luckily, DOM APIs are usually available in the IDL format, and a proof of concept exists that can parse it and generate bindings automatically. We still need high-quality bindings for the basic JavaScript classes to exist for it all to work well, so this is still in progress.

Long term

  • As I mentioned different WebAssembly features in the previous point, we should be aware of what’s on the horizon to be able to support it. While multi-threading support would be great, the WebAssembly reference types proposal is even more exciting to me. Not only we would no longer need to maintain a lot of interop code that bridges Swift and JavaScript, this could potentially allow Swift developers to import WebAssembly modules with a simple import statement and get most of the type information exposed properly out of the box.

  • Integrating Swift for TensorFlow with SwiftWasm would be a nice expansion of limits of what’s possible with Swift, as running TensorFlow models in the browser has a lot of practical use cases.

Come join us!

As you can see, there are many interesting and research-focused goals to accomplish. As the ecosystem is in its early stage, it’s easy to make a substantial contribution to its foundations. Please review and file new issues, submit your PRs, and improve the documentation. Build your own projects and let me know of your experience, either on Twitter or via email!


If you enjoyed this article, please consider becoming a sponsor. My goal is to produce content like this and to work on open-source projects full time, every single contribution brings me closer to it! This also benefits you as a reader and as a user of my open-source projects, ensuring that my blog and projects are constantly maintained and improved.