Over the past few years, we’ve heard over and over again about an exodus from Mac to Windows in the creative community. Here at Astropad, we’ve kept a close eye on this shift, knowing that Windows would be a big part of our company’s future. Our flagship products — Astropad Studio and Luna Display — primarily serve the creative pro-market. Both products run on our low-latency, high-fidelity video streaming technology called Liquid that was designed to meet the demands of professional illustrators, animators, and photographers.
When we were first building our products, we used the tools we were most comfortable with, like Objective-C and the Cocoa APIs. This allowed us to move quickly, launch Astropad 1.0, and establish product-market fit in a relatively short period of time. But as we grew, we made the mistake of doubling down on Objective-C, and we pushed off the Windows effort because it created a catch-22 situation of engineering hurdles. Our Liquid engine was tightly wrapped around the Apple ecosystem, and the thought of unraveling ourselves was hard to imagine.
Comparing languages for our core tech
The Liquid platform is comprised of 40,000 lines of Objective-C/C++ code that handles networking, GPU acceleration, and video compression — so ideally we wanted to reuse that battle-tested code. Before taking the plunge with Rust, we tried to port our core Liquid platform to Windows via WinObjC and GNUStep. We had the most success with WinObjC, a project started by Microsoft to make it easier to port iOS apps to UWP (the Windows Universal Platform). We were able to run some of our internal networking tools, but we ultimately abandoned WinObjC after it became clear that Microsoft was no longer supporting it and no major apps were using it (outside of Djay).
We also tried to use the GNUStep project, an open-source implementation of the Cocoa libraries, to bootstrap an environment for us to build our code. However, GNUStep on Windows proved to be brittle at best and it lacked support for the latest compiler and key components like libdispatch that we’d have to add in ourselves. We also realized that we’d have to repeat this process for each platform we wanted to run Liquid on. Plus, Objective-C is an old language that lacks many modern language features, making it a poor choice for the long term.
So if we weren’t going to build on Objective-C, then what? The obvious choice was C++. But C++ is a complex language famous for how dangerous it can be. In the words of Bjarne Stroustrop, the creator of the language: “In C++ it’s harder to shoot yourself in the foot, but when you do, you blow off your whole leg.” Our engineering team wasn’t too excited about writing C++, so we needed an alternative.
What would our ideal language look like for rewriting our Liquid platform? Since a key feature of Liquid is its speed and low latency, performance and the lack of a garbage collector were an absolute must. We also needed broad cross-platform support, as we want to run Liquid everywhere, from iOS, Android, Mac, Windows, Linux, and even embedded devices. This hypothetical language would need C interoperability with no performance overhead so that we could integrate with existing components and dip into raw memory access when performance demanded it. Ideally, this language would also be safer than C/C++, helping us to prevent common programming errors at compile time. It turns out there is a language that does all that and more…
The Rust trifecta
Rust is a language designed to provide C-like performance with a type system that maintains memory and concurrency safety. Rust checks all the boxes for what we need in a language, including high performance, a broad range of platform support, and easy C interop with close to no performance penalties. It also doesn’t use a garbage collector, but instead relies on the type system and an ownership model to manage memory.
When comparing speed and safety across programming languages, Rust has outshined other options. It almost seemed too good to be true! We repeatedly asked ourselves, where’s the catch? But since we’ve started diving in, Rust has lived up to the hype and we’ve already seen how the Rust “trifecta” of safety, speed, and concurrency has changed our code for the better. Today, every member of our engineering team is learning the Rust programming language to bring our code cross-platform. Let’s take a closer look at what makes Rust special:
High-performing, complex languages, such as C and C++, don’t leave much room for mistakes when coding. What’s more, the errors that can cause crashes or memory leaks often don’t reveal themselves until runtime. Rust is able to help prevent these problems from cropping up by using it’s incredibly powerful type system during compile time:
The Rust compiler (called rustc) is very intelligent and can detect all these problems while compiling your code, thereby guaranteeing memory safety during execution. This is done by the compiler by retaining complete control over memory layout, without needing the runtime burden of garbage collection. In addition, its safety also implies much less possibilities for security breaches. The Rust compiler forces you to get a lot of things right at compile-time, which is the least expensive place to identify and fix bugs. [ From Rust Essentials ]
Common errors in C like dangling pointers, double frees, leaks, and NULL pointers, can’t happen thanks to the Rust compiler. To put it simply, Rust’s type system casts a wide safety net to prevent you from common memory errors.
As we’ve seen with other languages, there is usually a push and pull between performance and safety in programming:
Fast, safe, easy to write—pick any two. That’s been the state of software development for a good long time now. Languages that emphasize convenience and safety tend to be slow (like Python). Languages that emphasize performance tend to be difficult to work with and easy to blow off your feet with (like C and C++). [ From InfoWorld ]
Rust doesn’t force us to sacrifice on any front. We can compile higher-performing code with results that are comparable to C and C++, with a lower risk of crashes. Already we’ve seen parts of our networking code increase in speed by 13%. Our existing Objective-C code was no performance slouch either, it had been highly tuned over many years. Not only is our new Rust code simpler and cross-platform, but it’s faster to boot!
Another huge win for us with Rust is its emphasis on correct concurrency and thread-safety. Since Liquid is optimized for latency, our system is designed to be highly concurrent. In fact, the Liquid video encoder splits incoming frames into hundreds of pieces to maximize the use of multi-core systems.
Rust uses both the ownership system (what’s used for memory management) and the type system to prevent common threading errors at compile time. For example, data races, which can be subtle and ruthlessly difficult to reproduce, are entirely eliminated by the Rust compiler — it won’t let you run code that has them. Here’s an example: say you have a variable that your main thread passes off to a background thread to use. Meanwhile, your main thread hangs on to the variable and modifies it while the background thread does its work. In C/C++ programs this would compile and run but would be incorrect (at least without a mutex). With Rust, the above example would simply fail to compile, indicating incorrect use of the variable.
Concurrency was designed into Rust from early on, and it shows. With a powerful type system and useful primitives like channels, Rust empowers us to build concurrent code that is much safer and more stable — not to mention preventing some of the common threading pitfalls and making us more confident in writing concurrent code.
The business impacts of going cross-platform
Another important factor in bringing our code cross-platform was transitioning to something that would last long into the future. The fact that Rust has been adopted by numerous large companies, including Dropbox, Facebook, Google, Amazon, and Microsoft, means it’s likely not going anywhere. Rust is the first potential successor to C/ C++ for system programming to gain traction in a long time, and our hope is that it can be our systems programming language for the next 10+ years!
And unlike languages like Swift (Apple) and Kotlin (Google with Jetbrains) that have an interest in keeping their languages tied to their own platforms, Rust has a cross-platform fidelity. It was originally built and supported by Mozilla, the developer of Firefox — so it’s in Mozilla’s best interest to prioritize cross-platform functionality.
Plus, engineers love Rust! The expressiveness of the language, combined with the raw performance makes for an exciting combination. Our team is having a blast working with Rust; we’re excited to be working with a next-generation language, and other developers agree. Rust has been voted the “most loved programming language” four years in a row on Stack Overflow.
What’s the catch?
There have been some hurdles we’ve run into with Rust, the most prominent being the learning curve. Rust is a sophisticated language that does things differently than other languages (especially the ownership model) so it can take some time to learn how to write idiomatic Rust code. It’s also a young language that’s still developing, with parts still in flux and an arguably slim standard library. The tooling around Rust is still rather immature, and the long compile times can be a drag. Despite all these caveats, we’ve been exceedingly happy with Rust and the benefits far outweigh the negatives.
With Rust, we’ll have a high-performance and portable platform that we can easily run on Mac, iOS, Linux, Android, and Windows. Not only will this drastically expand our potential market size, but we also see many interesting new uses for our Liquid technology that we’ll be able to pursue with our Rust based platform. We’re confident that we’ll finish our Rust journey with stronger code, better products, and an optimistic outlook for Astropad’s future.