Embedded Rust vs. C - Stream-lining the Software Development Process
Introduction
Embedded systems drive a wide range of devices, from home appliances to industrial equipment. These systems rely on the coordination between hardware and software, using programming languages optimised for microcontrollers and specialised hardware. The C programming language has been the primary choice for embedded development for many years due to its efficiency and versatility, making it the top choice for embedded engineers.
This article explores the promise of Rust in the realm of embedded systems development, exploring its unique features, how they streamline the development process, and comparing Rust’s approach with the tried-and-trusted methods of C.
C - Still going strong after 50 years
Embedded software development has long been synonymous with the utilisation of the C programming language. Since its inception in the early 1970s, C has been the bedrock upon which countless embedded projects have been built. Its efficiency, portability, and close-to-the-metal control have made it the de facto standard for programming microcontrollers, embedded processors, and other constrained devices.
The endurance of C in the embedded domain can be attributed to several key factors:
In safety-critical systems, a single bug could lead to catastrophic failures
Proven Track Record: Over several decades, C has proven itself in a myriad of embedded applications, from consumer electronics to automotive systems. Its simplicity and versatility have made it a trusted companion for developers navigating the intricacies of hardware interfaces and real-time constraints.
Abundance of Tools and Libraries: The extensive ecosystem surrounding C provides developers with a wealth of tools and libraries tailored specifically for embedded systems. From device drivers to communication protocols, C’s ubiquity ensures that virtually every aspect of embedded development is well-supported.
Performance and Efficiency: At its core, C is a language designed for performance. Its minimal runtime overhead and direct access to hardware make it an ideal choice for resource-constrained environments where every clock cycle counts.
However, despite its longevity and widespread adoption, C is not without its challenges in the realm of embedded systems. The low-level nature of the language leaves room for a wide range of programming issues, from memory leaks to buffer overflows. These vulnerabilities can have severe consequences in safety-critical systems, such as medical devices or automotive control systems, where a single bug could lead to catastrophic failures.
Memory leaks occur when memory that has been assigned during runtime is not unassigned as expected. Each iteration of the code path assigns additional memory, gradually consuming all of the available space until the operation fails.
Buffer overflows occur when a program writes more data into a fixed-size memory area than it was meant to hold. Each pass through the affected code path exceeds the buffer’s limits, overwriting nearby memory and causing data corruption or instability. Over time or with certain inputs, this unintended overflow can lead to crashes or serious security issues.
To mitigate these risks, developers rely on a variety of supplementary tools and standards aimed at ensuring code quality and safety. Static analysis tools, such as Clang Static Analyzer or PC-lint, scan codebases for potential issues before they manifest as runtime errors. Additionally, industry-specific guidelines like MISRA-C and CERT define best practices for writing robust and reliable code.
While these tools serve their purpose in improving the quality of the code base, they come with their own set of challenges. Integrating them into existing workflows can be cumbersome, requiring additional time, training and effort on the part of developers.
In the face of these challenges, developers are increasingly turning to alternative languages to streamline the embedded development workflow. One such language that has garnered attention in recent years is Rust. Born out of a desire to address the shortcomings of existing embedded programming languages, Rust offers a fresh take on embedded development, with a focus on safety, concurrency, and developer productivity.
Introducing Rust
Rust represents a departure from the status quo of embedded systems development, offering a modern alternative to the C language. Developed by Mozilla Research and first released in 2010, Rust was designed with a clear set of goals in mind: to provide developers with the performance and control of low-level languages like C and C++ while eliminating common sources of bugs and vulnerabilities.
Eliminate common sources of bugs and vulnerabilities
At the heart of Rust’s design philosophy lies a commitment to safety and correctness. Unlike C, which allows developers to create unchecked memory accesses and data races, Rust enforces strict rules at compile time to prevent such pitfalls. This is achieved through a combination of innovative language features:
Ownership Model: Rust’s unique ownership model ensures memory safety without the need for garbage collection. Every value in Rust has a single owner, and ownership can be transferred or borrowed through a system of references and lifetimes. This eliminates common sources of bugs like null pointer dereferences and use-after-free errors, leading to more robust and reliable code.
Garbage collection is an automatic memory management process used in many programming languages to free up memory no longer in use. While useful in many software systems, it can cause performance issues in embedded systems due to its unpredictable execution and memory overhead.
Concurrency Guarantees: Whether an embedded system uses an operating system or relies on interrupts, if multiple sections of code must execute concurrently, managing shared states can become very complicated very quickly. Rust addresses this challenge with its ownership and borrowing system. This system ensures that data races and other concurrency bugs are caught at compile time. Developers can therefore write concurrent code with confidence, knowing that it will be free from race conditions and deadlocks.
Memory Safety: Memory safety is vital in modern systems, where a single memory corruption bug can have serious consequences and possibly expose a cybersecurity risk. Rust’s strict type system and static analysis tools provide strong guarantees against common memory-related vulnerabilities, such as buffer overflows and dangling pointers. By catching these issues at compile time, Rust enables developers to write safer and more secure code.
Performance: Despite its emphasis on safety and correctness, Rust compiles down to small and efficient code. Thanks to its zero-cost abstractions and efficient runtime, Rust code can often match or exceed the performance of equivalent C code. This makes Rust a compelling choice for performance-critical embedded applications, where every nanosecond counts.
Developers can write concurrent code with confidence
Deciding whether Rust is the right language for your embedded application
With both Rust and C-based languages to choose from, what factors affect the selection of the right language for your application?
From a technical viewpoint, Rust emerges as a compelling choice for embedded development due to its unique blend of features designed to enhance productivity, safety, and performance. At the heart of Rust’s appeal is its proactive approach to bug detection. Unlike traditional languages like C, Rust identifies potential issues early, often at compile time. This early defect detection mechanism not only saves developers from the time-consuming task of tracing bugs in extensive codebases but also accelerates feature development and customer value delivery.
Rust’s memory safety can play a significant role in issues concerning cybersecurity. Reports commissioned by the White House (1) named Rust as one of the languages developers are encouraged to use to reduce the risks involved with memory access by malicious actors. In an embedded environment this can play a role where all communication channels can become an attack vector. The added cybersecurity of Rust can help prevent access to sensitive data.
Early defect detection has long been shown to reduce remediation costs dramatically as development progresses. Empirical studies report that defects discovered in system testing or later phases can cost orders of magnitude more to repair than those found during requirements or design (2).
Additionally, Rust offers an option to quickly integrate libraries into code using crates (Rust packages). This can allow developers to quickly integrate re-used code into their project, or utilise crates shared by other developers on a service such as crates.io. This can lessen the development time by shortening the time spent writing code for standard operations, such as serialising data, or creating hash maps.
Rust also offers interoperability with C, enabling developers to leverage existing C libraries and frameworks while benefiting from Rust’s advanced features and safety guarantees.
Finally, as mentioned earlier, Rust is designed to be inherently safe, allowing developers to focus on functionality, and is liable to create fewer issues that come to light later during testing. Rust’s inherent safety benefits do not come at the expense of performance compared to C. Developers report seeing no performance decrease, or a small performance increase after porting an application to Rust.
While Rust brings a host of technical advantages to the table, it also presents certain logistical challenges that could make it a less appealing choice for some embedded development projects.
Although installing Rust and Rust packages (crates) is handled cleanly, the initial setup of Rust for a given microcontroller can be a significant hurdle. Commercial chip vendors have been slow to broaden their support offerings beyond C. Most libraries and software development kits (SDKs) that a vendor provides for a microcontroller are written in C, and although Rust can ‘bind’ to C code, configuring these to work with a Rust compiler can be a complex and time-consuming process. Given the lack of vendor support, developers are left to rely on internet community resources, and iterative trial and error to resolve these issues, potentially causing project delays.
Additionally, the Rust ecosystem, though growing, is not as mature as that of C. While it is possible to integrate C SDKs with Rust, doing so can take significant effort. Few vendors provide the native Rust SDKs needed to accelerate adoption of the language, putting the often-demanding task of SDK integration onto the development teams working in Rust.
While the Rust community is supportive and growing, it is still much smaller than the C community, and is fragmented, with only a subset of Rust developers interested in embedded development. This means fewer resources are available for learning Rust and troubleshooting problems, which could pose challenges for teams new to the language.
This highlights the final argument against selecting Rust at present. Language familiarity is generally poor at the moment. C has been around for over 50 years. A senior developer with over 20 years of experience in embedded software development will have spent all that time working with C. The depth and breadth of Rust knowledge within an embedded development team simply won’t match that of C.
Summary
In the realm of embedded development, the choice between Rust and C-based languages can be difficult.
On one hand, Rust offers proactive bug detection, reduced debugging expenses, and inherent safety mechanisms. It allows developers to write high-level code with zero-cost abstractions and provides excellent interoperability with C. These features can lead to more reliable, maintainable, and efficient embedded systems.
On the other hand, the initial setup for Rust can be expensive and time-consuming. The availability of tools, libraries, and SDKs for Rust is still growing, and there are fewer resources available for learning Rust and resolving problems.
Developers’ familiarity with Rust is also lower than with the much more established C. By its nature, this higher level of experience is generally essential to produce safe, reliable applications using C. In contrast, Rust’s inherent safety features mean that less experience with the language is required to develop robust applications.
The decision between Rust and C-based languages ultimately depends on your specific needs and constraints. It’s a balance of the time-saving benefits from Rust’s advanced features and safety guarantees against the penalties of dealing with its steeper learning curve and initial setup challenges.
At DCA, we understand these challenges and can help you navigate them. Our experienced team can provide guidance and support, helping you make the right choices for your project. We’re committed to ensuring that your embedded application is robust, efficient, and secure, regardless of the language chosen.
- White House Office of the National Cyber Director. (2024). Back to the building blocks: A path toward secure and measurable software (Final ONCD Technical Report). The White House. https://bidenwhitehouse.archives.gov/wp-content/uploads/2024/02/Final-ONCD-Technical-Report.pdf
- National Institute of Standards and Technology. (2002). The economic impacts of inadequate infrastructure for software testing (Planning Report 02-3). U.S. Department of Commerce. https://www.nist.gov/system/files/documents/director/planning/report02-3.pdf