Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

In OS programming, there is a strong bias towards favoring C as the only serious programming language. Rust is one of the few programming languages that has the potential to break through this barrier, especially because the ability to support bare-metal programming is one of its design goals.

But the key word there is "potential"--Rust, as it stands right now, is not ready to be used in production OSes. Features such as inline assembly or other developments for #[no_std] support need to be worked on and supported in non-nightly modes for any production OS to take notice, for example.



I feel like if C++ didn't break through this barrier then I wouldn't hold my breath for Rust.


I don't follow that argument.

C++ didn't break through because the benefits didn't outweigh the drawbacks compared to straight C (meaning you can implement C++ like concepts in straight C, and not have to deal with a lot of baggage C++ brings).

Rust is a whole other kettle of fish. It has more or less been proven by this point that people cannot write memory safe C, even experts in the language are still introducing memory-unsafe bugs in 2018. Therefore Rust is bringing features the C language cannot offer.

Which isn't to say I know that Rust will be "successful." I don't know that. But I do know it is worth TRYING to see if it can be successful, and C++'s lack of success isn't really a counter-argument.

I'm still sad that Microsoft's Singularity OS[0] never got further developed, interesting concept.

[0] https://en.wikipedia.org/wiki/Singularity_(operating_system)


> ... It has more or less been proven by this point that people cannot write memory safe C, ...

Or concurrency safe. There aren't many languages that can help with this class of bugs. Like race conditions. (Someone on HN said Pony is another language that has concurrency/thread safety guarantees.)


Bias is everything, Joe Duffy briefly mentioned at his RustConf keynote that many at WinDev did not believe Midori was possible, even when shown running in front of them.


i think this would be the link, if anyone is interested: https://www.youtube.com/watch?v=EVm938gMWl0


Not to shill but I'm working on a Singularity/Midori-inspired Rust JIT-compiled operating system. If that ever gets anywhere I'll post a link/Show HN.


Yeah I was sad about Singularity too. As for C++, yes it doesn't being an absolute guarantee of memory safety, but C is so error prone that it's ridiculous to suggest anything below an absolute 100% guarantee wouldn't be a compelling improvement. C++ was a gigantic improvement in the safety of C (i.e. such that following good coding practices actually gives you legitimate confidence your code is memory-safe) while still having all of its flexibilities, and yet it still got shunned, so I'm unconvinced that a nitpicky language like Rust makes all the difference just because of its formal verification. If kernel folks wanted compiler help with memory safety they could've embraced C++ and gotten most of the way there.


C++ can bring very little baggage. That's why its widely used in Embedded environments.

And I'd wager that C++ can write far better optimized abstractions than a human in C every could. In most cases.

Writing memory-safe C is clearly possible - many other enviroments are written in C. It come down to using a safe subset.


> Writing memory-safe C is clearly possible - many other enviroments are written in C

Show me one major piece of C software that does not have frequent memory-safety bugs and I might believe that.


djbdns/qmail might qualify here.


qmail had LP64 memory corruption bugs.


One ever right? I wouldn't count that as "frequent" - lots of programs in memory-safe languages have more/worse bugs than qmail.

Of course, the existence of one significant not-too-vulnerable C program doesn't really prove a lot either way...


I though Georgi Guninski found several.


It did, just not on pure UNIX clones.

Symbian, BeOS, Haiku, Genode, ARM mbed, macOS IO Kit, Android (specially after Treble), Fuchsia, Windows, IBM mainframes (old PL/S code has been partially replaced with C++) ...


I guess by OS programming I assumed they meant the kernel. Fuchsia is hardly an established OS at the moment so I don't really compare against it, and for Windows, MS's documentation has repeatedly discouraged C++. I'm sure some use C++ in the kernel anyway but I'm not under the impression it's generally accepted.


All the examples I gave, except for Fuchsia, are examples of C++ in production for kernel code.

Windows has been slowly moving into C++ since Windows 8, as Microsoft considers C++ the future of systems programming in Windows.

