How far can i get with Swift WebAssembly in 30 minutes?

i’m posting this because i am Completely New to Swift WebAssembly, and i’ve wanted to learn it for a while now, so i figured i would give myself 30 minutes to see how far i could get with Swift WebAssembly and document what i saw along the way in the hopes it might illuminate ways to improve the “onboarding” process even more.


:grin: i’m gonna google swift webassembly

so i start by googling 'swift webassembly', and i write this down because a lot of times the helpful Swift documentation does not show up on the first, or third page of Google search results. but luckily, this does not happen here. the very first result is swiftwasm.org!

the website is really pretty. i like this a lot. A+ to whoever came up with this design.

:grin: does the fiddle work?

this website has a fiddle on the home page, which is great, and even better, the fiddle works when i click Run. marvelous!

:cold_sweat: how to get started…

the next thing i did was click the how to get started link above the fiddle, which takes me to the middle of this TSPL-like documentation website.

it tells me that i need to use something called the “Tokamak UI framework”, which is similar to SwiftUI, apparently.

in the next paragraph it tells me i can’t start using Tokamak UI either, first i have to start using another framework called “carton” before i can start using Tokamak UI, and that it will be Very Very Painful to use Swift WebAssembly without this framework.

i suppose whoever wrote this is correct. i will Eventually Have To start using Tokamak UI and carton, because this is how real swiftwasm apps are built. but i do not want to build a Real Application yet, i just want to get to hello world.

luckily, when i Look To My Left, i see that the link dropped me off in some intermediate chapter called Creating a browser app, and that there is an earlier chapter called Hello, World so i just jump over to that.

:face_with_monocle: hello world!

okay, so this is what i was actually looking for.

now, i’m not playing dumb, and i can see that there is a preceeding chapter called Installation, which suggests i would need to install something to make this work. but for the sake of science, i want to know what happens when i run these commands with the Swift toolchain i already have.

$ swiftc -target wasm32-unknown-wasi hello.swift -o hello.wasm
error: missing external dependency '/home/ubuntu/x86_64/6.0.3/usr/lib/swift/wasi/static-executable-args.lnk'

okay, nothing surprising here. it needs some SDK and i have not installed it.

:skull: is this thing still maintained?

so i go to the Installation page, and it tells me to install SwiftWasm 5.10, which is based on the swift-5.10.0-RELEASE toolchain.

this is not the swift-6.0.3-RELEASE toolchain we are using today, and it’s not even the swift-5.10.1-RELEASE patch release that came out after the original 5.10 release.

i have seen this movie many times before, so my first thought is: is this project still maintained?

but when i go to the actual GitHub repository, i see that the project is indeed actively maintained, and they have nightlies tracking Swift 6.1.

i click through a few more links and find the 6.0.2 release, which is not the 6.0.3 release, but that’s Close Enough for me.


now i took a lot of screenshots to document the path i took, but truth be told, i did not have a hard time locating the latest swiftwasm release. the only point where they really could have potentially lost me was when reading the original anachronistic installation guide, which made me think the project was no longer active.

:grin: installing the sdk

i run the command that’s shown next to the release card, and it works! great!

$ swift sdk install https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0.2-RELEASE/swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle.zip --checksum 6ffedb055cb9956395d9f435d03d53ebe9f6a8d45106b979d1b7f53358e1dcb4
Downloading a Swift SDK bundle archive from `https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0.2-RELEASE/swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle.zip`...
                                                                                               Downloading
100% [=====================================================================================================================================================================================]
Downloading swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle.zip

Swift SDK bundle archive successfully downloaded from `https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0.2-RELEASE/swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle.zip`.
Verifying if checksum of the downloaded archive is valid...
Downloaded archive has a valid checksum.
Swift SDK bundle at `https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0.2-RELEASE/swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle.zip` is assumed to be an archive, unpacking...
Swift SDK bundle at `https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0.2-RELEASE/swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle.zip` successfully installed as swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle.
$ swift sdk list
6.0.2-RELEASE-wasm32-unknown-wasi

:thinking: using the sdk

i re-ran the command from the Hello World tutorial, to see if it works this time. it doesn’t.

$ swiftc -target wasm32-unknown-wasi hello.swift -o hello.wasm
error: missing external dependency '/home/ubuntu/x86_64/6.0.3/usr/lib/swift/wasi/static-executable-args.lnk'

i also tried fuzzing the arguments for a few seconds, unsuccessfully.

$ swiftc --swift-sdk wasm32-unknown-wasi -target wasm32-unknown-wasi hello.swift -o hello.wasm
error: unknown argument: '--swift-sdk'
$ swiftc -swift-sdk wasm32-unknown-wasi -target wasm32-unknown-wasi hello.swift -o hello.wasm
error: unknown argument: '-swift-sdk'
$ swiftc -sdk wasm32-unknown-wasi -target wasm32-unknown-wasi hello.swift -o hello.wasm
warning: no such SDK: wasm32-unknown-wasi
error: missing external dependency '/home/ubuntu/x86_64/6.0.3/usr/lib/swift/wasi/static-executable-args.lnk'
$ swiftc -sdk 6.0.2-RELEASE-wasm32-unknown-wasi -target wasm32-unknown-wasi hello.swift -o hello.wasm
warning: no such SDK: 6.0.2-RELEASE-wasm32-unknown-wasi
error: missing external dependency '/home/ubuntu/x86_64/6.0.3/usr/lib/swift/wasi/static-executable-args.lnk'

luckily, i already knew that Swift SDKs are related to SwiftPM, so i didn’t waste too much time trying to get swiftc to work. (but not everyone knows that? maybe? who knows…)

i noticed there were example commands for how to build things with the SDK and SwiftPM, and i am Not Allergic to SwiftPM, so i then spent a minute or two creating a rudimentary SwiftPM layout with my Hello World program in a SwiftPM executable target.

:partying_face: using SwiftPM

like many build systems, SwiftPM has a funny tendency to say that it failed when it actually successfully built an executable, and swiftwasm is no exception. it Just Works, even though at first glance, it looks like it didn’t.

$ swift build --swift-sdk wasm32-unknown-wasi 
Building for debugging...
warning: Could not read SDKSettings.json for SDK at: /home/ubuntu/.swiftpm/swift-sdks/swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle/6.0.2-RELEASE-wasm32-unknown-wasi/wasm32-unknown-wasi/WASI.sdk
<unknown>:0: warning: libc not found for 'wasm32-unknown-wasi'; C stdlib may be unavailable

Build complete! (2.05s)
$ swift build -c release --swift-sdk wasm32-unknown-wasi 
Building for production...
warning: Could not read SDKSettings.json for SDK at: /home/ubuntu/.swiftpm/swift-sdks/swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle/6.0.2-RELEASE-wasm32-unknown-wasi/wasm32-unknown-wasi/WASI.sdk
warning: Could not read SDKSettings.json for SDK at: /home/ubuntu/.swiftpm/swift-sdks/swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle/6.0.2-RELEASE-wasm32-unknown-wasi/wasm32-unknown-wasi/WASI.sdk
[5/5] Linking test.wasm
Build complete! (0.90s)
$ ls .build/release
ModuleCache  description.json  swift-version--75355AAB4E86B17C.txt  test.build  test.product  test.wasm

:pensive: running the binary

i’m probably not going to get to Firefox in 30 minutes, but i at least want to try running this thing using wasmtime.

so i go to docs.wasmtime.dev and run the command they tell me to run, and it doesn’t work.

$ curl https://wasmtime.dev/install.sh -sSf | bash
  Installing latest version of Wasmtime (v28.0.1)
    Checking for existing Wasmtime installation
    Fetching archive for Linux, version v28.0.1
https://github.com/bytecodealliance/wasmtime/releases/download/v28.0.1/wasmtime-v28.0.1-x86_64-linux.tar.xz 
######################################################################## 100.0%
    Creating directory layout
  Extracting Wasmtime binaries
tar (child): xz: Cannot exec: No such file or directory
tar (child): Error is not recoverable: exiting now
tar: Child returned status 2
tar: Error is not recoverable: exiting now
cp: cannot stat '/tmp/tmp.vcHYenQBsa/wasmtime-v28.0.1-x86_64-linux/wasmtime': No such file or directory
cp: cannot stat '/tmp/tmp.vcHYenQBsa/wasmtime-v28.0.1-x86_64-linux/LICENSE': No such file or directory
cp: cannot stat '/tmp/tmp.vcHYenQBsa/wasmtime-v28.0.1-x86_64-linux/README.md': No such file or directory

i do a little debugging and i realize this is because GitHub uses an HTTP redirect (presumably to protect itself from scrapers), and the curl command is missing an -L option.

