5 ms·
Hm. I'm not sure about the addition of unions. Why add something that is unsafe to read or write? You need an additional mechanism to let you know which type it
by jmull 9y ago
Hm. I'm not sure about the addition of unions. Why add something that is unsafe to read or write? You need an additional mechanism to let you know which type it is OK to access.
They mention the case where the type can be distinguished by the lest significant bit, but wouldn't it be better to handle that case as an enum? That is, the least significant bits define the enum tag, while the remaining bits define the associated value.
(By the way, I really mean this as a straight question, not a criticism in the form of a rhetorical question. I really don't know enough about it to be criticizing it.)
- vvanders 9y agoThey're largely meant as an easier interop with the C ABI and C APIs that use unions. You wouldn't be using them in your higher level Rust library.
- steveklabnik 9y agohttps://github.com/rust-lang/rfcs/blob/master/text/1444-union.md#motivation https://github.com/rust-lang/rfcs/blob/master/text/1444-unio... TL;DR: FFI with C is much harder without unions. There are smaller reasons as well.
- kbsletten 9y agoI believe that they've framed it in this article as mostly for compatibility with C/C++. A simple example is JS objects are often represented as a single f64 with the NaN space punned for 32-bit pointers and 53-bit integers. If you want to put some code written in Rust in that environment, if you could define a union that could expose a safer API by using bit twiddling you could. Presumably if you have the memory to spare or are unconstrained in design you won't use this feature which is part of why it took so long to get into the language.
- GolDDranks 9y agoUnions are used in C APIs so having them is nice for FFI. Besides, enums aren't always optimal for some things. Let's say that you have an array of 32-bit values whose prime-numbered indexes contain either a signed or unsigned 32-bit integer, depending on a single global boolean tag. You can't encode that invariant into a (non-dependent) type system. If you want to do it safely, using enums, you are going to have a tag for every value, and that's going to cost you. (A bit crazy example, but bear with me.) Unions are a way to circumvent the type system a bit. They allow you to keep track of the stuff inside memory slots using the way you see the best. But there's a reason they're unsafe - the responsibility is on you!
- fanf2 9y agoYou don't need to get as tricky as that. Rust doesn't let the programmer specify the layout of enums in detail right now, so you can't specify where the compiler should place the discriminator. The example I care about is a word which is a pointer if the bottom bit(s) are 0, but otherwise contains a bunch of packed fields, where the least significant bits are a nonzero value I can do something useful with.
- kibwen 9y agoAs other commenters have mentioned, the demand here is almost all due to the desire for smoother interoperation with C code. What's gone unsaid so far is that, despite still requiring the `unsafe` keyword for many operations, this feature helps make Rust code more safe when calling into C, because it simplifies the interface and eliminates the need for hacked-up handwritten workarounds. IOW, a little bit of standardized unsafety to replace a larger amount of bespoke unsafety.
- Manishearth 9y agoFWIW the hacked up workaround that bindgen uses is _beautiful_ https://github.com/servo/rust-bindgen/blob/master/tests/expectations/tests/union_fields.rs https://github.com/servo/rust-bindgen/blob/master/tests/expe... Basically, if you have a union of A, B, C, you create a struct with three zero-sized fields using BindgenUnionField<A>, and then add a field after that containing enough bits to actually fill out the size. Because the BindgenUnionField is zero sized, a pointer to it is a pointer to the beginning of the struct, and it has an accessor that treats the pointer as the contained type. This makes the API for field access `union.field.as_ref()` instead of `union.field`, but that's still pretty clean. It's still a hack, and I'll be happy to see it go, but it's a really fun hack.
- kibwen 9y agoIf only English already had a word for "beautiful, yet reprehensible". :)
- zem 9y agosirenlike
- tomsthumb 9y agoYou know this word in another language?
- andrewflnr 9y ago
- Manishearth 9y agoOther commenters have mentioned the use case for FFI, and there's a very small niche use case for type punning, but there's one thing that this enables you to do that was absolutely not possible with the language previously. This is zero cost stack allocation of data in a way which avoids destructors. Basically, currently, in Rust, if you want to allocate a type and avoid destructors being run, you have to write a wrapper around `Option<YourType>` that nulls the option in its destructors. Or you heap allocate it and turn the box into a raw pointer after allocation. Both have overhead. The zero-overhead way of doing it is to stack allocate an array and cast pointers, but then you need to know the size beforehand. With unions, you can stack allocate a `union Foo {x: YourType}` and just use that. Unions don't have destructors, so this stack allocates enough space for your type, and lets you unsafely but conveniently access it as your type (no ugly pointer casting hacks), but you can guarantee that destructors won't be run. The obvious question here is -- why is this even necessary? Surely you can just call mem::forget to avoid destructors right before the function returns. However, destructors also get run whilst panicking, so if your function accepts a callback, and that callback panics, you can't avoid destructors without the overhead mentioned before. For a concrete example of this use case, check out ArcBorrow::with_arc() (https://doc.servo.org/servo_arc/struct.ArcBorrow.html https://doc.servo.org/servo_arc/struct.ArcBorrow.html). ArcBorrow<'a, T> is basically a borrowed reference to a T that is known to be backed by an Arc (atomic reference counted type). You can obtain borrowed references to an Arc normally, which is great -- lets you share the data without bumping atomic reference counts all the time and paying the atomic overhead. But if you have an &T -- a borrowed reference to a T -- there's no way to bump the reference count on that if you need to escape the borrow; since there's no guarantee that the &T borrows from an Arc allocation and not something else. So you must pass down an &Arc<T>, and that has double indirection. There are other reasons (pertaining to the existence of RawOffsetArc, which has to do with FFI constraints) as to why &Arc<T> won't work for us there, but I won't get into those. ArcBorrow<'a, T> lets us freely pass around borrows of &T which can be cloned as an Arc if necessary. Anyway, ArcBorrow has a with_arc method (https://doc.servo.org/servo_arc/struct.ArcBorrow.html#method.with_arc https://doc.servo.org/servo_arc/struct.ArcBorrow.html#method...). This takes a closure and passes an &Arc<T> to it. But we don't have an Arc<T>, we have an ArcBorrow<T>, which has a different representation (in particular, ArcBorrow contains a pointer to the data, whereas Arc contains a pointer to the allocation, which starts earlier because of the refcount). So we construct a fake Arc<T> on the stack (https://doc.servo.org/src/servo_arc/lib.rs.html#884-907 https://doc.servo.org/src/servo_arc/lib.rs.html#884-907), and share it with the closure. Because it's a fake Arc<T> we can't actually let its destructors be run, so we put it inside NoDrop, which on nightly uses unions (but on stable uses the non-zero-cost methods I mentioned above). I use this same trick in array-init (https://github.com/Manishearth/array-init/blob/a0cb08928b42d73fecda7f0a9399bfb2c4f2cac8/src/lib.rs#L103-L113 https://github.com/Manishearth/array-init/blob/a0cb08928b42d...), where I stack allocate an uninitialized array and let you fill in the elements with a closure. If the closure panics the destructor of the _partially_ initialized array should not run, so again, it's in a NoDrop. In general when writing unsafe abstractions you often need escape hatches like these.