https://docs.microsoft.com/en-us/cpp/build/reference/kernel-...

https://www.reddit.com/r/cpp/comments/4oruo1/windows_10_code...


Wow that's good to hear, thanks. (I wasn't familiar with the other OSes so that's why I didn't comment on them.)



Using C++ for kernel development means you first have to drop most of the useful parts of C++ and take usability hits with almost all the other features.

Rust and C have an advantage because both are fairly close to the metal, Rust in no_std mode isn't much different than C but the compiler is angry at you all the time.


>Using C++ for kernel development means you first have to drop most of the useful parts of C++ and take usability hits with almost all the other features.

This is commonly accepted wisdom, but it's not entirely true. (It's only a little bit true.)

The kernel is a freestanding environment where big chunks of the standard library are missing. However, the most useful parts, like unique_ptr<T>, can be reimplemented as needed.

The kernel absolutely can support RTTI and exceptions. Porting libcxxrt is relatively simple, for example, even if implementing the entire Itanium C++ ABI is a big task. Though, the kernel should still avoid throwing and catching exceptions in sensitive code like interrupt handlers. This is not radically different from other C++ projects. The use of exceptions is typically discouraged, for example, in tight inner loops.

It is true that enabling RTTI and exceptions causes code size to explode. This is a valid concern. While memory is cheap, cache misses are not cheap. Mark functions and methods with "noexcept" as needed where this becomes a problem.

Finally, it's absolutely true that exceptions make it impossible for the kernel to make solid real-time guarantees. Fortunately, most operating systems only have soft real-time constraints, if that.


> Using C++ for kernel development means you first have to drop most of the useful parts of C++ and take usability hits with almost all the other features.

What? No... the most useful parts of C++ are its compile-time facilities... templates and type utilities and RAII/smart pointers/etc. (I could go on and on) all of which are still there. You'd lose some runtime stuff like the ability to throw exceptions and some RTTI, which some people already advise each other to avoid in user mode too (not to say I agree, but they're still finding a lot of use in C++ without this). If losing those is equivalent to losing most of C++ for you then you're not really taking advantage of C++ to begin with.


> Rust in no_std mode isn't much different than C

This isn't really true; you still have memory safety, and generics and a sane stdlib.


Memory safety flies out the window when you write a kernel and the stdlib isn't available in no_std mode. You only get a very limited subset in the core lib.

Memory safety in kernels is hard because you might have to do things like switch stack or even address space and the rust compiler handles such actions rather poorly. Whether or not writing to the rsp register will crash the kernel or successfully pivot the stack is guesswork at best and only works reliably if you code it entirely in inline assembly.


Look at the embedded Rust stuff like RTFM - they find clever ways to build safe abstractions over unsafe primitives. If you can get 95% memory safe device drivers, thats already a huge win.


Correct and in my own project I want to achieve 100% memory safety for non-primitive drivers (Disks, GPU, Ethernet, USB Devices) and 100% memory safety for any non-privileged code of primitive drivers (ie, PCIe bus, SATA, USB, etc.).

Any privileged code can then be written with those 95% and that's the only weak point left.


Memory safety in kernels is also welcomed and is very present in mainframe OSes and high integrity systems where human lives are at stake.

Yes, there are a couple of areas where unsafety is required, there isn't any other way.

However 100% of the kernel source code doesn't need to be unsafe.


There is two types of memory safety that you talk about there, I think.

One is the memory safety that the rust compiler wants to hold and the other is the memory safety you program into a system.

Those can be entirely different with incompatible guarantees, depending on the mission statement of the software.

Some things a kernel must do are unsafe for the rust compiler. Dereferencing a null pointer, for example, is basically a crash in most operating system's userland.

In kernel space a null pointer may be valid memory that points to real data you need to use.

Setting the page tables is unsafe as well; it's almost impossible to guarantee that the following instructions can run in a static manner. You have to validate the page tables you're about to load at runtime and even then the result could be a process crashing.