$ curl https://github.com/bytecodealliance/wasmtime/releases/download/v28.0.1/wasmtime-v28.0.1-x86_64-linux.tar.xz -o wasmtime-v28.0.1-x86_64-linux.tar.xz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
$ ls
Package.swift  Sources  wasmtime-v28.0.1-x86_64-linux.tar.xz
$ ls -l
total 8
-rw-r--r-- 1 ubuntu ubuntu  274 Jan 20 00:19 Package.swift
drwxr-xr-x 3 ubuntu ubuntu 4096 Jan 20 00:16 Sources
-rw-r--r-- 1 ubuntu ubuntu    0 Jan 20 00:23 wasmtime-v28.0.1-x86_64-linux.tar.xz
$ curl -L https://github.com/bytecodealliance/wasmtime/releases/download/v28.0.1/wasmtime-v28.0.1-x86_64-linux.tar.xz -o wasmtime-v28.0.1-x86_64-linux.tar.xz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100 8605k  100 8605k    0     0  14.4M      0 --:--:-- --:--:-- --:--:-- 14.4M

i peer into the install.sh script, and i confirm that it is indeed missing an -L option, but then again i did not write this script, and i only spent a few seconds looking at it, so who knows.

EDIT: or not! it does actually have a --location option, which is long for -L. the mystery deepens.

download_release_from_repo() {
  local version="$1"
  local arch="$2"
  local os_info="$3"
  local tmpdir="$4"
  local postfix=$(release_file_postfix $os_info)
  local filename="wasmtime-$version-$arch-$os_info.$postfix"
  local download_file="$tmpdir/$filename"
  local archive_url="$(release_url)/download/$version/$filename"
  info $archive_url

  curl --progress-bar --show-error --location --fail "$archive_url" \
       --output "$download_file" && echo "$download_file"
}

i manage to get a hold of the wasmtime binary, and i use it to execute the binary i built with SwiftPM. it works! beautiful!

$ wasmtime-v28.0.1-x86_64-linux/wasmtime .build/release/test.wasm 
Hi Barbie!

conclusion

overall, i would say i had a very positive experience in my first 30 minutes of Swift WebAssembly. the bad parts were… Poor/Stale Documentation (shocker), and a very bumpy path to installing the wasm runtime. but the second problem isn’t really Swift WebAssembly’s responsibility, it’s just WebAssembly’s responsibility. and the first problem isn’t any defect in the underlying technology, we just need to update those docs, and maybe change a couple links to point to the beginning of the tutorial, instead of a chapter in the middle of the book.

hopefully i have time later to actually run stuff in a browser and explore this Tokamak UI thing.

great job to everyone who worked on Swift WebAssembly! this was truly a refreshing experience :smiley:

36 Likes

You're right. A lot of that needs to be re-written. Even I go to it and get confused about what does and doesn't work.

I don't use Tokamak at all. It didn't really exist when I started building my own UI framework, but also I hate SwiftUI. I wonder if better documentation would make me choose otherwise.

I have never gotten SwiftWASM to run properly on a mac. (I also give up too early, tbf) so I build on Ubuntu. I did learn that I need to make a new Ubuntu instance before using a new version of SwiftWASM because sometimes it just doesn't work for various reasons I can never explain. But I think that has happened significantly less than it did a few years ago.

Using carton has improved a lot. I really enjoy all the new debugging features they have added. When they switched over to carton being a plugin, it did confuse me a lot (again a documentation problem).

For context, I use SwiftWAM for a real-life production app running in a real website that is required for customers to login.

I ended up building an entirely proprietary Framework that is extremely similar to UIKit and actually vends UIKit on iOS and HTMLElements on Web (It also vends GTK on Linux, but I've abandoned progress on it, because I couldn't get a lot of GTK to work properly. I actually started to work on bridging it to GNUStep so I could get an interface on Linux going, it was just a lot.) It manages creates nearly identical user interfaces (and highly complex) on both iOS and Web. I am still in shock every day how wonderfully it works. I went so far as to build a layout system for this UI framework that works almost identically to HTML/CSS layout rules instead of trying to get constraint based layout or SwiftUI style layout working on Web.

I ended up having to re-write a lot of Foundation/XML/Networking to work on WASM (I have my own versions of URLRequest, URLSession, XMLParser, NSAttributedString, et cetera on SwiftWASM), but it was all well worth it.

The app is an interface to some proprietary software that runs a business, billing, contracts, online curriculum building. It includes a desktop-class publishing tools as well. And it's all a front-end to a server also written in Swift.

I have found SwiftWASM to be the most important piece of technology, and I desperately fear it going away or being abandoned.

2 Likes

Hello all,

Thank you for the feedback regarding the documentation. I apologize that it was outdated and confusing.

While the SwiftWasm project itself is actively maintained including feature additions (such as recent multi-threading support and code coverage support), I admit that I haven't been able to dedicate enough time to keeping the guides up-to-date.

I expect to have more time to work on this project starting around mid-February but contributions, including a documentation overhaul, are always welcome!

I'm thrilled to hear that you're using SwiftWasm in a real-life production app, and it's very encouraging to know that it's working well for you.

14 Likes

@taylorswift: Last year I built a set of example projects to guide people through SwiftWasm step-by-step. They may save you some time, as they should address most of the common pitfalls. I also gave a talk about them:

3 Likes

You'll probably want to add a SDKSettings.json to your wasm SDK, just like the static Musl SDK does and which my Android SDK bundle followed. That'll stop those warnings that @taylorswift reported above from getting spit out.

5 Likes

thanks everyone for the tips!

:bomb: running carton

i tried tonight to follow @svanimpe ’s example, trying to use carton to run a minimal example, and very quickly ran into the sobering reality which is that carton does not support my operating system, Ubuntu 24.04.

$ swift run carton dev --custom-index-page Web/index.html
Building for debugging...
[1/1] Write swift-version--75355AAB4E86B17C.txt
Build of product 'carton' complete! (0.54s)
- checking Swift compiler path: /home/ubuntu/.carton/sdk/wasm-6.0.2-RELEASE/usr/bin/swift
- checking Swift compiler path: /home/ubuntu/.swiftenv/versions/wasm-6.0.2-RELEASE/usr/bin/swift
Fetching release assets from https://api.github.com/repos/swiftwasm/swift/releases/tags/swift-wasm-6.0.2-RELEASE
Response contained body, parsing it now...
Swift/ErrorType.swift:253: Fatal error: Error raised at top level: This version of the operating system is not supported

💣 Program crashed: Illegal instruction at 0x00007c1401ee299f

Thread 4 crashed:

0 0x00007c1401ee299f _assertionFailure(_:_:file:line:flags:) + 351 in libswiftCore.so
1 0x00007c1401f32e1e swift_errorInMain + 829 in libswiftCore.so
2 async_MainTY2_ + 41 in carton at /swift/wasm-test/.build/checkouts/carton/Sources/carton/main.swift:35:11

    33│ import CartonDriver
    34│ 
    35│ try await main(arguments: CommandLine.arguments)                                                                                                                                                                            
      │           ▲
    36│

Backtrace took 0.03s

Press space to interact, D to debug, or any other key to quit (23s) 

Swift makes this look a lot worse than really is, because it insists on backtracing the crash it exits with whenever it catches an error thrown from main. this is actually a controlled exit thrown from here:

Swift on WebAssembly does not actually distribute binaries for Ubuntu 24.04. but surely we have seen that Swift on WebAssembly is capable of running on Ubuntu 24.04. so i modified the code to acquire the ubuntu22.04 binary on Ubuntu 24.04, which assumes that libc on Ubuntu 24.04 is compatible with libc on Ubuntu 22.04.

          case "24.04":
            return "ubuntu22.04"

this worked, and i was able to use this modified carton to run the carton dev command.

:vertical_traffic_light: on localhost

the carton dev command complains effusively about Sendable violations. it takes everything Swift Concurrency thinks is wrong about Carton, and swift-nio/Sources/NIOCore/ChannelPipeline.swift and prints it to the console. SwiftPM prints multiple copies of each warning, making it difficult to tell what is really going on.

in the end though, carton dev successfully starts up a server on localhost:8080.

when i look at the page, at first there is no sign the WebAssembly is running, but after a short moment, it appears. in the web inspector network tab, it shows that the WebAssembly is 59.96 MB in size.

although that is very hefty, surely this is just the debug binary size, and the release WebAssembly will be much smaller… right?

:gear: optimizing the binary

i want to know how small the production WebAssembly is, so i try running the bundle command, which is supposed to be the “release mode”.

$ swift run carton bundle --custom-index-page Web/index.html
Build of product 'Hello' complete! (6.72s)
Right after building the main binary size is 56.59 MB

After stripping debug info the main binary size is 49.96 MB


Running wasm-opt -Os --enable-bulk-memory --enable-sign-ext /swift/wasm-test/Bundle/main.wasm -o /swift/wasm-test/Bundle/main.wasm
wasm-opt -Os --enable-bulk-memory --enable-sign-ext /swift/wasm-test/Bundle/main.wasm -o /swift/wasm-test/Bundle/main.wasm

wasm-opt process failed.

Warning: wasm-opt failed to optimize the binary, falling back to the original binary.
If you don't have wasm-opt installed, you can install wasm-opt by running `brew install binaryen`, `apt-get install binaryen` or `npm install -g binaryen`
Copying resources to /swift/wasm-test/Bundle/JavaScriptKit_JavaScriptKit.resources
Bundle successfully generated at /swift/wasm-test/Bundle

