Check out the latest updates! View News & events

Cosylab Logo Cosylab Logo
  • Solutions
    • Radiation therapy
      Enable the best cancer care, streamline workflows, treat more patients and reduce your development risks and time-to-market with our innovative, integrable software.
      Read more
    • Fusion
      Fusion projects are dynamic environments, and success is measured in milestones. Our experts in control, prototyping, diagnostics and subsystems development can help your project stay on track.
      Read more
    • Quantum
      Bring your quantum system to the market sooner with our control system components and integration while focusing on core innovation! Industrial quality and dependability hardware and firmware are our business.
      Read more
    • Accelerators
      With decades of experience in control systems for all particle accelerator types, we can help you mitigate development risk, shorten delivery time and reduce the total cost of ownership.
      Read more
    • Complex medical devices
      Leverage our vast engineering expertise in developing complex medical software to bring your innovative device to the market and patients sooner.
      Read more
    • Semiconductor
      Gain some breathing space while shortening development cycles with our advanced software and electronics engineering solutions
      Read more
    • Space
      Let us help you develop top-class software systems for your scalable space missions faster, reducing risk and time-to-market in a highly regulated environment.
      Read more
    • Astronomy
      Astronomy projects are increasing in cost and complexity while timelines are shortening. You can count on us to provide well-designed, standards-based software to reduce your project's risk.
      Read more
  • Customer stories
  • Expertise
  • About us
  • Careers
  • Blog
  • News & events
Get in touch
  • sl
  • en
  • zh
  • ja
Get in touch
  • sl
  • en
  • zh
  • ja

Solutions

(121 results)

Search Result Image
Space
Bring your space mission to life with expert engineering force
Search Result Image
Expertise
Bring your space mission to life with expert engineering force
Search Result Image
Some space solution
Bring your space mission to life with expert engineering force

Articles

(21 results)

Search Result Image
Article about space
Bring your space mission to life with expert engineering force
Search Result Image
Article about space
Bring your space mission to life with expert engineering force
Search Result Image
Article about space
Bring your space mission to life with expert engineering force
Search Result Image
Article about space
Bring your space mission to life with expert engineering force
Search Result Image
Article about space
Bring your space mission to life with expert engineering force

Content

(21 results)

Search Result Image
Content about space
Bring your space mission to life with expert engineering force
View all results
Developing programmer team reading computer codes Development We
  1. Homepage
  2. Control System Integration Challenges and Introducing Safe Programming to EPICS

Control System Integration Challenges and Introducing Safe Programming to EPICS

Publish date:
17. June 2025
Category:
Accelerators
Author:
Jure Varlec
About improving programming practices in EPICS by examining the needed changes in the integrator’s mindset, which are not so much technical in nature as social.
Control System Integration Challenges and Introducing Safe Programming to EPICS
Share:
  • Facebook
  • Instagram
  • Twitter

(EPICS, Mindsets and Rust Versus C++)

The push to adopt memory-safe languages instead of C and C++ has intensified in the last decade, driven by both the programming community and governmental directives, with Rust emerging as a viable replacement.

However, there are open questions on how to transition an entire ecosystem to a new language, especially one such as EPICS, where backwards compatibility is a high priority. 

It seems inevitable that introducing a memory-safe language into the core of EPICS is going to be a slow process. In this blog, I will show that modern C++ already allows the programmer to adopt a mindset and practices that increase the memory safety and quality of software, with the advantage of backward compatibility afforded by C++.

I will present several coding examples, ranging from a humble scope guard, managing inter-thread locking, to re-imagining the user-facing API for the aSub EPICS record type. 

The Push for Memory Safety in Programming 

Memory safety refers to programming practices that prevent errors like null pointer dereferences, use-after-free errors, buffer overflows, data corruption and data races—bugs that can crash systems or create security vulnerabilities. In C++, it’s all too easy to make these mistakes, and the consequences can be severe, especially in EPICS systems controlling expensive, high-energy machines.

On the other hand, Rust offers a compelling solution by enforcing memory safety at compile time, eliminating entire classes of bugs without relying on garbage collection, which is not optimal for real-time systems.

