3 questions for c programmers using rust

  1. why rust?
    not meant to be an antagonistic question. i’ve personally been messing around with both rust and zig on side projects i’d otherwise use c on

  2. what has rust actually changed about the way you write systems code?
    generally, is there anything you approach fundamentally different in rust v c?

  3. anywhere you still prefer c to rust?
    basically, any particular kinds of projects, environments, api, or constraints where c is still the better choice, in your opinion?

Here's a C++ perspective (not strictly C):

  1. It really does comes down to Rust's unique selling point: guaranteed memory safety (with the 'unsafe' caveat) without sacrificing performance. This guarantee applies not only when you write the code, but as you (and others) are modifying it, which IMO is when a disproportionate number of bugs are introduced. It's hard to overstate how powerful this guarantee is.

Rust also has many nice extras (strong enums, pattern matching, Cargo, etc.), and they're great. But they're not game changers in the way that the "Rust guarantee" is.

  1. I rarely use C++ these days, but when I do, I make much more use of std::optional and std::tuple, now that Rust has shown me how useful these concepts are. I also tend to steer away from classes, in favour of pure functions.

  2. Can't say from personal experience, but I'd guess if you're writing a small, straightforward, single-threaded piece of code (maybe a very simple Linux kernel module), C might still be a better choice than Rust.

There are many reasons, but there was a specific point where I snapped and realized I needed to look at modern alternatives to C: I once realized that I had spent an entire day fiddling around with the build system for a project. I had a lot of actual coding to do, but I couldn't get on with it because I was trying to get the build system working on all our supported platforms. At that point, I realized the build system - that is supposed to make my life easier - was making my life more difficult and wasting time that I needed to put into writing actual code.

This was before I started using Rust, before I actually knew what cargo was. I had just read that modern languages made building easier, but I had no idea how much easier it would be.

Basically, Rust allows me to focus on actual programming instead of bureaucracy surrounding programming. It made programming much more fun for me. Also, I think I can write fairly robust code in C, but it takes a very long time. I can write even more robust code in Rust in a fraction of the time.

I reason a lot more around ownership and lifetimes in C code.

I tend to think further ahead than I used to. I used to just write code and reshape it as needed. After using Rust for a while, I find myself planning things out more.