> Rust and C have an advantage because both are fairly close to the metal, Rust in no_std mode isn't much different than C but the compiler is angry at you all the time.

Those things the compiler is angry about? Those are things that would be runtime errors in C.

`rustc` is pedantic because it needs to be in order to provide the assurances it gives. There is a subset of "valid" Rust code that `rustc` rejects because it isn't smart enough to understand it would be safe, but I would guess that those cases are less common than than the cases of "seemingly valid" Rust code that is in reality problematic if it were accepted.

Beyond that, I would like to hear what errors have made you feel like the compiler "is angry" at you instead of feeling the compiler "is trying to help me". We spend an inordinate amount of effort on 1) accepting code that we should accept by improving ergonomics, 2) provide extensive suggestions when "seemingly correct" code hits the wall of `rustc`'s understanding, language design or things addressable by ergonomics, and 3) provide as good explanations as possible for all other cases. Because of this, I believe the experience of a newcomer today is much better than even a year ago, even though it can still be improved.


>Those things the compiler is angry about? Those are things that would be runtime errors in C.

I'm very very aware of that though this is not always true.

For example Rust doesn't have a sane way to have a reentrant mutex without including std, which is bad because for interrupts in a kernel you need to either have very fine grained locking and fallbacks or rely entirely on lockless operations. Interrupts can occur on top of eachother in which case it behave a lot worse than simply being reentrant.

rustc yells at me a lot for the solutions this requires because the way I solve it violates ownership rules of rust the hard way to make the code simpler and understandable.

Other times it's when for bizarre reason rust wants the code to implement the Sync and Send traits despite the kernel running alone on the core (there is no threading in that kernel) and I would love to be able to tell rustc to just shut up about this and simply ignore Send, possibly even Sync, on a module level.


You shouldn't have to worry about Send and Sync if you aren't using generic types that require that their type parameters be Send and Sync. What such types are you using?


Various no_std libs use Send+Sync in places which makes things fun.

Another would be static mutable variables, which are perfectly safe in a single-threaded environment, doubly so if you map that memory on a per-core basis to ensure each kernel has a unique variable value. Esp if you need lazy_static then there aren't many options other than wrapping the data in a mutex for no good reason.


If it's per-core you should look into making `#[thread_local]` statics work, which remove some of the restrictions.

`static mut` is not always safe even in a single-threaded environment, because of reentrance - see also https://github.com/rust-lang/rust/issues/53639.


I'm not sure if Thread local would work since i'm not certain if the bootloader can handle a TLS section in the ELF file, my kernel cannot do this either as I've yet to implement threading.

Reentrance is not the issue here, I guard this inside the data structure, interrupts will be worst case reentrance most of the time.


Then you can use the techniques in the thread I linked.

But without guarding mutability behind some way to indicate interrupts are disabled, or an outright lock, and without guaranteeing no data races (Sync), it's not actually safe.

And the compiler can't make `static mut` safe to use as any of those requirements could be broken and then it's not safe at all anymore.


If you know your types are Send and Sync, then you should implement Send and Sync for them. That's how you "tell rustc to just shut up about this."


They aren't Send and Sync, ie not multithreading safe, they are however, reentrant safe, which is all I need. rustc complaints and I don't want to implement traits on types that don't need them and don't adhere to the contract of the trait.


Linux's kernel runs on more than one core and is pre-emptible, so that's just not generally true.


In the context of a single core, this happens. Multiple cores make it harder because on top of an interrupt taking locks, other cores now also take locks and you can no longer rely on various promises that a single core gives you.

While the Linux kernel can do this, it's not easy to write from scratch, especially because Linux is in C so the compiler doesn't complain about weird things you do.


> Those things the compiler is angry about? Those are things that would be runtime errors in C.

> There is a subset of "valid" Rust code that `rustc` rejects because it isn't smart enough to understand it would be safe

Way to tell him he's wrong only to then proceed to directly contradict yourself...


