Server-side Swift: Making Canopy (1/6)
Recently I built and released Canopy, a service that provides instant push notifications for activity on your GitHub repositories with apps for Mac and iOS.
Both client and server-side are Swift.
The amount of information out there regarding server-side Swift is low, which is not surprising, but I’m here to tell you it’s not that much of a stretch if you already know Swift and also know your way around the macOS command line.
Swift is Great
Swift is not the best language at everything, but it is the best set of trade-offs that make it the best language overall for anything (it supports) right now. Since it was announced the clear and careful design decisions of Swift-Core have impressed me enough to commit to it entirely. I have programmed in many, many languages and often find that the language and its standard library are inconsistently good, Team-Swift give a damn about their language and its standard library at a level beyond anything else out there.
This week I am focusing on some specifics of why Swift was great for the server. Obviously protocol-driven-development, functional programming, optionals, the error-handling model, etc. make Swift great in general, but I won’t call those out specifically. Certainly these things helped make writing Canopy a joy.
There is no JSON
The apps and the server communicate with JSON, but I don’t know or care about that, because it’s
Codable at both ends baby.
Create my struct, transmit, receive my type-safe struct. It feels amazing.
Intrinsic type-safety cross-process with barely any work from me was super comfy and made the 1,000 miles between me and my server seem no different to passing data between view-controllers.
However the lack of dynamism means even adding a single parameter to these structs means versioning my endpoints or making new parameters
Optional. It’s not all roses, but frankly, versioning has worked well and means my code for interpreting incoming data is not a logic bomb of if-statements, it is carefully, separately encapsulated and tested, type-safe endpoints.
The server code “crashed” once in production due to using too many file-descriptors because I had to write my APNs (Apple Push Notification service) code using the (C-library) libcurl (I wrote Swift, not C, another great feature of Swift is being able to interface trivially with C libraries) and I did it wrong because
UnmanagedPointers are hard. So the kernel terminated it.
One crash: Swift is amazing. You have to actively aim to write Swift code that isn’t robust. And of course Swift gives you robust code and high performance.
Great Developer Experience
Xcode is great (well it sucks, but it’s relatively amazing when considering equivalent developer tools), and the Swift Package Manager means the tooling you need for cross platform development comes with the Swift distribution, there is no epic toolchain of a thousand components that may go wrong and be a bitch to install, it’s one binary from the App Store for your Mac and one binary from swift.org for your server.
You can develop and test your server code on your Mac while simultaneously running your client-side app and having them communicate together—just using Xcode.
To do this is currently more clunky than we want, you have to get SwiftPM to generate you an Xcodeproj (
swift package generate-xcodeproj) from a
Package.swift that describes your server-side app, and then you embed that in your
.xcodeproj (drag and drop) for your client-side apps. Xcode knows what to do after this. Doing this also makes it easier to share code between your various server and client apps.
Since SwiftPM doesn’t really support iOS yet you’ll still need Carthage for your iOS app dependencies, but if you make a macOS app you can use the dependencies that SwiftPM fetches if you’re using similar dependencies, which you probably are. This is non-ideal for sure, but it will be better by 2020 (so we’re told–better Xcode integration is coming).
You have to use Xcode (I guess you could probably use AppCode) at this time since pleasant use of Swift absolutely requires auto-completion. Apple acknowledge this is a hindrance to wider adoption however and have recently committed to providing a Language Server Protocol service so other editors and other platforms can have a first-class Swift experience for their developers.
Running your code server-side is as simple as an
rsync followed by
swift run foo. Obviously you should have a better deployment process than this, but I’m trying to demonstrate how simple it is.
The Code Works First Time
One of the many reasons I love Swift is the feeling after writing it that if it compiled it is going to just work. Obviously this is not always the case, but mostly it is, and with Canopy, mostly it was true. The language is strict and makes you write it strictly but in a way that is still an absolute joy to code. And here I am weeks later with only one crash in production and a system that trucks along consuming barely any CPU or memory, but working and working. Reliable, robust, beautiful.
Make your errors
Codable, then send them from server to client. Bliss. With the right choice of stack you can even
throw server-side and have that magically appear in your client’s
Still you often have to write the
Codable implementation manually since you probably are using Swift’s (superb)
enums with associated-values and Swift cannot auto-generate the
Codable implementation for these. So again, not all roses. Sourcery could probably solve this (update: it can).
I have a single enum that defines the server-routes (eg. server.com/foo) and I consume it both in the client and on the server. There’s no chance of typos and usage is type-safe. It’s just another little thing that Swift on both ends can give you.
Most languages you might use server-side have more limited standard libraries than Swift which leads to a rabbit-hole of figuring out which of the dozens of choices you should use for things like parsing JSON, handling unicode and often even networking. Swift has Foundation, which though somewhat incomplete on Linux provides solutions to common problems out the box and its API is 20 years mature and thus 20 years battle-tested. This only applies to the API since Linux Foundation launched mostly empty requiring the community to step up and write it, but writing against an established blueprint has gone quite fast.
Though admittedly you could pick Java here, but I’m focusing on alternatives that are more modern than the (relatively) ancient Java, which as a consequence lacks modern language features that we want.
Foundation has thorough solutions for complicated scenarios like, locales (et. formatting, currencies, locale-aware-string-sorting etc.), dates, timezones, currencies, networking, IPC/XPC etc., etc. It is awesome.
Simple Integration with C Libraries
The holes in Foundation include cryptography (provided by CommonCrypto on macOS, but this is closed source by Apple), but since Swift easily integrates with system libraries like OpenSSL, people quickly stepped up to make such capabilities available to SwiftPM, and thus we have mature cryptography capabilities without anyone having to do the hard task of writing such things from scratch.
And for sure, cryptography is too important to rely on something that isn’t old and battle-tested.
Nothing is all great and anyone who tells you otherwise is being obtuse.
Swift on Linux is not as complete as Swift on macOS. For example Linux
Foundation is incomplete. During development I would experience low uptime because
URLSession has a few
NSNotImplemented fatal-errors that would crop up non-determistically. If you go look at the sources you’ll see Foundation’s network layer for Linux is built on libcurl and it’s incomplete.
I migrated my networking away from Foundation instead using the libraries that the server platform I chose provided (they provide it because Foundation is spotty on Linux).
This is unfortunate, but it’ll be a thing of the past over the next few years and is a golden opportunity to contribute to Swift itself, which I would do in this case but it seems that Apple plan to fix this themselves using the recently released Swift-NIO.
Still, debugging cross platform code where it behaves differently on different platforms is a bitch, and debugging on Linux is a super-bitch. I couldn’t get the debugger to work at all, and just gave up instead using gross
Some Foundation bridging is lacking on Linux (where you would
as NSSomething) and you will only know when you compile on Linux, so that sucks, but it is a fairly rare problem.
Having to write
Codables is a pretty fast exercise in translation; the Swift compiler does most of the work.
The worst parts of Swift on Linux are coming in follow up weeks because they concern build-systems and general Linux woes.
Open Sourcing Canopy
I will open source Canopy (client and server) if my Patreon goals are met.
Next week: server-platform choices.