I've spent a good amount of time with C, nowhere near mastery though. Is it worth still writing C, or better off just learning Rust if my goal is to write embedded/systems code?
Rust is worth learning, but your C skills will continue to be useful for embedded for a long time. Rust support in the embedded world is still growing and you will find yourself going between Rust and C on most projects unless you can carefully pick your platform for Rust support up front.
Coming from C I don’t think you’ll find Rust too foreign, once you internalize how the ownership rules work. In my experience the formal rules of Rust overlap a lot with behaviors that are good practice in C/C++ anyway, but there are some complicated concepts that you need to wrap your head around before expressing them in Rust becomes second nature.
Depends on if your embedded system has a nice toolchain for it and/or how much suffering you are willing to endure. Another consideration is if you have to collaborate with others. Most programmers don't know Rust.
Whether it would suit you, depends if you can learn to like Rust's approach of moving more work to the type system. In Rust you do certain things the Rust's way, period. Programmers used to C being unopinionated about everything find that objectionable.
C is a tool which requires expertise but then goes out of your way and let's you do things, and do things rather efficiently, with no overhead, and exactly how you want. If you want it to cut off your arm it will do this too. But if you want to abstract things away behind types, this can also be done too (and arguable should be done more often in C). Somebody should write a C to more modern and safe C migration book.
Depends how you define "can", because you can invent your own conventions not checked by the compiler, or even make a compile-to-C language.
But more directly, C barely lets you define non-NULL pointers. It doesn't have pointers that guarantee the data behind them is initialized, it doesn't have never-leaves-this-thread data types. Const merely guarantees that you can't (strongly shouldn't) mutate data, not that it definitely won't be mutated by any thread.
Most of this can be done just fine in practice. NULL safety is not so much an issue as people claim as trapping is safe on most implementations and then similar to a panic in other languages. If you create data types via constructors you can then make sure those are initialized. Yes, this is a convention but easy to check. The const issue is not a problem IMHO. If you violate type safety yourself then this comparable to using escape hatches in other languages. Only the never-leave-this-thread point is something where Rust truly has an advantage, but one can have abstract data types that are thread-safe in C.
This is why I think a book is missing, people think C is much worse than it is because they do not understand what can be achieved.
Thinking that C's lax null handling is fine because it traps misses the entire angle of preventing problems by construction, making invalid states unrepresentable.
Having a program potentially crash is very weak compared to eliminating the crashing code path entirely, guaranteed statically. This matters more and more as the programs grow, because series of 99% correct components will take correctness towards zero (failure probability mulitplies), while 100% correct scales.
Non-nullable types not groundbreaking. I'm just using this example, because it's basic enough that C has a relatable counterpart. Although some values are truly optional and Rust still has to deal with those, having &/&mut references that are never-ever NULL (and never dangling, never uninit, never misaligned, never unexpectedly mutated) makes this problem go away for all the code paths using them (which is the majority in practice).
But C completely lacks the real good stuff in Rust, like Send/Sync. You can shake some bugs with tsan, and have a static analyzer try to figure out effects and propagate them across functions, but like all analysis in C, it's hampered by lack of explicit info in the code (heuristics causing false positives), difficulty of performing true whole-program analysis, and dynamic code being a dead-end (unlike Rust where the traits are part of function types). Until you try it, you won't know how awesome it is to write thousands of lines of pervasively multi-threaded code, and have it work on the first try, without a single UAF or data race (and before you say what about deadlocks and higher-level logical races - there are library building blocks which usually prevent these too).
> people think C is much worse than it is because they do not understand what can be achieved.
I've been writing for ~25 years, from microcontrollers to compilers. Rust for 11. I think I have very good understanding of what is possible. C is stuck in 1970s. Rust at least advanced us to 1990's :)
I think it's much more common for people to not grasp how far Rust is ahead, and think it's merely a slight refinement of "be careful, use tooling" approach in C, rather than a shift towards a level correctness never seen in C.
I think Rust helps a lot for people who would other struggle because they would otherwise create a mess by enforcing certain high-level structure that rules out certain problems. But you can just use very similar structures yourself in C programs! You can prevent problems in C as well by construction, even when the compiler does not enforce it. Often you can build data types where the interfaces enforces it. And my C programs also usually work on first try! I do not have to switch to another languages to experience this.
Sure, compiler enforcement can help, but the advantages of it are massively exaggerated. The "stuck in the past" narrative I heard very often, with OO, with managed languages, etc. I would say that Rust goes in the wrong direction with monomorphization and with safe but too rigid and simplistic rules.
And the "never seen level of correctness". Just look at some of the bugs found in Rust, it is just the same old nonsense as everywhere. The favorite bug I use as an example is this: https://github.com/advisories/GHSA-5gmm-6m36-r7jh A language where you can get the trivial check for the dimensions of a matrix transpose wrong just like in C is not at another level of correctness. Or the famous cloudflare bug where some unhandled .unwrap brought the whole internet down. That you theoretically can prevent all this with the type system is great, but IMHO misses the point.
Depends exactly what you mean by embedded but Rust isn’t as common as people make it out to be there. In some aread it’s almost entirely unheard of. C will be king for a while yet. There’s a lot of Ada floating around too.
Coming from C I don’t think you’ll find Rust too foreign, once you internalize how the ownership rules work. In my experience the formal rules of Rust overlap a lot with behaviors that are good practice in C/C++ anyway, but there are some complicated concepts that you need to wrap your head around before expressing them in Rust becomes second nature.