I'd like to announce two crates I've been working to integrate Rust components into LinuxCNC. You can find them at linuxcnc-hal (high-level interface) and linuxcnc-hal-sys (low-level interface). The rest of this post is a getting started tutorial, so follow along if you have a cool idea for a custom bit of CNC hardware and an itch to write the interface in Rust!
LinuxCNC is a popular, open source machine controller. It's very powerful and is an excellent way to get into CNC on an absolute budget. LinuxCNC is also expandable with custom components called HAL components or HAL comps.
Up to now, most components are written in C or Python. Pretty unsafe or slow - these languages don't make the best choice for a potentially heavy/dangerous/fast machine! With its safety and low to zero overhead, Rust is a prime replacement for writing HAL comps, however there doesn't seem to be any way to integrate Rust with LinuxCNC... until now!
A primer on HAL components
LinuxCNC has the concept of a HAL (Hardware Abstraction Layer). The HAL allows (among other things) custom components to be loaded at startup which hook into the HAL using virtual "pin"s. These pins allow comps to take inputs from LinuxCNC and provide outputs to it. A HAL component is often used to communicate with custom hardware such as VFDs, toolchangers or information readouts.
A HAL component is a normal binary with a standard
main() function, however there are certain
component-specific actions that must be taken to hook it into LinuxCNC. The two crates mentioned
above (linuxcnc-hal and
linuxcnc-hal-sys) facilitate this linking in Rust.
Let's write a basic component with one input and one output pin to get a feel for the interface. Set
up a binary project and add
linuxcnc-hal as a dependency:
You might need to install cargo-edit if
cargo addisn't found:
cargo install --force cargo-edit
The code described below should go into
First, we need some imports. Both the input and output are going to accept
f32 values, so we'll
HalPinF64. Other types
use ; use ;
main(), we need to change the signature slightly as the component is going to return a
Result for better error handling. Replace any existing
main() with this:
Now let's populate
main(). First, a
HalComponentBuilder needs to be created. This is the first
thing that should happen as it registers the component with LinuxCNC's HAL and gets an ID assigned
to it. In this example, we'll register a component called
hello-comp. This is equivalent to a call
hal_init() in C land.
let mut builder = new?;
Next, we need some pins. We'll create one input and one output called
respectively. These are the HAL pin names you'll see in LinuxCNC.
let input_1 = builder.?; let output_1 = builder.?;
Once the pins are registered, the builder can be consumed into a complete HAL component. This step
signals to LinuxCNC that the component has registered all pins and is ready to use. It's important
to note that LinuxCNC will hang if
ready() isn't called. In C land, you'd call
at this point.
let comp = builder.ready?;
HAL pins can't be registered after
ready() is called in a component, and we take care of that with
Rust's type system and the type state pattern. The
builder.ready() call above consumes the builder into a
HalComponent which doesn't have any way
to register pins on it, preventing invalid operation order. In a C HAL component, an error is logged
if you do register a pin after the
ready() call and probably lands you in an invalid state. It's
obviously a lot safer to capture that error at compile time, so we'll let Rust's rich type system
help out here. Yay Rust!
Anyway, now we're initialised, let's start the main control loop of the component. We'll check
comp.should_exit() every iteration to see if a Unix signal has been received from LinuxCNC asking
the component to quit.
let start = now; while !comp.should_exit
This simple loop sets the
output-1 pin's value to the current elapsed time in seconds every
iteration. It also prints the value of
input-1 to the console. If you hook
input-1 up to, say,
spindle.0.speed-out pin in a
.hal file, you should see the spindle RPM value printed to the
So as not to lag LinuxCNC up too much, we call
poll/update the pins every second. This delay can be made shorter for a more responsive component.
An emergency stop control would need to respond much faster than 1 second!
The above loop will cycle forever until a
SIGKILL signal is received.
LinuxCNC will send one of these on shutdown, ending the loop. Once the loop is over the component
should exit with a success status. Add the following to the bottom of
At this point in C, you'd have to remember to call
hal_exit. Not too hard, but it's possible to
linuxcnc-hal there's a custom
Drop impl for
HalComponent. It automatically calls
hal_exit for you when it goes out of scope at the end of the program. Rust lets us be lazy and
If the above is difficult to follow, here's the complete and final
use ; use ;
With any luck, running
cargo build will successfully compile your new component.
cargo run is
unlikely to work, as the raw
hal_* functions aren't defined by the crate. They're defined in
liblinuxcnchal.so which is loaded by LinuxCNC.
Loading into LinuxCNC
I'm assuming some basic knowledge of how the LinuxCNC HAL is configured in this section. If you need a primer, the official LinuxCNC docs are a good place to start. I'm also assuming you have a LinuxCNC config ready to go, either a simulator or a real machine.
Disclaimer: I do not accept responsibility for any loss or injury caused by following this tutorial with real hardware. CNC can be dangerous. Use your common sense.
cargo build will create a binary artifact at
./target/debug/hello-comp. We now need to
hook this into LinuxCNC and wire some pins up using a
To create a basic
.hal config, add the following to
hello-comp.hal in the crate root.
loadusr /full/path/to/comp/target/debug/hello-comp net input-1 spindle.0.speed-out hello-comp.input-1
The config first loads the component into userspace. We're not creating a realtime component here.
It then creates a net called
spindle.0.speed-out to the input pin
Add this to your machine
.ini config like so:
HALFILE = /full/path/to/comp/hello-comp.hal
You might get some pin conflicts on startup, so if LinuxCNC crashes check the error messages and fix
any conflicting pin errors in your
Start LinuxCNC as you normally would. Note that you'll need to run it from the console to see the
input pin value printed. With luck, you should see
pins.output-1 under the
Pins tab of Machine -> Hal Meter. Select
pins.output-1 and you should see the current
elapsed time in seconds.
Rust's type safety and ownership rules mean we get a safe, easy to use interface to the underlying LinuxCNC HAL methods.
There's still a lot more left to do on
linuxcnc-hal. There isn't a way to register signals yet,
and a lot of the
autogenerated methods don't
have a safe wrapper yet. That said, hopefully the pins-only interface is useful to see where the
pain points/bugs are in
Thanks for reading, and happy machining!
If you found this article or
linuxcnc-hal itself useful, please consider
becoming a Github sponsor. Every little helps!