TL;DR: We decided to go with 🦀 Rust (not Go or C++) for the Synergy 3 background service (currently written in Node.js), because we believe it will give our customers a better experience. Java wasn't considered.
We're not choosing Rust just because it's Stack Overflow’s most loved language for four years in a row, though most who made the leap to Rust have fallen in love and stayed. We're choosing Rust mainly for it's memory safety benefits. Incidentally, Rust is a bit more green than other languages, and the Rust community is the most welcoming to trans people (thinking about our hiring strategy). If you just want to laugh at funny stuff, scroll to the bottom of this article.
This article is a bit technical, so strap in...
Synergy 3 is made up of three parts:
The service provides auto-discovery (though mDNS), keeps the configuration synchronized between all computers, and will provide other features in future such as 1-click auto update for all computers. I'll discuss the service more later on in this article.
After deciding that the service needs to be written in something with better performance than Node.js, we eventually settled on Rust and Go as possible options (excluding C++ early on based on our lessons from Synergy 2). The team took some time to develop a few small demo projects in both Rust and Go to get a feel for the languages.
Rust and Go are very different languages, so they're hard to compare. Nonetheless, we had a meeting with the dev team, and came up with a spreadsheet-matrix.
The formula and weighting are somewhat arbitrary, but essentially the score on each row is multiplied by the weight in a hidden column, which is then summed at the bottom. So, the further down the list, the less important an aspect is in terms of it being a deciding factor. The sum is then expressed as a fraction of 160 (which is basically a magic number). The point wasn't to create a brilliant equation to find the perfect choice, but simply to have some way of expressing what the out-of-five scores meant overall (which are arbitrary anyway). This will do; it's good enough.
Here's a few definitions of what we mean in our decision making table. A lot of the scoring is very subjective, team-specific, and project-specific. So, those numbers are maybe not useful for making decisions in other projects or with other teams, but it should give you some insight into how we made the decision for Synergy.
We uncovered that memory bugs are our biggest concern, since the Synergy 3 service is a long-running process. We also considered that in 2019, Microsoft uncovered that 70% of their bugs were memory-safety related.
For memory safety, we have a few options. Code written in Node.js and Go could be made memory-safe with enough work, but they're both GC based, which inevitably leads to engineers easily missing memory safety issues, unconsciously relying on the language runtime to clean up after them. C++ on the other hand has no GC, so you have to be more disciplined with memory management, but the language itself isn't going to protect you. You can't blame C++ for memory leaks, only yourself. For example, break encapsulation or mix C pointer arithmetic with C++ and bam, memory bugs... but it's easy done, especially with less experienced developers. How about unique_ptr? Certainly, but you don't have to use it.
Rust, on the other hand, simply doesn't let you be unsafe with memory (unless you leak memory on purpose, of course). This is a double edged sword, because of two reasons:
The Synergy 2 service (where mistakes were made) was written purely in C++, so initially we were deciding between option 1) rewriting the Synergy 3 Node.js service in C++, and option 2) sticking with Node.js for the service.
Node.js is excellent for breaking ground and writing code quickly, so it was great at the prototype stage. That would have been a bit more of a chore in Rust, Go, or C++ since it was changing so much and we weren't totally sure if the code we were writing would be the right solution for our customers. But, we're quite certain about what we have now, so the whole development team feels that it's time to port the code to something that uses less memory and CPU.
The Synergy 3 service background process will be running on a user's desktop for months on end, quietly working away in the background. So, memory bugs result in a horrific user experience (and if the user isn't very technical, they have no idea why their computer has turned into a laggy mess). We actually saw similar memory safety issues in Synergy 2's C++ service, so it's a real problem that we have experienced.
For our product, using Rust instead of Node.js will result in a better user experience.
The service's main function is to provide auto-discovery (we use mDNS for this) for networks that allow this, so you don't need to type IPs. Previously, we used Bonjour in Synergy 1, but took it out because it didn't work very well. You can still enter IPs manually if your network doesn't permit multicast, or if you have different subnets. The service also keeps the configuration synchronized between all of the computers in your setup (so you can change any setting from any computer, unlike Synergy 1). In future, we're planning on using the service to provide other features such as a 1-click update for all computers, so you can update all of your Synergy installations from a single computer (if you want to) instead of doing them individually.
It's easy to think that the background service might be just a simple state management process or just a watchdog for the Core. If only... as it turns out, in it's prototype form, it's currently a really expensive state management process in terms of resources because it is running on Node.js.
Big change... the background service in Synergy 3 now runs on every platform (Windows, macOS, and Linux), and now enables Synergy to work on the macOS login screen (I tested it the other day, and it works really well, if you don't use FileVault).
The new service in Synergy 3 is quite different to the Windows-only service in Synergy 1, which basically has one job; to launch the Core in the correct Windows session (something that Microsoft introduced in Windows Vista, for security). Actually, in Synergy 3 we still have this original background process, but we've called it the daemon. However, we'll be attempting to merge the Windows daemon into the new service so there's only one background process to worry about. Much of what the old daemon does (such as getting Windows session info from Win32) isn't possible in anything but C++, so we'll need to use foreign function interfaces (FFI), otherwise known as an API wrapper, regardless of if we use Rust, Go, or Node.js.
I spoke with around 20 of my most trusted CTO contacts. To keep it objective, each conversation started with something like: "know anything about Rust or Go?"
Here is one of those conversations. His response:
"[Go is] just another C-like language at the end of the day. Rust is another beast entirely. On the basis of anecdotal information from other people I've spoken to, most people like the idea of it but find it extremely difficult to work with. If you ask me I think it will be short-lived for that reason but it's anyone's guess."
I asked him if he thinks that Rust might be short lived because it's harder to work with than Go. To which he replied:
"Well, maybe just hard in general, rather than in comparison to Go explicitly. They are, to my understanding, really quite different in their target market: Rust is, I think, supposed to be a modern replacement for C++ - i.e. something very close to the bare metal; whereas Go is, I think, aimed at a higher level of application. So I think it's really about Rust vs. C++ rather than Rust vs. Go."
Go developers on the Discord Gophers server tend to agree with this point of view; Rust is not really comparable, as it has a totally different purpose.
Interestingly, Rust seems to have a reputation of being hard to work with. Most likely because of the borrow checker and ownership concept, which enforce memory safety. But, existing Rust developers of course have a different view.
"I find rust far easier than C++ to work with."
"The borrow checker specifically can be a pain, but overall, it's far easier."
"Rust trades short term ease of use for long term robustness."
Source: Rust Discord server
Another one of my CTO contacts had an interesting perspective on Rust vs Go. On the topic of choosing between the two...
"Definitely Rust, especially if you’re interested in shipping a binary that just works. Rust is a fantastic dev experience for near C++ type work. So maybe a wee less dev friendly than Node.js - but you can get your DevOps flow nearly identical. Plus you can ship a WASM, which may give you some creative opportunities for things like Chromebooks.
My experience with Go was cool, but really for shipping microservices into environments I control. I was only mildly into the language itself - it felt like it was sniffing in the right direction, but I kinda feel like Rust looked at C++, C#, Node.js, Python and Golang to get inspiration and took most of the best bits - for the language, compiling, targets, ecosystem, community, etc."
Not that it had anything to do with our decision making, but... Interestingly, it turns out that using Rust is more energy efficient than C++ (in some cases) according to a study in 2017 by the University of Minho. It's about three times greener than Go, too. TypeScript is apparently far more energy-hungry (and we're not likely to use pure JavaScript).
I don't really want the green aspect to be the focal point of this post. I added this section purely because I thought it was quite an interesting way to look at Rust. The topic of Rust being a green language seems to cause a bit of annoyance in the Rust community, with some having the view that: talking about energy saving or sustainability in programming is just a marketing trick or some kind of post-rationalization! 🧐
Incidentally, we recently migrated our PHP code to Node.js which uses TypeScript (so, more energy savings there by about 30%). Though, we're actually now considering rewriting some of that code in Rust which would not only result in lower energy use, but lower latency responses too (which would improve the customer experience).
AWS also wrote about sustainability with Rust. Even with the millions of Synergy users who will eventually benefit from Rust, Amazon's case is of course much more impactful than ours. Power consumption is an important topic to consider in data centers, since according to the article, "Worldwide, data centers consume about 200 terawatt hours per year. That’s roughly 1% of all energy consumed on our planet."
However, something worth acknowledging is that our move from Qt to Electron in the GUI code will certainly be more expensive in terms of energy (since Qt uses C++), but the customer experience is important enough to justify using Electron. And, we could well have chosen JavaScript for our Node.js code, though we're not likely to do this due to TypeScript being much more convenient to work with by comparison.
From the Reddit thread, "I haven't understood what rust is for", these quotes are said in jest, but I actually think that like many jokes, they're deeply rooted in truth. So, for the truths that they convey, it's definitely worth including them in this article.
"The original purpose behind Rust was to give people the ability to smugly talk about how unsafe C and C++ are, but it's recently been repurposed as a viable systems level language"
Most of the discussion about Rust tends to lean toward memory safety. And, while I haven't come across anyone in the Rust community actually being smug, I can see how this might happen... I mean, Rust is obviously better for memory safety (oops).
"Rust is a language where you try to convince the compiler that your code is right and end up losing most of the time"
Many developers who are new to Rust tend to struggle at first with ownership, the breakout feature of Rust which removes the need for GC; master ownership, and Rust will be your best friend for memory safety. If you ignore ownership, you'll constantly fight with the compiler.
Also, there's the borrow checker, does many things to ensure memory safety, such as ensuring that all variables are initialized before they are used. It also enforces that you can't move the same value twice, and you can't move a value while it is borrowed.
"Rust is a programming language made for Rust developers."
The uniqueness of the compiler really sets itself apart from any other language, and the learning curve is initially quite steep. So, when you start learning Rust, it can be tough, but once you've got the basics and you really lean into the concepts, the language becomes easy to use.
"Rust is used to calculate the correct band width of stripy pink and white thigh highs for devs."
Let's leave it there shall we.