Some feedback from my short experience with SwiftWasm

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"))
14 Likes