> [...] but I would guess that those cases are less common than than the cases of "seemingly valid" Rust code that is in reality problematic if it were accepted.

I would guess that most people starting to try out Rust are hitting the latter more often than the former. "Valid" Rust code that `rustc` rejects is uncommon (and for the most part, a bug), but wanted to acknowledge it happens. A "common" case of it would self-referencing structs, but that is considered a bug to be solved. I've only personally experienced problems with valid code being rejected when expressing complex trees involving associated types with their own independent lifetimes[1][2][3].

[1]: https://github.com/rust-lang/rust/issues/55756

[2]: https://github.com/rust-lang/rust/issues/54378

[3]: https://github.com/rust-lang/rust/issues/54895


Who says that C++ didn't break through that barrier, though? Sure, the big popular OS projects like (I'm assuming) Windows and Linux mostly stick to C in their kernels, but Haiku (for example) seems to be doing quite alright being written predominantly in C++, including in the kernel: https://git.haiku-os.org/haiku/tree/src/system/kernel

Outside the kernel, C++ seems to have more significantly broken through that barrier.


You've repeated one of the existing responses from 9 hours ago.


Sorry about that.

In my defense, I linked directly to example source code.


Here's the explanation from Linus for C++

http://harmful.cat-v.org/software/c++/linus


This is a prime example of the gatekeeping nature of C programming you see: the idea that, if you're not using C, you're not a serious programmer, so go away and play with your toy language.

The main actual objection to C++ is that, well, the exception handling model is controversial, and it is especially ill-suited to kernel programming. RTTI can also get thrown in the lump here. Standard libraries aren't going to work in the kernel anyways, and so if you strip that out, what benefits does C++ get you over C? The correct answer, actually, is that you get RAII for added safety, and tooling such as IDEs have better support for finding all implementations of a virtual function than they do function pointers, but for a lot of C programmers, this can be a sense of "real programmers shouldn't need such assistive technology."


>"The main actual objection to C++ is that, well, the exception handling model is controversial"

I am not familiar with this. Can you way why exactly it is considered controversial?


The implementation of C++ exceptions is what is often called "zero-cost exception handling." However, the zero-cost actually turns out to be a lot of cost, just cost omitted from normal accounting.

The way zero-cost exception handling works is you build this large table that says "if there's an exception where the program counter is in this range, jump to this point." There's no added code to the program in normal control flow (hence the name), but when you get an exception, you have to do a mildly expensive unwind and table lookup to get to the code. The table itself also comes with the price of requiring extra relocations for all of the PC offsets, which also comes with a startup hit.

One of the main costs is that unwinding through a function is assumed for all functions, unless proven otherwise. This means, even if you're not using exceptions yourself, the compiler still needs to generate code to catch an exception after every function call, run all destructors in reverse order, and then rethrow the exception. And remember that destructors and constructors are themselves function calls, which therefore might throw exceptions as well. Generating the code to actually do all of this destruction correctly requires adding code to the function to handle this that is not in the normal execution path, which can also cause issues with things such as instruction caches.

A final issue is that exception handling requires RTTI to be able to dispatch to different catch handlers. RTTI needs to be generated for every potential type (this includes POD structs, for example), which is again a codesize bloat issue. For types that have virtual functions, the RTTI information need to be included in the vtable, which means they're not going to be eliminated by dead global elimination passes (as unreferenced POD structs would tend to be).

In short, RTTI and exception handling require a lot of extra tables that have to be generated even if you don't use them. Furthermore, a major concern for kernels, exception handling requires unwind support, which is generally not part of the kernel library repertoire and can be tricky to do manually. Compiling without exceptions and without RTTI is not unusual for large C++ applications for these reasons.


> This means, even if you're not using exceptions yourself, the compiler still needs to generate code to catch an exception after every function call, run all destructors in reverse order, and then rethrow the exception.

For every non-inlined function call (or one that it otherwise knows additional information about, e.g. with LTCG). For C++, this is a very big difference, since idiomatic C++ has a lot of inlined functions.


