Advantages of Rust for Orthanc Plugin Development
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 withOrthancPluginFreeMemoryBuffer()
.
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 🎉