Skip to main content

Advantages of Rust for Orthanc Plugin Development

· 8 min read
Jennings Zhang
Research Developer @ Boston Children's Hospital
Orthanc Logo

Orthanc is an open-source PACS server written in C++ with an extensive interface for plugin development. Here we announce orthanc_sdk, a library for developing Orthanc plugins in safe and idiomatic Rust. This article explains why Rust is a good language for Orthanc plugin development and the design of the orthanc_sdk crate.

Background

Orthanc is a lightweight and open-source PACS: a database of medical images implementing the Digital Imaging and Communication in Medicine (DICOM) protocol. Plugins for Orthanc extend its features in flexible ways, e.g. to add support for custom S3 storage, or to implement clinical automations by handling on change events.

Orthanc is amazing: it is well-known by most people in the medical imaging and informatics communities. On the other hand, the Rust programming language is not that popular (yet) in our field. But it should be! Rust is best known for its performance. Considering Orthanc's goal of being lightweight and to support low-end hardware, Rust is a great choice for writing Orthanc plugins.

Additionally, Rust emphasizes software reliability. Although this aspect is sometimes overshadowed to users by its impressive performance, many Rust developers would say that the type system is just as important if not more. Rust code is easier to maintain, which is particularly relevant to academia where there is a tendency for projects to be abandoned the moment their authors finish their Ph.D. or postdoc and move on.

In this article I will explain the concepts of orthanc_sdk and how the language features of Rust facilitate safety when interoperating with C interfaces and correctness of API design. Performance, reliability, or easy to develop? Pick three.

Memory Safety

Most third-party Orthanc plugins are developed using Python which, quoting the official documentation, can be preferable to “the more complex C/C++ programming languages.” Python is an interpreted language with automatic memory management. It needs to do a lot of work at runtime (parsing syntax and garbage collection), which explains why it is so slow. Rust does the same work but at compile time instead, leading to its performance.

C/C++ leaves memory management to the responsibility of the programmer. Indeed, the Orthanc plugin SDK documentation often says things like:

answerBody: The target memory buffer (out argument). It must be freed with OrthancPluginFreeMemoryBuffer().

Manual memory management is an easy source of bugs that can crash the program at best, or even security vulnerabilities at worst. Last year, the U.S. White House even published a press release urging the adoption of memory-safe programming languages.

orthanc_sdk wraps internal FFI calls to Orthanc so that memory is freed automatically by implementing the Drop trait.

// RestResponse<D> wraps the output of `OrthancPluginCallRestApi()`.
impl<D> Drop for RestResponse<D> {
fn drop(&mut self) {
// free_memory_buffer is a wrapper for `OrthancPluginFreeMemoryBuffer()`.
unsafe { free_memory_buffer(self.context, self.buffer) }
}
}

Rust's Drop trait is similar in concept to Python context managers (i.e. with block syntax).

with open('file.txt') as f:
data = f.read()

# at the end of the indentation block, f.close() will be called implicitly.

Unlike Python context managers, the Rust compiler can know to call .drop() the instant the variable goes out-of-scope. This eliminates the need for with keyword usage and excessive indentation levels, keeping Rust code clean. It also makes CPU and memory usage more predictable, keeping system resource utilization low while avoiding gc thrash.

API Type-Safety

Orthanc assigns each resource a random UUID. Regardless of whether it's for a patient, study, series, job, etc., Orthanc IDs all look the same. The crate orthanc_api (which is re-exported by orthanc_sdk) distinguishes between PatientId, StudyId, SeriesId, InstanceId, JobId, and QueryId types using the newtype pattern. When making API calls it is impossible to mix up what is what, e.g. the compiler will stop you from trying to call a patients-related API with a JobId. (Instead, it makes it easy and type-safe to get the Patient of a ResourceModification-type job.)

ID types are related to their detailed types via the ResourceId trait, e.g. JobId is associated with the API endpoint /jobs/{id} which is denoted to return type JobInfo.

Static Typing of RequestedTags

DicomResourceId e.g. PatientId, StudyId, SeriesId, and InstanceId, can be retrieved from the Orthanc API with a parameter requested-tags to indicate which DICOM tags to list in the response. For example:

