Why Rust in medical imaging? A reflection on modern technologies for next generation systems

Modern digital imaging technologies have brought forth a plethora of innovative and immensely useful applications. From the moment a medical study arrives, doctors can easily and conveniently visualize high quality imaging series on their workstations, and even through mobile devices while off the premises of their healthcare centers. Federated picture archive and communication systems (PACS) can collect the full history of a patient from multiple registries and send the patient a summary by email. In a matter of seconds, a computer-assisted diagnosis system provides insights over a clinical case and a second opinion to aid the decision support process through artificial intelligence.

All the while, the demand for medical imaging has increased overall, as a consequence of an increased supply of acquisition devices, evolved image quality, and aggravated health concerns during the COVID-19 pandemic. Also not restricted to radiology, the surge and acceptance of new imaging modalities has been witnessed over time, such as pathology, these too carrying their own set of requirements and challenges for implementers of medical information systems. 

Under these expectations, there is always an opportunity to evaluate the underlying technology stack which our software is based on, as a continuous effort of delivering quality systems while improving on key characteristics, such as efficiency, performance, stability, and quickness of feature delivery cycles. Deciding on the programming languages to employ in the development of new or existing software has direct consequences on these characteristics. In the next few sections, I will cover the benefits and limitations of some of the technologies often used in medical imaging systems, and explain how Rust can be a good fit for your upcoming project.

 

A pitch for Rust

So why is Rust the focus of this article? While most technologies choose different levels of language complexity and runtime requirements to fulfill their goals, Rust is, for the most part, living evidence of being able to have your cake and eat it too. In many ways, Rust takes the best of several aspects, by fulfilling the following trifecta:

 

Memory safety and concurrency without a garbage collector

Many enterprise systems use Java as the language and base ecosystem. Their dependency on a Java virtual machine provides a uniform approach to software delivery, as compiled packages can be executed on any supported platform. This is rarely a burden, but it is particularly tricky to decouple if necessary. The Java memory model introduces many inefficiencies when working with class values, which add up once the program requires the use of memory in larger quantities. Garbage collection has evolved substantially over time, but it can still be a source of undesirable overheads, not only in Java, but in other runtimes which depend on it, such as Go and JavaScript.

In contrast, and on a level more commonly found in embedded systems and older software, C++ still is a very relevant programming language in medical imaging. Its core principles enable writing powerful abstractions with minimal overhead, making it suitable for operations which require high performance and resource efficiency. Its descriptive power is also a window to serious problems, as the language does not stop the programmer from writing memory-unsafe code. Undefined behavior is a concept which most programmers do not have to worry about, but in C or C++, it is the reason why a small misstep in a piece of code can be unpredictably catastrophic. In medical imaging systems, an exploited vulnerability from bad memory access could translate to moments of downtime, the provisioning of inaccurate information, and even compromised medical data.

Rust shows how it is possible to attain memory safety without a dynamic garbage collector nor a substantial runtime component, by keeping track of resource scopes at compile time. Any kind of resource, whether it is a vector in memory, a file descriptor, a network socket, or anything else, is automatically managed and released once dropped out of its scope, without introducing non-deterministic overheads.

Thanks to the compiler’s borrow check system, resources can be safely shared to various parts of the program without the risk of introducing inconsistencies, data races, or using resources after they are already freed. The expression “fearless concurrency” is a bit worn, but still describes the experience of concurrent programming in Rust, as all attempts of producing data races are impeded by the compiler, and parallelization of existing code is often an easy task thanks to libraries such as Rayon.

 

Productivity and power without overhead

Rust encompasses many resources that we learned to expect in a programming language ecosystem, namely a standard building tool, linting and formatting tools, and a package manager. Cargo, the standard Rust building tool, is prepared to build projects, maintain projects in several ways through extendable subcommands, and manage dependency trees. As such, working on a Rust software project is usually a very streamlined and uniform experience.

Even just at the language level, you will find powerful constructs such as sum types, pattern matching, closures, type inference, generic programming, and macros. Like in C++, many abstractions created in pure Rust are optimized to the minimum machine code necessary to fulfill the task, as if no abstraction was used at a higher level. This phenomenon is referred to as zero cost abstractions. A powerful language can be leveraged to write great APIs and end user applications in any domain, while taking performance concerns into account.

This creates a contrast between technologies which depend on a heavy runtime such as Java, which not only is memory inefficient, but also lacks many constructs and guarantees often found in other languages, such as tuple types, inline array slicing, const correctness, and mutable references to primitive values. Other useful features such as variable type inference and records were only more recently introduced in the language.

 

Stability without stagnation

The slow adoption of modern iterations of Java is also a potential source of technical debt: it has been estimated that more than 80% of Java software in production still used Java 8 in 2020, a major version released more than 7 years ago. At the time of writing, Java 16 is the latest stable version, with new versions released every six months.

The Rust compiler has new releases every six weeks, which are almost always fully compatible with code written for previous versions. An edition mechanism is also implemented, so that substantial changes to the language (such as introducing new keywords and syntax) can be added to the language under a new edition, without breaking compatibility with components written in another edition. As of today, the editions “2015” and “2018” are in place, with “2021” coming soon.

Although it is portrayed as a modern technology, Rust is certainly not immature in the field of creating production software. Companies big and small have invested in Rust, usually with remarkably positive results. Moreover, some companies already use Rust in diverse tasks related to medical imaging informatics, such as imaging data processing pipelines and voxel renderers for visualization. DICOM-rs, an open source project under active development, aims to become a reference open implementation of the DICOM standard in pure Rust, and to be as practical to use as other well known DICOM frameworks (to name a few: dcm4che in Java; DCMTK and GDCM in C++; pydicom in python). Overall, the assortment of technologies for working with DICOM data might sometimes not feel very mature, but is still ripe for experimentation and full of opportunities.

One other concern often brought up is the atypical learning curve of Rust. Concepts which are generally unique to the language may feel a bit harder to grasp, especially once the compiler starts rejecting code, often related to lifetimes, the borrow checker, or UTF-8 string manipulation, that seems fine at first glance. For instance, attempting to store a value and a reference to that value in the same struct is forbidden and prevented by the compiler, because the mere act of moving the struct would invalidate the reference. As another example, most languages would easily let you fetch the nth character in a string, at the risk of hitting a character boundary or fetching the wrong information in a string with non-ASCII characters, whereas Rust requires you to traverse an iterator of characters. With experience, these obstacles become easier to overcome, and more idiomatic patterns avoiding them are followed earlier during development. During this process, the Rust compiler is equipped with friendly and elucidating error messages, a prime perk also seldom seen in other technologies.

Conclusion

Like any engineering endeavor, the benefits and drawbacks of a particular decision in the project should be determined or estimated, with the technology stack being one of them. When deciding whether to use Rust in an upcoming project, contemplate the following topics:

  • Whether the key business logic is mostly I/O bound, or also CPU bound;
  • Whether your project can face performance issues in the long run as it scales;
  • On what kind of devices the software will effectively run;
  • How experienced in Rust your current development team is, and whether you are willing to invest time and money in training.

You can start learning Rust today through a number of great resources, including the official book available online for free. An assortment of interesting Q&As are also available on Stack Overflow. The Rust community has several venues to look out for as well.

Further reading

 

This piece is by the authorship of Eduardo Pinho (PhD).

Comments are closed.