However, transitioning EPICS to Rust isn’t straightforward as the controls framework emphasises backward compatibility, and, anyway, the integrator’s role is often to be an integrator first and a coder second. 

Rust’s emergence has sparked a “rewrite the world” trend as major projects, like web browsers, are now being refactored piece by piece. Even the Ubuntu Linux distribution is considering replacing GNU Core Utilities with Rust-based alternatives like uutils/coreutils, and the Linux kernel is slowly incorporating Rust for new device drivers and security-sensitive code. However, C and C++ remain dominant for performance-critical components. 

 

Introducing Rust to EPICS: Opportunities and Challenges

Rust offers a more robust solution for memory safety than C++. Its ownership model, enforced by the borrow checker, prevents null pointers, dangling pointers, and data races at compile time. For EPICS, where device drivers and client libraries are critical, Rust could eliminate entire classes of bugs. However, integrating Rust into EPICS’ C++-centric ecosystem is in its infancy.

Early proofs of concept, like NickeZ/epics-sys, brunoseivam/epics-base-rust, binp-dev/epics-rs, and Araneidae/rust-epics-ca, are mostly defunct, with last commits dating back five to seven years. TODO turn these references into links to GitHu

More recent efforts include agerasev/epics-ca, a Channel Access client library, ChannelFinder/recsync-rs (part of the EPICS directory service), and wtup/epics_gen, a standalone utility from our colleague at Cosylab for converting Excel tables into EPICS databases.

These projects show Rust’s potential, but they’re far from replacing C++ in core components like device support drivers, which requires bridging Rust’s ecosystem with EPICS’ C-based APIs, a non-trivial task.

Rust vs. C++: A Feature Comparison 

To understand Rust’s potential in EPICS, we’ve compiled a comparison table highlighting the benefits and cons of using Rust versus C++ for memory safety: 

Feature  Rust  C++ 
Memory Safety  Enforced by the compiler, prevents common errors like null/dangling pointers, data races, and buffer overflows.  Relies on programmer discipline. Modern C++ (smart pointers, RAII) improves safety, but is still prone to errors without careful use. 
Performance  Comparable to C++, with no runtime overhead for safety checks. Excels in concurrency-heavy tasks.  High performance with fine-grained control, but safety features (e.g., bounds checking) must be manually implemented, risking overhead. 
Learning Curve  Steep due to ownership model and borrow checker. Requires rethinking traditional C++ patterns.  Steep learning due to complex syntax and legacy patterns, but familiar to EPICS developers. Modern C++ simplifies some tasks. 
Ecosystem Maturity  Growing rapidly but less mature for specialised hardware interfaces than C++. Focused on application developers, not integrators.  Mature with extensive libraries tailored for systems programming. Well-integrated with EPICS, but includes unsafe libraries. 
Backward Compatibility  Limited interoperability with C++ code. Requires FFI (Foreign Function Interface) to interface with EPICS’ C APIs.  Excellent compatibility with C and legacy EPICS code, ensuring seamless integration. 
Community Support  Vibrant, inclusive community, but smaller than C++. Popular in new projects.  Large, established community. More resources for troubleshooting. 
Tooling  Ecosystem is organised around Cargo, which is both a build system and package manager.  Mature compilers (gcc, clang) and build systems (CMake), but there are many, and the ecosystem is fragmented. Stable but complex for large projects. Package management is not in scope. 
Adoption in EPICS  Early stage with defunct proofs of concept and limited libraries.  Dominant in EPICS core, device support, and modules. Widely used and tested. 

Rust’s advantages include guaranteed memory safety at compile time and safe concurrency, which are crucial for multi-threaded control systems. However, its less mature ecosystem poses challenges, particularly for EPICS-specific needs. C++, while familiar and performant, requires disciplined use of modern features to mitigate memory safety risks. 

About EPICS

The Experimental Physics and Industrial Control System (EPICS) is a set of open-source software tools, libraries, and applications developed collaboratively and used worldwide to create distributed soft real-time control systems for Big Science systems/machines.

Its importance lies in managing complex, large-scale distributed control systems, ensuring reliability and precision in scientific research and industrial applications. Historically, EPICS has relied heavily on C and C++ due to their performance and low-level control capabilities.

