3 ms·
I've long been a fan of strongly typed languages, but have settled into using dynamic types a bit mostly for ecosystem reasons. Recently I've gotten into embedd
by darksaints 2y ago
I've long been a fan of strongly typed languages, but have settled into using dynamic types a bit mostly for ecosystem reasons. Recently I've gotten into embedded programming and initially my thought was that I would have a much easier time getting into it by learning C and C++ simply because those are pretty well standardized on embedded devices. And it has been so fucking painful. The build systems absolutely suck...cryptic, finicky, and archaic. Small variations in developer environments cause so many errors. The package management is basically non-existent. Maybe you use Boost or a couple other libraries, but mostly you avoid them because the package management is terrible and build systems are even worse. You're basically writing everything from scratch. The thing I overlooked the most though was the type systems. C and C++ are statically typed, but weakly so. And therefore, the thought of using the type system for safety guard rails doesn't exist. You specify types so that programs will compile, and that's it.
In comes Rust. I know the memory management is the thing that sells it, but in an embedded project, I'm simply not doing any dynamic memory allocation, so I didn't think I'd see much benefit. I mostly tried it because Cargo is an amazing build system. But the thing that has sold me on it is the type system. Within my first 5 minutes of porting a magnetic encoder driver, the type system caught an error in my ported code. I used the wrong pin for my SPI MOSI connection to my driver. It absolutely blows me away that the type system knew I was using the wrong pin. Turns out the code I could never get to work in C was broken because I was referencing the wrong pin, and I never knew why. Fifteen minutes later, it caught another error: by sharing the SPI bus, it could identify that there were more than two devices connected, because there were two CS pins declared, and because the device wasn't exclusive, I had to wrap the bus in a refcell for memory safety. Absolutely amazing.
I never thought that embedded programming could be fun, and strong types were what changed my mind.
- sinuhe69 2y agoYeah, I see the same problem with C and C++ in embedding, too. My guess is that people in embedding were working originally in low-level language like assembly where types don’t exist (at least that's what I did with the PIC and older AVR microcontrollers). So when they introduced higher level languages such as C into it, they did it in an assembly-like fashion and not truly in the spirit of high level languages. For example macros are still extensively used for definition of a lot of things, including IO-pins, and they undergo no type checking. C is not a strong -typed language anyway and this mode of thinking and programming continues to spill-over when C++ was introduced. Type is used merely to limit memory allocation and ensure memory alignment, not truly used in its conceptual sense. Of course, the resource constraints and efficiency of the compiled code play a significant role here. So unless a new memory efficient strict-typed language/compiler comes along with a modern mindset, the power of type will not come into play.
- darksaints 2y agoThe last time I used Rust for any sort of project was two years ago, and the code size concerns were one of the first things I thought about when contemplating writing for embedded targets in Rust. Executable sizes were really bad, especially at -O1 optimization levels, but I haven't really seen much of a problem in practice today so maybe they fixed it. From a performance and memory efficiency standpoint, Rust is on par with the C/C++ projects I've worked with. Some things slightly worse, some things slightly better, but nothing egregious in either direction. I will say that the coroutine-based async facilities in Rust's embedded-hal and embassy-rs projects make task-based concurrency so easy to do with bare-metal code that there isn't much need to use an RTOS in many of the situations that a C/C++ project could do. That sort of OS-less concurrency is no doubt possible in C, but I shudder to think what it would look like.
- lelanthran 2y ago> Within my first 5 minutes of porting a magnetic encoder driver, the type system caught an error in my ported code. I used the wrong pin for my SPI MOSI connection to my driver. It absolutely blows me away that the type system knew I was using the wrong pin. Turns out the code I could never get to work in C was broken because I was referencing the wrong pin, and I never knew why. The type system in C allows you declare the pins as enums with literal bitmaps and declare functions that take only those enums to perform logical bitwise operations. Trouble is, the libraries that (probably) came with your system (ESP32, or whatever) have these things defined as macros. Still, you can have this type of type-safety at this level, and get compile-time errors when doing the wrong thing. > Fifteen minutes later, it caught another error: by sharing the SPI bus, it could identify that there were more than two devices connected, because there were two CS pins declared, and because the device wasn't exclusive, I had to wrap the bus in a refcell for memory safety. This is also something that can be done within the C typing system so that you get compile-time errors. > I never thought that embedded programming could be fun, and strong types were what changed my mind. Good for you. You should note, however, that none of these two scenarios you listed are specific to Rust - you could have solved both those issues by leaning on the typing guarantees already in C. A big problem is that using the escape hatches in C to break the typing guarantees are not really frowned upon, so much of the code you see does extra work to break the typing. And then newbies come along, look at the code that does extra work to break the type system, and conclude that the type system can't prevent some class of problems. The reality is that, until and unless you put in the extra effort to break the typing (such as throwing around casted void pointers), you will get quite a lot of compiler errors when you mismatch types in C.
- darksaints 2y agoHow do you write a function in C that restricts its parameters at a type level to a subset of an enum's set of values? I'm aware of how to do that at runtime, but I don't think I've ever seen it done at a type level. Same goes for detecting two different usages of a resource at a type level...got an example? I'm not saying you're wrong, just that my beginner level understanding of C has never seen anything remotely close to that, and from my attempt at googling, its apparent I don't even have the right vocabulary to find it. Admittedly, a lot of this complaint with C/C++ might not be technical but cultural. The pervasive use of macros and templates might have an origins in avoiding some technical painpoint of the languages or compilers, but they have a real effect in how they are used. For example, I didn't know that C had consts until I started reading K&R...real world usages in open source code almost always use define. And the same goes for rust...for example, the extremely restrictive type system of Rust means that a lot of effort has gone into making type errors extremely helpful, so that you don't end up wrapping your whole program in an unsafe block. In C, you are told that you have a type error, but in Rust, you are told why, and given extra context, and in common cases you are pointed towards what you probably wanted.