okay, that’s not good. the production mode binary is 56.59 MB, which is only 3 MB smaller than the debug binary. stripping debug symbols takes it down to 49.96 MB, but that’s still enormous!

but it also looks like there’s another pass that needs to run, which requires installing some other tool binaryen. this step takes it down to 44.77 MB.

Build of product 'Hello' complete! (0.51s)
Right after building the main binary size is 56.59 MB

After stripping debug info the main binary size is 49.96 MB


Running wasm-opt -Os --enable-bulk-memory --enable-sign-ext /swift/wasm-test/Bundle/main.wasm -o /swift/wasm-test/Bundle/main.wasm
wasm-opt -Os --enable-bulk-memory --enable-sign-ext /swift/wasm-test/Bundle/main.wasm -o /swift/wasm-test/Bundle/main.wasm
`wasm-opt` process finished successfully
After stripping debug info the main binary size is 44.77 MB

Copying resources to /swift/wasm-test/Bundle/JavaScriptKit_JavaScriptKit.resources
Bundle successfully generated at /swift/wasm-test/Bundle

perhaps this code is very repetitive. i wonder what would happen if we used gzip on this?

$ gzip -9 Bundle/Hello.04ebb5a28d0d45fc.wasm 
$ ls -l Bundle/
total 17408
-rw-r--r-- 1 ubuntu ubuntu 17683279 Jan 21 02:07 Hello.04ebb5a28d0d45fc.wasm.gz
drwxr-xr-x 3 ubuntu ubuntu     4096 Jan 21 02:07 JavaScriptKit_JavaScriptKit.resources
-rw-r--r-- 1 ubuntu ubuntu    56740 Jan 21 02:08 app.d38bdc6abc41d009.js
-rw-r--r-- 1 ubuntu ubuntu      337 Jan 21 02:08 index.html
-rw-r--r-- 1 ubuntu ubuntu     1614 Jan 21 02:08 index.js
-rw-r--r-- 1 ubuntu ubuntu    62313 Jan 21 02:08 intrinsics.js
-rw-r--r-- 1 ubuntu ubuntu       46 Jan 21 02:08 package.json

alright, so effective baseline Hello World size is 17.7 MB compressed.

:beach_umbrella: what now?

17.7 MB is not unworkable if the WebAssembly is the entire web app. you can host this statically, serve it through a CDN, and walk users through updates as they roll out. in a lot of ways, this is no more annoying than updating an iOS app.

on the other extreme, it seems to be possible to get ultralight binaries by going the Embedded route. but a lot of this seems geared towards folks who are trying to ship WebAssembly on the scale of 500 KB or less (200 KB compressed?).

i’m curious to know if there is a way to target WebAssembly in the range of 5–10 MB uncompressed, that avoids dependencies such as Foundation, while not being restricted completely to the Embedded subset of the language. does such a thing exist?

you can’t really integrate something that is 17.7 MB into an existing web application, that kind of overhead is only justifiable if you are starting from scratch and the WebAssembly is the entire application. i want to know if it is possible to incrementally adopt WebAssembly with the eventual goal of having it replace the rest of the application.

6 Likes

There has been a lot of discussion on this topic on github, https://github.com/swiftwasm/swift/issues/7 as it was definitely an ongoing issue.

You're right. Althought today I don't worry about it as much, phone speeds have improved a lot since 2020 and so have download speeds. Still, definitely some room for improvement.

I am a little perplexed at this, the wasm file that I host is only 18.8mb. It's not compressed into a gzip/zip. I just tried to compress it, and it's 5mb compressed.

My output is

`wasm-opt` process finished successfully
After stripping debug info the main binary size is 17.92mb

My project is quite large. 300k-500k lines of code. I compared the output and they looked the same, so I am wondering where the large difference is coming form. (this often means I need sleep).

2 Likes

Thanks for reporting this. I just realized I forgot to check the examples on Ubuntu 24.04. I started out on 22.04, then switched to macOS in early 2024.

Does it work with nightly builds? For SwiftWasm, I always used nightlies, not releases.

i tried to debug this today, by replicating @svanimpe ’s example project as closely as i could.

$ git clone https://github.com/pwsacademy/swiftwasm-examples
$ cd swiftwasm-examples/
$ cd basics/
$ swift run carton bundle --custom-index-page Web/index.html

i was again loudly reminded with a crash and stack trace that carton does not support my Operating System. why Swift performs seppuku when throwing from main is something i fail to understand since Swift 5.9. but that is hardly carton’s fault.

i switched the dependency in Package.swift over to my local fork, and everything went smoothly, Sendable noise notwithstanding.