Practical Improvements in EPICS with Modern C++ 

(Enhancing C++ for Safer EPICS Programming) 

Given the advantages of Rust, we believe that EPICS should move towards embracing it. However, using Rust in EPICS base and core modules is problematic. These reasons for this are social in nature, not technical. Explaining them will take another blog post, so ‘we’ll take on this topic in a future blog. 

Instead, let us explore the idea of not adopting Rust. We will see that we can immediately start improving C++ practices in EPICS to enhance safety.  

Modern C++ (C++11 and beyond) offers tools like smart pointers, closures, easier metaprogramming, and other features that enable new and safer approaches, such as functional programming. However, EPICS’s reliance on legacy C APIs and traditional coding patterns often undermines these benefits. We’ll walk through three examples to show how changes in mindset and practice can yield safer code. 

We should start easy, with the basics, which is managing C resources. 

Example 1: Managing Resources with Scope Guards 

Resource management is a common source of errors in C++ when interfacing with C APIs. Consider a typical C-style approach to opening a file: 

cpp 

int fd = open(“/path/to/file”, O_RDONLY); 

… 

if (error) { 

    puts(“Houston, we have a problem …”); 

    close(fd); 

    return 1; 

} 

… 

close(fd); 

return 0; 

Here, we must manually close the file descriptor in every error path, which is prone to errors. Contrast this with the idiomatic C++ way: 

cpp 

std::ifstream file(“/path/to/file”); 

… 

if (error) { 

    puts(“Houston, we have a problem …”); 

    return 1; 

} 

… 

return 0; 

As programmers, it is our responsibility to release the resource when we’re done, and that includes all the cases where we need to finish early, like returning from a function on error.

This is error-prone because in robust software, error paths can be exceedingly complex and can dwarf the “happy path” of execution. Using std::ifstream, the file is automatically closed when the object goes out of scope, thanks to RAII. But when working with C APIs, we often revert to manual management because no RAII wrapper exists. Creating such a wrapper is usually not considered when you are using a C API in anger.

But for simple cases, creating a bespoke wrapper is not really necessary. A small utility called a scope guard can be used instead. It acquires a resource by creating an object that is also the owner of this resource, and when this object is destroyed, the resource is released automatically: 

cpp 

int fd = open(“/path/to/file”, O_RDONLY);
auto fd_guard = make_guard([fd] () { close(fd); });
…
if (error) {
    puts(“Houston, we have a problem …”);
    return 1;
}
…
return 0;

make_guard takes a function as an argument, and in this function, you put all the code that is needed to release the resource — and you’re done. The scope guard automatically closes the file descriptor, simplifying error handling. TODO add a link to an implementation, either ours or something from GitHub. 

For a C++ programmer, the scope guard does not represent a significant change in mindset. What we need to do is change our approach to “lousy” APIs that ‘don’t enforce constraints that they demand and adopt utilities that help with this. It’s about saying “we can do better” instead of blindly using a less-than-ideal API.

Example 2: Safer Thread Synchronisation 

Thread safety is critical in EPICS, where device support often involves multiple threads. The code shown here is adapted from a well-known EPICS module. The code has been in use for many years and works well. It is not shown here because it would be bad, but because it demonstrates the traditional approach to inter-thread locking in C and C++. 

The module contains a big structure representing a device that EPICS needs to talk to. Importantly, there are two threads, one for sending and one for receiving messages, and we need locking to prevent simultaneous access to variables:

cpp 

struct aBigStructure { 

    char* name; 

    char* server; 

    int serverPort; 

    unsigned int inSize; 

    unsigned int outSize; 

    unsigned char* inBuffer; 

    unsigned char* outBuffer; 

    int swapBytes; 

    SOCKET sock; 

    epicsMutexId mutex; 

    epicsMutexId io; 

    epicsTimerId timer; 

    epicsEventId outTrigger; 

    int outputChanged; 

    IOSCANPVT inScanPvt; 

    IOSCANPVT outScanPvt; 

    epicsThreadId sendThread; 

    epicsThreadId recvThread; 

    double recvTimeout; 

    double sendIntervall; 

}; 

