To quote Grissom from CSI: I've learned that sometimes you can go faster by going slow ;-)
> I think this profession is hard enough as it is.
Exactly, I think so too. Don't think I need another source of annoyance like spending days on digging at obscure segfaults and data races caused by logical mutability mistakes.
Another dimension to it is that the borrows checker rules also apply in most other languages if you want to write valid code. They are just not enforced and you have to rely on your own discipline instead (which humans are bad at).
The problem with unsafe programming in the traditional C style (with its heavy reliance on shared, mutable, possibly aliased pointers) is that it's inherently non-compositional: proving safety or correctness of such a piece of code is a 'global' problem that cannot be meaningfully decomposed or modularized. That's why Rust's choice of explicitly restricting these features to "unsafe" blocks feels intuitively right - the "safe" part of Rust is essentially the part that can be worked with in a highly modular way, where safety is checked 'locally' via a type system. Future versions of Rust may well make some currently-unsafe features slightly easier to use, perhaps even more idiomatic in a way that might appeal to C developers, but some inherent constraints will remain.
Yes, ARM processors are used more and more, but there are really A LOT of embedded systems out there, which are still, for example, 8 bit. As an embedded dev, who is using pretty much everything from 8 to 64 bit, the kind of memory safety errors you talk about I have yet to see with on anything based on (say) a 8051 or PIC. On a system without OS and without dynamic memory allocation, the borrow checker does not buy you much. It's a different story with an ARM processor and an OS (real-time or not does not matter).
Of course Rust has a more powerful type system, but my experience is, that the preferences for a C type type system dominate the embedded world. Not because it is better or simpler, but because most embedded devs actually like low level. They are simply not interested in type driven development (yes it's just their personal taste) and its benefits.
Or let me say it like this. I see two kinds of embedded devs:
- Those who abstract way the problem ("It's all just a big char array.", "It's all just ints.", "It's all just data structures").
- Those who abstract away the machine ("It's all just objects.", "It's all just types.").
Right now, my observations tell me that the first group is still dominating. Time will tell if this is going to change.
As an embedded dev one of the things I love about Rust is the ability to use the type system to enforce the machine's invariants. Some register has a bunch of bit fields? Break it up using a struct, and unlike C's struct bitfield notation you don't have to shift the values around every time.
Rust forces you to check all enum variants, not just in switch statements like C (and that only with the -Wswitch-enum or equivalent warning) and disallows any unlisted value (there's no "default" for an enum).
Those things alone probably aren't enough to switch, but as a machine-focused embedded dev they're certainly nice to have.
But over the time this argument loose fairness in some direction. Today, in 2020, I would feel awful if in my meetings I argued that creating unit tests is meaningless, as this will point bugs in my code that I need to fight with. Or that QA is a bunch of mean people that do not understand how to use my library and only want to point to "bugs" that is more work to me to "fix".
IMHO the borrow checker _is_ this test that I do not want to write, or this QA engineer that exercise the code to find a corner case when I am causing a memory corruption in my not-perfect work.
My approach of learning any language with "difficult syntax" is to treat those as language features rather than a burden.
It is the language bridging the coder to the complexity under the hood (the computer). This comes from Rust's nature of being a low-level PL, and it is really awesome to be able to abstract those minute details so that one can write Rust and "look, it's not that different from JavaScript"
Another analogy would be `gerund` in English. English has the `gerund` feature for its speakers to nounify a verb. Other languages don't have that, some even use the same symbols for both the verb and the noun form.
Both Rust's "difficult syntax" and English's gerund needs only one-time learning session. Once you're fluent on it the sentences come out on their own.
Also many processor manufacturers have hundreds of different models with similar but different port mappings. They often supply C-header files which helps making your software compatible with numerous different processor types. So even while Rust has a macro system, it would need to be adapted by those manufacturers or C will likely stay the more convenient choice for the time being.
https://www.misra.org.uk/Activities/MISRAC/tabid/171/Default...
https://www.autosar.org/fileadmin/user_upload/standards/adap...
https://en.cppreference.com/w/cpp/freestanding
Certifications that Rust has yet to achieve.
Most of the embedded platforms of old will be 16 bit, and as soon as somebody upgrades them to 32 bit (or more), the problem will most likely vanish. Then the next problem arises. Most embedded platforms are governed by strict regulations (mobile phones, medical, automotive, etc) and requires thorough testing. Even if you upgrade your hardware (and if the current, cheap, 16 bit hardware works, why bother ?), you do not want the challenge of rewriting 20-30 years of code in a new language.
In many cases you're looking at 30+ years of development by 50-100 developers, and while the code itself probably hasn't grown by a factor 30, chances are that the system evolved from a small system where core business was understood by "everybody" into a behemoth where only a few developers/architects actually understand everything , and everybody else just understands what their little corner does.
Many people mistakenly think embedded means small, which is often far from the truth. I used to make sortation devices, and the total codebase for a single sortation device would often reach 1GB of C source code. Around 750MB was "common code", but the last 250MB would be customer customization.
A GSM handset in the late 90's had about 350MB C source code, with the majority of it being protocols (GSM, Edge, Bluetooh, SMS/EMS/MMS, WAP). The lower level ones of those have mostly been replaced by hardware these days, only to have the UI take up much more code.
It's essentially the same problem that keeps COBOL in this world. Newer and (probably) better languages exist, but COBOL has a massive codebase, and while "edge" systems are easy to replace, at some point you're left with "core business" which has been running as a patchwork of COBOL programs for 50 years. There's no easy way to do it, and you need to rewrite everything.
So I perfectly know what is doable when one actually cares on how to do stuff properly.
And then there are those Jason demos on how to target C64 with C++.
This sounds completely bonkers, aren't you including a bunch of static assets like images or whatever else?
I dress slowly because I’m in a rush.
"Time is money".
;-).
Edit: Corrected typo.
* Rust's no_std means you can run bare metal with less memory overhead than even newlib based c
* no_std also makes it easier to port to new platforms, as nonportable parts are isolated out of the std lib
* algebraic data types and in particular enums that can carry disjoint associated data reduce the number of error prone magic constants and casts in your code, as well as catching all missing switch cases
* unsafe makes it very easy for you to audit where data races or memory errors are possible, instead of needing to audit an entire c codebase
* The borrow check makes you work more, but in many cases this is justified by it leading you to a less brittle design
The examples, particularly on cliff's blog post, make pretty convincing arguments oo these points imo.
I also wouldn't discount EEs so much. There's plenty of complexity to Verilog and all the other tools they use.
No. no_std means that all standard library functions that involve allocation aren't available. Things like std::collections::{Vec, HashMap} etc. You have access to std::core (https://doc.rust-lang.org/core/index.html), which is pretty useful.
* unsafe makes it very easy for you to audit where data races or memory errors are possible, instead of needing to audit an entire c codebase
I have seen this argument a couple of times before. But I can't make myself buy it.
Because I always have to think about embedded systems in the car industry. Look at this list:https://betterembsw.blogspot.com/2018/09/potentially-deadly-...
The argument is basically "the possible fail places are easily grepable", right? I said in another comment in this thread (jokingly) "time is money". It seems for the devs in the car industry this saying is of utter most importance.
So how does Rust prevent something like this throughout the code base (and those are BIG code bases):
"LOL. I need to get this done. unsafe { ... } "
In some sense, it doesn't. But in another sense, you can't just throw unsafe around things. Unsafe doesn't turn off checks. Unsafe adds new unchecked constructs. This means you can't have written a bunch of code, run into errors, and just toss unsafe {} around it. You'd have to actually re-write it with unsafe constructs in the first place.
You can very much do that, if you want. But it's not a trivial escape hatch.
My point was that, with "unsafe", Rust provides a technical construct which allows to carry over some negative development styles, possible in the C(++) world, without friction (if wanted). And from the list I linked, there seems to be a lot of momentum for this kind of behaviour in certain industries.
You'd have to actually re-write it with unsafe constructs in the first place.
This is what I think would happen, and tried to describe. Someone trying to implement a feature, failing with safe mode and resorting to "unsafe" from scratch, just to get it done.So it stays the way it is, i.e. I can't buy the "possible fail places are easily grepable" argument.
Because embedded software is increasingly networked (BLE, TCP/IP, ZigBee, etc.) and you can continue writing your communication stacks in C and having memory corruption security vulnerabilities or you can suck it up and try something else for once.
> Also in this world all major HALs and SDKs are written in C. The toolchains usually are very fixed, since doing embedded systems is hard...
1) The embedded world has effectively converged on ARM. This means that the toolchain is whatever ARM shoves out and HALs and SDKs will comply or get no traction.
It also means that you can use actual, real software tooling (VSCode, Meson, Ninja, etc.) instead of "Yet Another Broken Vendor IDE". It's soooo compelling that I personally can run rings around some vendor teams. To be fair--this is NOT limited to Rust. VSCode and its ecosystem enables even C programmers to be stupidly more productive. However, the embedded Rust folks seem to rattle the VSCode folks cage quite a bit more than just plain embedded C folks--this keeps the VSCode guys quite a bit more honest about cross-platform support.
2) Anything RISC-V is in flux, and folks like Bryan Cantrill are leading the charge so the toolchain will be forced to accommodate more than "just C".
3) Even if Rust isn't the answer, you should root for its tooling to break the C hegemony. If Rust finally forces C tooling to acknowledge that "Hey, just maybe we should think about playing nicely in the sandbox and have some useful API's instead of telling everybody to cope or leave because we're the 500lb gorilla.", the successor to Rust will have a vastly easier time.
For instance, a developer writing the software for the sensors of a forklift has enough to worry about to not crushing the skull of someone passing near it. Edit to add: is not that security bugs are not important, but is not what we (embedded developers) are always chasing or taking away our sleep at night. Embedded is wide enough beyond computer networks.
Please take a look at the room around you. Can you count every little chip in there? Including the one in your keyboard, LED lamps, coffee machine? Are all these ARM? NO. Are all these connected to the network? NO. Do you expect them to work 100% of the time without hiccups? YES, and they probably do without you noticing it.
And they are probably running 1000s of lines of C. That's embedded.
And you don't think having the compiler check memory usage would be useful in this case? In my mind it means more effort can be put on making sure the logic itself is correct.
Would the compiler check a write outside the buffer that is filled with sensor data? Is that it? I'm not allocating anything so I cannot double free. The program executes in a main loop so there is no data races (perhaps one if the data buffer is shared with an interrupt, but it's a problem as old as the bible and we already have 1000 libraries with safe circular buffers).
Or is the check done at runtime? Will it waste my cycles?
Is that all I should change my language for?
Would the sensor manufacturer help me if I give them a failing case written in Rust?
Is it worth the risk of compiling on a nightly thing that will change next week? Note that people might get hurt for real.
Damn! Now my MCU/sensor is going NRND. Should I freak out because Rust is still not supported for that new MCU yet, and I should write the thing all over again?
I'm getting really pissed off about people downvoting who have nothing constructive to add to the conversation.
Rather than downvoting, fucking REPLY. A downvote is lazy--it's actually worse than doing nothing on a low-vote thread--it's a sin on the level of turning down the premise when doing improv comedy. An articulated position furthers the conversation and actually represents effort.
When did HN readership become such prissy hothouse flowers? FFS.
(EDIT: I turned down the swearing a bit)
I suspect Rust will be a clear win on some subset of projects, and will not be worth it others.
Industrial is a very poor choice of counterexample. You should have stayed with consumer space counterexamples. Everybody in the industrial setting wants their telemetry and data transmitted to centralized computing whether they know what they are going to do with it or not.
As for sensors, how are those distance sensors connected to the microcontoller? Perhaps a redundant automotive bus with real-time constraints like CAN? CAN often has a fairly decently sized, badly written, vendor supplied communication stack in order to use it.
Is that forklift's sensor telemetry going anywhere? If not, then you're probably going to lose out to someone who does when you sell that forklift to Amazon or WalMart. They probably want to know about where the skulls are (gotta track those lazy humans) more than they want to know about where the forklifts are.
> Are all these ARM? NO.
Actually, they increasingly are. USB-C PD negotiation chips, for example, are basically ARM M3/4 cores with specialized hardware. The cost differential between 8-bit and 32-bit microcontrollers is so low that unless you have extreme volumes, you might as well use the 32-bit one and gain all the infrastructure that provides.
> Are all these connected to the network? NO.
Are you sure? Hotel locks had security vulnerabilities because they had a USB port.
The problem isn't whether I can use the I2C bus on your Cuisinart to break into it--if you used a "standard" driver written in C I probably can. These small system don't have to worry about "secure" simply because they aren't worth breaking into--until some really creative person makes them worth breaking into. (Hey, there's a speaker on that innocuous thing that's loud enough that we can talk to Alexa. YAAAARRRR, Me Mateys!)
Regarding ARM, it depends. It's not just a matter of bits over price. Pretty often the combination of features you want are not available in an ARM chip. For example, long term support, some specialized tool, temperature/power supply range, price, available peripherals, etc. And sometimes you might be required to work on some strange 32-bit CISC from Renesas just because the salesperson took the purchasing manager to dinner somewhere.
And yes, everything is hackable no matter the language you use. If you open the case and cut some PCB traces you might read and inject some data making some device to misbehave. But that's the way it is and I don't see the world falling apart because of this. If it really matters, then (electronically) tamper-proof your case would be my advice.
Again, is my honest opinion and the opinion of many embedded developers I work and know: vulnerability FUD is just noise. I'm just being sincere. Of course if you make locks you have to make your system secure. The other billion of embedded applications might not. And that's it.
No argument. But this is becoming less and less true since ARM has even become standard in automotive.
> And sometimes you might be required to work on some strange 32-bit CISC from Renesas just because the salesperson took the purchasing manager to dinner somewhere.
HAH! Been there ... screamed about that.
We're probably not as far apart as you think, if I'm just writing 100 lines, I'll just reach for C and be finished in an hour or two.
My problem is that none of my projects ever stop at 100 lines. They evolve and only get bigger. Eventually they adopt a "stack" (CAN, BLE, TCP/IP, etc.) from some manufacturer, and it's all downhill from there.
I need help to manage that explosion of complexity and the torrent of bugs it creates, and C just doesn't provide it. I don't think I've programmed an embedded device smaller than the original PDP that K&R developed C on in probably 10 years--maybe more.
And this includes FDA cleared products. The days of just sitting on an unchanging product for 15 years after getting clearance are gone.
Let me give a very simple example in golang. Function returning single value and function returing multiple values.
func foo(x int, y int) int {}
func bar(x int, y int) (int, int) {}
Even though below synatx works,
func bar(x int, y int)(int) {}
why there is a additional way to do it by removing parenthesis ?
This looks very simple example. But it breaks consistancy of syntax. Adds 2 rules to remember over what saving 2 keystrokes ? In the age of 100x full stack devs, life really sucks.