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.
- Download January 22, 2024 Trunk Development (main) snapshot from Swift.org - Download Swift
- Use "Install for me" option when installing on macOS (makes it easier to clean up later)
- 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)
- 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!")
}
- 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>
- 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
- 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
- Launch an HTTP server with this command
python -m http.server
- 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"))