Sure, there are a few cases. I have a package manager for an embedded platform that I actually have three versions of: A C version, a C++ version and a Rust version. When I build the package manager on the actual native platform, the C version builds in like 5 seconds. The C++ and Rust versions take roughly 800 quadrillion years to build, despite them not actually having more features. (Though the runtime of the tools are basically the same, it's just building them that are a nightmare).

Also, kernel drivers. (But that won't be true for much longer).

  1. I've been fed up with C's warts from 1970's that will never be fixed due to back compat. Windows vs Unix schism makes supporting both annoying (and means that even the few C warts that have been fixed take 10+ years to be usable in MSVC). Even simple dependencies are an endless struggle if you support more than a couple of Linux distros. It's so nice to leave fragmented build systems and baggage of the C preprocessor behind, and have have painless dependencies and build even large projects with the same command on all major platforms.

  2. I can make everything multithreaded by default without regretting it. I don't need Valgrind! The last time I had a memory leak was from integrating a library via a C API. My programs crash less than Python, but still run at C speed. I can just use a HashMap and unit tests without copypasting some single-header DIY macro hack thing. I can make network connections without a single #ifdef.

  3. For 16-bit CPUs maybe I'd use zig. I can't imagine going back to C.

Out of curiosity: what makes Zig such a good fit for 16-bit machines, and in particular, a better fit than Rust would be?
Is it Zig's cross-compilation support?

No, it's that 16-bit platforms are so tiny that you basically have your entire program in your head. Rust's features for large scale programs never apply. There's no OS, no allocator, no threads. Ecosystem and dependencies don't matter when you can't afford extra function calls or local variables.

So when every byte matters and you need to do unsafe hacks, you may as well do it in a less safe lang, and zig has better tooling than C.

Rust has only a specific niche where it's a king. For example, the guy picked Rust to write OS and ended with over 800 unsafe blocks.


So his Rust code has no benefits over C. So I would say - as long as you are not going to write OS, or OS drivers, you can choose Rust over C. Another criteria can be - zero unsafe blocks, do not read it literally.

  1. I was tired of writing C and having to remember all the things to not do. Using other people's C code is a source of problems too because they could easily make mistakes too. The options came down to Rust or Zig and I didn't like Zig, it's too verbose for me.

  2. Not much. I guess I use more libraries?

  3. C is the better choice when you're working on projects already in C. It's a lot of effort to spend time setting up scaffolding and wrapping if you're not writing that much C code.

Nearly seven years ago I was once again looking around for a replacement for C and C++. Something that fixed up all the issues with C and preferably was not an ever growing monstrous mess like C++ that despite its desirable features did not fix the problems it inherited with C. In former lives I had used languages like Corral 66, PL/M, Ada and others. I had learned C in the early 1980's and was disturbed how sloppy it was compared to those others from the beginning. I was amazed C got adopted for safety critical or mission critical systems, that seem like negligence know that we already had better. As it happened, nearly seven years ago, Rust popped up on my Radar, it ticked all the boxes, so that is where I (and our company) landed. Later I get to hear of Zig, Odein, whatever. Maybe they are fine but I have not heard anything about them that would get me to switch again. It would have to be really good.

What do you mean "systems code". I'm not into building operating systems or essential system building tools like compilers, databases, etc. When it come to embedded systems not much has changed for me, the embedded systems we work on are almost always running Linux and work proceeds almost like developing any other Linux apps.

I still have a soft spot for C. Despite its limitations and flaws. It is small and simple, pretty much perfectly designed for the job it was created for, rewriting Unix from assembler into a high level language to make it portable to different machines. Working well on the much slower and smaller systems of the time. I would likely reach for C on small, 8 and 16 bit, embedded systems if I ever had to deal with them again.

But more on that "what has rust actually changed about the way you write systems code". Thing is, for many tasks I would have not used C because I wanted to work at a much higher level of abstraction and avoid all the silly errors it is so easy to make in C, I would to have tried to avoid C++ on account of the complexity of doing anything with it. For those tasks I would have turned to Python or Javascript (node.js), heck even Java or Scala. But now is the very nice situation that it is not necessary to use a different language. From small embedded system code to server side services and web servers, even in the browser, I have not found anything I cannot do in Rust.

How so? We have seen all kind of people using Rust very successfully in all kind of situations. From small memory limited micro-controllers to automotive to databases to language compilers to operating systems (including use in the Linux kernel) to graphics stack (WebGPU) to command line tools, code editors, AI harnesses. Etc, etc.

As far as I can tell there may be some places some other language is better suited but I don't see any big difference.

What is it you think Rust is not suited to? All I can think of is sometimes in the web browser where getting setup with WASM just adds complexity over just writing a bit of Javascript. But even there most of web pages HTML is generated by Rust server side.

Interesting. Do you have a link to that OS? Of course if you are going to write an OS you are going to need to use "unsafe", if only to get access to I/O. I don't think that is a bad thing. In general such "unsafe" gets wrapped in safe abstractions which I think is a far better situation than having everything unsafe everywhere. So I can''t agree that Rust code has no benefits over C just because it includes some "unsafe". There is a whole bunch of operating systems written in Rust that get along just fine. And as noted Rust is now used in the Linux kernel.

Not a real C programmer, but I wrote a lot of C code here and there long before I met Rust.

For any long-running software two things are importaint: an ownership and a lifetime of objects.
You must design them carefully and clearly understand when should particual object be destroyed (and destroying it doesn't only mean to free memory, it is also about releasing OS resources).

In C, compiler doesn't really help you: it is up to you to come with a solution using documentation, charts and so on. And then follow it.

In Rust, the compiler is on your side, and this is the main reason I prefer Rust.
And it also has usefull standard lib.

C is a Lingua Franca of operating systems. Whenever I read something about OS API, it is always expressed in C. So when I want to understand OS better or investigate some obscure bug I write dead-simple C programs. For example, I've reported several bugs in WSL with strace output and a small C program to demostrate it.

More a "no-C-programmer". These are the reasons I didn't keep coding C after uni:

I have ADHD (didn't know it until recently - so this is retrospective). While I find the "hard stuff" easy, I find the "easy stuff" hard. So:

  • I will forget to clean up memory or resources ("silly little mistakes")
  • I will index out of bounds ("silly little mistakes")
  • I will use after free ("need hold too many things in my head at the same time")
    Rust solves all of those.

I can manage python's with ... syntax as it comes first and is block-based. I find zig great fun, and at least defer comes early but I still prefer much prefer not to worry at the call-site and to impl Drop once, when I'm focussed on the data strcuture in question.

For me the biggest change was how I think about strings. (&str vs &[u8] vs String vs Vec<u8>)

Nope, I don't feel safe writing it. I'm fine with unsafe rust - as I have a ringfenced block and a SAFETY comment as a checklist.

Except the actual OS ABI (binary interface) is not any version of any C API or ABI. That actual is OS interface defined in terms of sys calls that you cannot even write in C. That is what we have libc to adapt to that. The original WSL is a fine example of that adapting a Unix/Linux interface to whatever Windows provides.

I see no reason there could not be a Rust only world where the OS in in Rust the applications are in Rust and still there would be a Rust equivalent of libc that takes care of a sys call interface to the OS. With some assembler help, like libc.

I don't think cleaning up memory, index out of bounds, use after free are the "easy stuff". Sure it all looks simple and obvious when shown in small examples. But I have suffered from these problems in large code bases written by teams many times. They can be very difficult to track down and fix.

Or is it that we all suffer form ADHD?

I think you are right about strings. Doing anything non-trivial with strings in C is very difficult and error prone. C does not really have a string type, except what is loosely suggested by the standard library str* functions.

My friend tells - I can write everything in Fortran. We discuss not a possibility, we discuss an effort.

Yes indeed. All languages are essentially the same. Turing compete as they say.

There are some practical matters though. Besides the theoretical. Like can I reach memory directly, can I reach I/O directly in the language? Is the performance of the resulting code adequate? Are there compilers available for my target? Does the language eco system provide libraries and such that help me get the job done?

I too am talking about effort. A programming language is the interface between the programmer and the machine. How easy is that interface to use? Does it allow writing less code for the same effect? Does it provide abstractions that relieve the programmer from having think about a lot of details. And so on.

My contention is that Rust checks all the boxes and does well in a huge range of application areas, from real-time embedded systems to serve side to desktop applications. It is not just limited to some niche area.

I am not heavy C user. But I actually want better Unsafe Rust. Like currently unsafe Rust has aliasing footgun that is not present in C. If only that footgun is catched at compile time (which I think it can), it would make writing code enjoyable. I don't aggree with the false logic (improving unsafe would make people write it more) I actually read people talk like that. That is just very very non sense. When people reach unsafe, they already say loud I know what I'm doing, that is already valid contract aka user already aggree it is on their own risk, so the language job is just provides the best tool possible. So that people that needs it can write it with nice experience. So Rust can be used in very low level space where it have to use raw pointer without downside compared to other languages, so there is no need to switch language. User get the Rust features (generic, cargo, pattern matching, trait, etc) and pointer access that doesn't has additional footgun compared to other language's pointer

A high level thought of how to catch aliasing violation at compile time is making the compiler can detect if pointer alias with reference. So const pointer is allowed to alias with const pointer, mutable pointer, and immutable reference, but isn't allowed to alias with mutable reference because it is UB. Mutable pointer is allowed alias with const pointer and other immutable pointer, but isn't allowed to alias with immutable reference and mutable reference because it's also UB

That said, if almost all the code needs unsafe because very low level, or the unsafe part is big enough, I would choose Zig instead for that part because it provides better experience than C and unsafe Rust. Another advantage of Zig compared to Rust in very low level space is it supports cross compiling C code natively, so if the project does binding to C code part/library, cross compiling it is not complicated. Cross compiling pure Rust code is great, it starts becoming complicated to setup if the Rust code calls C code aka binding. So integrating Zig to a C project is easier to setup

The point of using raw pointers is to deal with cases where you need the complicated runtime rules for pointers instead of the simplistic statically-checked rules.

Because, keep in mind, the “true” rules about pointer manipulation have runtime state. (They involve the stuff called “provenance” you’ve probably heard about.) A static analysis tool like the borrow checker is incapable of perfectly reasoning about arbitrary runtime behavior. (See Rice’s theorem.)

You’re also being overly broad about calling stuff UB… both examples you gave aren’t UB. It’s “merely” the case that doing frequent pointer->reference and reference->pointer conversions makes it very easy to make mistakes that lead to UB.

Meanwhile, I’ve seen someone convert a &[u8] to a &'static mut [u8]. (They even got lucky, and the optimizer didn’t notice and take advantage of the UB in their tests.) No amount of compiler diagnostics can stop a sufficiently determined person from executing UB if they think they know better than the rules :expressionless:

Another nice thing about rust is that you will never see an API design with a comment like this:

Using this function to get details about an interface is typically a two-step process:

  1. Get the required buffer size. Call SetupDiGetDeviceInterfaceDetail with a NULLDeviceInterfaceDetailData pointer, a DeviceInterfaceDetailDataSize of zero, and a valid RequiredSize variable. In response to such a call, this function returns the required buffer size at RequiredSize and fails with GetLastError returning ERROR_INSUFFICIENT_BUFFER.
  2. Allocate an appropriately sized buffer and call the function again to get the interface details.

Exactly, between getting a size and using the size, can happen billions operations, so you will use a size of non existing thing.