GET /series/801c56c0-9d21d0f9-2f29f815-e9978a5d-4293b5a3?requested-tags=ProtocolName;Modality

Might return:

{
"ExpectedNumberOfInstances": null,
"ID": "801c56c0-9d21d0f9-2f29f815-e9978a5d-4293b5a3",
"Instances": [
"04fa7506-7b8322c5-e54592ac-b451c97a-95538c93",
"0ddacd01-7bb614c0-d5400253-2a2a551e-0d14755c",
"12a50c31-432b0592-65bb9048-5243ce9f-b3c3da0f",
],
"IsStable": true,
"Labels": [],
"LastUpdate": "20250721T173227",
"MainDicomTags": {
"BodyPartExamined": "FOOT",
"ImageOrientationPatient": "0.00000\\1.00000\\0.00000\\0.00000\\0.00000\\-1.00000",
"Manufacturer": "TOSHIBA",
"Modality": "CT",
"ProtocolName": "ANKLE/FOOT 3mm",
"SeriesDate": "19591127",
"SeriesDescription": "Sft Tissue Sagittal 3.0 Sagittal",
"SeriesInstanceUID": "1.2.276.0.7230010.3.1.3.1493828352.1.1753119146.667883",
"SeriesNumber": "9",
"SeriesTime": "095342.746"
},
"ModifiedFrom": "5c5691ae-85ba4fd5-7e58d690-a1394370-2872cd52",
"ParentStudy": "11231bb6-426dbf6b-69897722-7ebab601-bd2c626c",
"RequestedTags": {
"ProtocolName": "ANKLE/FOOT 3mm",
"Modality": "CT"
},
"Status": "Unknown",
"Type": "Series"
}

RequestedTags in the response has a dynamic schema, though we can define a static type for what keys we expect to be present.

use orthanc_sdk::api::DicomClient;
use orthanc_sdk::api::types::{RequestedTags, Series, SeriesId};
use orthanc_sdk::bindings;

fn get_protocol_and_modality(context: *mut bindings::OrthancPluginContext, id: SeriesId) {
let client = DicomClient::new(context);
let series: Series<Details> = client.get(id).unwrap();
let details = series.requested_tags;
println!(
"ProtocolName={:?} and Modality={}",
details.protocol_name, details.modality
);
}

/// `RequestedTags` response type.
#[derive(serde::Deserialize, Debug)]
struct Details {
/// ProtocolName of a series. The tag might not have a value, so it is [Option].
#[serde(rename = "ProtocolName")]
protocol_name: Option<String>,

/// Modality of a series. Suppose in this case we require there to be a value.
#[serde(rename = "Modality")]
modality: String,
}

impl RequestedTags for Details {
fn names() -> &'static [&'static str] {
// specify value of `?requested-tags=<...>` in the URI query
&["ProtocolName", "Modality"]
}
}

In the example above, client.get(id) knows you want ProtocolName and Modality because it is specified by the type Series<Details>.

API Response Data Types

Thanks to Orthanc's OpenAPI specification, it was possible to automatically generate type definitions for most API request and response models as well as an HTTP client library using OpenAPI generator. This library is published as orthanc_client_ogen on crates.io.

Orthanc's OpenAPI specification is incomplete and some response types are untyped. Such is the case for the response from /jobs/{id}. Hand-written type definitions supplementing the automatically generated OpenAPI models are provided in another crate called orthanc_api. Its manually coded models take advantage of serde enum representations to deserialize the complex response body from /jobs/{id} as Rust enums.

Conclusion

orthanc_sdk is still in early development, nonetheless I am excited to share it with the community.

To get started with building an Orthanc plugin, please read through the example. Then:

# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Start Rust project
cargo new --lib my_plugin
cd my_plugin

# Set output library type
cat >> Cargo.toml << EOF
[lib]
crate-type = ["cdylib"]
EOF

# Add dependencies
cargo add orthanc_sdk tracing http
cargo add serde -F derive

# Add code
echo 'mod plugin;' > src/lib.rs
wget -O src/plugin.rs https://github.com/FNNDSC/orthanc-rs/raw/refs/heads/master/example_plugin/src/plugin.rs

# Build Orthanc plugin
cargo build

Congratulations, you made a Rust Orthanc plugin 🎉