I never saw the need for it, really. I just stuck with the language constructs in C++ that makes it basically impossible for memory leaks/use after free/use outside of bounds to occur or if they do occur, explode loudly in debug environments. I haven't used new/delete in a personal project in like a decade; longer than Rust has been around.
Now I'm working on a project where the main developer is...a cowboy. Everything is insane. Not only is new/delete everywhere, but so is malloc/free. In C++ code. You have to navigate to where the thing is allocated to figure out whether to use free or delete. Everything leaks, everything crashes, everything races, everything deadlocks.
Oh and the guy is my boss.
So on the one hand, rust might fix these problems, on the other hand, rust is a non-starter.
So now what. I still don't need rust because I don't code like a crazy person. And the people who do code like crazy people will never use it anyway because it doesn't let them...express themselves.
One positive aspect I see about "rewrite it in Rust" is that you can to some degree expect a random Rust project to not leak and crash and expose vulnerabilities quite as much as a random C++ project. It's silly, but "written in Rust" acts somewhat as a badge of safety and performance, whereas "written in Java" and "written in C++" each only carry one of the two.
Of course, developer skill is also huge. You can write slow Rust code or fast Java code, yadda yadda.
How do you want to play this boss? I can see you’re focussing more on the bigger picture and the code is just a means to an end, given that, how about you delegate technical leadership on the codebase to me to let you focus on the product & commercial aspects?
If no - maybe words come back to the effect of “it’s my pet/child, how very dare you” yadda yadda - then I can’t see a happy path beyond “thanks for the opportunity, all the best for the future” but there’s certainly room for many mediocre outcomes short of parting ways if that’s preferred.
If yes - “hey, we don’t have infinite money and we prob can’t afford to hire the help to do this to a gold standard so how about we agree these commercial milestones (once change delivery falls below X days on average, or once feature Y that you always wanted lands in prod, or once the defect rate drops below Z per week etc etc) - I get bonus $$$”
Rewrite it in C#. It’s safer than Rust, because VM. Both standard and third party libraries are often way better. With modern versions of the language, GC allocations are avoidable if that’s what needed for performance reasons. C interop is equally simple.
You can also enable the warnings/errors that complain about missing using declarations for class and structs with the Dispose pattern.
This reduces the expressive power of the language, for example LINQ from the standard library is probably out because based on the delegates which require memory allocations.
Still, the language is very usable even without GC allocations. For example, that library re-implements a subset of ffmpeg for Raspberry Pi4 running 32-bit Linux, with no memory allocations in runtime: https://github.com/Const-me/Vrmac/tree/master/VrmacVideo
By design, Rust requires unsafe code to implement any non-trivial data structures (except trivial POD types). This applies to both Rust standard library, and third-party crates.
The issue is not a theory, security bugs actually happened in reality. Here’s an example about the Rust standard library: https://shnatsel.medium.com/how-rusts-standard-library-was-v...
By contrast, thanks to the VM and the GC, C# allows to implement very complicated data structures without any unsafe code or unmanaged interop. The standard library is also implemented in idiomatic memory-safe subset of the language. For example, here’s the hash map: https://source.dot.net/#System.Private.CoreLib/src/libraries...
> falls to prevent null pointer exceptions and modified collection exceptions
Yes indeed, but these exceptions are very unlikely to cause security bugs in the software.
According to bing.com chat, https://github.com/dotnet/runtime has 3.5M LOC, and https://github.com/rust-lang/rust has 6M LOC. The right panel of https://github.com/dotnet/runtime says 80% of the .NET runtime is written in C#.
This makes me wonder, do you happen to have a link for your “vastly outweighs” statement?
Now if we remove in tree tests from the totals, we arrive at 1.5M lines of C++ (most tests are written in C# as you would expect) and 1.7M lines of Rust.
However, this does not exclude safe Rust code. I don't have a tool off hand that can provide a precise count of lines of unsafe code but we can get some general estimates. There are 1958 instances of "unsafe fn" out of 103,205 instances of "fn ". Further there are 11,545 instances of "unsafe " in the Rust repo while there are 10,768 instances of "unsafe " in the runtime repo.
Given that unsafe functions comprise less than 2% of all functions in the Rust repo, I think my claims are reasonable.
Also plenty of crates are bindings to C and C++ libraries with nice unsafe blocks.
Then was that Axium drama.
The point is the "look at what I say, not what I do", when talking about safe languages and dependencies into C and C++ libraries and compiler toolchains.
When Rust gets fully bootstrapped in self hosted toolchain you'll have a point.
That seems like a gross overstatement.
https://github.com/rust-lang/rust/blob/master/library/std/sr...
CTRL-F: unsafe
Only one result, an optional utility function: "pub unsafe fn get_many_unchecked_mut"
There's far more unsafe there: https://github.com/rust-lang/hashbrown/blob/f2e62124cd947b5e...
Regarding "rewrite in Rust", I believe that it's a smaller part of the appeal of Rust than it appears based on the amount of code actually written: the main excitement is from people who never really ventured from heap+gc languages into manual memory management but who love the idea of being able to write gc-less native code. And many of them would rather write it slowly, one borrow-check at a time, than with all the memory bugs they created dipping their feet in naive C/++. Those people rarely write much Rust, but their excitement infects some in malloc/free land and for them a rewrite is super attractive because it skips the entire explorative part of software development that is really not the strong point of Rust.
(yes, this is pure projection, not only was I describing what draws me to Rust, I also failed to pick up on the codebase of my previous boss, too much "how can we make this entire thing less slow and error prone" and too little "cram in requirement X, even if it might turn out to become requirement Z because it pushed the codebase over the edge to terminal unmaintainability")
This is what keeps me orbiting but never quite landing on Rust. Things I'm pretty damn sure are safe come back red-stamped, and once I've painted things the way Rust deigns, I find I've lost my appetite. When desire finally returns, I find the fixes I've made to enable compilation also disallow the changes I wanted in the first place.
For all its faults and runtime bloat, I find myself going for Go on new things I would've attempted in Rust before. Though, I've never tried Zig; I wonder how that is...
But if it's not software that needs to go fast, then Rust is not adding anything for you. And if you considered Java to begin with you probably don't need top tier performance.
On the other hand I would forbid an average developer the use of c/c++ and similar. It is just not worth it.
i'd recommend embracing hungarian naming (if you call it apps-hungarian you've not embraced it fully enough, read older documentation) and be as rigid as rust about using it. i-, c-, p-, and Max are your best friends, don't declare one of those without declaring all the others that you will need.
using hungarian, adopt naming conventions about Open/Close, Init/Finish, Alloc/Free, Start/End, Alpha/Omega, or whatever, and apply them rigidly to any class/struct (with indentation that's obvious). When you put in an Open, you go and put the Close in at that moment, just as you would close any paren you opened. don't return from all over the place in a function, goto endblock and free what needs freeing, you need to work on autopilot not thinking through the twists and turns to play monte carlo with getting it right. If you are going to return an allocation, your name needs to be Open, Init, etc. Use whatever words you want, but it's got to be something that makes you as the caller think "this is an open paren, i need to close it"
using ifdef debug type mechanisms, hook malloc and free and put guard word asserts at the beginning and end of every allocation, and magic number ids and reference counts in all structs, and hook main() and exit() so you check those for leaks. all. the. time.
work in a systematic way that avoids problems and make that your priority, everything else like functionality is slaved to that.
How do you "hook' a function, like you said about malloc,free, main and exit?
I guess it means intercept calls to it and do something additional to what it already does, something like Python decorators, but how do you do it in C or C++? I used C a lot (but not C++), but much earlier, and don't remember any method of hooking. atexit(), or something like it?
for normal/average operating environments, I prefer to just indirect through an extra procedure call, gives you a place to put a breakpoint, and then if you need streamlined performant code you can #define it all away. But, if you plan to #define it all away, make sure that is a regular part of your work flow beforehand. you can have a procedure version, a heavyweight define and a lightweight define (and use your defines inside your procedure to test them there). On a regular basis you need to make sure your infrastructure is all doing what you think it is.
Same is true for main(), use it for setting up infrastructure, have it call Main() which is "your main".
if you are on a large enough project you can budget time for tools, the actual "__main.asm" code that calls C main() is also accessible and you can hook away in there too. Have to do it for all compilers, and track compilers, but on the scale of a large project, that's not the end of the world.
Zero memory safety mistakes is a tall order. But the overwhelming majority of memory errors we see in the wild can be easily prevented by good practices. And for the memory errors that do happen stack protection flags make a big difference (https://developers.redhat.com/articles/2022/06/02/use-compil...).
At my day job, I work on a project that's 75% C++ and 25% C#. In the code that we've shipped, when there's a crash in code that I've written, (as opposed to business logic bugs) it's usually a memory safety mistake in C#. There was an interesting architectural choice written a decade before I joined the company where most classes have a synchronous constructor, then an asynchronous initializer, then an asynchronous uninitializer, then a destructor that gets run by the GC. There's no end to bugs relating to crashes because the initializer hasn't been run yet or the uninitializer has already been run.
When I get C++ bugs put across my desk in code that we've shipped to customers, it's usually a bug somewhere else. For instance, the last crash dump that we've gotten from customers in C++ is because a we called a Windows UTF-8/16 conversion function. The Windows function itself is all raw pointer nonsense, so we put a pretty wrapper around it so you put a std::string_view in and get a std::wstring out, or you put a std::wstring_view in and get a std::string out. Well, it turns out Windows is a fucking dogshit operating system. If you initialize a thread "wrong", ie, by calling the C11 function thrd_create, and your locale is set to a CJK language, it will ignore you when you tell it the code page of the multibyte string is UTF-8 and will assume it's the system locale's code page, and will call abort() when it hits a UTF-8 sequence. (instead of perhaps returning an error or null pointer) Those are the sorts of C++ crash bugs that I deal with.
My 'secret' to dealing with memory in C++ is to not deal with it. Make the STL do everything. Make RAII do everything. Can this object be POD with constexpr accessors? Do that. Can these methods be const? Do that. Can these objects live in a STL/boost container? Do that. Do they need to live in a unique_ptr/shared_ptr instead? Fine I guess but it would really be better off as a value instead. Does this class need to have a special destructor/copy constructor/move constructor? Find some other way to do it. If you must, try to find some other way to do it anyway. If you must, spend like 10x as much time scrutinizing it, the way a Rust programmer would do with unsafe. If thing must have special destructor/copy/move constructors, factor out all of the things that need special consideration into a class with the barest minimum. If it must have a special destructor, explicitly delete the copy/move constructors/operators if you can. Avoid indexing into arrays; use range based for loops, or <algorithm> stuffs like transform, reduce, or transform_reduce. The solution isn't to use vector::at() (which does bounds checks) instead of vector::operator[] (which doesn't) the solution is to not use indexes at all.
You become the boss or responsible for some product. If you decide at the beginning to start off using Rust or rewrite some ancient, unmaintained library using Rust, then you prevent people like your cowboy developer to mess stuff up with this category of faults.
Either you would not hire him in the first place because he never came to grips with Rust so you only end up with developers on your team that understand and actively chose the tradeoffs that Rust offers.
Some rather "questionable" C/C++ code can, and are missed during code review sessions. One could put the blame on code reviewers, but let's be honest, code reviewing is a mindnumbing task for many of us.
This is not a foolproof argument because safety problems can easily result from unforeseen interactions among parts of the code that all seem to be OK and not "crazy" locally. A significant benefit of something like the borrow checker is that it can suss out these problematic interactions in a comprehensive way, even at the cost of forbidding some code patterns that would be perceived as safe in most cases. (Idiomatic use of Rust then requires you to either rewrite such code or stick it in an unsafe block and figure out what the preconditions are for it to be used safely.)
use std::sync::Arc;
use std::thread;
use std::time::Duration;
fn main() {
let shared_data = Arc::new(42); // Create an Arc
let weak_ref = Arc::downgrade(&shared_data); // Create a Weak reference
let thread_handle = thread::spawn(move || {
// Simulate some work
println!("Thread Data: {}", shared_data);
thread::sleep(Duration::from_millis(10));
// The Arc is dropped at the end of this thread
});
// Give the other thread a little time to start up (this is part of the race condition)
thread::sleep(Duration::from_millis(10));
// Try to upgrade the Weak reference and unwrap directly in the print statement
println!("Weak Data: {}", weak_ref.upgrade().unwrap());
// Wait for the other thread to finish
thread_handle.join().unwrap();
}The fix is trivial: just use an Arc instead of a Weak. In addition, `upgrade().unwrap()` should be a sizable red flag (like any unwrap, really) since fallibility is kind of the entire thing of Weak.
But Weak can be needed in case you have self-referential structures, right?
I was wondering about this in the context of the C++ programmer in post above who likes to use both new/delete and malloc/free in his code.
Sure, Rust will give him far fewer ways to screw up. But if he can't be asked to at least not use malloc/free in C++, he probably won't be too careful about unwrap() either?
My point here is mainly that a good programming language won't make good programmers out of bad ones.
Well, technically, no, you can just use Arc and leak memory all over the place :^) but that's not very useful either. Most self-referential structures need some kind of "owning" thing too though. For example, a graph could use Weak for the edges, but in order to keep vertices alive you need e.g. a Vec<Arc<Node>>. Then, if upgrading fails, you know that your edge leads to a deleted vertex (and as such should be considered nothing, hence Option).
> Sure, Rust will give him far fewer ways to screw up. But if he can't be asked to at least not use malloc/free in C++, he probably won't be too careful about unwrap() either?
There's one big advantage of messing up with unwrap vs malloc/free: unwrap 'only' crashes your program (and can even be caught in some scenarios), whereas use-after-free can do literally anything. Unwrap is also much easier to debug because you usually get a clear stack-trace, and the program semantics have been well-defined at all points.
> My point here is mainly that a good programming language won't make good programmers out of bad ones.
Very true! But I think the point of Rust advocates tends to be more along the lines of "a bad programming language makes a bad programmer out of a decent one". It is very easy to misuse C++, so mistakes are made with it way more often. 'Dangerous' constructs do exist in Rust, but are generally far less pervasive, making it easier for the programmer to understand what they are doing.
Saying Rust has no benefits over C++ because you can mess up in both is like saying that anti-lock brakes are useless because you can still understeer by pressing the accelerator too hard (sorry for the car analogy, I had to). Preventing entire classes of mistakes is valuable, even if other classes are still possible. (If it's worth the cost is of course another question. My personal opinion is yes, although that's especially for memory safety and a bit less for race safety.)
Sorry, that wasn't my intent. It was more to point out that bad programmers are surprisingly inventive when it comes to write bad code. New languages are an improvement, but not a panacea.
I don't want rust for memory safety. I want it for things like proc macros, a sane module system, a good and accepted error handling system, destructive move, constrained generics, unified static and dynamic polymorphism, language level customization points, and many more things.
While I’m not yet out to rewrite all the things in Rust, I understand the appeal of rebooting archaeological projects with a modern approach.
I can have a proper type system like an ML, and touch the metal like C? Yes please.
Rust saved some of us from a lifetime of exactly the problems you have with Rust, maybe let us have this one :)
For me there are three reasons why Rust is suitable for many usecases:
- performance
- safety
- static binary compilation with targeting different cpu architecture
After spending majority of my time with Python and Java in the last 10 years these are things i really learned to appreciate.
The only reason left to really use Rust is safety.
Speaking as someone who is learning Rust and really liking it, I just want to note that this comment is sort of emblematic of what is wrong with the Rust community. It comes off as pretty condescending—"Rust is for the people who are smart and don't give up easily, if you're less smart or give up easily you should really go find a language for wimps".
I ended up jumping over to Zig and have been really enjoying it. I ported the same hobby 2D game engine project from C++ to Rust, and then over to Zig. A simple tile map loader and renderer took me about a week to implement in Rust and 3 hours in Zig. The difference was a single memory bug that took 15 minutes to figure out.
I've found that data structures with _lots of helper methods_ (thinking about things like `Result` in particular) tend to be nice. You do have to learn about Rust-specific things to figure out how to nicely structure your code for everything to work. But the payoff is less pain.
There are still a lot of futzy ownership questions, and even when you write out your supposedly performant and cool system, easy outs like cloning end up hiding in your system leading to some awkward performance questions. Fortunately the systems are merely slow, and it tends to show up in profiling.