5 ms·
This bothers me about Rust. There's too much "unsafe" code in libraries. I like how with Rust you use one or two unsafe blocks and everyone loses their mind
by valarauca1 10y ago
This bothers me about Rust.
There's too much "unsafe" code in libraries.
I like how with Rust you use one or two unsafe blocks and everyone loses their mind. But in C/C++ you spatter your code with undefined behavior and nobody bats an eye. I get Rust is _safe_ so violating this contract is in a way self defeating. But even with a handful of unsafe blocks you are miles ahead of other guarantees C/C++ give you. Lastly unlike C/C++ Rust makes you call out I'm doing dangerous stuff here!
The language is unable to express some essential concepts.
Not really. The same code the parent poster highlighted [1] is undefined behavior in C/C++ (with standard types). So really no language has the ability to express those concepts. Your doing pointer casts and possibly unaligned dereferences at the same time. This has zero consistency between CPU vendors.
[1] https://docs.rs/crate/seahash/2.0.0/source/src/buffer.rs https://docs.rs/crate/seahash/2.0.0/source/src/buffer.rs
Known areas of trouble include partial initialization of an array, needed to implement growable collections
You mean Vector? C Doesn't have grow-able array's. They have re-alloc but Rust's Vector does that.
https://doc.rust-lang.org/std/vec/struct.Vec.html https://doc.rust-lang.org/std/vec/struct.Vec.html
and single ownership doubly linked lists.
They already did, and it is in the standard library.
https://doc.rust-lang.org/std/collections/struct.LinkedList.html https://doc.rust-lang.org/std/collections/struct.LinkedList....
If Rust let you access a slice of bytes as an slice of ints, alignment and length permitting,
the code above could be much more straightforward.
I mean C/C++ do, but without stdint.h you do this at your own peril. Even then you'll likely use Unions which are undefined behavior.
What would happen on a 32-bit machine if someone allocated a buffer bigger than 2GB? Exploitable?
You can state this about C/C++ also.
- wahern 10y ago> The same code the parent poster highlighted [1] is > undefined behavior in C/C++ (with standard types). So > really no language has the ability to express those > concepts. Your doing pointer casts and possibly unaligned > dereferences at the same time. This has zero consistency > between CPU vendors. It's undefined behavior in Rust, too. Rust code that type-puns an unaligned pointer into an integer would crash on MIPS or SPARC just like the C code would. And that crash could potentially be triggered by malicious input. "unsafe" doesn't make behavior well-defined in Rust anymore than an explicit cast makes it well-defined in C. Moreover, type-punning is generally a warning of bad code in C. Both C and Rust will generally[1] diagnose a type-pun. And you can silence the diagnostic in both languages by using special syntax--unsafe in Rust, a cast in C. But in both cases the way that you silence the warning is over-broad; you often need to silence it for good reason, X, while accidentally silencing the diagnostic for usage Y. The correct way to read in a little-endian integer in C is the same way you'd do it anywhere else: unsigned char *p; size_t plen; uint64_t n; // initialize p and plen _Static_assert(CHAR_BIT == 8, "CHAR_BIT != 8"); // [2] n = 0; for (size_t i = 0; i < MIN(plen, sizeof n); i++) { n |= p[i] << (8 * i); } It's correct regardless of endianness and regardless of whether the address is aligned. And the above loop can be unrolled, too, so that it pipelines well. If performance is so important that you can't be bothered to care about alignment constraints, you may as well drop down into assembly and use SIMD instructions directly. Type-punning with a C-style cast or Rust-style unsafe block is just a bad idea, IMO. I've never seen a situation in C where type-punning of this sort was a good idea. The performance aspect is negligible. My parsers generally runs rings around code that uses type-punning. The gains are nothing compared to what you can get by better restructuring of higher-level code. For example, with hashing functions you're generally hashing small strings; thus the alignment checks you would need to add would typically cost more than the benefit because they couldn't be amortized well. If you really had to, then C11 provides _Alignof that can be used to type-pun in a safe manner just like you could in Rust. (If a builtin type has padding issues, so would the same Rust code. It just so happens that Rust affirmatively has selected at the outset to never support such architectures. Thus, running the same code on the same architecture would work correctly in both languages.) It's not even type-punning if in addition to correct alignment you can prove that all bits are value bits. That would be the case for both the fixed-sized integer types, as well as for unsigned types where you can prove there are no padding bits (which can be accomplished using well-defined code as well). So for general usage, if you really want to you could implement two versions of the code--one that type-punned correctly for long strings, and a simpler, more concise, more obviously correct one for typical strings. So it can be done correctly while still reaping the same performance; it's just more hassle than writing incorrect code. But even the incorrect code is more obtuse than the trivially correct version, which is why I've never had a good reason to type-pun. [1] The exception in C is implicit conversions through a void pointer. But in C++, which lacks implicit void pointer conversions, engineers will often instinctively add an explicit cast where you would normally use a void pointer in C, substantially blunting the benefit of removing the implicit conversion from the language. And that habit to cast can easily lead to more bugs, just like in this case, where unsafe was used to permit the use of a broken idiom that is poor code even in C. Good C rarely uses casts. Avoiding casts is a good habit to get into in C. And I've personally never seen good reason to type-pun anything, period, in C. [2] The code could be trivially made correct on platforms where CHAR_BIT > 8 if the convention was that input strings only filled the bottom 8 bits of char. It would just be a distraction here, though.
- wahern 10y agoApropos of the mention of SHA-3 elsewhere in this thread: I recently implemented my own version of SHA-3 in C. The original reference code from the authors (Google "Keccak-readable-and-compact.c") used type-punning in the inner loops (inside the the round function) on little-endian. On non-little-endian systems a macro was used to read and convert the 64-bit value. This is typical premature optimization that is habitual among some developers, and another example of needless use of type-punning. My code uses the simple code above to read bytes into the uint64_t buffer in the outer loop (outside the round function). Not only is my code simpler and easier to understand, it's no slower than the type-punning code even on x86-64. Seriously, people, just don't type-pun. It's bad practice--in C, in Rust, in any language when using a remotely modern compiler. The only time it _might_ make sense is in very peculiar circumstances with peculiar access patterns, you've exhausted other easy gains, _and_ you've benchmarked and confirmed that type-punning is an improvement worth the cost in code complexity. And even then, at least implement it correctly and safely. If you've already met the prerequisites above, the additional effort is negligible in the grander scheme of things. And committing to always writing correct and safe code keeps you honest when assessing whether performance hacks are truly necessary.
- Gankro 10y agoType punning is a mess in C because you have to jump through hoops to make it legal. The language subsequently failed to provide ergonomic solutions to the very real problems type punning solves in a systems language. Type punning is perfectly allowed in Rust, I'm not aware of any lints against it. Although you need to use an annotation to specify the struct layout algorithm to do it "correctly" for custom types. We use it in BTreeMap to implement a kind of inheritance between nodes. Internal nodes have the same layout as Leaf nodes, except internal nodes have an extra field for their array of edges (which you don't want to allocate for leaf nodes). So everything stores pointers to leaf nodes, and mostly manipulates all nodes as leaf nodes, but sometimes you "down cast" them to internal nodes to manipulate the edges. In this particular case we use the standard C++ pattern of making a LeafNode the first field of an InternalNode. https://doc.rust-lang.org/nightly/src/collections/up/src/libcollections/btree/node.rs.html#57-115 https://doc.rust-lang.org/nightly/src/collections/up/src/lib... The other common usage of punning in Rust is to gain access to some raw representation of a type. For instance, last time I worked on Rust, this was how fat pointers (&[T], &Trait) were constructed and decomposed at the lowest level. I can't speak to whether the usage of punning in this code is particularly good though.