Swift Concurrency noise
$ swift run carton bundle --custom-index-page Web/index.html
Updating https://github.com/swiftwasm/JavaScriptKit
Updating https://github.com/apple/swift-nio.git
Updating https://github.com/swiftwasm/WasmTransformer
Updating https://github.com/apple/swift-argument-parser.git
Updated https://github.com/swiftwasm/JavaScriptKit (0.24s)
Updated https://github.com/swiftwasm/WasmTransformer (0.27s)
Updated https://github.com/apple/swift-argument-parser.git (0.27s)
Updated https://github.com/apple/swift-nio.git (0.41s)
Computing version for https://github.com/swiftwasm/JavaScriptKit
Computed https://github.com/swiftwasm/JavaScriptKit at 0.21.0 (0.01s)
Computing version for https://github.com/swiftwasm/WasmTransformer
Computed https://github.com/swiftwasm/WasmTransformer at 0.5.0 (0.00s)
Computed https://github.com/swiftwasm/JavaScriptKit at 0.21.0 (0.00s)
Computing version for https://github.com/apple/swift-argument-parser.git
Computed https://github.com/apple/swift-argument-parser.git at 1.3.1 (0.01s)
Computing version for https://github.com/apple/swift-nio.git
Computed https://github.com/apple/swift-nio.git at 2.79.0 (0.01s)
Updating https://github.com/apple/swift-atomics.git
Updating https://github.com/apple/swift-system.git
Updating https://github.com/apple/swift-collections.git
Updated https://github.com/apple/swift-collections.git (0.28s)
Updated https://github.com/apple/swift-atomics.git (0.28s)
Updated https://github.com/apple/swift-system.git (0.28s)
Computing version for https://github.com/apple/swift-system.git
Computed https://github.com/apple/swift-system.git at 1.4.0 (0.00s)
Computing version for https://github.com/apple/swift-atomics.git
Computed https://github.com/apple/swift-atomics.git at 1.2.0 (0.01s)
Computing version for https://github.com/apple/swift-collections.git
Computed https://github.com/apple/swift-collections.git at 1.1.4 (0.01s)
Building for debugging...
/swift/carton/Sources/SwiftToolchain/AsyncFileDownload.swift:36:9: warning: non-final class 'FileDownloadDelegate' cannot conform to 'Sendable'; use '@unchecked Sendable'; this is an error in the Swift 6 language mode
 34 |     public var receivedBytes: Int
 35 |   }
 36 |   class FileDownloadDelegate: NSObject, URLSessionDownloadDelegate {
    |         `- warning: non-final class 'FileDownloadDelegate' cannot conform to 'Sendable'; use '@unchecked Sendable'; this is an error in the Swift 6 language mode
 37 |     let path: String
 38 |     let onTotalBytes: (Int) -> Void

/swift/carton/Sources/SwiftToolchain/AsyncFileDownload.swift:38:9: warning: stored property 'onTotalBytes' of 'Sendable'-conforming class 'FileDownloadDelegate' has non-sendable type '(Int) -> Void'; this is an error in the Swift 6 language mode
 36 |   class FileDownloadDelegate: NSObject, URLSessionDownloadDelegate {
 37 |     let path: String
 38 |     let onTotalBytes: (Int) -> Void
    |         |- warning: stored property 'onTotalBytes' of 'Sendable'-conforming class 'FileDownloadDelegate' has non-sendable type '(Int) -> Void'; this is an error in the Swift 6 language mode
    |         `- note: a function type must be marked '@Sendable' to conform to 'Sendable'
 39 |     let continuation: AsyncThrowingStream<Progress, Error>.Continuation
 40 |     var totalBytesToDownload: Int?

/swift/carton/Sources/SwiftToolchain/AsyncFileDownload.swift:40:9: warning: stored property 'totalBytesToDownload' of 'Sendable'-conforming class 'FileDownloadDelegate' is mutable; this is an error in the Swift 6 language mode
 38 |     let onTotalBytes: (Int) -> Void
 39 |     let continuation: AsyncThrowingStream<Progress, Error>.Continuation
 40 |     var totalBytesToDownload: Int?
    |         `- warning: stored property 'totalBytesToDownload' of 'Sendable'-conforming class 'FileDownloadDelegate' is mutable; this is an error in the Swift 6 language mode
 41 | 
 42 |     init(
/swift/carton/Sources/SwiftToolchain/AsyncFileDownload.swift:36:9: warning: non-final class 'FileDownloadDelegate' cannot conform to 'Sendable'; use '@unchecked Sendable'; this is an error in the Swift 6 language mode
 34 |     public var receivedBytes: Int
 35 |   }
 36 |   class FileDownloadDelegate: NSObject, URLSessionDownloadDelegate {
    |         `- warning: non-final class 'FileDownloadDelegate' cannot conform to 'Sendable'; use '@unchecked Sendable'; this is an error in the Swift 6 language mode
 37 |     let path: String
 38 |     let onTotalBytes: (Int) -> Void

/swift/carton/Sources/SwiftToolchain/AsyncFileDownload.swift:38:9: warning: stored property 'onTotalBytes' of 'Sendable'-conforming class 'FileDownloadDelegate' has non-sendable type '(Int) -> Void'; this is an error in the Swift 6 language mode
 36 |   class FileDownloadDelegate: NSObject, URLSessionDownloadDelegate {
 37 |     let path: String
 38 |     let onTotalBytes: (Int) -> Void
    |         |- warning: stored property 'onTotalBytes' of 'Sendable'-conforming class 'FileDownloadDelegate' has non-sendable type '(Int) -> Void'; this is an error in the Swift 6 language mode
    |         `- note: a function type must be marked '@Sendable' to conform to 'Sendable'
 39 |     let continuation: AsyncThrowingStream<Progress, Error>.Continuation
 40 |     var totalBytesToDownload: Int?

/swift/carton/Sources/SwiftToolchain/AsyncFileDownload.swift:40:9: warning: stored property 'totalBytesToDownload' of 'Sendable'-conforming class 'FileDownloadDelegate' is mutable; this is an error in the Swift 6 language mode
 38 |     let onTotalBytes: (Int) -> Void
 39 |     let continuation: AsyncThrowingStream<Progress, Error>.Continuation
 40 |     var totalBytesToDownload: Int?
    |         `- warning: stored property 'totalBytesToDownload' of 'Sendable'-conforming class 'FileDownloadDelegate' is mutable; this is an error in the Swift 6 language mode
 41 | 
 42 |     init(
[29/29] Linking carton
Build of product 'carton' complete! (2.99s)
- checking Swift compiler path: /home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a/usr/bin/swift
- checking Swift compiler path: /home/ubuntu/.swiftenv/versions/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a/usr/bin/swift
Fetching release assets from https://api.github.com/repos/swiftwasm/swift/releases/tags/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a
Response contained body, parsing it now...
Response successfully parsed, choosing from this number of assets: 10
Local installation of Swift version wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a not found
Swift toolchain/SDK download URL: https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a-ubuntu22.04_x86_64.tar.gz
Archive size is 1042 MB
                                                                                                        Downloading the archive
99% [=================================================================================================================================================================================================================---]
saving to /home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a.gz
Download completed successfully
Unpacking the archive: /usr/bin/tar xzf /home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a.gz --strip-components=1 --directory /home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a
Running "/usr/bin/tar" "xzf" "/home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a.gz" "--strip-components=1" "--directory" "/home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a"
- checking Swift compiler path: /home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a/usr/bin/swift
Inferring basic settings...
- swift executable: /home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a/usr/bin/swift
Swift version 6.1-dev (LLVM f4b733c38006ec1, Swift 5c7509bef50dbd7)
Target: x86_64-unknown-linux-gnu
Running "/home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a/usr/bin/swift" "package" "--triple" "wasm32-unknown-wasi" "--scratch-path" "/swift/wasm-test/swiftwasm-examples/basics/.build/carton" "--disable-sandbox" "plugin" "carton-bundle" "--output" "Bundle" "--custom-index-page" "Web/index.html"
Building for debugging...
/swift/carton/Sources/CartonHelpers/Basics/Condition.swift:11:19: warning: '@preconcurrency' attribute on module 'Foundation' has no effect
 9 | */
10 | #if !_runtime(_ObjC)
11 |   @preconcurrency import Foundation
   |                   `- warning: '@preconcurrency' attribute on module 'Foundation' has no effect
12 | #else
13 |   import Foundation

/swift/carton/Sources/CartonHelpers/Basics/FileInfo.swift:12:19: warning: '@preconcurrency' attribute on module 'Foundation' has no effect
10 | 
11 | #if !_runtime(_ObjC)
12 |   @preconcurrency import Foundation
   |                   `- warning: '@preconcurrency' attribute on module 'Foundation' has no effect
13 | #else
14 |   import Foundation
/swift/carton/Sources/CartonHelpers/Basics/Condition.swift:11:19: warning: '@preconcurrency' attribute on module 'Foundation' has no effect
 9 | */
10 | #if !_runtime(_ObjC)
11 |   @preconcurrency import Foundation
   |                   `- warning: '@preconcurrency' attribute on module 'Foundation' has no effect
12 | #else
13 |   import Foundation

/swift/carton/Sources/CartonHelpers/Basics/FileInfo.swift:12:19: warning: '@preconcurrency' attribute on module 'Foundation' has no effect
10 | 
11 | #if !_runtime(_ObjC)
12 |   @preconcurrency import Foundation
   |                   `- warning: '@preconcurrency' attribute on module 'Foundation' has no effect
13 | #else
14 |   import Foundation
/swift/carton/Sources/CartonHelpers/Basics/Process/Process.swift:20:22: warning: using '@_implementationOnly' without enabling library evolution for 'CartonHelpers' may lead to instability during execution
  18 | #endif
  19 | 
  20 | @_implementationOnly import TSCclibc
     |                      `- warning: using '@_implementationOnly' without enabling library evolution for 'CartonHelpers' may lead to instability during execution
  21 | import TSCLibc
  22 | import Dispatch

/swift/carton/Sources/CartonHelpers/Basics/Process/ProcessEnv.swift:72:36: warning: conformance of 'Dictionary<Key, Value>' to protocol 'Sendable' conflicts with that stated in the type's module 'Swift' and will be ignored; there cannot be more than one conformance, even with different conditional bounds
 70 | }
 71 | 
 72 | extension ProcessEnvironmentBlock: Sendable {}
    |                                    `- warning: conformance of 'Dictionary<Key, Value>' to protocol 'Sendable' conflicts with that stated in the type's module 'Swift' and will be ignored; there cannot be more than one conformance, even with different conditional bounds
 73 | 
 74 | /// Provides functionality related a process's environment.

Swift.Dictionary:1:11: note: 'Dictionary<Key, Value>' declares conformance to protocol 'Sendable' here
1 | extension Dictionary : @unchecked Sendable where Key : Sendable, Value : Sendable {
  |           `- note: 'Dictionary<Key, Value>' declares conformance to protocol 'Sendable' here
2 | }
/swift/carton/Sources/CartonHelpers/Basics/Process/ProcessEnv.swift:72:36: warning: conformance of 'Dictionary<Key, Value>' to protocol 'Sendable' conflicts with that stated in the type's module 'Swift' and will be ignored; there cannot be more than one conformance, even with different conditional bounds
 70 | }
 71 | 
 72 | extension ProcessEnvironmentBlock: Sendable {}
    |                                    `- warning: conformance of 'Dictionary<Key, Value>' to protocol 'Sendable' conflicts with that stated in the type's module 'Swift' and will be ignored; there cannot be more than one conformance, even with different conditional bounds
 73 | 
 74 | /// Provides functionality related a process's environment.

Swift.Dictionary:1:11: note: 'Dictionary<Key, Value>' declares conformance to protocol 'Sendable' here
1 | extension Dictionary : @unchecked Sendable where Key : Sendable, Value : Sendable {
  |           `- note: 'Dictionary<Key, Value>' declares conformance to protocol 'Sendable' here
2 | }
/swift/carton/Sources/CartonHelpers/Basics/Process/ProcessEnv.swift:72:36: warning: conformance of 'Dictionary<Key, Value>' to protocol 'Sendable' conflicts with that stated in the type's module 'Swift' and will be ignored; there cannot be more than one conformance, even with different conditional bounds
 70 | }
 71 | 
 72 | extension ProcessEnvironmentBlock: Sendable {}
    |                                    `- warning: conformance of 'Dictionary<Key, Value>' to protocol 'Sendable' conflicts with that stated in the type's module 'Swift' and will be ignored; there cannot be more than one conformance, even with different conditional bounds
 73 | 
 74 | /// Provides functionality related a process's environment.

Swift.Dictionary:1:11: note: 'Dictionary<Key, Value>' declares conformance to protocol 'Sendable' here
1 | extension Dictionary : @unchecked Sendable where Key : Sendable, Value : Sendable {
  |           `- note: 'Dictionary<Key, Value>' declares conformance to protocol 'Sendable' here
2 | }
/swift/carton/Sources/CartonHelpers/Basics/Thread.swift:54:41: warning: converting non-sendable function value to '@Sendable () -> Void' may introduce data races
 52 |         }
 53 | 
 54 |         self.thread = ThreadImpl(block: theTask)
    |                                         `- warning: converting non-sendable function value to '@Sendable () -> Void' may introduce data races
 55 |     }
 56 | 
/swift/carton/Sources/CartonHelpers/Basics/Thread.swift:54:41: warning: converting non-sendable function value to '@Sendable () -> Void' may introduce data races
 52 |         }
 53 | 
 54 |         self.thread = ThreadImpl(block: theTask)
    |                                         `- warning: converting non-sendable function value to '@Sendable () -> Void' may introduce data races
 55 |     }
 56 | 
/swift/carton/Sources/CartonHelpers/Basics/Process/Process.swift:20:22: warning: using '@_implementationOnly' without enabling library evolution for 'CartonHelpers' may lead to instability during execution
  18 | #endif
  19 | 
  20 | @_implementationOnly import TSCclibc
     |                      `- warning: using '@_implementationOnly' without enabling library evolution for 'CartonHelpers' may lead to instability during execution
  21 | import TSCLibc
  22 | import Dispatch
/swift/carton/Sources/CartonHelpers/Basics/Process/Process.swift:20:22: warning: using '@_implementationOnly' without enabling library evolution for 'CartonHelpers' may lead to instability during execution
  18 | #endif
  19 | 
  20 | @_implementationOnly import TSCclibc
     |                      `- warning: using '@_implementationOnly' without enabling library evolution for 'CartonHelpers' may lead to instability during execution
  21 | import TSCLibc
  22 | import Dispatch
[56/56] Linking carton-frontend-slim-tool
Build of product 'carton-frontend-slim' complete! (5.01s)
Building "HelloSwiftWasm"
Building for production...
[0/2] Write swift-version-22C80AB982ED8698.txt
Build of product 'HelloSwiftWasm' complete! (0.86s)
Right after building the main binary size is 8.12 MB

After stripping debug info the main binary size is 5.79 MB


Running wasm-opt -Os --enable-bulk-memory --enable-sign-ext /swift/wasm-test/swiftwasm-examples/basics/Bundle/main.wasm -o /swift/wasm-test/swiftwasm-examples/basics/Bundle/main.wasm
wasm-opt -Os --enable-bulk-memory --enable-sign-ext /swift/wasm-test/swiftwasm-examples/basics/Bundle/main.wasm -o /swift/wasm-test/swiftwasm-examples/basics/Bundle/main.wasm
`wasm-opt` process finished successfully
After stripping debug info the main binary size is 3.71 MB

Copying resources to /swift/wasm-test/swiftwasm-examples/basics/Bundle/JavaScriptKit_JavaScriptKit.resources
Bundle successfully generated at /swift/wasm-test/swiftwasm-examples/basics/Bundle

wow! that’s exactly what i was looking for! now, 3.71 MB uncompressed isn’t nothing, but that is definitely something i can work with :smiley:

now the question becomes: why does this build produce a 3.71 MB binary, but the other one produces a 56 MB binary?

:thinking: what is different about this build?

now i skim through that Concurrency-laden output from earlier, and immediately something sticks out to me.

Build of product 'carton' complete! (2.99s)
- checking Swift compiler path: /home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a/usr/bin/swift
- checking Swift compiler path: /home/ubuntu/.swiftenv/versions/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a/usr/bin/swift
Fetching release assets from https://api.github.com/repos/swiftwasm/swift/releases/tags/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a
Response contained body, parsing it now...
Response successfully parsed, choosing from this number of assets: 10
Local installation of Swift version wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a not found
Swift toolchain/SDK download URL: https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a-ubuntu22.04_x86_64.tar.gz
Archive size is 1042 MB
                                                                                                        Downloading the archive
99% [=================================================================================================================================================================================================================---]
saving to /home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a.gz
Download completed successfully
Unpacking the archive: /usr/bin/tar xzf /home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a.gz --strip-components=1 --directory /home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a
Running "/usr/bin/tar" "xzf" "/home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a.gz" "--strip-components=1" "--directory" "/home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a"
- checking Swift compiler path: /home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a/usr/bin/swift
Inferring basic settings...
- swift executable: /home/ubuntu/.carton/sdk/wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a/usr/bin/swift
Swift version 6.1-dev (LLVM f4b733c38006ec1, Swift 5c7509bef50dbd7)

it’s not using a release toolchain, it’s using some random nightly DEVELOPMENT-SNAPSHOT-2024-10-27-a.

that’s “bad”. a lot of libraries crash the compiler when built with nightlies, because the nightlies have compiler assertions enabled. there is often nothing you can do when you have a library that is crashing the compiler. and even if all your libraries build with the nightly, you can forget about using sourcekit-lsp or any other “modern” IDE tools. one lesson this Swiftie learned brutally and repeatedly early in her career is that if you are relying on nightly Swift toolchains, you are going to have a very tough time getting anything done.

but is it really the nightly toolchain that is key to achieving reasonable binary size? let’s find out…

:weary: it’s the .swift-version, stupid

so it looks like carton is using swiftenv, and there is a .swift-version file in the repo that contains the string wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a, so that’s probably where the nightly is coming from.

i try replacing this with 6.0.3-RELEASE, even though i know there is no such thing as swiftwasm 6.0.3. to my surprise, this works, and causes swiftenv to download Swift 6.0.2, which is probably what i would have done too. hooray?

$ swift run carton bundle --custom-index-page Web/index.html
Building for debugging...
[1/1] Write swift-version--75355AAB4E86B17C.txt
Build of product 'carton' complete! (0.27s)
- checking Swift compiler path: /home/ubuntu/.carton/sdk/wasm-6.0.2-RELEASE/usr/bin/swift
- checking Swift compiler path: /home/ubuntu/.swiftenv/versions/wasm-6.0.2-RELEASE/usr/bin/swift
Fetching release assets from https://api.github.com/repos/swiftwasm/swift/releases/tags/swift-wasm-6.0.2-RELEASE
Response contained body, parsing it now...
Response successfully parsed, choosing from this number of assets: 8
Local installation of Swift version wasm-6.0.2-RELEASE not found
Swift toolchain/SDK download URL: https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0.2-RELEASE/swift-wasm-6.0.2-RELEASE-ubuntu22.04_x86_64.tar.gz
Archive size is 908 MB
                                                                                                        Downloading the archive
99% [=================================================================================================================================================================================================================---]
saving to /home/ubuntu/.carton/sdk/wasm-6.0.2-RELEASE.gz
Download completed successfully
Unpacking the archive: /usr/bin/tar xzf /home/ubuntu/.carton/sdk/wasm-6.0.2-RELEASE.gz --strip-components=1 --directory /home/ubuntu/.carton/sdk/wasm-6.0.2-RELEASE
Running "/usr/bin/tar" "xzf" "/home/ubuntu/.carton/sdk/wasm-6.0.2-RELEASE.gz" "--strip-components=1" "--directory" "/home/ubuntu/.carton/sdk/wasm-6.0.2-RELEASE"
- checking Swift compiler path: /home/ubuntu/.carton/sdk/wasm-6.0.2-RELEASE/usr/bin/swift
Inferring basic settings...
- swift executable: /home/ubuntu/.carton/sdk/wasm-6.0.2-RELEASE/usr/bin/swift
Swift version 6.0.2 (swift-6.0.2-RELEASE)
Build of product 'HelloSwiftWasm' complete! (7.75s)
Right after building the main binary size is 56.56 MB

After stripping debug info the main binary size is 49.95 MB


Running wasm-opt -Os --enable-bulk-memory --enable-sign-ext /swift/wasm-test/swiftwasm-examples/basics/Bundle/main.wasm -o /swift/wasm-test/swiftwasm-examples/basics/Bundle/main.wasm
wasm-opt -Os --enable-bulk-memory --enable-sign-ext /swift/wasm-test/swiftwasm-examples/basics/Bundle/main.wasm -o /swift/wasm-test/swiftwasm-examples/basics/Bundle/main.wasm
`wasm-opt` process finished successfully
After stripping debug info the main binary size is 44.77 MB

Copying resources to /swift/wasm-test/swiftwasm-examples/basics/Bundle/JavaScriptKit_JavaScriptKit.resources
Bundle successfully generated at /swift/wasm-test/swiftwasm-examples/basics/Bundle

…and there it is. the release toolchain cannot produce WebAssembly binaries of reasonable size. only the unstable nightly toolchains can do this.

so i guess the basic tradeoff here is that if you want to do Serious WebAssembly with Swift, you must use nightly toolchains. but we know that we cannot do Serious Development with nightly toolchains, because they cannot compile libraries, and their sourcekit-lsp does not work while editing code. so it seems that Swift on WebAssembly is in a sort of catch-22.

theoretically, we could mitigate this problem by distributing nightlies that are built with noassert, like the release toolchains on Linux, or the Xcode toolchains on macOS. does such a thing exist?

6 Likes

I will repeat that every assertion failure is a potential miscompile, and therefore you cannot do Serious Development with a noassert toolchain if your reason for doing so is to avoid assertions.

6 Likes

A while back I made a toy interpreted programming language called galah, and used SwiftWasm to set up an online playground for it. The galah interpreter Wasm builds to 9.6mb, optimizes down to 4.3mb, and gzips down to 1.6mb! I believe that the secret is avoiding Foundation.

swift build \
    --swift-sdk 5.10-SNAPSHOT-2024-04-09-a-wasm \
    --product GalahWeb
cp .build/debug/GalahWeb.wasm .

wasm-strip GalahWeb.wasm
wasm-opt -Oz GalahWeb.wasm -o GalahWeb.wasm

gzip -9 GalahWeb.wasm

Carton didn't want to work for me at the time, so I just manually pulled the SwiftWasm JS runtime files into my static site and made my own index.html. All of the files for the static website are located in this directory.


Painpoints

To continue the theme of this thread, here are a few pain points I've encountered and failed to resolve.

Invalid WASM binary produced most of the time

In my setup, the wasm binary produced by SwiftPM almost always has the first big chunk of it zeroed out, making it invalid and leading to "bad magic bytes" when I attempt to strip/optimize it. It only seems to produce a valid wasm binary if I first do a release build, which fails due to missing symbols in swiftrt.o, and then switch back to doing a debug build. It then proceeds to fail again if I repeat the exact same debug build. Clean builds also don't help.

Release builds don't work (with the only SDK I've gotten working, see next section)

As mentioned in the previous section, release builds fail with a ton of ld errors referencing swiftrt.o. I understand that this may have been resolved in the time since Swift 5.10, however 5.10 is the only SwiftWasm SDK version that I've gotten working so far.

wasm-ld: error: /Users/stackotter/Library/org.swift.swiftpm/swift-sdks/swift-wasm-5.10-SNAPSHOT-2024-04-09-a-macos_arm64.artifactbundle/5.10-SNAPSHOT-2024-04-09-a-wasm/wasm32-unknown-wasi/swift.xctoolchain/usr/lib/swift_static/wasi/wasm32/swiftrt.o: undefined symbol: __stop_swift5_protocols
wasm-ld: error: /Users/stackotter/Library/org.swift.swiftpm/swift-sdks/swift-wasm-5.10-SNAPSHOT-2024-04-09-a-macos_arm64.artifactbundle/5.10-SNAPSHOT-2024-04-09-a-wasm/wasm32-unknown-wasi/swift.xctoolchain/usr/lib/swift_static/wasi/wasm32/swiftrt.o: undefined symbol: __start_swift5_protocols

Can't get 6.0.2 toolchain working

I've installed the 6.0.2 SwiftWasm SDK to check if it fixes any of the above issues, and can't get it to work at all. When I swap out --swift-sdk 5.10-SNAPSHOT-2024-04-09-a-wasm with --swift-sdk 6.0.2-RELEASE-wasm32-unknown-wasi Swift spits out No available targets are compatible with triple "wasm32-unknown-wasi" and exits basically immediately. The command-line I'm using matches the example I detailed earlier.

2 Likes

and again, you are correct, and yet i believe that building software is about finding solutions to problems, not about persuading myself and others why something is impossible to attempt. so when i find myself with two alternatives,

  1. ship something that might contain miscompiles
  2. ship nothing at all

the question i am really asking myself is whether i am going to give up and use a different language or whether i am going to build something.

now the situation here is not quite that dramatic. instead of shipping nothing at all, you could ship the 17 MB compressed binary. but nobody is going to wait for that to load unless it’s the literal entire app and they are already strongly committed to using that app. if you’re just adding a small WebAssembly component to an existing website, you are Shipping Nothing in spirit, because your visitors have likely already navigated away from the page by the time the WebAssembly has loaded.

as to potential miscompiled binaries, there is really a world of difference between miscompiled server code and miscompiled client code. you should never really be relying on client code to be responsible for anything security-essential, and if the client code misbehaves, the worst thing that can happen is the app just doesn’t work for the user. which is the same thing that would have occurred if you had Shipped Nothing.

1 Like

Leaving aside that I work on an e2ee app where there are very bad consequences for client-side miscompiles, it is definitely worse than Shipping Nothing if the user spends 40 hours with your app and then it deletes all of the user’s data irrecoverably. If you had Shipped Nothing, they would have spent those 40 hours somewhere else.

The developer’s time may be more important than the compiler engineer’s, but the user’s is more important than either. “Swift cannot be used for Serious Development in Web Assembly (yet)” is a reasonable take if the alternative is potential user data loss. (There might always be bugs in software, so it’s always a possibility, but “I found a bug but it doesn’t seem to affect my use case” is pretty damn cavalier for “Serious Development”.)

4 Likes

well… that’s my point exactly isn’t it?

let’s not go to the extreme of assuming every app has users who are using it for 40 hours without once saving, a lot of apps that turn over a lot of revenue are Calculator-like apps where the visitors land on the page, drag and drop some file, get some output, and immediately bounce.

if the app crashes, they bounce and go to the next Google search result. if the app takes more than 5s to load, they bounce and go to the next Google search result. i imagine that web apps that users are entrusting with irrecoverable data make up a small proportion of all web apps.

1 Like

anyway, getting this thread back on track…
the next thing i am going to aim to do today is:

~ actually get an app that Does Something running in Firefox ~

of course before i build an app i first need to come up with an idea for an app.

:thought_balloon: brainstorming an app

now i work a lot with MongoDB lately, and something i’ve complained about to anyone else who will listen is that is no online BSON inspector known to Google.

Firefox_Screenshot_2025-01-22T00-40-53.010Z

if you install the MongoDB developer tools, you can convert the BSON to Extended JSON, and look at the JSON, but that only really works for very small BSON. for comparison, the BSON on Swiftinit can be many megabytes in size, and contains a lot of BSON-specific structures that are not particularly readable when converted to JSON.

i could write a web app in JavaScript, but parsing BSON with JavaScript is probably going to be slow, and i want this to work with giant BSON blobs like the ones on Swiftinit.

i could parse the BSON server-side, and return the rendered HTML to the user that way, but then i would have to stand up a server which would then become the bottleneck. so this sounds like the type of problem that Swift on WebAssembly could really help with.

:grin: setting up my project

it sounds like Swift on WebAssembly requires a nightly toolchain to get binaries of a reasonable size, so i add a .swift-version to my project, and i figure that since i have already resigned to using a nightly toolchain, i might as well use the most recent nightly toolchain available, so i replace wasm-DEVELOPMENT-SNAPSHOT-2024-10-27-a from the example project with wasm-6.1-SNAPSHOT-2025-01-18-a, which is the most recent one they have listed on GitHub.

i need to use JavaScriptKit to do anything that involves DOM, so my Package.swift looks like this, like the example:

// swift-tools-version:6.0
import PackageDescription

let package:Package = .init(name: "test",
    products: [
    ],
    dependencies: [
        // .package(url: "https://github.com/swiftwasm/carton", from: "1.1.3"),
        .package(path: "../carton"),
        .package(url: "https://github.com/swiftwasm/JavaScriptKit", from: "0.21.0"),
    ],
    targets: [
        .executableTarget(name: "BSONViewer",
            dependencies: [
            .product(name: "JavaScriptKit", package: "JavaScriptKit"),
        ]),
    ])

and in BSONViewer, i’ve kept the placeholder code:

import JavaScriptKit

let document = JSObject.global.document
var div = document.createElement("div")
div.innerHTML = "Hi Barbie!"
_ = document.body.appendChild(div)
$ swift run carton dev --custom-index-page Web/index.html

i get a bunch of Sendable noise, like with the older toolchain, and i also get these, which sound benign at least.

wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/lib_FoundationCollections.a: archive member '_FoundationCollections.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/libFoundation.a: archive member 'Foundation.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/libFoundationEssentials.a: archive member 'FoundationEssentials.autolink' is neither Wasm object file nor LLVM bitcode
wasm-ld: warning: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/lib/swift_static/wasi/libFoundationInternationalization.a: archive member 'FoundationInternationalization.autolink' is neither Wasm object file nor LLVM bitcode

but it Does Work, and i can see that i really can manipulate DOM with JavaScriptKit, which is cool!

:partying_face: do i really need Tokamak UI?

i barely know anything about JavaScriptKit, but at first glance, it does not include an HTML renderer, which is perfect because it would have really sucked if JavaScriptKit forced me to use a particular HTML rendering framework.

i’ve heard about Tokamak UI, but i also know from this thread that @austintatious is doing Cool Stuff with Swift on WebAssembly without using Tokamak at all, so i am very curious: do i really need Tokamak to do Serious WebAssembly?

i take a look at its GitHub README, which is a pretty good README as far as GitHub READMEs go, because it tells me all of the important information i need to know about the framework without going into excessive detail.

  • it tells me that it has a lot of current users (66 on the Discord!), which makes me want to use the framework.

  • it tells me that it is incomplete, which does not make me want to use the framework.

  • it tells me that it’s designed to mimic SwiftUI, which i guess would be a huge plus if i were porting a SwiftUI app, but i am not doing that, so that doesn’t apply to me.

so i imagine the algorithm for choosing Tokamak is: “if you are invested in SwiftUI, then Definitely Yes; otherwise, Explore Your Alternatives.

i scroll down a bit more, and i see that Tokamak does HTML rendering. that confirms that i was hoping for — JavaScriptKit does JavaScript interop, and Tokamak does HTML rendering (along with some “modern” front-end reacty stuff i don’t need just yet.) so this is Very Good. the Layering Makes Sense. neat!

:grin: can i bring my own HTML renderer?

the HTML example on the Tokamak README looks like this:

struct SVGCircle: View {
  var body: some View {
    HTML("svg", ["width": "100", "height": "100"]) {
      HTML("circle", [
        "cx": "50", "cy": "50", "r": "40",
        "stroke": "green", "stroke-width": "4", "fill": "yellow",
      ])
    }
  }
}

not bad, but i would much rather use swift-dom.

the first thing i notice when i go back to my code is that sourcekit-lsp is not online. but i had already been warned about this, and apparently you have to build the project for the host to enable syntax highlighting.

this is annoying but not the worst thing in the world. let’s make some HTML!

let html:HTML = .init
{
    $0[.svg, { $0.width = "100" ; $0.height = "100" }]
    {
        $0[.circle]
        {
            $0.cx = "50"
            $0.cy = "50"
            $0.r = "40"
            $0.stroke = "green"
            $0.stroke_width = "4"
            $0.fill = "yellow"
        }
    }
}

the first thing i try to do is render this HTML to a string, since that’s what it looked like i was doing in the example code. but that doesn’t work.

div.innerHTML = "\(html)"
// Cannot assign value of type 'String' to type 'JSValue'

okay, so we weren’t actually assigning a string to innerHTML, it was going through some kind of ExpressibleByStringLiteral sugar that doesn’t support string interpolations. code completion leads me to this instead:

div.innerHTML = .string("\(html)")

i suspect JavaScriptKit cannot bridge a native Swift String to JavaScript, so there is probably a buffer copy going on here. HTML stores its content as a plain old [UInt8], and provides it through HTML.utf8 so i wonder if there is a way to copy directly to a JavaScript string without converting to String in between. but that is a problem for another day.

:slightly_frowning_face: previewing with carton

i re-run carton dev and reload the browser tab and… nothing loads

in the VSCode terminal, i see this:

open browser failed: Executable xdg-open not found in PATHINFO [org.swiftwasm.carton.dev-server] GET /
INFO [org.swiftwasm.carton.dev-server] GET /dev.js
INFO [org.swiftwasm.carton.dev-server] GET /main.wasm
INFO [org.swiftwasm.carton.dev-server] GET /favicon.ico
INFO [org.swiftwasm.carton.dev-server] GET /JavaScriptKit_JavaScriptKit.resources/Runtime/index.mjs
INFO [org.swiftwasm.carton.dev-server] GET /
INFO [org.swiftwasm.carton.dev-server] GET /dev.js
INFO [org.swiftwasm.carton.dev-server] GET /main.wasm
INFO [org.swiftwasm.carton.dev-server] GET /favicon.ico
INFO [org.swiftwasm.carton.dev-server] GET /JavaScriptKit_JavaScriptKit.resources/Runtime/index.mjs

An error occurred, here's a stack trace for it:

An error occurred, here's a stack trace for it:

i reload the page, and this time it loads successfully. the circle is there!

i would brush off the failed page load as a one-time thing, but as i use carton more and more, i notice it continuing to happen. moreover, sometimes the WebAssembly just doesn’t load at all while Firefox says it’s “transferring data”, forever. it almost feels like there is some kind of browser session state that carton has, because it will only start loading when i open the URL in a Private Browsing Window.

so from now on, i just use Private Browsing to preview WebAssembly with carton.

:thinking: implementing drag-and-drop

let’s try and add some event handlers to that div!

this is Pretty Tough. JavaScriptKit relies almost entirely on @dynamicMemberLookup, which means no code completion.

the API is not intuitive. this does not compile:

div.addEventListener("dragover") 
{
    $0.preventDefault()
}
//  error: Extra trailing closure passed in call

the diagnostic doesn’t give me any leads at all. that function is @dynamicCallable, so there is no signature when i hover over it in VSCode. i jump back over @svanimpe ’s examples, and am lucky enough to come across connect-four/Front-end/Sources/WebUI.swift which has an example of how to call addEventListener:

thank you Steven! now i have this:

_ = div.addEventListener("dragover", JSClosure.init
{
    $0[0].preventDefault()
})

:sunglasses: printing things to the console

i then tried printing a message to the terminal

_ = div.addEventListener("dragover", JSClosure.init
{
    print("DRAGGED")
    $0[0].preventDefault()
})

that didn’t compile, which i didn’t understand at first, because i thought this was a Void-returning function, but it turns out that preventDefault returns a JSValue, which was being used as the closure return type because of Swift’s implicit return syntax. (please, let us not add any more implicit syntax to the language!)

it seems JavaScript does not have Void functions, so we must explicitly return .undefined.

weird at first, but it makes sense and you get used to it over time.

_ = div.addEventListener("dragover", JSClosure.init
{
    print("DRAGGED")
    _ = $0[0].preventDefault()
    return .undefined
})

funny enough, Swift WebAssembly is smart enough to connect Swift.print to console.log, so the print actually worked, even though i didn’t really expect it to. neat!

:cold_sweat: the drop handler

next up is the drop handler!

JavaScriptKit is really hard to use. it has its patterns, which make sense after a while, but it was very unintuitive as someone Completely New to the framework. i often found myself tunneling through the source code for JavaScriptKit to figure out how to do basic things with this library.

_ = div.addEventListener("drop", JSClosure.init
{
    print("DROPPED")
    let event:JSValue = $0[0]
    _ = event.preventDefault()

    guard
    case .object(let data) = event.dataTransfer
    else
    {
        return .undefined
    }

    print(data)
    print(data.files)

    guard data.files.length == 1
    else
    {
        print("Need exactly one file")
        return .undefined
    }

    return .undefined
})

one thing that stumped me for a really long time was how to call functions.

guard
case .object(let file) = data.files[0],
case .object(let promise) = file.bytes()
else
{
    print("Not a file")
    return .undefined
}
Value of optional type '((any ConvertibleToJSValue...) -> JSValue)?' 
must be unwrapped to a value of type 
'(any ConvertibleToJSValue...) -> JSValue'

App.swift(55, 38): Coalesce using '??' to provide a default when the optional value contains 'nil'
App.swift(55, 38): Force-unwrap using '!' to abort execution if the optional value contains 'nil'

…huh?

as it turns out, file.bytes is not a real instance method, it’s a functor thing you have to unwrap with optional chaining, even though this is not required anywhere else.

- case .object(let promise) = file.bytes()
+ case .object(let promise)? = file.bytes?()

🫨 typechecking with JavaScriptKit

typechecking with JavaScriptKit is really, really confusing.

the next few lines i wrote were this:

    promise.then?(JSClosure.init
    {
        guard case .object(let typedArray) = $0
        else
        {
            print("Not bytes")
            return .undefined
        }

        return .undefined
    })

that suddenly caused the code i had written 30 lines above to stop type checking!

/swift/wasm-test/Sources/BSONViewer/App.swift:40:17: error: value of tuple type '()' has no member 'undefined'
38 |     else
39 |     {
40 |         return .undefined
   |                 `- error: value of tuple type '()' has no member 'undefined'
41 |     }

…what? value of tuple type '()' has no member 'undefined'?

of course, the actual problem was i had forgotten JSClosure passes an array of arguments instead of a single argument.

    promise.then?(JSClosure.init
    {
-       guard case .object(let typedArray) = $0
+       guard case .object(let typedArray) = $0[0]

but that had nothing to do with tuples having members undefined, and it was in a completely different scope of the code.

i spent a lot of time trying to figure out what was going wrong here. i also spent some time trying to figure out how to convert a Uint8Array to a [UInt8], but i suppose that is a less common use case.

        let bytes:[UInt8] = JSTypedArray<UInt8>.init(
            unsafelyWrapping: typedArray).withUnsafeBytes
        {
            [UInt8].init($0)
        }

i learned how to do this by searching GitHub for 'Uint8Array' and skimming the source code of JSTypedArray.swift.

:bomb: parsing BSON with Swift WebAssembly

okay, pretty cool! i can get raw bytes from drag and drop files with WebAssembly! now let’s parse some BSON!

i hook up swift-bson to Package.swift, and bind the array to BSON.Document in the app code.

.package(url: "https://github.com/tayloraswift/swift-bson", from: "1.0.0"),
.executableTarget(name: "BSONViewer",
            dependencies: [
            .product(name: "JavaScriptKit", package: "JavaScriptKit"),
            .product(name: "BSON", package: "swift-bson"),
            .product(name: "HTML", package: "swift-dom"),
        ]),
        let bson:BSON.Document = .init(bytes: bytes[...])

i build it on the host, so far so good!

$ swift build
Building for debugging...
/swift/wasm-test/Sources/BSONViewer/App.swift:78:13: warning: immutable value 'bson' was never used; consider replacing with '_' or removing it
76 |         }
77 | 
78 |         let bson:BSON.Document = .init(bytes: bytes[...])
   |             `- warning: immutable value 'bson' was never used; consider replacing with '_' or removing it
79 | 
80 |         return .undefined
/swift/wasm-test/Sources/BSONViewer/App.swift:78:13: warning: immutable value 'bson' was never used; consider replacing with '_' or removing it
76 |         }
77 | 
78 |         let bson:BSON.Document = .init(bytes: bytes[...])
   |             `- warning: immutable value 'bson' was never used; consider replacing with '_' or removing it
79 | 
80 |         return .undefined
[12/12] Linking BSONViewer
Build complete! (1.73s)

now let’s build for WebAssembly…

$ swift run carton dev --custom-index-page Web/index.html
Building for debugging...
[1/1] Write swift-version--75355AAB4E86B17C.txt
Build of product 'carton' complete! (0.76s)
- checking Swift compiler path: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/bin/swift
Inferring basic settings...
- swift executable: /home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/bin/swift
Swift version 6.1-dev (LLVM 38a3fc37c5f5e7a, Swift eedae18ae4df60a)
Target: x86_64-unknown-linux-gnu
Running "/home/ubuntu/.carton/sdk/wasm-6.1-SNAPSHOT-2025-01-18-a/usr/bin/swift" "package" "--triple" "wasm32-unknown-wasi" "--scratch-path" "/swift/wasm-test/.build/carton" "--disable-sandbox" "plugin" "carton-dev" "--custom-index-page" "Web/index.html" "--pid" "170487"
Building for debugging...
[1/1] Write swift-version-5F410D2014D93A3.txt
Build of product 'carton-frontend' complete! (1.09s)
Building "BSONViewer"
Building for debugging...
[0/16] Write sources
[8/22] Write swift-version-5F410D2014D93A3.txt
...
[175/207] Compiling BSONDecoding BSON.TracingDecoder.swift
error: compile command failed due to signal 6 (use -v to see invocation)
Failed to reconstruct type for $sSY_s8SendablepSS8RawValueSYRts_XPXmTD
Original type:
(existential_metatype_type thick
  (protocol_composition_type
    (parameterized_protocol_type
      (base=protocol_type decl="Swift.(file).RawRepresentable")
      (struct_type decl="Swift.(file).String"))
    (protocol_type decl="Swift.(file).Sendable")))
Please submit a bug report (https://swift.org/contributing/#reporting-bugs) and include the crash backtrace.
Pass '-Xfrontend -disable-round-trip-debug-types' to disable this assertion.
Please submit a bug report (https://swift.org/contributing/#reporting-bugs) and include the crash backtrace.
Stack dump:
0.      Program arguments: ...
1.      Swift version 6.1-dev (LLVM 38a3fc37c5f5e7a, Swift eedae18ae4df60a)
2.      Compiling with the current language version
3.      While evaluating request IRGenRequest(IR Generation for file "/swift/wasm-test/.build/carton/checkouts/swift-bson/Sources/BSONDecoding/Decoding/BSON.KeyspaceError.swift")
4.      While emitting IR SIL function "@$s7BSONABI4BSONO12BSONDecodingE13KeyspaceErrorV5spaceSY_s8SendablepSS8RawValueSYRts_XPXpvg".
 for getter for space (at /swift/wasm-test/.build/carton/checkouts/swift-bson/Sources/BSONDecoding/Decoding/BSON.KeyspaceError.swift:15:13)
Stack dump without symbol names (ensure you have llvm-symbolizer in your PATH or set the environment var `LLVM_SYMBOLIZER_PATH` to point to it):
0  swift-frontend 0x000060c2a8fd7157
1  swift-frontend 0x000060c2a8fd4d0e
2  swift-frontend 0x000060c2a8fd77fa
3  libc.so.6      0x0000733ce7045320
4  libc.so.6      0x0000733ce709eb1c pthread_kill + 284
5  libc.so.6      0x0000733ce704526e gsignal + 30
6  libc.so.6      0x0000733ce70288ff abort + 223
7  swift-frontend 0x000060c2a211f51e
8  swift-frontend 0x000060c2a211f8bd
9  swift-frontend 0x000060c2a21166a2
10 swift-frontend 0x000060c2a2117282
11 swift-frontend 0x000060c2a2134b45
12 swift-frontend 0x000060c2a2133f2f
13 swift-frontend 0x000060c2a1fd23ff
14 swift-frontend 0x000060c2a1e4ccc9
15 swift-frontend 0x000060c2a1ebde33
16 swift-frontend 0x000060c2a1ebdd89
17 swift-frontend 0x000060c2a1e56e8f
18 swift-frontend 0x000060c2a1e4f50e
19 swift-frontend 0x000060c2a19d6b79
20 swift-frontend 0x000060c2a19d2812
21 swift-frontend 0x000060c2a19d154b
22 swift-frontend 0x000060c2a19e1ff5
23 swift-frontend 0x000060c2a19d43f5
24 swift-frontend 0x000060c2a19d34ac
25 swift-frontend 0x000060c2a17ab803
26 libc.so.6      0x0000733ce702a1ca
27 libc.so.6      0x0000733ce702a28b __libc_start_main + 139
28 swift-frontend 0x000060c2a17aa885

well, we were always going to run into this sooner or later. kids, this is why we don’t use nightly toolchains!

:smiling_face_with_tear: marty, we have to go back!!!

okay, so wasm-6.1-SNAPSHOT-2025-01-18-a was always going to be a gamble, and we can’t use 6.0.2-RELEASE in production, but for curiosity’s sake, i at least want to know if my code does the thing i’m expecting it to.

so i switch my .swift-version back to 6.0.2-RELEASE. i add a couple lines to dump the BSON to the console:

        let bson:BSON.Document = .init(bytes: bytes[...])
        print(bytes.count)
        print("\(bson)")

i drag and drop a random BSON file i had lying around, and boom! it shows up!

:thinking: conclusions

we are so close. so close. i have learned how to run Swift Code that depends on Real Swift Libraries directly in Firefox. this is really cool!

but the binaries are quite large. they take a very long time to download, which severely limits their practical applications.

these problems can be mitigated in theory by using experimental Swift compiler features, but this fails when you try to use Swift on WebAssembly to build anything bigger than a minimal test project.

i am very interested in hearing from anyone using Swift on WebAssembly in production who has figured out a way to work around the frequent compiler crashes.

11 Likes

To mitigate some of the difficulties with types in JavaScriptKit, you could try using WebAPIKit. I was unfortunately never able to get it to a fully finished state but it should provide “type-safe” access to all the symbols defined by the various web specifications. You can start off with globalThis from import DOM which has all the global variables in JS available like document. The methods are all properly typed so you can use things like trailing closures. There’s still no documentation so it works best if you have MDN open in another tab while using it, though. (A neat extension would be to scrape the intro paragraphs from there to generate doc comments!)

4 Likes

i isolated the compiler crash to this reproducer

var x:any (Sequence<String> & Sendable).Type

nice, i’ll take a look at that

2 Likes

@taylorswift not sure if you've seen it, but I was exploring this space as well a while ago and posted about it here:

I agree that going embedded is a tall order for now, but the elementary-dom package should also work just fine with carton and "full swift". I think it could be a nicer path to happiness rather than trying to get Tokamak in "lean web" shape.

so, "for your consideration" ; )

1 Like

IMO, it is much more realistic to expect that Embedded Swift gains features that you need (if you highlight those as required for your use cases and file corresponding issues at swiftlang/swift/issues) than to expect non-embedded binaries to get significantly smaller. In fact, at least in the context of Wasm, I suggest reading "Embedded mode" as "small binaries mode" and adopting it as such.

In the non-embedded mode, try using FoundationEssentials instead of Foundation, or avoid using any Foundation modules altogether if FoundationEssentials brings in too much for your use case. Annotating/refining swift-foundation and swift-corelibs-foundation codebases for compatibility with Embedded Swift also seems to be a goal worth pursuing.

Starting with Swift 6.1, I'd also recommend package authors interested in small binaries to consider providing separate package traits, e.g. a Foundation-less trait, and maybe an embedded trait, or utilize @_unavailableInEmbedded API annotation, for users who aren't satisfied with sizes of binaries produced with default traits.

5 Likes

I absolutely agree that "embedded swift" should be the focus for small wasm binaries (eg: browser apps) - shipping a heavy runtime in your binary feels so wrong once you realize how little you need of it.

however, IMHO, as it stands, utilizing package traits hardly moves the needle towards this goal. there are still so many rough edges... off the top of my head:

  • SwiftPM still cannot really link a binary in embedded (with versioned dependencies)
  • linking @_cdecl and @_extern symbols from dependencies does not work
  • string handling intricacies not ideal (unicode support)
  • no good solution for Codable (web apps without easy handling of JSON? what are we even doing....)

And more generally, I think relying on every package to carefully offer an "embedded" trait is not exactly optimal for adoption/portability either, especially when a lot of code would "just work" anyway.

1 Like