Going from memory here, since I can't find any references.

I believe there are problems in the handling of exceptions in constructors and destructors, which permeates into other parts of the language such as how arrays are constructed, etc. For one, the only way a constructor can fail is through an exception, and that's pretty heavy-handed for a language which markets itself as "don't pay for what you don't use." So you can't do RAII in C++ without exception handling. (Note that Rust solves this by not even having constructors.)

There are also some problems with exceptions being slow code paths, but that probably varies by compiler.


You can do RAII without exception handling... you just don't throw from the constructor. There's no stipulation you have to throw from your constructor.


So your constructor has to be infallible. That means either you can't allocate memory in your constructor, or that all classes that are backed by dynamic memory have to have a tacked-on error state in case the constructor "fails".

An operating system can't just crash if memory allocation fails, because it's actually normal for the operating system to run out of memory. If your OS is even a little bit like Windows 95 or System V, it uses all of the "left-over" memory for the file system cache, so every time it tries to allocate memory, it probably needs to flush a page to disk, which takes long enough that the OS will also want to go back into the scheduler to try to find some more work to do while that runs.

Ironically enough, C++ has fallible allocation in the standard library in spite of being ill-suited for it (unless you have exceptions; those can support fallible allocation just fine), while Rust went with the let-it-crash approach in its standard library, despite the fact that the language itself allows you to just return an Option from all the functions that allocate memory.


I was just saying you can have an error state, like iostreams already do. It doesn't imply you give up RAII.


If you're doing that, you will often end up needing an init() method, and you arguably don't get RAII if you're not doing initialization during construction. I mean, you can write safe code this way, but if C++ hadn't backed itself into a corner by trying to make user types act like builtin types, this wouldn't even be a question. Constructors wouldn't have to be a whole domain of study unto itself, they could just be functions and you're done learning about them.


Huh? No. You can perfectly fine have a constructor that just leaves the object in an error state if it fails to acquire its resources. And a destructor that doesn't destroy unless acquisition is successful. No need for init(), though you can have that too. And your "arguably" is not arguable, it's just plain wrong. You don't have to successfully acquire something on every single invocation in order to have RAII. Otherwise e.g. initializing a smart pointer with null would suddenly imply smart pointers are not RAII...


This is the first paragraph on wikipedia:

>In RAII, holding a resource is a class invariant, and is tied to object lifetime: resource allocation (or acquisition) is done during object creation (specifically initialization), by the constructor, while resource deallocation (release) is done during object destruction (specifically finalization), by the destructor. Thus the resource is guaranteed to be held between when initialization finishes and finalization starts (holding the resources is a class invariant), and to be held only when the object is alive. Thus if there are no object leaks, there are no resource leaks.

All of this is violated if you are constructing objects in an 'invalid' state. The whole point of RAII is that you don't do this. If you have to check if the object is in an error state at the start of every method call, then you're not benefiting from RAII.


No, you're just arguing and misinterpreting things for the sake of arguing. RAII doesn't mean you absolutely have to hold a resource all the time. Like I said, nobody (other than you) says e.g. shared_ptr isn't RAII just because it can be in a null state that holds no resources. If you understand RAII you know that no benefit of RAII goes out any window if a smart pointer or some other object is initialized in a state that happens to hold nothing.

If you want to redefine things to be different just to win an argument, you can, but it's not something I'm going to continue entertaining as I'm pretty tired of it now.


Look, I said it was arguably not RAII, so I can see how one could make the point that it still works if you do it that way. But you saying "I'm just plain wrong" to say that RAII actually means resources must be allocated on initialization... I don't know what to tell you. That's literally what the acronym means.

And just because some low-level pointer primitive allows itself to have a null state doesn't mean your Files and DBConnections, etc., should have free reins to do the same.


Thanks for the detailed explanations. Cheers.


Just replying to your first sentence: I'm actually curious how many NEW OS projects are being written in C. I would suspect your comment is more geared towards entrenched operating systems where C was already the groundwork.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: