It can do (roughly) everything C can, i.e. it's natively compiled, gives control of memory layout, and doesn't depend on a GC runtime.
Previous C killers were either dependent on a fat runtime (which isn't a big problem in general, but is a problem for some of niches dominated by C), or didn't offer meaningful improvement in safety/concurrency/expressiveness.
* "First viable replacement for C" - C++ was invented decades before Rust.
* "Well-written C programs translate almost 1:1 to Rust" - C programs can be compiled with minimal changes as C++.
* "or didn't offer meaningful improvement in safety/concurrency/expressiveness." - C++ offers major improvements in all of those areas.
A lot of C programmers don't want to switch to C++ and they won't want to switch to Rust either, because they favor simplicity and neither Rust nor C++ are simple. They'll probably switch to golang for some stuff if anything and just stick to C for the rest.
Rust is complex because it's powerful.
C++ is complex because it's a mashup of overlapping features from different eras.
I don't write C++, because whatever I write will crash for subtle reasons and I'll be told it's my fault, because of course you don't do this when you use that feature.
I can write Rust with all its complexity, and as long as I get it to compile, I'm confident it works more reliably than any C++ that I could have written.
Note: Rust executables can be trimmed down by stripping them (not done by default, even for release builds) and using libc malloc instead of jemalloc. But even then, C wins by a lot.
Relevant blog post: https://lifthrasiir.github.io/rustlog/why-is-a-rust-executab...
C links dynamically to its libraries. Rust does not (by default).
This overhead is a constant overhead so it rarely matters. When it does, you can strip it down further and get it to the same level as C.
This is not evidence of a fat runtime, just a different default compilation strategy that prodces larger binary executables. jemalloc is (an optional) part of the runtime, but the rest isn't.
I suspect rust hello world using those libraries is due to a lack of LTO? Hello world doesn't need any of those.
You'll get different results if you compile them with musl for truly static and fair binary size comparisons.
if I remember correctly you should compile with --nostd flag and provide _start function for loader to load your executable!
you can do same with rust too (But I think you should provide a special flag for compiler). You can find all of it on documentation.
I've written C-like programs in Rust, and that goes very well. But I really like function overloading, generic operators that I can overload from the left and the right, integer parameters for my templates/generics, copy semantics as the default (opt-in for moves), placement new, explicit destructors, and maybe a few other niceties. Rust does none of these things the way I want, and so I'm stuck with C++.
There are a few other things where I think Rust just made the wrong choices and didn't improve over C++. I've always hated the dichotomy in C++ strings between const char* pointers and an actual string class. Rust had the chance to make strings feel as comfortable as integers, but instead they introduced their own dichotomy with String and &str.
Speaking of integers, Rust's choice of 32 bit integers as the default for literals is painful given that every machine most of us will ever care about is now 64 bit. I routinely deal with arrays and files that are too large for a 32 bit integer. This means I have to remember to suffix all of my integers in for loops etc... C++ has this wrong too (backwards compatibility), but Rust had a chance to make a clean start and did not.
It makes perfect sense when you understand the differences and reasoning behind it. A `String` is a heap-allocated string that can grow in size. On the other hand, a `str` is basically a fixed-size string array, but you'll never interact directly with this type because there's no point in it. It's tucked away inside the `String` that created it.
Meanwhile, an `&str` is a slice of that fixed-size `str` array that's hidden within the `String`. You can think of a `str` as an `[char]` with it's own special `str` methods, and an `&str` as an `&[char]` which has access to all of the `str` methods as well.
> Speaking of integers, Rust's choice of 32 bit integers as the default for literals is painful given that every machine most of us will ever care about is now 64 bit. I routinely deal with arrays and files that are too large for a 32 bit integer.
I'm pretty sure the default integer is a `usize`, which is 32-bit on 32-bit systems and 64-bit on 64-bit systems.
Rust will type error if you try to use u32s with an array so it's all good though. Just means that you need to specifically `: usize` things.
If you need an integer type for counting stuff, u32 is fine. If you actually need to talk about memory, use usize. The compiler will force you to do it.
Maybe I'm missing something, but it seems to be i32: https://play.rust-lang.org/?gist=c79d4afef7fa20c81ba14de61f5...
Arrays are indexed by usize, so if you're on a 64-bit machine, then you shouldn't need a cast. It's _unconstrained_ numbers that default to i32, not anything without a suffix.
I would prefer a str be a str be a str, regardless of how you got it. Lowercase type-name and fundamental like an integer.
I'm fairly certain I understand why Rust made the choice they did. I've read the forum threads at HN, Reddit, and users.rust-lang, and I've seen previous replies by you and other Rusties, so I hope you won't try to educate me about the performance advantages of having slices as references and another string type as an ownership class or why we need OsStr and friends.
If strings are the fundamental processing concern in your application, then I think you should be able to opt-in to that kind of micro-optimization and complexity, but it would've been better to spare the rest of us who have different concerns. I don't want to become a string expert to build a filename, and the default implementation could (at least conceptually) be always on the heap for all I care. Go one step further and implement the "small string optimization" (Alexandrescu's fbstring), and you'd probably get back most of the performance without nearly the complexity.
> Arrays are indexed by usize
I've spent the last half hour trying to troubleshoot why this line hangs:
let aa = vec![1u8; 10e9 as usize];
However, I'm at home using the Ubuntu under Windows thing, so maybe there's some bad mojo between Rust and my less than usual setup. (If you're interested, I have 32 Gigs of memory, and the equivalent C malloc and memset code runs just fine, so I don't think Rust is doing the right thing here...).Anyways, I wanted to test the commented out line, but I'm stuck for now. If that line would work, I'll admit I was wrong, but I think the uncommented line shows a similar complaint.
for ss in 0..63 {
//print!("{}\n", aa[1<<ss]);
print!("{}\n", 1<<ss);
}
> It's _unconstrained_ numbers that default to i32, not anything without a suffix.Looking at the present and the future, why is that a sensible default? Both x64 and ARM are going to use a 64 bit integer register for the operations, and many of those operations are going to be 1-clock throughput. You can probably find a counter example, but 32 bit integers aren't generally faster than 64 bit ones.
The lowercase type names are reserved to primitives. The String type is not a primitive but a comprehensive data structure, hence the capital S. The String type contains an `str` primitive though, along with size information.
> I don't want to become a string expert to build a filename, and the default implementation could (at least conceptually) be always on the heap for all I care.
Is it that hard to understand that when you create a string, you will create it as either a `String` or `PathBuf`? File methods are designed to automatically convert input parameters into a `&Path` so it doesn't matter what string structure you provide.
There is also no way (currently) to create a stack-allocated string with the standard library out of the box. You can do this with crates like `arrayvec` though. It's very much opt-in for that performance.
let path = String::from("/tmp/file");
let mut file = File::open(&path).unwrap();
> Looking at the present and the future, why is that a sensible default? Both x64 and ARM are going to use a 64 bit integer register for the operations, and many of those operations are going to be 1-clock throughput. You can probably find a counter example, but 32 bit integers aren't generally faster than 64 bit ones.
No need to use a 64-bit integer when you only need a 32-bit integer. You can fit two 32-bit integers into a single 64-bit integer and perform a calculation on both simultaneously with a single cycle, versus spending two cycles to calculate two 64-bit integers. There's also no need to pay that memory cost either.
No, I'm interested in what tradeoffs you would have made differently. I now understand. Thanks! (I disagree, but at least I understand.)
> why this line hangs:
It compiles and runs effectively instantaneously for me on Ubuntu under Windows as well, so that's very strange. Maybe file a bug?
> I think the uncommented line shows a similar complaint.
Yes, there's no constraint on that literal, so it's going to be an i32. When we made this decision, we did some analysis, basically no numbers in real world programs weren't constrained, it was often tests, toy programs, and documentation. It should be a rare thing. YMMV.
> why is that a sensible default?
Your assertion about the speed was the opposite of what was asserted while we had the discussion, basically. And not everybody is running on 64-bit hardware, so it's a broader default.
Follow-up: I tried it with -O (don't know why I didn't think of that earlier), and it runs fine. So maybe the debug version is just generating terrible code, initializing by iterating through 10 billion bounds checks or something?
Anyways, more importantly, it works as I would like and does not behave as a 32 bit integer. I think I understand what you mean by "constrained" now. And clearly, I was wrong.
However, if most un-suffixed integers in real-world programs will become constrained (as you claimed), this further confuses me why i32 is the unconstrained choice. It doesn't seem like something so rare could be enough of a performance problem to justify being anything but the largest supported size.
I remember installing it by cut and pasting one of the "curl ... | sh" commands there.
> Nothing about that code should be doing bounds checks, as it's just allocating an array.
I didn't dive into the macro definition for vec!, but I assume there is a loop in there to Copy the initialization element 10 billion times. I think you guys do bounds checking on the lower level reference to a slice that Vec uses. But I really don't know. If it's not that, then it was hanging or spinning doing something else. (Debug version of your memory allocator?)
The fact that C and C++ compilers generally chose to leave int at 32 bit on 64 bit platforms, combined with the standards requiring "usual promotions" for smaller types to go to int bites me all the time. I'm very happy that Rust dodges the promotions problem altogether, and I'm sorry if I'm wrong about the array subscripting thing (does the snippet I provided panic at 1<<32 or 1<<34?).
> And not everybody is running on 64-bit hardware, so it's a broader default.
That argument could be used to justify 8 or 16 bit integers... :-)
Overflow is a "program error", and in debug builds, is required to panic. In other builds, if it does not panic, it's required to two's compliment overflow. Rustc currently just overflows, but in the future, we'll see.
That's true except our 16 bit support is nonexistant at the moment :)
If you pay attention to your usage statistics, I'll bet you drop 32 bit support before the May 2025 deadline we were discussing in the other thread.
I do too, and I'd like to think that rust needs all those things to be a replacement, but realistically I think that the only killer feature that rust is still missing is reasonable interoperability with C++ (which admittedly might require implementing a few of those features).
Disclaimer: I have been following rust since Graydon initial announcement, but I have yet to write a single line of code in it.
If you're stuck with C++, might as well do it safely:
(shameless plug) https://github.com/duneroadrunner/SaferCPlusPlus
I might be wrong though. What happens with your vector in this code?
using namespace mse::mstd;
vector<double> data(10);
double& dangling = data[0];
data.resize(100000);
double crashing = dangling;
You use a lot of typedefs, so I couldn't tell for sure, but I think your operator[] returns a C++ reference right?The problem here is there is only one operator[] for both reading and writing. This is a simple contrived example, and taking a reference like that looks artificial, but there are a lot of other ways in real programs to stumble on to this. (I don't think it's as bad as the Rusties do, but I stumble into this bug once or twice a year...)
The Rust folks seem to believe you need a borrow checker to solve this problem, but I think that a different container library in C++ could do the trick. For instance, favoring value copies instead of references, and returning a proxy object from operator[] instead of a reference.
double& dangling = data[0];
you could make it auto not_dangling_iter = data.begin();
// not_dangling_iter += 0;
C++ references are the one unsafe element that does not have a "compatible" safe replacement. Unfortunately, you have to convert your references to pointers (or iterators). I don't think there is a way to create a "safe" reference with an interface compatible with native references. Apparently C++ will at some point add the ability to overload the dot operator, but I'm not sure that will be enough to be able to emulate C++ references.And while I can overload the & (address of) operator to "prevent" you from getting a native pointer to a "safe" object, I don't know if there's a way to prevent you from getting a native reference. If you wanted to somehow enforce a prohibition on the use of unsafe C++ elements (like references), that would probably require some sort of static tool that is not yet available. But should be fairly straightforward to implement, I think.
But if you just want some confidence in the safety of the code you write, it doesn't take much effort to reliably avoid using C++'s unsafe elements.
Oh yeah, maybe. But that would still require the ability to overload the dot operator, wouldn't it? And how would you know when to deallocate the proxy object? And presumably there would be some run-time overhead. Hmm, I don't know if it wouldn't be more practical to create a static tool (or "precompiler") to automatically convert references to (safe) pointers (or iterators).
mse::mstd::vector<mse::TRegisteredObj<std::string>> data(10);
data[0] = "some text";
mse::TRegisteredRefWrapper<std::string> dangling = data[0];
data.resize(100000);
try {
std::string crashing = dangling;
}
catch (...) {
// expected exception (not a segfault)
int q = 3;
}
Does that work for you? I'm not an expert on std::reference_wrapper, so I'm not sure when it can and cannot substitute for a reference. (Btw, if it's a little verbose for you, there are shorter aliases available. Just search for "shorter aliases" in the header files.)[1] https://github.com/duneroadrunner/SaferCPlusPlus#tregistered...
Yes, the language is supposedly not undergoing breaking changes without strict deprecation and feature-gating. However, as new features are added to the language, libraries are usually updated to leverage them, and suddenly you are also forced to keep up with that rapid release cadence to ensure your project still builds.
Meanwhile, C code from the mid-80s still compiles with modern compilers, often without any changes whatsoever, and without any feature gates.
Don't get me wrong. I like Rust and what the project is trying to do. But I see two major problems that are preventing me from using it for anything major at the moment: 1) the extremely rapid rate of change, and 2) difficulty of implementation (both of which, taken together, lead me to expect no serious alternative implementations any time soon).
Before I will be willing to adopt Rust as a C replacement in full, there needs to be a solid, stable language definition that compiler writers can target and programmers can rely on for a good chunk of time. I have a feeling we'll get there eventually once the number of new, useful features that are missing from the language starts to dwindle and it begins to stabilize naturally.
In the meantime, I'd settle for an "LTS" release or something. That could work.
Your dependencies don't upgrade themselves. Its true that if you upgrade your dependencies, they may depend on a more recent verison of rustc, in which case you need to upgrade rustc as a part of upgrading that dependency. But if you are using rustup this is not harder than upgrading those dependencies - probably easier in fact.
In exchange for downloading a tarball at most every six weeks, you get a language that is developing new features.
The actual Rust API has changed very little since the 1.0 release. All the changes that have occurred have been mere additions to the language -- nothing breaking. Hence why the 1.0 release was termed as a 1.0 release. There won't be anything happening that causes breakage until a 2.0 release.
You'll find that C compiler and library development is also rapid like Rust. There's new versions of LLVM/Clang and GCC being released on a regular basis. There are constant updates to musl and glibc. These changes tend to break far more often than Rust, which is developed in Rust and can be guaranteed to be much safer.
That Rust is being openly developed on GitHub with a significant number of developers, with an official compiler + library + doc + comprehensive suite of resources collectively being maintained, that's a significant incentive to work with Rust. There's a solid RFC process to prevent anything silly from entering the language. No random decisions by random people -- it takes a lot of convincing to add a new feature to Rust.
Anyway, I don't think that's a problem with my concept at all. What I want for Rust, and what I think will eventually be feasible for Rust as the language matures, is a standard that can be shared by compiler writers and Rust programmers. A standard is worthless if it's inadequate (see ISO Pascal or ISO Basic, for example), so it's a good thing that right now Rust is iterating quickly and improving rapidly. But eventually, there's going to need to be a stationary target for compiler writers to aim for, and for Rust programmers to be confident that the code they write today won't be littered with deprecated features, anti-patterns, and worst practices tomorrow.
Frankly, I don't see wide adoption happening in the corporate world until there's an LTS compiler at the very least.
What you're describing is the spec and not the implementation. An implementation would be glibc (GNU Libc with open source proprietary extensions) and musl (MIT implementation that strictly adheres to spec). These libraries are updated often.
Meanwhile, Rust offers more than just a specification. It offers an implementation. This is libcore-rust + libstd-rust. This is what receives updates each 6 weeks. It also offers a compiler that comes with it, rustc, which is also what receives updates each 6 weeks.
Rust strictly adheres to semantic versioning, so changes to the API are not allowed without a major version bump. Code that was written for Rust 1.0 will still compile on Rust 1.15. Code written for Rust 1.15 may not compile for a Rust 1.0 compiler though because bumping the second version implies adding new features. Rust doesn't yet utilize a patch version though. There's never been any critical issues discovered in each stable release.
> Frankly, I don't see wide adoption happening in the corporate world until there's an LTS compiler at the very least.
Can't say that I understand why a LTS compiler would be required for adoption. Many in the corporate world are already using Rust, either publicly as an official friend of Rust or privately in smaller projects. There's nothing a LTS compiler would provide that would be beneficial to the corporate world. Semantic versioning and the ease of managing Rust toolchains with rustup has everything pretty well covered.
As to my own concerns (rather than corporate concerns), here's where I'm coming from:
When I wind up choosing C for a particular project, I don't use it because I like it (I don't particularly like C). Typically, I use it because there are three factors I've considered: 1) This particular project needs the low-level access or performance characteristics provided by a systems language, and 2) C is such a language, 3) C is ubiquitous.[1]
The thing is, every platform for the last 25 years or so has had a reliable ANSI C compiler. If the goal of my project is to be cross-platform, then a carefully-written application in standard C is guaranteed to be able to reach the widest number of users, many of whom won't have to do or install anything to build my application, since many environments already include a C compiler without any additional installation necessary (think Unix, BSD, a shrinking number of Linux distros, ...). Using C puts my application on the path of least resistance for potential users.
Similarly, if I'm writing a library, every language with an FFI can trivially access a library written in C. Even without an FFI, most languages can interface with C code through some kind of bindings (like the Lua-C API, for example). Again, choosing C is a path to wide adoption.
Again, I don't particularly like C as a language. However, right now, it has an undeniable advantage as a platform. I do like Rust, however, and I would love to see Rust give C the boot. In order to do that, it needs to eventually (one day when the rate of additions/changes to the language has slowed and it has grown more stable/mature) offer a platform that can be relied on to "just work" on any given platform with minimal fuss and without any sudden changes. As such, both the "end-programmer" and the compiler-writer in me would like to see some version of Rust (again, eventually) that is "set in stone" and widely available, which really would solidify that notion of stability/maturity.
I'm not advocating for Rust to stop innovating. For all I care, the "standard" can be a simple snapshot of some state of the language, the way the Haskell Reports seem to be nowadays: GHC keeps on iterating and innovating the language, yet there's still a standard updated every x years that provides a static target. But for that designated snapshot, I want to see a compiler (or two, or several) and a bunch of libraries that are supported for the long haul.
It's simply a matter of "C has this, it's an important property thereof, and any replacement will need it, too".
[1]: Some other projects demand C because they constitute contributions or extensions of other projects that are already written in C; e.g., kernel code for an existing OS that is written in C.
The fact that someone says on HN "we're backwards compatible" doesn't quite offer the same level of trust as an ISO standard. Especially when there are some conflicting reports out there i.e. there are many super-awesome libraries that only work with nightly (I think it's called), because many of those maintainers are early adopters that love to try out new features.
Programming languages and libraries are building blocks, I don't want them to change under me in the middle of a project. This is why I dropped Swift, the 1.0 -> 2.0 migration was a very painful, and from what I hear recent major version changes aren't running smooth either.
This is totally orthogonal to whether or not Rust stable is backward compatible, which it is. People put a lot of work into making sure it is, and this sort of FUD is very frustrating to read.
Then you shouldn't take the word of a random person but from the Semantic Versioning guidelines. If you're not following the semantic versioning guidelines then you're doing it wrong.
> Especially when there are some conflicting reports out there i.e. there are many super-awesome libraries that only work with nightly (I think it's called), because many of those maintainers are early adopters that love to try out new features.
Totally irrelevant. Very few libraries, if any, require a nightly compiler. The libraries that do offer features that require a nightly compiler have those features as optional. Anyway, once the new Macros update releases, the need for that nightly compiler will go away. These libraries are just taking advantage of compiler plugins.
> In the meantime, I'd settle for an "LTS" release or something. That could work.
This is something we've discussed. I imagine it will happen. At first, I would expect six month LTSes, then longer ones as time goes on.
The company that employs me just upgraded to a C++11-capable version of GCC three months ago. Even C++'s three year cadence seems like it might be too fast! Thing is, the company wants to vet our entire platform, which itself only changes yearly. They want to know they can get bug fixes, and vetting/upgrading the compiler is a lot of work regardless, so they tend to do one and stick with it for a while. OpenBSD does the same thing for security reasons. If Rust wants to replace C and/or C++, these are things that will need consideration eventually. I don't see my company allowing Rust any time soon, unfortunately.
As a compiler-writer, I'm more concerned that Rust is a quickly-moving target at the moment. I think that as Rust becomes more mature, it will naturally begin to stabilize feature-wise, in which case something akin to the Haskell model would work well, with innovation happening in the flagship compiler and "snapshots" of the language being taken as standards periodically. That's just a matter of time, though.
And why should one invest time for such a low-level component in their toolkit every 6 months?! I want to build software and solve problems, not spend my time keeping up with programming languages and library updates.
You seem to come from an experience where software updates are kinda sorta mostly backwards compatible. Rust works very hard to actually be backwards compatible. There is infrastructure to check for regressions across the entire package ecosystem, which is run regularly on HEAD and on any commit we think might potentially be breaking before we even land it.
You say that like it's a good thing that with C, you can ignore 30 years of progress in software development. Colour me unconvinced.
That's not a responsible metric. 30 year old software was written against 30 year old APIs vulnerable to known attack vectors, and with 30 year old notions about security. The belief that this software is suitable for modern use is absurd IMO.
And if the types of programs you're talking about are small UNIX-like utilities, well then they're small and easily updated aren't they?
Lots of software we use day-to-day is old. For example, a couple years ago I fixed a 20-year-old bug in netcat, which itself hadn't been updated since the mid 90s. After I fixed the bug, I didn't need to fish out an old compiler or any compatibility libraries; I just compiled it with the latest GCC and glibc, and carried on with my day.
Don't get me wrong, I like Rust a lot and I'm excited that there's finally a real contender for the systems programming throne after all these years. I'm just less enthused about the prospect of refactoring my codebase once every few months to stay current. This is something that I think will level out with time, though. C itself was almost 20 years old before it was standardized, after all. I don't think it will take Rust that long, and I look forward to the day my copy of the Rust standard arrives on my doorstep.
Let's assume this is true, brand-spanking new software will be written against 30 year old APIs that have survived 30 years of attacks and audits, but will avoid the ones that have proven vulnerable.
> I'm just less enthused about the prospect of refactoring my codebase once every few months to stay current.
The perception that you need to do this is false. You might feel pressured to refactor simply because of new syntactic improvements that make code clearer (like switching try!() macro to infix ?), but that pressure is a fiction. There's no real need to do it.
Could you show me an example where post-1.0 you wouldn't be able to do this with Rust?
Except most people don't consider those as criteria at all. The only criteria considered are "does it run and produce the output I want".
What the rapid releases do affect is the rate at which new releases of libraries stop working on older compilers, which absolutely has to do with ignoring progress.
Or stop using other peoples' code and just use core language features and build everything from scratch. Which is what I end up doing, and it has some benefits. But it definitely slows you down.