This structure uses two mutexes (mutex and io) to protect variables, but it’s unclear which variables each mutex guards. This opacity invites errors, like forgetting to lock a mutex or locking the wrong one. It’s like leaving tools on a table with a note saying, “Press this button when using the sharp ones.

Oh, and please press it again when you are done with them.” Relying on human discipline without some sort of enforcement is often ineffective, and no one would design a personnel protection system this way. Yet this is how programs have been written for a long time, and still are.

<place the two pictures of the work shed here> 

 

One can do better, and not only in C++, but even in C. Let’s reduce the above code example to something smaller: 

C 

struct MyStruct { 

    int someField; 

    int protField1; 

    float otherField; 

    int protField2; 

    epicsMutexId mutex; 

}; 

 

Even in this small case it’s not clear which fields are shared between threads. The programmer needs to know (e.g. from documentation) that protField1 and protField2 may only be accessed with the mutex locked, while the other fields can be read (but not written!) without locking. Contrast this with the following rearrangement of fields: 

struct MyStruct { 

    int someField; 

    float otherField; 

    struct { 

        epicsMutexId mutex; 

        int protField1; 

        int protField2; 

    } shared; 

}; 

First, grouping the shared fields into a substructure together with the mutex makes it clear at a glance what the intent is. Furthermore, given an object obj of type MyStruct, the programmer needs to type obj->shared->protField1 to access a shared field. This additional indirection reminds the programmer that there is a mutex that needs to be locked. 

Because it conveys intent and guides the programmer, this approach is much better. It is also as good as it gets in the C language. In C++, we can improve this further by adding some enforcement of the rules. 

cpp 

struct MyStruct { 

    struct Shared { 

         int protField1 = 0; 

         int protField2 = 0; 

    }; 

    int someField; 

    float otherField; 

    Synchronized sync{Shared{}}; 

}; 

 

The shared fields are now completely hidden inside the sync object as private data. They cannot be accessed directly. The only way to access them is as follows: 

 

 

cpp 

 

MyObject obj; 

obj.someField = 42; 

obj.otherField = 3.14; 

 

{ 

    auto sd = obj.sync.make_guard(); 

    sd->protField1 = 1; 

    sd->protField2 = 0xdeadbeef; 

} 

Here, some fields can be accessed directly, but fields shared between threads are accessed through a guard. The guard created by the synchronisation object locks the mutex on creation and unlocks it on destruction. This enforces correct locking and makes the code’s intent clear. The guard has the arrow operator through which you access the protected fields.

Compared to the scope guard, the synchronisation object is a much bigger change from the traditional mindset. This change is not just about a cool C++ utility, there is a bigger picture that we need to see. Taking a step back and looking beyond mutexes and thread synchronisation, the more general problem is this: in C++, more often than not, the direct, the obvious, the easy way to do something is incorrect.

The standard mutex type is just an example of this: sure, you can put it next to some variables and kindly ask users to lock it before they access them. But the direct and easy way to access those variables is to simply ignore the mutex.

The more elaborate synchronisation object shown above turns this on its head: the protected variables cannot be accessed without going through the synchronisation object. 

Changing the outdated C++ mindset 

We can see that in C++, the obvious way to do something is often not the best way to do something, and in many cases, it’s an outright wrong way to do it — while the right way is not enforced. But we should change our approach so that the right way becomes easy, and the wrong ways more difficult or, preferably, impossible.
 

Readers familiar with the Rust language will notice that the synchronisation object is similar in principle to the Mutex type found in the Rust standard library. This speaks volumes about the difference in mindsets between Rust and (traditional) C++. This is the real reason why Rust is gaining popularity: while most people focus on memory safety and the borrow checker, I firmly believe that it is the modern APIs offered by its standard library and the culture and programming practices such APIs foster in third party libraries that are the big win. 

However, it is important to note that the synchronisation type shown above did not come about as an imitation of the Rust mutex. It was conceived independently by a coworker here at Cosylab. Beyond that, various talks at events such as the CppCon conference offer ample evidence that the C++ community is moving in the direction of better APIs and practices and has been for years. Starting from a clean slate, it is possible nowadays to design much better APIs in C++ than was possible 20 years ago. But one rarely starts with a clean slate, with interoperability and backwards compatibility making it quite difficult to change one’s mindset. 

Let’s take a look at an example of what it takes. 

Example 3: Re-imagining the (un)friendly aSub API 

The EPICS aSub record allows users to inject C or C++ code for custom logic, but its API is notoriously unfriendly. This is a shame, given the intended use of subroutines. In EPICS, one can do a lot with simple declarative code, connecting records in the EPICS database representing functional blocks. When this is not enough, there are several ways to use procedural code. Of these, subroutines (sub record for scalar data, aSub record for array data) are the simplest. They are records that run user-provided code and are what an EPICS user reaches for when the usual declarative logic falls just a little bit short but does not warrant writing a sequence program or even a whole new EPICS module. Users who find themselves in that situation are often not very proficient programmers, and dealing with the complexity of writing a subroutine correctly can be too much. 

The problem is that the interface is too wide. In the user-provided code, the C structure representing the record is available directly, and the API provides no hand-holding. This allows a lot of flexibility because the user-provided subroutine can do pretty much anything. But doing anything correctly requires doing it in a specific way. With an interface this wide, dealing with the complexity of EPICS record internals is punted to the user. A user who just wants to do one little thing that they can’t do using purely declarative logic cannot be happy with such an interface. Even when they have read and understood the documentation, they need to contend with idiosyncrasies such as: 

  • Because inputs and outputs of an aSub record need to be able to connect to arrays of any type and size, they are exposed to C as void*. The programmer declares the type and size in the database definition, and the C code needs to honour that. This means unsafe pointer casts and array access that can easily cause undefined behaviour. Types need to be checked in the initialisation routine to assert that they match between the code and the database, but such checks are often omitted. 
  • Arrays that aSub takes as input and writes as output have a dynamic size. Few people manage to figure this out, fewer still how to use it correctly. The allocation size must be checked at initialisation, and the current size must be checked every time the array is accessed, which is typically not done. 
  • Even when programmers understand how array sizes work, they keep making mistakes because record fields that hold array sizes have inconsistent names. For example, aSub fields describing input A are called NOA and NEA, holding the maximum size and the current size, respectively. Input A links to a waveform record, where the corresponding fields are called NELM and NORD. This swap between the NE- and NO- prefixes is an endless source of mistakes. 
  • Subroutine code is a C function that the programmer writes. Its return type, which represents a status code, must be long. This goes against the longstanding C tradition that status codes are of type int. When a programmer instinctively uses int instead of long, they invoke undefined behaviour. Sometimes, such a program will crash. Other times, it won’t, and the bug can lurk for a long time before some unrelated change causes it to manifest itself. 
  • A numeric return value is insufficient. The status code chooses whether the aSub record should write out its outputs, and whether it should set an alarm status. However, it cannot set an arbitrary alarm status and severity; there is a separate API for that. The status codes are also unnamed, and the programmer needs to know their numeric values. 
  • When a subroutine performs a long computation or an action that can block (such as reading from a file), it must be made asynchronous. This involves a particular dance of setting a record field that is documented. However, the programmer needs to provide a thread where the subroutine code will run and schedule execution there. They can either use the EPICS callback thread pool, their own thread pool, or a dedicated thread. All of these are quite involved. In practice, the only way to reliably write an asynchronous subroutine is to copy-paste from a previous successful implementation. 

 

Let’s take a look at a small example. Here is the database containing a single aSub record named “aSubTestRec”: 

db 

record(aSub, “aSubTestRec”) { 

    field(SNAM, “mainASub”) 

    field(INAM, “mainASubInit”) 

    field(FTA, “USHORT”) 

    field(NOA, 13) 

    field(FTB, “FLOAT”) 

    field(FTD, “STRING”) 

    field(FTVE, “LONG”) 

    field(NOVE, 1300) 

} 

For those not familiar with EPICS, it declares the following. The record will run the C function called “mainASubInit” once when the program is initialised, and the function “mainASub” every time this record is processed to do its thing. There are three input fields, A, B and D, and one output field, E. Input A is of type “USHORT”, which maps to uint16_t and its alias, epicsUInt16. The allocation size for the underlying buffer of input field A is 13 16-bit numbers; while the actual amount of data can vary at runtime, this is the maximum size. Input B is a single floating-point number. Input D is a single string, which in EPICS is equivalent to char[40]. Finally, output E is an array of 1300 integers of type epicsInt32. 

