I like Rust a lot, it deserves major credit for the first thing moving the Pareto optimum in awhile, but there are still glaring holes for low level stuff. No stable allocator API, no placement new, no not hacky way to allocate things on the heap without them going on the stack first, many traits still only work for tuples up to length 12, almost no operations are permitted on const generics, the list goes on. I’d love to stop using C++ but this stuff is making it hard.
Sadly, really low level stuff requires nightly today. It’s a shame. I think one alternative would be taking a page from erlang and publish versiones crates that have access to nightly features that will eventually land, but that have no finalized API. Only project crates could have that access. It’d be a middle step between what we have now, and std being distributed over crates.io.
For this only changing the global allocator is stable, but per container allocator is nightly, and would suspect will continue to be for the short term.
no placement new, no not hacky way to allocate things on the heap without them going on the stack first
Aren’t these the same thing? :)
As alluded in a couple of replies (including your oun) you can get the same behavior using MaybeUninit and for genericity you need macros. Between the two, you can do quite a bit when it comes to specific projects. But a language level solution is needed. The last effort found some tricky technical issues, sources of UB that weren’t anticipated (that I don’t recall now, and the author of that effort doesn’t either because it’s been so long 😅).
many traits still only work for tuples up to length 12
For a second I was gonna ask you to elaborate, but then I realized you said tuples and not arrays. For arrays impls are now generic over length, but as you say tuples are not (because we have no concept of variadics). This means that the only tool available to API authors, including the std, is using macros to enumerate every impl.
almost no operations are permitted on const generics
This is being actively worked on. The reason progress seems slow is because the resulting behavior is being fully specified, the implementation needs to be sound, and there can be no divergence between comp time and run time. (I’m not sure what the conclusion was with floating point, I think that might end up being the only possible source of divergence between one or the other, unless floating point operations are done entirely on software on some toplchain targets).
Agreed. You don’t need C++ CTFE often, but when you do, you really want it. No, typenum is not a good substitute. (Although typenum’s download number shows how much people want it.)
In C++ there are two forms of the new operator. The common one everyone is familiar with, new T(arg) which allocates memory on the heap and runs the constructor and the less commonly known placement new new(p) T(arg) which only runs the constructor, using the memory already available at p. It’s essential for implementing things like vector without unnecessary copies or moves. Since C++11 vector<T> has had emplace_back, which takes the constructor arguments needed by T and creates a T “in place” right where it’s going to live in the vector, without creating it on the stack first and moving it copying it in, which is all Rust can do. In Rust you can’t express emplace_back because 1) no placement new 2) no variadic generics so you can’t write the signature.
You can approximate the effect with MaybeUninit in Rust, but without variadics you can’t make it work generically, only in cases where you know the specific type ahead of time.
Placement construction is a bit of a controversial issue in Rust, there is quite a bit of bikeshedding. A few macros exist in the crates.io registry, some of them from people involved in Rust-for-Linux because they need that there, especially for big structs.
Atm the only downside of not having placement semantics is that you can’t construct a single non-referential structure larger than your remaining stack size, but this is something you hit rarely in practice (but triple as annoying when you do hit it).
edit: Though to clarify there, placement semantics are being worked on. People do want to have an option on how to construct something from a heap reference, ie, MaybeUninit without the unsafe parts.
Placement construction is a bit of a controversial issue in Rust,
I think it’s more controversial that Box::new([0i32; 1024 * 1024]) causes a stack overflow in a systems programming language, where “preallocate a big chunk of memory” is kind of a routine thing to do (yes, vec![0i32; 1024 * 1024].into_boxed_slice() kinda gets around this, but shouldn’t the obvious thing obviously work?)
but this is something you hit rarely in practice (but triple as annoying when you do hit it).
I disagree, I’ve hit this many times writing Rust for the last few years. In async it’s extra annoying because there’s no easy workaround, boxed futures must be allocated on the stack (and futures can be enormous!).
I don’t think it’s controversial that the example you mentions causes a stack overflow, but the solution isn’t something you can just slot into the language like that.
Just consider that Box is not a privileged type. It means that trying to do a placement new onto a Box isnt’ something the compiler can “just figure out”. The pointer type that’s subject to placement new needs to explicitly support it (not all pointers can just be used like this!). So there is some new traits that need to be worked out and stabilized that would enable placement new. Or, to name another example, Fallible Placement. The placement execution can obviously fail for a number of reasons (invalid data or OOM), so it needs the option to handle failure to place or allocate. Or how to do placement on types that are !Unpin.
And hence, it requires time. The original issues relating to this are from 2015, but the majority of the work was done in 2018 with most current work focusing on bikeshedding the result so that it can be included in the standard library and be safely used.
FWIW, Objective-C also has the separation of allocation and construction with the [[NSLobster alloc] init] pattern. In the past, there was arena allocation via zones, used via i.e. allocWithZone:, so that necessitated the separate steps.
There’s another reason to make sure that higher-level applications are pleasant in Rust: it means that people can build their entire stack using one technology.
I’ve seen it play out as we built Aurora DSQL - we chose Rust for the new dataplane components, and started off developing other components with other tools. The control plane in Kotlin, operations tools in Typescript, etc. Standard “right tool for the job” stuff. But, as the team has become more and more familiar and comfortable with Rust, it’s become the way everything is built. A lot of this is because we’ve seen the benefits of Rust, but at least some is because the team just enjoys writing Rust.
I’m not so sure…I’ve been doing some embedded Rust (which sort of sits at the same place on bare hardware as the kernel) and async has been really helpful for keeping the code organized. I think about it as essentially making state machines automatically for me. There is actually a tradeoff here because it’s almost too ergonomic — it’s easy to forget what it’s really doing and run into issues because something was a “local” variable (i.e., a field of the invisible state machine state) when you really needed to give it external ownership. But that didn’t take long to internalize.
My belief is that Rust’s async story shines particularly bright[0] when writing software for embedded systems, which Niko includes in his “foundational” category.
[0]: The whole complexity mess comes a lot from the fact that you want to provide zero-cost state machines for futures, without boxing nor a GC.
I like Rust a lot, it deserves major credit for the first thing moving the Pareto optimum in awhile, but there are still glaring holes for low level stuff. No stable allocator API, no placement new, no not hacky way to allocate things on the heap without them going on the stack first, many traits still only work for tuples up to length 12, almost no operations are permitted on const generics, the list goes on. I’d love to stop using C++ but this stuff is making it hard.
Sadly, really low level stuff requires nightly today. It’s a shame. I think one alternative would be taking a page from erlang and publish versiones crates that have access to nightly features that will eventually land, but that have no finalized API. Only project crates could have that access. It’d be a middle step between what we have now, and std being distributed over crates.io.
For this only changing the global allocator is stable, but per container allocator is nightly, and would suspect will continue to be for the short term.
Aren’t these the same thing? :)
As alluded in a couple of replies (including your oun) you can get the same behavior using
MaybeUninitand for genericity you need macros. Between the two, you can do quite a bit when it comes to specific projects. But a language level solution is needed. The last effort found some tricky technical issues, sources of UB that weren’t anticipated (that I don’t recall now, and the author of that effort doesn’t either because it’s been so long 😅).For a second I was gonna ask you to elaborate, but then I realized you said tuples and not arrays. For arrays impls are now generic over length, but as you say tuples are not (because we have no concept of variadics). This means that the only tool available to API authors, including the std, is using macros to enumerate every impl.
This is being actively worked on. The reason progress seems slow is because the resulting behavior is being fully specified, the implementation needs to be sound, and there can be no divergence between comp time and run time. (I’m not sure what the conclusion was with floating point, I think that might end up being the only possible source of divergence between one or the other, unless floating point operations are done entirely on software on some toplchain targets).
macros+MaybeUninit aren’t enough for generics to really work, the problem is macros don’t have access to type information.
Agreed. You don’t need C++ CTFE often, but when you do, you really want it. No, typenum is not a good substitute. (Although typenum’s download number shows how much people want it.)
can you explain what do you mean by this?
In C++ there are two forms of the new operator. The common one everyone is familiar with,
new T(arg)which allocates memory on the heap and runs the constructor and the less commonly known placement newnew(p) T(arg)which only runs the constructor, using the memory already available atp. It’s essential for implementing things like vector without unnecessary copies or moves. Since C++11vector<T>has had emplace_back, which takes the constructor arguments needed by T and creates a T “in place” right where it’s going to live in the vector, without creating it on the stack first and moving it copying it in, which is all Rust can do. In Rust you can’t express emplace_back because 1) no placement new 2) no variadic generics so you can’t write the signature.You can approximate the effect with MaybeUninit in Rust, but without variadics you can’t make it work generically, only in cases where you know the specific type ahead of time.
Placement construction is a bit of a controversial issue in Rust, there is quite a bit of bikeshedding. A few macros exist in the crates.io registry, some of them from people involved in Rust-for-Linux because they need that there, especially for big structs.
Atm the only downside of not having placement semantics is that you can’t construct a single non-referential structure larger than your remaining stack size, but this is something you hit rarely in practice (but triple as annoying when you do hit it).
edit: Though to clarify there, placement semantics are being worked on. People do want to have an option on how to construct something from a heap reference, ie, MaybeUninit without the unsafe parts.
I think it’s more controversial that
Box::new([0i32; 1024 * 1024])causes a stack overflow in a systems programming language, where “preallocate a big chunk of memory” is kind of a routine thing to do (yes,vec![0i32; 1024 * 1024].into_boxed_slice()kinda gets around this, but shouldn’t the obvious thing obviously work?)I disagree, I’ve hit this many times writing Rust for the last few years. In async it’s extra annoying because there’s no easy workaround, boxed futures must be allocated on the stack (and futures can be enormous!).
I don’t think it’s controversial that the example you mentions causes a stack overflow, but the solution isn’t something you can just slot into the language like that.
Just consider that Box is not a privileged type. It means that trying to do a placement new onto a Box isnt’ something the compiler can “just figure out”. The pointer type that’s subject to placement new needs to explicitly support it (not all pointers can just be used like this!). So there is some new traits that need to be worked out and stabilized that would enable placement new. Or, to name another example, Fallible Placement. The placement execution can obviously fail for a number of reasons (invalid data or OOM), so it needs the option to handle failure to place or allocate. Or how to do placement on types that are !Unpin.
And hence, it requires time. The original issues relating to this are from 2015, but the majority of the work was done in 2018 with most current work focusing on bikeshedding the result so that it can be included in the standard library and be safely used.
Thanks!
FWIW, Objective-C also has the separation of allocation and construction with the
[[NSLobster alloc] init]pattern. In the past, there was arena allocation via zones, used via i.e.allocWithZone:, so that necessitated the separate steps.I’ve seen it play out as we built Aurora DSQL - we chose Rust for the new dataplane components, and started off developing other components with other tools. The control plane in Kotlin, operations tools in Typescript, etc. Standard “right tool for the job” stuff. But, as the team has become more and more familiar and comfortable with Rust, it’s become the way everything is built. A lot of this is because we’ve seen the benefits of Rust, but at least some is because the team just enjoys writing Rust.
Will be interesting to hear if he sees the focus on async supports foundational software. Maybe for a data plane, but probably not in any kernel code.
I’m not so sure…I’ve been doing some embedded Rust (which sort of sits at the same place on bare hardware as the kernel) and async has been really helpful for keeping the code organized. I think about it as essentially making state machines automatically for me. There is actually a tradeoff here because it’s almost too ergonomic — it’s easy to forget what it’s really doing and run into issues because something was a “local” variable (i.e., a field of the invisible state machine state) when you really needed to give it external ownership. But that didn’t take long to internalize.
True that. I’ve been doing some STM32 hacks myself and the last one I did, I tried embassy. Surprisingly nice to have async on embedded.
My belief is that Rust’s async story shines particularly bright[0] when writing software for embedded systems, which Niko includes in his “foundational” category.
[0]: The whole complexity mess comes a lot from the fact that you want to provide zero-cost state machines for futures, without boxing nor a GC.
Cancellation is extra important in foundational code.
If I was making a brand new kernel today, it would probably be mostly async.