I like the concepts proposed by Rust but do not like fighting with the borrow checker or sprinkling code with box, ref, cell, rc, refcell, etc.
At some point there’s going to be a better designed language that makes these pain point go away.
I like the concepts proposed by Rust but do not like fighting with the borrow checker or sprinkling code with box, ref, cell, rc, refcell, etc.
At some point there’s going to be a better designed language that makes these pain point go away.
I'm not sure why this would be confusing or disliked by a C++ dev.
Rust's Box<T> is similar to C++'s std::unique_ptr<T>.
Rust's Rc<T>, Arc<T>, and Rc<RefCell<T>> serve similar uses to C++'s std::shared_ptr<T>.
Rust's Weak<T> is similar to C++'s std::weak_ptr<T>.
Verbosity of both is nearly identical. The big difference is that Rust enforces the rules around aliasing and mutability at compile time, whereas with C++ I get to find out I've made a mistake when my running code crashes.
The Option is important, Rust's Box<T> is always a boxed T, but std::unique_ptr<T> might not be a boxed T, it might be "disengaged" and there isn't a T
C++ move operations are thus closest to Rust's core::mem::take function, they not only move something, they also need to always replace it with some empty default state, in Rust's case specifically Default::default. Box<T> may not implement Default, but Option does so unconditionally, because its default is always just None.
You may find when converting some code that you didn't want Option<Box<T>> but only Box<T> because in fact you always have a boxed T here - it's never disengaged, and if you do that then Rust's type system has helped in a small way to clarify your code so that's nice.
As you write this, do you not start to see why this would be confusing?
Yes, C++ is pretty bad at this too.
The fact that these exist and the programmer has to always be consciously be aware of it is an indication that something has gone wrong in the language design.
Imagine if you were to write C code in 1970, but you always had to keep track of which registers each variable corresponded to. That is how I look at these.
(There were, indeed, early 'high level' languages that required you to do this :)
This is what separates a systems programming language suitable for OS and embedded development from managed languages, which are not.
The complexity ultimately stems from unavoidable details of the hardware. Languages which do not offer similar representations will be incapable of making full use of the underlying hardware, including writing certain projects like bootloaders, firmwares, OSes, etc. Rust is pretty close to state-of-the-art in terms of providing reasonable abstractions over the hardware.
It sounds to me like you're used to managed languages with a runtime which are great for certain applications, but unable to be specific enough about memory layout, how data is formatted in memory, etc. for some tasks. Those choices were made for you by the language runtime's authors and necessarily limit the language's applicability to some problems. Rust, C++, and other systems programming languages don't have such limitations, but require you to understand more of the complexity of what the system is actually doing.
Any language which provides adequate representations for taking full advantage of the hardware is going to be on a similar order of complexity as Rust, C++, or other systems programming languages, because the hardware is complex. Managed languages can be nice for introducing folks to programming, precisely because much of the complexity is hidden in the runtime, but that can be a double edged sword when it comes time to approach a systems level task.
Instead of wishing the language didn't offer those representations, it may be more productive to ask why C++ and Rust converged on such similar ones. Exploration of that question will be enlightening.
These are fundamentally hacks around the compiler’s inability to understand ownership and lifetimes, at least the way Rust (and C++) are designed.
These exist in Rust because otherwise you would have to use unsafe blocks all the time to write any reasonable code.
The comment about memory layout was an additional point that dealing with hardware requires that you format, align, and position information in memory as the hardware expects, which requires either exposing those details, or encoding them in a runtime.
I write compilers (and have contributed to the Rust compiler!), and there has been approximately zero times in twenty or so years that thread safety has been a concern.
1. You work on esoterically simple problems where nothing is worth threading (implied by you questioning whether it bought any meaningful performance).
2. You code in languages like Rust where it’s not a problem or significantly less of one (Go, Java, c#, Haskell, Ocaml, etc).
You’re being awfully dismissive of people who’s experiences don’t match yours, especially when it seems like your experience isn’t the norm.
Rust folks always seem to assume people write C++ like it is 1995.
> implied by you questioning whether it bought any meaningful performance
This is the first thing you should always ask when you do anything with parallelism! Are you familiar with Amdahl’s law?
Also I’ve worked on a lot of different codebases, from iOS at Apple to Android at Meta, from machine learning to VR video streaming. Claiming that people aren’t using raw threads in C++ a) isn’t borne out by the evidence b) is ignoring the challenges of writing thread safe code that has nothing to do with manually creating a thread. Hell, my team hit a libc++ bug in std::conditional_var.
What kind of charmed life do you lead to where use of reliable abstractions over threads is common in C++ codebases you touch?
My experience has been the exact opposite.
Okay, can you try and find a relatively modern large open source C++ codebase that actively uses raw threads?
On a different note: why should the kernel even handle IPC or scheduling? Those take the basic capabilities of context switching, timer management, and memory management. Even "core functionality" can be a context switch (or a few) away; is a syscall not just a message to a system server? Only the most basic form of communication is necessary to delegate arbitrary functionality, so a true microkernel should only introduce that as an abstraction. Everything else either follows from the hardware or is left to the whims of software.
The kernel and memory manager: probably yes, the device drivers: not necessarily, the language runtimes for managed languages: not necessarily.
> On a different note: why should the kernel even handle IPC or scheduling?
Because any other solution will quickly run into chicken-and-the-egg style problems.
> Those take the basic capabilities of context switching, timer management, and memory management.
Yes. But only one timer, the rest should be free to use by other applications.
> Even "core functionality" can be a context switch (or a few) away; is a syscall not just a message to a system server?
No. A syscall is usually defined as a call to a ring one level in from the one where you currently are. But lots of things that are syscalls right now do not necessarily have to be.
> Only the most basic form of communication is necessary to delegate arbitrary functionality, so a true microkernel should only introduce that as an abstraction.
Indeed. And they do.
> Everything else either follows from the hardware or is left to the whims of software.
A lot of the stuff that 'follows from the hardware' can be dealt with at the application level.
The parts that touch hardware (or similarly bare kernel interfaces) must be so. Sure, you could split device driver implementations and so on, but somewhere there's a meaningful lower level of software within the system.
> Because any other solution will quickly run into chicken-and-the-egg style problems.
No. The kernel must provide for context switching. It would be like migrating threads IPC, but one-way. No threads, no scheduler, no dedicated data transfer. In other words, the bare minimum necessary to make a sensible abstraction around switching processes.
seL4, according to its developers, is not absolutely a microkernel. I believe the rationale mainly points to the in-kernel scheduler, but seL4's IPC interacts with the scheduler and is noticeably more elaborate than a mere context switch. Even if seL4's IPC is, by most standards, minimal, I do not consider it to be so objectively. I described a meaningfully more minimal alternative.
Delegating scheduling to userspace is trivial. If necessary, designate a scheduler to run if no scheduling decision is available. It has been done before, and the only usual objection is performance.
> No. A syscall is usually defined as a call to a ring one level in from the one where you currently are. But lots of things that are syscalls right now do not necessarily have to be.
Just as hardware interrupts can be abstracted into messages, syscalls can be abstracted into messages. I'm not saying that the hardware implementation directly conforms to the abstraction.
That's just a choice and no you are incorrect. It is perfectly possible to deal with hardware in managed languages.
> Sure, you could split device driver implementations and so on, but somewhere there's a meaningful lower level of software within the system.
This is optional. Been there, done that. Many times.
> No. The kernel must provide for context switching. It would be like migrating threads IPC, but one-way. No threads, no scheduler, no dedicated data transfer. In other words, the bare minimum necessary to make a sensible abstraction around switching processes.
Yes, IPC is context switching. Timer interrupts can cause extra context switches.
> seL4, according to its developers, is not absolutely a microkernel. I believe the rationale mainly points to the in-kernel scheduler, but seL4's IPC interacts with the scheduler and is noticeably more elaborate than a mere context switch. Even if seL4's IPC is, by most standards, minimal, I do not consider it to be so objectively. I described a meaningfully more minimal alternative.
It is a pretty poor implementation in my opinion.
> Delegating scheduling to userspace is trivial. If necessary, designate a scheduler to run if no scheduling decision is available. It has been done before, and the only usual objection is performance.
It's trivial, except for the little problem that your userspace program can be killed and then you have no scheduler.
> Just as hardware interrupts can be abstracted into messages, syscalls can be abstracted into messages. I'm not saying that the hardware implementation directly conforms to the abstraction.
Ok. If you want to use a different description of the term syscall than is common then that's fine but you should define that up front. Your definition of a syscall simply does not match mine.
Perhaps we're using the words in different ways. What I mean is: in order to interact with something, it must either be done directly or through abstraction. If abstraction is used, it must be realized without itself. A language such as Java or C#, at least for performance, must expose some things on a lower level that the runtime normally abstracts away. (Or, say, the OS memory manager can't use virtual memory before it enables virtual memory.) Technically, the lower level could be programmed with a dialect of Java/C# or something like that, or there may be another level of abstraction that hides the details (a compiler generating code in the background, for example). But something has to be the first step at the bottom.
> Yes, IPC is context switching.
(Considering the diversity of how OSes approach context switches/IPC, I use "context switch" to mean "CPU mode/address space switch", more or less.)
Usually, IPC is context switching and overhead. seL4's IPC has overhead involved with scheduling, threads, and message buffering. My design isolates the context switching, which is common to all IPC designs.
> Timer interrupts can cause extra context switches.
I don't see what your point is. I assure you that my system is not uniquely fragile to timer interrupts.
> It's trivial, except for the little problem that your userspace program can be killed and then you have no scheduler.
Then don't let the scheduler be killed. What is the difference between a privileged userspace program and a kernelspace program? Isn't that what microkernels demonstrate? Again, this has been done before.
> Ok. If you want to use a different description of the term syscall than is common then that's fine but you should define that up front. Your definition of a syscall simply does not match mine.
It wasn't quite a definition, though I did write poorly. In the abstract, a syscall can be seen as a message. If IPC ("to a process") is done by putting some data somewhere, setting a few registers to special values, and executing a magic syscall instruction, then a normal syscall can be considered IPC "to the kernel".
Saying "There should be no such difference." is a bit like saying bicycles should be allowed on the highway and semi trucks should be accepted on walking paths. The difference is inherent in the thing. A result of how they were built. And what they can and can't accomplish as a result.
You could have made that point without the strawman, and what a ridiculous thing to say anyway.
> The distinction describes how the language has been implemented, which is based on the choices of the language authors alone, and is usually down to practical considerations about how to implement the thing at all.
That is so obvious I do not understand what point you are trying to make here.
> Saying "There should be no such difference." is a bit like saying bicycles should be allowed on the highway and semi trucks should be accepted on walking paths. The difference is inherent in the thing. A result of how they were built. And what they can and can't accomplish as a result.
No, the error is yours: you are interpreting my sentence in a way that is blatantly wrong and then argue with the outcome. I'm not saying that there shouldn't be 'trucks or bicycles' in terms of programming languages. What I'm saying is that the boundary between where you use a 'systems programming language' and where you use an 'application programming language' is artificial and that we are using too much of the former in a place where we probably should be using the latter.
Well, everything humans have ever created is artificial, so I'm not sure even how to parse this sentence. There are objectively tasks which can be accomplished with systems languages which cannot be with others. That is an indisputable fact, and not determined by anything but the language specification and implementation.
> we are using too much of the former in a place where we probably should be using the latter
If I loosen this sentence to mean that we should write more code in languages which have the familiar properties of applications languages - memory and thread safety, low boilerplate, helpful tooling, reduced cognitive load - then I can agree with it. Otherwise it seems an opinion without reason.
Where that code needs speed, uninterrupted control of execution, to be embedded, target wasm, or might want to do any of those things in the future, I think Rust is an excellent choice which provides many of those benefits.
I'd rather not have runtimes sneaking into low level performance critical areas like kernels and drivers. But some folks are into that sort of thing - Microsoft famously worked on https://en.wikipedia.org/wiki/Singularity_%28operating_syste... in C# but then again C# has very similar memory semantics to Rust. Microkernels seem to work well enough, with several extant examples. Anyway, you do you! I look forward to ogling your OS project from the sidelines.
> If you ever wonder why people get a bit tired of Rust advocates, this is why.
I'm no Rust advocate. Just a developer who's worked in the languages being discussed, who is happy to talk about them. If I'm an advocate for anything, it's more sophisticated, powerful, safer, better tooling, in every language. I appreciate it when any of them make advancements.
Have fun! Toodles!
Every task does not need speed and safety. Therefore, "everyone" doesn't need Rust.
But I could easily see a future where C++ is relegated to legacy language status. It has already had decades of garbage-collected languages chipping away at most of its general-purpose uses, but Rust seems capable and in a position to take away most of its remaining niches.
It's kind of why the old C++ programmer that I am decided to learn Rust in the first place - seemed like a good idea at the time to skate where the puck is heading.
Yes, agreed. My prediction is that the replacement is a friendly language that makes Rust's ideas ergonomic to use.
Is your claim that the borrow checker is the problem? That’s a really difficult design space to beat Rust in:
1. Shared mutable data structures to allow high performance code
2. No GC to allow high performance code
3. Non lexical lifetimes to allow flexibility in memory ownership for expressivity and performance (ie you can’t restrict allocations to never escape a lexical lifetime)
Capability based systems might avoid the Rust borrow checker but they’re even more verbose with annotations and complex than Rust.
Effects systems show some promise but not yet proven they can actually be a general purpose language like Rust (ie do the ideas scale well to multiple different problem domains).
Anyway, there’s lots of alternative ways of designing languages but they all come with undesirable tradeoffs. Would be better if you actually made some concrete proposals that Rust gets wrong rather than “it’s too complex - they should have made it simpler” without taking any position - always easier to critique from the sidelines without making a proposal of your own that can be critiqued.
You are right. My prediction is some new language does it in 5-10 years.
> identical to the critiques people have about IPv6
Maybe. With the difference that IPv6 gets out of your way when you do not want the advantages the new tech brings. (Rust? Noo.)
Even with that it is barely at 50% adoption after 20 years. https://www.google.com/intl/en/ipv6/statistics.html
Rust is literally the fast stab at making a memory safe systems language.
Do you really think we are never going to be able to design a better one?
Graydon started developing Rust in 2006, 20 years ago. The guy is somewhat famously a compiler buff with experience in a half dozen of them. What about that comes across as a "fast stab" to you?
I didn't want to learn C++. I learned it anyway because it was the best in the niches I was interested in, as well as there being existing programs that I wanted to contribute to that were written in C++.
I can state for a certainty that if I had waited around for the perfect language before making the leap, it would have negatively affected both my career and hobbyist dalliances.
Did I mention waiting around? You should always use the right tool for the job, and right now if you need a memory safe systems programming language for a greenfield project, Rust is the tool.
My point is that Rust's ergonomic issues will see it replaced when someone figures out a way to net similar advantages in a friendlier language.
It's amusing to see strong beliefs on this being impossible!
Exactly right