One important thing to note that is not shown here is that the database makes it clear what the purpose of each field is. That’s because these fields are linked to other records by name, so one can immediately see what they represent. These links are not shown in this short example. 

The implementation of the two C functions might look like this 

c 

static long mainASubInit(aSubRecord *rec) { 

    /* Lots of stuff left out */ 

    if (record->fta != DBF_USHORT || record->noa != 13) { 

        return 1; 

    } 

    /* Lots of stuff left out */ 

   return 0; 

} 

 

static long mainASub(aSubRecord *rec) { 

    float num = 3.14; 

    if (strncmp((char*)rec->d, “The Name”, 40)) { 

        num = *(float*)rec->c; 

    } 

 

    for (int i = 0; i < rec->nea; ++i) { 

        num += ((epicsUInt16*)rec->a)[i]; 

    } 

 

    for (int i = 0; i < rec->nea; ++i) { 

        ((epicsInt32*)rec->vale)[i] = -((epicsUInt16*)rec->a)[i]; 

    } 

    rec->neve = rec->nea; 

 

    return 0; 

} 

 

First, the initialisation function goes over all inputs and outputs and verifies that their types and sizes are set the way the rest of the  C code expects. In this example, only the check of input A is shown, the rest follow the same pattern. This is important as future changes could cause the database and C code to differ: it is too easy to change one and forget to change the other. But because the person writing the database and the person writing the C code is usually the same person who can keep everything in their head, these checks are often not done. They are boring to write and actually get in the way during initial development. 

The main routine does three things. First, it checks whether field D contains “The Name”, and if so, it copies field C into the number it is calculating. Then, it iterates over the array in field A and adds each number to the one it is calculating. Lastly, it iterates over A again and copies its values into output E while negating them. The number of elements in output E is set to match the number in input A. 

Note that reading from and writing to input and output fields requires unsafe pointer casts. If there is a type or size mismatch between the database and the C code, bad things will happen. The names of fields are hard to read (data of input A is in rec->a and its size is rec->nea while data of output A is in rec->vala and its size is rec->neva). And the purpose of fields is quite unclear. So, there’s a string in input D, but what does it mean? What are the numbers in input A? What are the number going into output E? 

As an exercise, I tried to re-imagine what the aSub user-facing API might look like if it were designed using modern C++ features. Note that this is just an experiment, and things probably should be done differently if the EPICS community starts going in this direction. The aim of this experiment is to see what’s possible and to have something concrete to discuss. 

First, I want the C++ side to be just as declarative as the database side. This allows the code for checking the consistency between the database and C++ to be generated automatically. It also allows the API to offer a narrower interface for the user so that it’s harder to make mistakes. We’ll see what that looks like later. For now, let’s study the record interface definition that the user provides using the new API: 

cpp 

static constexpr auto mainASub = beginDefinition() 

    .useInput(‘D’, “name”_c, Type::String, FieldSize::scalar()) 

    .useInput(‘B’, “reading”_c, Type::Float32, FieldSize::scalar()) 

    .useInput(‘A’, “params”_c, Type::UInt16, FieldSize::precisely(13)) 

    .useOutput(‘E’, “numbers”_c, Type::UInt32, FieldSize::atMost(1300)); 

At first glance, this looks pretty much like repeating the declarations in the database file. That is the point: when something in the database changes, it is straightforward to also update it here, in one place. However, there is additional information provided. The first thing is that the database field names (A, B, C, …) are associated with arbitrary names given by the programmer. A well-chosen name communicates what the field represents, and referring to data by name in the code makes the code much more readable. 

Another addition is that specification of the field size uses the terms “scalar”, “precisely”, and “at most”. Down below, these all refer to the same number: the allocation size of the underlying buffer. However, from above, there are important distinctions. There is a semantic difference between a scalar and an array of one element. Also, an array of a fixed size is treated differently by the programmer than one that is expected to change size. Declaring this here makes things not just more readable but also allows the implementation of the API to insert appropriate size checks and offer a different interface for scalars and arrays. 

Note that the information about the subroutine is put together using the builder pattern and stored in an object that is declared as constexpr. This makes the information available at compile time. It is best to make as many checks as possible at compile time instead of runtime because it guarantees that errors will be caught and fixed, as opposed to hoping that the developer is doing sufficient testing. 

And now, let’s look at what the subroutine code looks like. There’s just the subroutine itself; the initialisation function does not need to be provided anymore because it is automatically generated. 

cpp 

Result myASub(auto rec) { 

    float num = 3.14; 

    if (rec.input(“name”_c) == “The Name”) { 

        num = rec.input(“reading”_c); 

    } 

 

    for (auto p: rec.input(“params”_c)) { 

        num += p; 

    } 

 

    using std::ranges::views::transform; 

    rec.output(“numbers”_c) = rec.input(“params”_c) | transform(negate); 

 

    return { 

        .processOutputs = true, 

        .severity = epicsSevMinor, 

        .alarm = epicsAlarmSoft, 

        .message = “Param out of bounds”, 

    }; 

} 

This code is equivalent to the C version above. It does the same things. But note that there are no pointer casts anywhere. All types are correctly typecast by the API itself. The floating point number from input C and the string from input D are returned as values, not as pointers. The string is a C++ object similar to std::string_view that is much richer than a char*. The arrays in input A and output E are also not returned as pointers, but as C++ objects similar to std::span that allow idiomatic iteration. With the C++20 version of the standard which supports ranges, one can even use the pipe (‘|’) operator to transform values, similar to the Unix shell. 

Importantly, the fields are referred to by their names, not letters, which makes things more readable. Attentive readers will note the peculiar syntax for these names, the _c appended to the double quotes. This syntax creates strings that are compile-time constants. The expression rec.input(“name”_c) is evaluated entirely at compile time. The rec object, which represents the instance of the aSub record, is a complicated type that contains all the information needed to do so. 

As for the return value of this function, it is a structure that allows specifying everything needed for the support code to put the record in the appropriate state when errors occur. In case of success, a default-constructed value can be returned, so things remain as simple as returning a zero in the old API. In case of an error, this API make it simpler to do things that in the old API are cumbersome. For example, printing errors to the console with throttling to ensure that fast-repeating errors don’t spam the console is possible but tedious. Here, the new API can take care of it automatically. 

Creating an asynchronous subroutine can be made trivial. We just take the subroutine declaration and add a single property: 

cpp 

static constexpr auto mainASub = beginDefinition() 

    .useInput(‘D’, “name”_c, Type::String, FieldSize::scalar()) 

    .useInput(‘B’, “reading”_c, Type::Float32, FieldSize::scalar()) 

    .useInput(‘A’, “params”_c, Type::UInt16, FieldSize::precisely(13)) 

    .useOutput(‘E’, “numbers”_c, Type::UInt32, FieldSize::atMost(1300)) 

    .setExecutionMode(ExecMode::DedicatedThread); 

That’s it, we are done. The subroutine will run asynchronously in a thread created just for this instance of the record, all taken care of by the API. Similarly, specifying ExecMode::EpicsCallback would schedule the subroutine to run in the callback thread pool. This is as easy as it can be. 

This redesign makes the API intuitive, reducing errors and the need for extensive documentation, and showcases using modern programming practices in C++.

A New Mindset, Improving Practices
(A Safer Future) 

The new aSub API I have shown above exists as a proof of concept. I have implemented most of the features shown, albeit by taking various shortcuts. The goal was to find out whether creating a modern interface on top of old EPICS interfaces is feasible. The answer is “yes”. 

The API prototype is implemented as a separate EPICS module which provides the new façade without modifying the underlying EPICS libraries. This shows the path forward for adoption of modern programming practices in EPICS. It starts with separate modules such as this; the first appropriate targets for modernisation are the APIs commonly used by EPICS users (instead of developers contributing to EPICS core).

These are, roughly in order of difficulty, subroutines, state machines, and device support. Putting modern interfaces into standalone modules is important for two reasons. First, it allows experimentation with the interfaces without affecting EPICS core.  

