Thoughts on improving programming practices in
Description: Thoughts on improving programming practices in EPICS Jure Varlec Senior Developer jure.varleccosylab.com EPICS Collaboration Meeting April 7-11, 2025 Rutherford Appleton Laboratory Safety in programming Calls for memory safety are becoming
Related Topics
Download Presentation
"Thoughts on improving programming practices in" is the property of its rightful owner. Permission is granted to download and print the materials on this website for personal, non-commercial use only, and to display it on your personal computer provided you do not modify the materials and that you retain all copyright notices contained in the materials. By downloading content from our website, you accept the terms of this agreement.
Presentation Transcript
slide1. Thoughts on improving programming practices in EPICS Jure Varlec Senior Developer jure.varlec@cosylab.com EPICS Collaboration Meeting
April 7-11, 2025
Rutherford Appleton Laboratory<br>
slide2. Safety in programming Calls for memory safety are becoming ever louder.
Strategies and recommendations of governments follow these trends.
Everybody is advised to avoid C and C++ languages.
But the world is built upon C and C++!
Many languages are considered memory-safe but can't replace C/C++.
The Rust language stands out:
It can replace C/C++ as a systems programming language.
It significantly raises the bar for programming safety.<br>
slide3. The world is being rewritten in Rust! Well, not quite all of it, but a lot:
Numerous hobby projects (re)written wholesale.
Bigger projects (web browsers) rewritten piecewise.
Ubuntu pondering a switch to uutils/coreutils.
Linux kernel incorporating Rust drivers.
EPICS is not quite on that train:
NickeZ/epics-sys, proof of concept, last commit 7 years ago
brunoseivam/epics-base-rust, proof of concept, last commit 6 years ago
binp-dev/epics-rs, archived, last commit 6 years ago
Araneidae/rust-epics-ca, client only, last commit 5 years ago
agerasev/epics-ca, client only, more recent
wtup/epics_gen, standalone Excel → Db library<br>
slide4. Introducing Rust into EPICS Should we do it? Yes, I think so!
It can't magically solve problems, but it is an opportunity to reimagine user-facing APIs.
How do we do it? I don't know!
Dealing with an ecosystem that is oriented towards developers, not integrators.
This is common to all modern programming languages.
This is not the subject if this talk at all ^_^
Can we program in a safer manner without introducing another language?
I think so, but it will require a change of mindset.<br>
slide5. Managing C resources int fd = open("/path/to/file", O_RDONLY);
…
if (error) {
puts("Houston, we have a problem …");
close(fd);
return 1;
}
…
close(fd);
return 0; std::ifstream file("/path/to/file");
…
if (error) {
puts("Houston, we have a problem …");
return 1;
}
…
return 0;<br>
slide6. Scope guard int fd = open("/path/to/file", O_RDONLY);
auto fd_guard = make_guard([fd] () { close(fd); });
…
if (error) {
puts("Houston, we have a problem …");
close(fd);
return 1;
}
…
close(fd);
return 0; Change your mindset!
Don't just accept lousy patterns encouraged by legacy code!<br>
slide7. Traditional approach to mutual exclusion 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;
};<br>
slide8. The traditional approach A better approach<br>
slide9. Synchronized forbids access without locking struct MyObject {
struct Shared {
int protField1 = 0;
int protField2 = 0;
};
int someField;
float otherField;
Synchronized sync{Shared{}};
}; MyObject obj;
obj.someField = 42;
obj.otherField = 3.14;
{
auto sd = obj.sync.make_guard();
sd->protField1 = 1;
sd->protField2 = 0xdeadbeef;
} The "right way" should be the "easy way" or the "only way"!<br>
slide10. The user friendliness of aSub The aSub API is very simple → complexity is punted to the user!
All data is void* → unsafe pointer casts.
Field types and sizes should be checked at initialization. But who does that!?
Dynamic sizing of arrays is confusing: NOA vs. NEA vs. NORD vs. NELM.
It's not even obvious that the size is dynamic and how record support handles it!
Making an asynchronous subroutine is … not trivial.
A subroutine returns a long:
The return value can cause three different things to happen; long is not appropriate.
Most people would use an int, which leads to undefined behavior.<br>
slide11. Making sure DB and C++ match 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)
} 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));<br>
slide12. The subroutine itself 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) | tranform(negate); The wrapper has a complicated type. A lot is evaluated at compile time! Completely equivalent to
num = *(float*) prec->b; Returns a string view Returns a std::span Pipe syntax, just like shell!<br>
slide13. Subroutine itself Result myASub(auto rec) {
...
return {
.processOutputs = true,
.severity = epicsSevMinor,
.alarm = epicsAlarmSoft,
.message = "Param out of bounds",
};
} Much more explicit than a simple long No need to look up stuff from recGbl.h This goes both into IOC console and AMSG<br>
slide14. Making an asynchronous aSub 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);<br>
slide15. What have I learned? Doing things better using C++ is definitely possible …
… but it's really hard: an uphill battle against traditional patterns.
The good part: it does not drag in another toolchain or an unstable ecosystem.
It makes sense to start with user-facing APIs: subroutines, device support, sequencer.
That's where better APIs have the greatest impact.
C++ improvements are virtually guaranteed to eventually be usable in EPICS core.
But it is possible that Rust will eventually stabilize sufficiently as well.
Perhaps it makes sense to try both C++ and Rust wrappers and see what sticks?<br>
slide16. Jure Varlec jure.varlec@cosylab.com<br>
April 7-11, 2025
Rutherford Appleton Laboratory<br>
slide2. Safety in programming Calls for memory safety are becoming ever louder.
Strategies and recommendations of governments follow these trends.
Everybody is advised to avoid C and C++ languages.
But the world is built upon C and C++!
Many languages are considered memory-safe but can't replace C/C++.
The Rust language stands out:
It can replace C/C++ as a systems programming language.
It significantly raises the bar for programming safety.<br>
slide3. The world is being rewritten in Rust! Well, not quite all of it, but a lot:
Numerous hobby projects (re)written wholesale.
Bigger projects (web browsers) rewritten piecewise.
Ubuntu pondering a switch to uutils/coreutils.
Linux kernel incorporating Rust drivers.
EPICS is not quite on that train:
NickeZ/epics-sys, proof of concept, last commit 7 years ago
brunoseivam/epics-base-rust, proof of concept, last commit 6 years ago
binp-dev/epics-rs, archived, last commit 6 years ago
Araneidae/rust-epics-ca, client only, last commit 5 years ago
agerasev/epics-ca, client only, more recent
wtup/epics_gen, standalone Excel → Db library<br>
slide4. Introducing Rust into EPICS Should we do it? Yes, I think so!
It can't magically solve problems, but it is an opportunity to reimagine user-facing APIs.
How do we do it? I don't know!
Dealing with an ecosystem that is oriented towards developers, not integrators.
This is common to all modern programming languages.
This is not the subject if this talk at all ^_^
Can we program in a safer manner without introducing another language?
I think so, but it will require a change of mindset.<br>
slide5. Managing C resources int fd = open("/path/to/file", O_RDONLY);
…
if (error) {
puts("Houston, we have a problem …");
close(fd);
return 1;
}
…
close(fd);
return 0; std::ifstream file("/path/to/file");
…
if (error) {
puts("Houston, we have a problem …");
return 1;
}
…
return 0;<br>
slide6. Scope guard int fd = open("/path/to/file", O_RDONLY);
auto fd_guard = make_guard([fd] () { close(fd); });
…
if (error) {
puts("Houston, we have a problem …");
close(fd);
return 1;
}
…
close(fd);
return 0; Change your mindset!
Don't just accept lousy patterns encouraged by legacy code!<br>
slide7. Traditional approach to mutual exclusion 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;
};<br>
slide8. The traditional approach A better approach<br>
slide9. Synchronized forbids access without locking struct MyObject {
struct Shared {
int protField1 = 0;
int protField2 = 0;
};
int someField;
float otherField;
Synchronized sync{Shared{}};
}; MyObject obj;
obj.someField = 42;
obj.otherField = 3.14;
{
auto sd = obj.sync.make_guard();
sd->protField1 = 1;
sd->protField2 = 0xdeadbeef;
} The "right way" should be the "easy way" or the "only way"!<br>
slide10. The user friendliness of aSub The aSub API is very simple → complexity is punted to the user!
All data is void* → unsafe pointer casts.
Field types and sizes should be checked at initialization. But who does that!?
Dynamic sizing of arrays is confusing: NOA vs. NEA vs. NORD vs. NELM.
It's not even obvious that the size is dynamic and how record support handles it!
Making an asynchronous subroutine is … not trivial.
A subroutine returns a long:
The return value can cause three different things to happen; long is not appropriate.
Most people would use an int, which leads to undefined behavior.<br>
slide11. Making sure DB and C++ match 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)
} 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));<br>
slide12. The subroutine itself 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) | tranform(negate); The wrapper has a complicated type. A lot is evaluated at compile time! Completely equivalent to
num = *(float*) prec->b; Returns a string view Returns a std::span Pipe syntax, just like shell!<br>
slide13. Subroutine itself Result myASub(auto rec) {
...
return {
.processOutputs = true,
.severity = epicsSevMinor,
.alarm = epicsAlarmSoft,
.message = "Param out of bounds",
};
} Much more explicit than a simple long No need to look up stuff from recGbl.h This goes both into IOC console and AMSG<br>
slide14. Making an asynchronous aSub 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);<br>
slide15. What have I learned? Doing things better using C++ is definitely possible …
… but it's really hard: an uphill battle against traditional patterns.
The good part: it does not drag in another toolchain or an unstable ecosystem.
It makes sense to start with user-facing APIs: subroutines, device support, sequencer.
That's where better APIs have the greatest impact.
C++ improvements are virtually guaranteed to eventually be usable in EPICS core.
But it is possible that Rust will eventually stabilize sufficiently as well.
Perhaps it makes sense to try both C++ and Rust wrappers and see what sticks?<br>
slide16. Jure Varlec jure.varlec@cosylab.com<br>