I have been looking at how to create an iOS app recently, and more particularly, its backend. SwiftUI as a front-end framework these days is quite lovely, but I am not convinced that Swift ecosystem is really good enough to do backend stuff - either on the device, or especially outside it (although Apple is making baby steps with Embedded Swift).
UI on the other hand seems to be the best done with Swift (and notably SwiftUI now). It seems considerably better than Interface Builder based objective C was that I used last time around.
Due to that, I spent some time recently evaluating two contenders that seemed most promising to me.
The mission
The candidates I looked at hard were Go and Rust. I have written quite a bit of Go, and Rust toy program or two. Producing something that does network request in separate thread, and asynchronously returns it back in a callback was pretty easy to write in both languages (not particularly prettily, mind you, as this is only test code).
After that, interfacing it with Swift is what took longer. Here are some notes and code links from that exercise.
Language: Go
I used mobile module - golang.org/x/mobile - Go Packages and its gomobile command - golang.org/x/mobile/cmd/gomobile - Go Packages tool to create the wrapper modules for Swift.
Pros:
- Go interface just out of the box was visible to Swift
- Fast compilation, relatively painless to set up
Cons:
- No customisation options for language bindings, and some of it was bit weird
- e.g. given package Foo, everything exported by it had Foo prefix
- Some types (notably, slices (essentially vectors in other languages)) were not supported
The actual code and more detail about its results: fingon/swift-go-test: Swift <> Go interoperability test app
Language: Rust
Rust seems to have multiple language binding mechanisms (like almost anything else, for that matter). Using Github stars as a measure of popularity (as well as recent release existing), I chose to go with mozilla/uniffi-rs: a multi-language bindings generator for rust.
Pros:
- Highly customisable language bindings
- Bit smaller binary than Go
Cons:
- Highly customisable language bindings
- (and some defaults were bit weird)
- Bit more verbose, and possibly scary semantics (mixing ownership between Rust and Swift)
- Release build took order of magnitude longer, and especially with LTO it took even longer
- The build process was not particularly well documented - I had to come up with swift-rust-test/backend/build.sh at main · fingon/swift-rust-test (as opposed to one command for Go: swift-go-test/backend/Makefile at 748d997001a837f15379320bcbe9c1448255e99d · fingon/swift-go-test)
The actual code and more detail about its results: fingon/swift-rust-test: Swift <> Rust interoperation test app
Actual numbers about build times and size
Non-release Rust builds are quite slow (and large), so they were not considered. So we are comparing Go defaults (with or without symbol stripping) to different Rust release (with or without LTO).
Compilation time from scratch (.xcframework, 3 arches)
Incremental build with both is reasonably fast, but in e.g. CI we may want to do build-from-scratch and the speed of that does matter.
Go | Rust |
---|---|
16s (default) | 82s (release) |
141s (release+LTO) |
Disclaimer: The Rust build process COULD have been sped up to some extent by parallelising some parts of build.sh
- it seems Rust pipeline in general is not particularly good at utilising cores unlike Go which uses all cores pretty much all the time.
Binary size (.xcframework, 3 arches)
Go | Rust |
---|---|
63MB (default) | 89MB (release) |
31MB (stripped symbols) | 19MB (release+LTO) |
Conclusion
For us, speed of development outweighs the somewhat smaller binaries we could get with fully optimised Rust choice. Simpler language is also a plus. So it seems we are going to go with Go.