Swift and WebAssembly for a browser

ยท 998 words ยท 5 minute read

Overview ๐Ÿ”—

I continue my journey in understanding tools and “segmentation” of Swift for WebAssembly.
My previous post includes “Dumb idea” - this actually gave me good insights into emsdk internals and I have leveraged those to create a new approach.

Recently there was an update to the documentation in the SwiftWasm book. Now it should be clear what is what. Please leave a comment if something is not clear in the documentation or create a ticket on GitHub.

There are numerous code samples and articles at this point, some of them rely on different dev toolchains. I will make a list of the most recent examples.

To start with Swift for a browser via Embedded, look into those code samples. You can take the official dev 6.1+ toolchain for those projects from swift.org.

I will give more information on my sample at the end and what is unique about it.

To get Swift for a Browser via SwiftWasm toolchain. Download 6.0.3+ from here, SwiftWasm book also points at this place. As said in documentation, this toolchain uses WASI SDK to bring libc and other WASI compatible APIs. This allows to enable subset of APIs.

Those should be enough to get started on both.

Project ๐Ÿ”—

Now you may wonder how my sample is different. Project setup is compilable via SwiftWasm and/or Swift Embedded toolchains. For Embedded, it leverages Emscripten SDK.

WASI ๐Ÿ”—

./build.sh wasi

To compile via SwiftWasm toolchain, it was pretty straightforward. Disabling limited features and wrapping with can import statement.

#if canImport(WASILibc)
import WASILibc
#endif

Compiled binrary output is about 8Mb and ~4.5 Mb after use of wasm-opt. Build script in the repository includes post build function to do optimisations and move binary to Bundle directory.

Embedded + emsdk ๐Ÿ”—

./build.sh emsdk

To make it work for Embedded, I looked at Emscripten SDK as part of my previous attempt to make those 2 toolchains work together. At the end, I came up with a setup where emsdk would be installed next to the project and activated as part of the build script.

There are a number of extra flags as for any Embedded project. In addition to that, I have created emswiften package. It is essentially a wrapper around emsdk with a module and adds a few other missing functions to make it possible to do simple things, such as string String interpolation with variables.

let n:Double = 9
let number = sqrt(n);
print("sqrt(\(n)) = \(number)")

I’m aware that there is a risk with a wrapper around emsdk. Simply because I don’t know if it has dependencies on other functions that are needed from other libraries or imported from JS. Not tested thoroughly. For demo purposes.

To review some dependencies, I’m using nm tool to research. Libc is pretty self-contained and requires only a few foundational functions to function. There are some emscripten_* functions marked as U, but my code has not touched those areas yet, and this is something I might look into later if necessity arises. An example is emscripten_get_now, which is an imported function from JS.

getitimer.o:
00000000 d .debug_abbrev
00000000 d .debug_line
00000000 d .debug_str
         U __getitimer
         U __stack_pointer
         U emscripten_get_now
00000001 T getitimer

As a reference on what is what in the output.

Symbol type Description
T Text (code) section
t Text (code) section (local symbol)
D Initialized data section
d Initialized data section (local symbol)
B Uninitialized data section (BSS)
b Uninitialized data section (local symbol)
U Undefined symbol
w Weak symbol

In the result I was able to port SwiftMath package with use of emswiften package by using math functions from libc.

I want to create something interesting to demonstrate with OpenGL. While OpenGL APIs are exposed via emsdk, there is no library to link with because emscripten imports APIs from library_webgl.js and a few other libraries.

I covered the absence of an OpenGL Swift library with an impromptu WebGLInterface JS module. Importing it in the same way as javascript_kit or WASI.

const { SwiftRuntime } = await import(`./index.mjs`);
const { WebGLInterface } = await import(`./webgl_interface.mjs`);

const swift = new SwiftRuntime();
const webgl = new WebGLInterface();

var imports = {
    javascript_kit: swift.wasmImports,
    webgl: webgl.wasmImports,
};

My desire to create something went out of hand, and I created a clone of Flappy Bird! I wrote a micro sprite renderer to do that. I’m going to skip the part where I go into details and describe how I did it. But not going to lie, I did very much enjoy the process! ๐Ÿ˜Š

Flappy Bird Clone ๐Ÿ”—

Thank you for reading this far! If you want to try my Flappy Bird, simply go ahead and tap/click. This is an iframe with a compiled WASM binary.

Size of the binary is about 340KB and optimized: 260KB (with wasm-opt).

Final thoughts ๐Ÿ”—

It might be disconnected from the main topic and looks like a mess of information. I was writing this post in a few iterations and maybe jumped a lot between topics. I hope it is still useful.

It took me some time to map JS WebGL functions to Swift. I had to compromise with texture loading and use JavaScriptKit to load images. Which is not cross-platform. I wanted to make it cross-platform as much as possible.

Another one, I actually did implement emscripten_get_now and created emscripten.mjs module for the future missing funcitons. I’m not sure if I will continue with this project, but I might consider extracting some parts to a separate package.

Ultimately, I have proven that it is possible to create a somewhat complex Swift application with Embedded + Emscripten SDK + JavaScriptKit for a browser.