At Formlogic, we’re taking a state-of-the-art, automated approach to precision manufacturing. To accomplish this, we use Rust for a wide variety of applications: various internal and external web apps, factory state tracking, orchestration of shop operations, telemetry data collection from our machines, parsing G-Code, and more.
Given the benefits we’ve seen from using Rust in our production environment, we wanted to share a few of both our positive experiences and pain points with anyone who might be on the fence about using Rust for their own projects.
In the age of social media and data analytics, a great deal of modern software development never leaves the digital realm.
One of the coolest things about working at Formlogic is that the systems we are building directly influence real-world, physical processes. There’s something particularly satisfying about being able to put your hands on a beautiful, high-precision machined part that couldn’t have been produced without the code you wrote.
However, physical processes have physical consequences.
Bugs in the code that control large, expensive CNC machines don’t produce a stack trace—they produce a costly stack of damaged metal. Even worse, malfunctions pose a risk of physical harm to anyone working in the vicinity of the renegade machine.
The statefulness of these machines, and of the physical world more generally, means that designing end-to-end integration tests is a much larger burden than it would be for a fully digital system. It simply isn’t feasible to run non-simulated tests with the same frequency as a typical modern CI pipeline.
Because of these challenges, anything we can do to verify the correctness of our systems up-front is a big win.
As software engineers, we always strive to simplify problems through abstraction. However, handling real-world edge cases often involves unique challenges (e.g., time zones, or names).
To produce and ship parts that meet our customers’ exacting requirements, there is a deluge of heterogeneous data to manage. At a bare minimum, we need to track the locations of raw materials, fixtures, and in-process parts; stay on top of calibration schedules and shipping and third-party process timelines; remain attuned to machine precision capabilities, part-specific tolerances, and inspection requirements; and much more.
However, being able to ship parts that meet a spec isn’t enough by itself—we also need to be able to provide detailed records of each step of our process, and there are a variety of different industry-specific traceability and auditability requirements we must meet as a prerequisite to working with the majority of our customers.
Moreover, in the interest of continuous improvement, we collect an ever-expanding set of telemetry data from our machines for data science and machine learning applications.
Ultimately, all of this means that all of our software needs must be designed to accurately model real-world requirements, and our software must be able to evolve as we expand the nature and quantity of data we trace through our system.
Revamping manufacturing is a daunting prospect: Software supply chains in the industry stretch many layers deep, with CNC controllers, CNC macros, CAM software, toolpath postprocessors, and machine simulators all frequently being developed by unrelated parties.
However, there is good reason for this cottage industry: Each component typically has its own quirks that require special care for proper integration, and mistakes can lead to extremely expensive (or even dangerous!) machine crashes. In most cases, third-party software doesn’t have a Stack-Overflow-equivalent, high quality public repository of information. Moreover, due to the irreducible complexity of the problem of designing a manufacturing process along with the extremely high cost of backwards-incompatible API changes, many components of the end-to-end manufacturing pipeline (especially those that interface with hardware) have stagnated for decades with minimal built-in protections.
While one might reflect on the situation and be inclined to just “start from scratch,” this is largely unrealistic. Modern CAD and CAM software has been built on millions of hours worth of highly skilled technical effort; the most popular geometry kernels used by popular CAD packages, for example, have been under active development for over 30 years. For many of the key pieces of software involved, “rolling your own” would be an ambitious enough task to require a standalone company in its own right.
Given this context, building an automated, vertically-integrated factory for precision manufacturing means we need to integrate with this expansive landscape of proprietary 3rd-party hardware and software, while simultaneously developing in-house solutions for the most critical aspects of our process.
While a programming language won’t solve anyone’s problems on its own, we feel that the language features, design choices, and development community behind Rust have helped us navigate many of the challenges described above. In the rest of this article, we’ll describe some of the ways in which we feel that Rust has helped us achieve our software goals.
Benefits of Rust
The type system
One of the reasons we initially chose Rust was its powerful type system and the benefits that brings in an environment where a subtle mistake can result in hundreds of thousands of dollars of damage in mere seconds. As a startup, we need to move quickly, but especially in our domain we also need to move correctly. We feel that Rust helps us do that better.
We’re aware that there is debate about the impact of type systems on software quality and development speed. Even if we can’t justify it with scientific evidence, our experience has led us to believe in the value of an expressive and powerful type system.
Performant by default
Having spent most of my own career as a Python developer, I’ll be the first to say that performance is often more of a reflection of overall system design and knowing how to effectively wield the tools at your disposal than something imposed by a language itself. But sometimes, it’s nice to be able to just write a for-loop and not worry about runtime overhead—especially in a resource-constrained execution environment, whether that’s a microcontroller or just a cheap cloud server. Rust allows for this.
Appropriateness as a systems language
Few languages are as well suited as Rust for spanning such a wide range of contexts, from high-level web service implementation to low-level binary protocols. Rust allows us to use the same language from top to bottom in our stack, with benefits similar to those that a traditional web development shop might see when sharing code between a browser-based frontend and a node backend.
Most of our developers use a Unix-based environment for daily development (Linux or MacOS), and our web server infrastructure is all Linux-based. However, the manufacturing industry at large is tightly coupled to Windows for most software applications, and integration with third-party software and hardware often requires deployment on a Windows-based platform. We’ve had very good experiences developing with Rust on one OS for deployment on another. Even with interpreted languages like Python, the presence of OS-specific behavior and compiled extensions (especially as transitive dependencies) has generally led to more pain than we’ve experienced tackling the same tasks with Rust.
Speaking of other languages, another advantage of Rust is its effectiveness in terms of language interoperability. Despite the somewhat immature Rust library ecosystem, the strong interoperability between Rust and Python, along with the ability to generate bindings to anything with C headers, means that even when we need to rely on major open source libraries, we aren’t forced to work in the least common denominator.
One of the biggest benefits of working in Rust is the official language tooling. We’ve found Cargo in particular to be a huge step forward relative to the arcane and poorly-documented build systems and approaches to dependency management used with most other viable language alternatives. Having Rustfmt, Clippy, and Rust’s approach to testing all built into the language with well-established community standards means that it’s that much easier to establish consensus around coding style and other development practices.
One of the biggest benefits to the high quality language tooling of Rust is that it’s much easier to onboard new users (this being especially important for a nascent language with less widespread adoption). Between Rust’s tooling and high-quality official documentation, we’ve been able to take frontend and Python developers from a complete lack of Rust knowledge to regularly contributing to our Rust codebases with minimal guidance or hand-holding. There are excellent learning materials for the basics of Rust, and the story for training developers to work in a systems language is much better with Rust than with C++ or C.
We’ve also discovered that the people who are writing in Rust tend to be passionate about coding and are keen to try new things. Rust has spawned a vibrant developer community of tinkerers who are eager to think outside the box.
So far, we’ve mostly focused on the benefits of using Rust. In our opinion, the pros more than make up for the cons, but we think it’s only fair to also reflect on some of the pain points we’ve experienced while working with Rust.
Long compilation times
This would make Rust a relatively poor choice for exploratory and iterative workflows such as data science or for the development of new geometry algorithms. We still tend to do most work of that nature in Python, given the ease of working in a notebook-style interpreted environment and the maturity of readily-accessible libraries.
Tracing and verbosity
In Rust, getting access to this level of detail in error messages requires a bit more plumbing out of the box. We’ve had good experiences with tokio’s tracing for logging and anyhow/thiserror for errors, and it should be noted that improvements to this situation are in progress.
Of course, any discussion of the drawbacks of a nascent language like Rust wouldn’t be complete without mentioning shortcomings in the library ecosystem relative to more established languages like C++, Python, etc.
For applications such as machine learning and computational geometry, the Rust ecosystem still has a ways to go before it can match the offerings available from the open source Python or C++ libraries.
That said, this has been surprisingly less of a disadvantage than I expected when we set out on our Rust journey.
While it is true that Rust doesn’t always offer the quantity or quality of libraries available in more established languages, the quality of language interoperability helps substantially reduce this as an issue: We can still rely on more established Python or C++ libraries to do the heavy lifting as necessary, while implementing our core business logic in pure Rust.
Rust allows us to front-load various safety concerns, interoperate with a variety of modern and legacy systems, and be members of a rich community of modern development efforts. Overall, we feel our adoption of Rust has helped us build a solid foundation for the future growth of our software development and our broader engineering efforts.
If using Rust to create next-gen space manufacturing is something you’re interested in, we’re hiring! We’re building out everything from internal tooling websites to automated robots.
Regardless of whether you already have a background in machining, manufacturing, or geometry, if you are a strong developer interested in working on production systems built in Rust, tell us a bit about yourself. We would love to hear why you're awesome.