Second, it allows use of the latest C++ standard, which is not acceptable for EPICS core itself. This would make these modules unusable for many existing facilities because they are stuck with older versions of C++ compilers. But that’s ok because they lose no functionality, while new facilities can benefit from using the safer and more ergonomic interfaces. In due time, more and more facilities will have modern compilers, and the use of these modules will spread. After many years, older compilers will be deprecated entirely, and even EPICS core will be able to adopt modern C++. 

There is precedent for this process. The pvxs module that implements the PV Access protocol of EPICS was written using the C++11 standard. It could not have evolved as part of the EPICS base libraries because of lack of compatibility with older compilers. Today, this module is slated for imminent inclusion into EPICS base; older compilers which do not understand C++11 will no longer be supported. 

The caveat with this path is that C++ can never be fully memory-safe, and modern utilities (e.g., Synchronised) aren’t standard, requiring custom implementations. It is also, as I have found while implementing the prototype of the new aSub API, incredibly difficult to design a modern and ergonomic API using C++. Even though modern C++ allows modern practices and patterns, one still has to fight against established practices and fundamental properties of the language which cannot be fixed without breaking backwards compatibility. And C++ is very serious about backwards compatibility, even more so than EPICS. 

However, these difficulties are for the API designer and implementor to contend with. If they do their job well, the users of the API will not need to deal with this complexity. Which is what makes going down this path worthwhile. 

But if we take a step back and look at this proposed path, it’s hard to see a reason why one would want to deal with the difficulties imposed by C++. Could we not walk the same path using Rust? If we start with implementing Rust APIs in separate modules without affecting EPICS base or core modules, wouldn’t the story of maturation and gradual adoption be the same? 

Perhaps. And that’s the catch: it is an open question. For C++, the language and its ecosystem are well understood, and it is virtually guaranteed that any code using C++20 today will be widely usable 15 years from now. With Rust, this is not so clear. As noted in the beginning of this post, the reasons for this deserve a longer explanation that does not belong in this post, which is already quite long. 

But “the future is not clear” is a bad reason not to try something. Given the clear advantages Rust has over C++, I believe we should try designing user-facing APIs with Rust and see what they’re like to use. And I think we should also do it in C++ at the same time. This way, we get to walk both paths and see which of them leads to a brighter future. 

One valid concern is that trying to go both ways might split the community. But personally, I am not afraid of that at all. As long as both paths build on top of the same foundation, they can coexist with little issue. EPICS is already a vibrant community where people try all sorts of things that other people don’t care much for, and it is not poorer for it—quite the contrary.

Together, we can make EPICS even more reliable and ready for the future.

About the Author

Jure Varlec is not only a Tech Lead and Senior Developer but also a free-software enthusiast. He has spent some time working on software packaging for Linux distributions, which has coloured his view of software engineering and coding technology ever since.
Back

The leading provider of cutting-edge expertise, software and electronics for the world’s most advanced systems and devices.

Our expertise
  • Expertise
Solutions
  • Radiation therapy
  • Complex medical solutions
  • Quantum
  • Accelerators
  • Fusion
  • Space
  • Astronomy
Media
  • Blog
  • News & events
About
  • Contact
  • About us
  • Careers
  • linkedin
  • facebook
  • instagram
  • twitter
  • Privacy policy

© 2025 Cosylab. All rights reserved.

We use cookies to personalise content and ads, to provide social media features and to analyse our traffic. We also share information about your use of our site with our social media, advertising and analytics partners who may combine it with other information that you’ve provided to them or that they’ve collected from your use of their services.

This website uses cookies

We use cookies to personalise content and ads, to provide social media features and to analyse our traffic. We also share information about your use of our site with our social media, advertising and analytics partners who may combine it with other information that you’ve provided to them or that they’ve collected from your use of their services. Check our privacy policy

Necessary
Necessary cookies help make a website usable by enabling basic functions like page navigation and access to secure areas of the website. The website cannot function properly without these cookies.
Always active
Statistics
Statistic cookies help website owners to understand how visitors interact with websites by collecting and reporting information anonymously.