Today, I’ve just finished laying a lot of the groundwork for ferrobot, a Rust FRC robot library (get it? Rust → Iron → “Ferro”…?). I want to take a look at what makes it different from past Rust robot libraries and why I basically flipped the entire scheme for it on its head.

The Others

If C++ can run on the robot, why can’t Rust?

…is what everyone things before starting “FRC in Rust” projects. I’m going to go over how they work and compare them to plain C++ WPILib.

Deploying to the Rio

The Rio runs Linux. When the Rio starts up, it runs a shell script which is located /home/lvuser/deploy directory and runs it. When deploying using WPILib, the shell script gets automatically generated by Gradle (or the Gradle WPILib plugin) and its main function is to either run your C++ code:

/home/lvuser/deploy/MyRobot # Runs the compiled C++ binary

or to run your Java code:

java -jar /home/lvuser/deploy/MyRobot.jar # Runs your "compiled" Java bytecode

What most Rust projects were doing was the following:

  1. Compile your code to the linuxathena target as a binary
  2. Put a runner to your code in the shell script

Congratulations, you have a hello world running on your robot. From here you have two options to actually get motors and stuff running.

Option 1 - Recreating WPILib

Since the projects forewent using WPILib entirely, they basically had to create it again, from scratch, in Rust. This is a lot of work.1 When you’re doing everything entirely on your own, you get access to the HAL,2 and you have to build up your abstractions from scratch.

Pros

  • You don’t have to use WPILib, it’s semantics, etc. Since you get to rebuild your abstractions, you are in control, and you are making your own ecosystem
  • You don’t have to try to get Rust to interface with C++ (See Option 2 for why that’s hard), just C
  • You likely unlock some extra performance
  • You do not have to conform to the 20hz periodic constraint

Cons

  • You do not get any access to libraries built from WPILib.
    • You instead need to build them from scratch in Rust, or
    • Try to get existing vendor dependencies to simultaneously play nice with your very custom code, and pray its not too difficult to bind Rust to their C++.
    • If vendor dependencies require WPILib to run,3 you’re cooked. The whole point of your project was not to use WPILib, but *surprise* you have to use it anyways. You’re basically locked to rebuilding their library in Rust, but sometimes, you can’t do that because not all vendor dependencies are open-source!4
  • One could argue that it’s not field-legal. I could be completely wrong about this, but I’m 80% sure FMS stuff exists in WPILib that are required for the field.

Option 2 - Binding to WPILib

WPILib is written in C++.5 C++ is notoriously difficult for Rust code to bind to properly.6 The reason why is due to the fundamental difference between Rust and C++: C++ is object-oriented, Rust isn’t. People spend so much time trying to get Rust to comply with classes, but the basics for them just don’t exist in the language!

Pros

  • No need to deal with the HAL, high-level abstractions are already provided for you
  • You get access to all WPILib C++ libraries pretty easily. Just bind to them as well!

Cons

  • You are still locked into all of WPILib’s constraints. The 20hz periodic, commands and subsystems—if you did it in C++, you’re doing it now.
  • Rust and C++ is extremely difficult.
  • You are not getting any performance improvements out of this.

My Approach

If Rust was added to the Linux kernel, why can’t it be added to the Robot?

Rust is a C-like language. Binding Rust to C (and vice-versa) is just plain easy compared to using C++. So here was my game plan:

  1. We’re still using WPILib C++ to run code.
  2. Rust gets compiled to a library and will essentially be glued onto the side of the C++ code
  3. A small C-friendly translation layer will sit in between the two, collecting together data from all the devices and sending it to Rust, and gathering things to do with devices and sending it back to be interpreted.7

C is a Subset of C++

Since any valid C code is also valid C++ code, I got away with exporting all of my Rust types and functions to C headers using cbindgen. I was actually able to construct my Rust types in C because of cbindgen!8

No Vec?

Since Rust types are not entirely 1:1 with C types,9 I could not use certain Rust data structures when exporting to C. By far, the biggest problem, was Vec<T>, which is a growable-shrinkable array.

A Vec contains three values:

struct Vec<T> {
	ptr: *mut T, // a raw, C-style pointer to the first element
	len: usize, // the number of valid elements in the array
	cap: usize, // the maximum number of available elements before reallocating
}

You can turn it into these parts with Vec::as_ptr(), Vec::len(), and Vec::capacity() respectively.

What we’re going to do is create specialized versions of Vec<T>, which are just structs that, instead of containing any T, stored one concrete type. Also, there would be a different struct for every type that needed a Vec in both C and Rust.

We also need to consider manual memory management.10 Rust makes this really easy for us, since when a value goes completely out of scope,11 it automatically runs the drop() function on our value, which can contain code to safely destroy the value.12 C does not have these niceties13 however, so we need to expose code that will destroy the Vec that C++ can call.

The End Goal

When I started this project, I didn’t expect it to get anywhere. Now I’m like 65% of the way to implementing a functional swerve drivetrain. The nice part about this library is that it’s going to use Rust’s uom (Units of Measure) library, which will make it incredibly simple to deal with units. WPI Java’s unit library has been a pain in my ass for the longest time, so it’s nice to know at least that problem will be fixed.

I also want to write a proc macro to emulate AdvantageKit features. This should be fairly simple. I hope I won’t have to recreate my own nt4 client though, that would be slightly annoying.

I know one thing for sure though—no matter how far I build this up, there is no way in hell the programming mentor is going to let me use it on the robot. I probably shouldn’t anyways—getting high schoolers with no programming experience to learn Java is annoying enough lol.

Footnotes

  1. [citation needed]

  2. Hardware Abstraction Layer, i.e. the lowest level of interfacing with the I/O on the Rio.

  3. i.e. wpimath, wpiutil, ntcore, etc. Not the HAL, everyone needs the HAL, even you.

  4. e.g. REVLib.Studica, afaik, is not open either.

  5. woah, shocker

  6. There are so many projects dedicated to getting C++ to just play nice. Even Google has their own! The caveat? Every single one of these projects have some features missing.

  7. A hundred or so asterisks should be put here.

  8. Hint: this is difficult

  9. To allow C to understand Rust types, you have to prefix them with #[repr(C)] which just means “forcefully use the C format for storing data types.” Also Rust types has generics, C types do not.

  10. Java/Python users beware

  11. This is tracked at compile-time using the borrow checker, I may make a complete explanation on how the borrow checker works in the future. In the meantime, go RTFM.

  12. When you allocate memory, you ask your operating system for it. Your operating system needs to know when you are no longer using the memory anymore so it can be used by other programs. When you don’t give it back it’s memory, its called a memory leak. The memory your program takes up will continuously grow until it uses all the memory in your computer, or you force-kill it (i.e. “end task” in Task Manager). Some types don’t borrow memory directly from the operating system, and don’t need custom destructors. If you’re interested, look up the differences between stack and heap memory. I may do an explanation on this in the future.

  13. C++ does, however, but we’re only using C-compatible stuff when interoperating.