Some feedback from my short experience with SwiftWasm

Hi,

I tried swiftwasm for the last several days and I'd like to share my feedback here.

First of all, I'm glad to be able to use swift language for wasm, which is mainly supported by the communities. It's great to achieve what it is now.

I also found some issues (or just comment) in my personal opinion. The biggest one is that wasm is not officially supported by swift. swiftwasm is a kind of fork of swift, which leads to several issues:

  • Windows support. swift has windows (not WSL) support but swiftwasm does not.
  • Wasm binary size is bigger than some other compiled language. (already mentioned in other threads in the forum)
  • wasm debugger support in chrome. Breakpoints and step-by-step debugging are supported but variable inspection is not (documented in https://book.swiftwasm.org) . It's sad to read some other articles saying that it will not be possible in the near future.
  • Wasm web framework (tokamak) is not very actively developed.

During the time I was trying swiftwasm, rust often came up as a comparison. Rust's wasm support is official, and there are many rust/wasm based web framework and tools being actively developed.
But as to the language itself, swift appeals to me more (I'm from c++ and some c#), so I really hope swiftwasm can be officially supported.

4 Likes

Hi Felix, thank you for your feedback!

  • Windows support. swift has windows (not WSL) support but swiftwasm does not.

Right, toolchains distributed by SwiftWasm cannot compile Swift code on Windows platforms since we currently don't have enough resources.

However, we have already upstreamed all essential compiler changes, so the latest development snapshot compilers distributed by swift.org are capable of compiling to WebAssembly. I haven't tested the Windows toolchain yet but I guess it works as other platform toolchains. Note that the swift.org snapshot doesn't include stdlib for WebAssembly, so it's very limited at this moment and it's too early to say "official", but we are actively working on it.

  • Wasm binary size is bigger than some other compiled language. (already mentioned in other threads in the forum)

Right, my next milestone is enabling --gc-sections linker option. Hopefully, it will cut off a bunch of unreachable bytes from the final binary. It's tracked here: Enable wasm-ld's `--gc-sections` option for code size and import requirements · Issue #5128 · swiftwasm/swift · GitHub

  • wasm debugger support in chrome. Breakpoints and step-by-step debugging are supported but variable inspection is not (documented in https://book.swiftwasm.org ) . It's sad to read some other articles saying that it will not be possible in the near future.

Recently many debug information improvements have been made to be DWARF friendly by debugger people, so Chrome's DWARF extension will be able to see more variables after the next next release.

  • Wasm web framework (tokamak) is not very actively developed.

TBH, I personally don't have enough time to take care of Tokamak recently, so it's a sad situation.

14 Likes

I'm also curious that when we can have Swift 5.8+ support for WASM.

One of my package support WASM. And due to the missing toolchain support of WASM for higher Swift version. I've been locked to support Swift 5.7.

Update

Check the repo again. I found 5.8 and 5.9 is released recently. I'll update to give it a try.

Thanks @kateinoigakukun

Update 2

Tried 5.8 and 5.9 WASM toolchain. It works and I've updated the CI pipeline.

A small feedback is we do not support Metrics for GitHub action. #3

Do we have any plan to support it in a near future?

2 Likes

Out of curiosity, I downloaded the snapshot version of swift (currently it's Swift version 5.11-dev (LLVM 79aab1b6aed58d0, Swift fd9726839309a33)) and tried like this:

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

I got this error:

error: missing external dependency '.../swift-DEVELOPMENT-SNAPSHOT/usr/lib/swift/wasi/static-executable-args.lnk'

where can I get wasi/static-executable-args.lnk file?

Out of curiosity again, I tested the 5.10 snapshot, and even the released 5.9.2, I had the same error message. So if I fixed the wasi/static-executable-args.lnk issue, then the released 5.9.2 can work with wasm target, am I right?

1 Like

Neither 5.9.* releases nor 5.10 snapshots from Swift.org - Download Swift are ready for building WebAssembly, you have to use the SwiftWasm toolchain and SDKs, or latest main snapshots from swift.org (which don't have a WASI SDK) for that.

It is included in the SwiftWasm SDK, and this is not the only file needed to build for the WASI triple, swift.org distributions lack a WASI SDK.

This is no longer true with latest main development snapshot and Embedded Swift, if you're ok with limitations of that mode and a very bare bones SDK that doesn't include WASI-libc and everything that depends on it as a result, including Foundation, XCTest, and many more packages downstream that depend on those.

You can try this in action yourself. The steps are bit convoluted and fiddly and are provided only for a one-off evaluation, but I hope it shows how one can build on top of this by coming up with some abstractions.

  1. Download January 22, 2024 Trunk Development (main) snapshot from Swift.org - Download Swift
  2. Use "Install for me" option when installing on macOS (makes it easier to clean up later)
  3. Run this in terminal, adjust PATH as needed if not on macOS
export TOOLCHAINS=$(plutil -extract CFBundleIdentifier raw \
~/Library/Developer/Toolchains/swift-latest.xctoolchain/Info.plist)
  1. Create hello.swift that looks like this:
@_extern(wasm, module: "console", name: "log")
@_extern(c)
func consoleLog(address: Int, byteCount: Int)

func print(_ string: StaticString) {
  consoleLog(
    address: Int(bitPattern: string.utf8Start), 
    byteCount: string.utf8CodeUnitCount
  )
}

@_expose(wasm, "hello")
func hello() {
  print("Hello, World!")
}
  1. Create hello.html that looks like this:
<html>
  <head>
    <meta charset="utf-8">
    <title>Simple template</title>
  </head>
  <body>
    <script type="module">
      const decoder = new TextDecoder();

      const importObject = {
        console: { log: (address, byteCount) => {
          const string = module.instance.exports.memory.buffer.slice(address, address + byteCount);
          console.log(decoder.decode(string));
        }},
      };
      const module = await WebAssembly.instantiateStreaming(fetch('hello.wasm'), importObject);
      module.instance.exports.hello();
    </script>
  </body>
</html>
  1. Build hello.swift with this command
swiftc -Osize -Xcc -fdeclspec -target wasm32-unknown-none-wasm -enable-experimental-feature Extern -enable-experimental-feature Embedded -wmo hello.swift -c -o hello.o
  1. Link to hello.wasm with this command (assumes you have LLVM installed with brew install llvm on Apple Silicon, adjust paths as needed):
/opt/homebrew/opt/llvm/bin/wasm-ld --no-entry hello.o -o hello.wasm
  1. Launch an HTTP server with this command
python -m http.server
  1. Open http://localhost:8000/hello.html in your browser with developer instruments console, you'll see "Hello, World" printed. On my machine the total for hello.wasm and hello.html is 892 bytes.

You can also see how small the final optimized module is:

❯ wasm2wat hello.wasm
(module
  (type (;0;) (func (param i32 i32)))
  (import "console" "log" (func $consoleLog (type 0)))
  (func $s4test5helloyyF (type 0) (param i32 i32)
    global.get $GOT.data.internal.__memory_base
    i32.const 1024
    i32.add
    i32.const 13
    call $consoleLog)
  (table (;0;) 1 1 funcref)
  (memory (;0;) 2)
  (global $__stack_pointer (mut i32) (i32.const 66576))
  (global $GOT.data.internal.__memory_base i32 (i32.const 0))
  (export "memory" (memory 0))
  (export "hello" (func $s4test5helloyyF))
  (data $.rodata (i32.const 1024) "Hello, World!\00\03\00"))

wasm-opt -Os pass on this module strips it down to 172 bytes, which I think is as close to a module hand-written in WAT as you can get, not taking into account literally a few padding bytes.

(module
  (type (;0;) (func (param i32 i32)))
  (import "console" "log" (func (;0;) (type 0)))
  (func (;1;) (type 0) (param i32 i32)
    i32.const 1024
    i32.const 13
    call 0)
  (memory (;0;) 2)
  (export "memory" (memory 0))
  (export "hello" (func 1))
  (data (;0;) (i32.const 1024) "Hello, World!\00\03"))
15 Likes

Hi Max, thanks for such a detailed reply that I can reproduce your work on Ubuntu 22 with the snapshot version (Swift version 5.11-dev)

I even achieved to get a .wasm size of 170 byte, because in my case the \00\03 is also optimized out, very impressive!

3 Likes

I hope that when new Windows main snapshots become available, one could also reproduce this on Windows, which would at least partially resolve your other request for Windows compatibility.

3 Likes

Update: I managed to enable --gc-sections by fixing several issues and hello world program is now 5% smaller.

8 Likes

Do we need to add flags while compiling to take advantage of that, or will it be built in to future swift compilers?

1 Like

No extra flag is needed. It's enabled by default in the current main branch.

4 Likes

Thanks so much for your work!

1 Like

I don't typically use Swift, but I did test SwiftWasm along with many other compilers and languages. The results were disappointing: it was extremely large and over ten times slower than comparable C code compiled with Emscripten.

However, following the announcement of Swift Embedded, I spent a few hours testing it and found it to be significantly better than SwiftWasm.

I made a post about my experience and how to compile it. In summary, I use the latest Swift Compiler to compile to LLVM-IR (using wasm32-wasm, non-WASI). The next step is provide such LLVM-IR to Clang (using the WASI-SDK) or Emscripten:

xcrun --toolchain swift swiftc \
  -O \
  -Xcc -fdeclspec \
  -target wasm32-unknown-none-wasm \
  -enable-experimental-feature Embedded \
  -wmo \
  -disable-stack-protector \
  -emit-executable \
  -c \
  -emit-ir \
  -o hello.ll
  hello.swift 

That will result in the LLVM-IR file, hello.ll. Then, you need to provide this file to Emscripten or Clang (with WASI-SDK/WASI-LibC). Since you have two options, you can try both of them:

emcc \
 -sALLOW_MEMORY_GROWTH=1 \
 -sSTANDALONE_WASM=1 \
 -sPURE_WASI=1 \
 -O3 \
 -flto \
 -o hello.wasm \
 hello.ll

OR

clang \
 -target wasm32-wasi \
 --sysroot=/path/to/your/wasi-libc/sysroot \
 -O3 \
 -flto \
 -Wl,--gc-sections,--strip-all \
 -o hello.wasm \
 hello.ll

The first method (Emscripten) generated a smaller WASM (3KB) and it's easier to install. The second method generates a larger WASM (10KB), and requires to install WASI-SDK.

You can run the code using wasmtime --invoke your_function hello.wasm. The hello.swift can use print and most Swift features (such as classes, of course).

Since Swift itself can't compile to wasm32-wasi and don't provide LibC functions, I choose to use Emscripten/WASI-SDK. I'm not sure if we have a better way to handle it.

The resulting WASM is only 3KB (compared to 7MB from SwiftWasm). It also handles WASI and LibC functions (such as Free/Aligned_Alloc), as Emscripten/WASI-SDK can provide the necessary LibC functions.

6 Likes