I didn't know about this, and it's super cool. It's part of a broader pattern where the rigid constraints Rust can impose on code allow both the compiler and the user to do things that would be wildly dangerous in a less-strict language.
I didn't know about this, and it's super cool. It's part of a broader pattern where the rigid constraints Rust can impose on code allow both the compiler and the user to do things that would be wildly dangerous in a less-strict language.
At first, this was special cased in the compiler, but then generalized.
enum Foo {
A = 0,
B = 1,
C = 2,
Unknown(i32),
}
where Unknown field cannot contain values 0, 1, or 2, thus Rust compiler will be able to pack this enum into single i32, like in C.I'd represent it like this:
#[repr(transparent)]
struct Foo(i32);
impl Foo {
const A: Foo = Foo(0);
const B: Foo = Foo(1);
const C: Foo = Foo(2);
}2) This is required for forward compatibility. For example protobuf explicitly requires it.
3) Your code is not forward compatible. The proper way is to implement it is as i32 and then unwrap it, or:
enum Foo {
A = 0,
B = 1,
C = 2,
_UNKNOWN_3 = 3,
_UNKNOWN_4 = 4,
_UNKNOWN_5 = 5,
_UNKNOWN_6 = 6,
// ... repeat few dozen times, to be forward compatible
}
Example: https://github.com/apoelstra/rust-bitcoin/blob/c37ab1f9c2392...P.S.
IMHO, Rust should allow to define enums as:
enum Foo {
A = 0,
B = 1,
C = 2,
_ , // Unknown values which must be handled by default case
}
or enum Foo {
A = 0,
B = 1,
C = 2,
_OTHER , // Unknown values which must be handled
}
because current workaround is usable for i8, maybe even for i16, but not for i32.In general, it _is_ possible for any C data layout to be represented in Rust -- but it's not necessarily the case that the representation has the same name. And it's also not the case that we can safely pass memory from C to Rust without validating the content, even if the representation is equivalent for all valid values.
[1] https://blog.rust-lang.org/2019/12/19/Rust-1.40.0.html#non_e...
[2] https://rust-lang.github.io/unsafe-code-guidelines/layout/en...
#[non_exhaustive]
#[repr(C)]
#[derive(Debug)]
#[allow(dead_code)]
enum Foo {
A = 0,
B = 1,
C = 2,
}
pub fn main() {
let bar = unsafe { std::mem::transmute::<i32, Foo>(33) };
println!("{:?}", bar);
let s = match bar {
Foo::A => ("A", bar as i32),
Foo::B => ("B", bar as i32),
Foo::C => ("C", bar as i32),
_ => ("Unknown", bar as i32),
};
println!("{:?}", s);
}
Standard Output
C
("Unknown", 33)> Your code is not forward compatible. The proper way is to implement it is as i32 and then unwrap it
That’s what they’re doing. It’s a newtype for i32 with a few named values but it can represent any i32, and you can unwrap it with .0 to get the i32 value.
I guess the compiler can't help you keep known and unknown values separate with that structure though, so maybe it's not enough.
Whereas the semantics in Rust are "we can do whatever we want unless you add an explicit #[repr] attribute, in which case you choose the semantics of layout and we don't mess with it."
It's not about the strength of the type system, it's about history.
template < typename T >
class optional< std::vector<T> > {
// ...
};
And write an implementation that (ab)uses knowledge of the exact layout of `std::vector<T>` to avoid an extra bool. This would of course be a breaking change to the ABI of optional<std::vector<T>> if implemented by std::optional, so compiler vendors tend to avoid such things.There is one stdlib example of this, but sadly it's more infamous than inspirational: std::vector<bool>. The standard relaxes several guarantees of std::vector<T> in the case of T == bool, to allow it to bit-pack the booleans instead of using sizeof(bool) - e.g. an entire byte - per element.
Some of those relaxed guarantees make std::vector<T> unsuitable for interop with C style APIs taking pointers + lengths if T might be a bool. Worse still, many (most?) implementations don't even implement the space optimization those guarantees were relaxed for - a worst of both worlds situation. If you actually need such space optimizations, you'll reach for an alternative such as boost::dynamic_bitset which provides them consistently.
However, to the best of my understanding, most types are exposed to other compilation units, so it is hard to establish invariants about how they are used. Unless the compiler can verify that nowhere in the program ever takes the address of a boolean variable in a struct, it isn't allowed to rearrange the struct to avoid having that extra boolean. That might be possible at link-time for statically linked programs, but I'm not sure.
Also, my knowledge is mostly from programming in C++ and watching CppCon talks, so I may be out of date from the latest optimization techniques.
179.art, one of the SPEC2000 benchmarks, has some poorly laid out structs. Sun introduced targeted optimizations for this benchmark and several other vendors also did so. I have also read papers about profile-guided struct reordering but don't have a citation on hand.
GCC also had a pass[0] for this optimization but it may have been removed.
[0] https://www.research.ibm.com/haifa/dept/_svt/papers/golovane...
Pretty cool to see that Rust can do more optimizations than other languages.
And it seems to play out well. I fondly remember Java being touted as being superior to C/C++ because it can do runtime optimizations, which will outperform C/C++. Somehow this never happened.
However, because there are no type guards for this stuff in C/C++ you're generally constrained by convention/intuitiveness, when picking these special values, in order to (hopefully) guard against accidental misuse. So in the NonZero case above, you probably wouldn't do that because it's pretty unusual, and you'd therefore use more memory.
And then, even when the "special values" are chosen "reasonably", they sometimes get misused anyway and we end up with bugs like last year's trivial sudo exploit: https://bit-sentinel.com/to-sudo-or-not-to-sudo-demystifying...
> Reading the manual for those functions we learn that -1 is a special value
> Supplying a value of -1 for either the real or effective user ID forces the system to leave that ID unchanged.
> If one of the arguments equals -1, the corresponding value is not changed.
> So, what happens when we send -1 as a parameter? Well, somewhere along the underlying code lines is a convention that -1 is represented as 4294967295, thus either values will get the wanted result.
Edit: In practice the above may be more common in C than modern C++
It is used in some other places though. For instance, std::string implementations typically employ small-string optimization (SSO) which means that there might be a limit on string::size() since some of the bits inside that value are used for different purposes (example: https://akrzemi1.wordpress.com/2014/04/14/common-optimizatio...). The "niche" in this case is the realization that strings are unlikely to be exabytes in size, so there is no reason to always store zeros in high bits of _size.
Do you think that's possible?
Only the libs team can really say, and I'm not on that team.
I suppose I appreciate SmallString's ability to alias itself to Rust's String.
But I'd immediately agree that this adds hidden complexity and the potential for it to bite you in the worst possible moment.
Because Rust has &str as the lowest common denominator for all string types, "non-standard" strings aren't difficult to use.
That’s what we say now, but a few dozen years down the line? Old Mac OS versions (pre X) would store extra bits of information in the MSBs of a pointer. “32 bit clean” Mac OS had to be created to allow usage on computers with more than a few dozen megabytes. That was an all too common problem when multiple gigabytes of RAM became commonplace.
Granted, I highly doubt we’ll see exabyte level string lengths, but you never know.
Fun fact: x86-64 mandates pointers be in “canonical” form where all unused bits of the address are the same as the MSB. So on a processor that has 48 address lines, the upper 16 must match the 48th bit. If you try to access memory with a “non canonical” pointer, the processor will throw an exception. This is because they don’t want people using them for flag bits and having a repeat of the “unclean” software problem.
TL;DR: I agree, but never say never
The small problem is that your program will be limited in memory space. That's usually okay. If you want a vast expansion of memory space you probably need to start using different algorithms anyway.
The big problem is that your program depends on the CPU ignoring those bits, and now it can't run at all on updated hardware. As you noted, x86-64 solves this problem by forcing programs to clean up pointers before actually using them.
Now that you mention pointers, Apple is using unused bits in their values for signing and authentication purposes https://googleprojectzero.blogspot.com/2019/02/examining-poi...
Pretty neat in my opinion
NaN-boxing / nun-boxing is a common optimization, as seen in Firefox's SpiderMonkey, Safari's JSC, LuaJIT, etc. If all of your valid pointers are at the top of the address space, you can use 0x0 as the constant you either add/subtract or xor with your values to box/un-box. In other words, Nan-boxing/unboxing becomes a no-op.
You can catch a hell lot of things at compile-time. Correctness is huge in Ada/SPARK. You write safety-critical software in it. I have a comment on Ada/SPARK in great detail, but I do not have the time to find it sadly. :(
Consider this:
Total : Integer range 0 .. Integer'Last
"Any attempt to assign a negative value to variable Total results in raising an exception at run time. During analysis, GNATprove checks that all values assigned to Total are positive or null."Analysis here means static analysis.
You can do stuff like this, too, for example:
subtype Natural is Integer range 0 .. Integer'Last;
subtype Positive is Integer range 1 .. Integer'Last;
The above is actually declared in the visible part of package Standard. #[rustc_layout_scalar_valid_range_start(x)]
#[rustc_layout_sclaar_valid_range_end(y)]
struct MyInt(i32);
to create integers with only valid values in range [x, y) that benefit from the niche optimizations.Like, have literally a project with almost 100k LOC which is all built on top of this.
I'm surprised how little a Google search turns up for them; the best I found was a mention (but no description or examples) in docs.rs: https://docs.rs/rustc-ap-syntax_pos/634.0.0/rustc_ap_syntax_...
Do these enforce against out-of-bounds values by panicking, the way arrays do? Or are they unsafe?
See https://github.com/rust-lang/rust/blob/master/library/core/s...
They are unsafe. The safe API of your type must prevent constructing invalid values.
It is trivial to create an integer type in Rust that only accepts certain values, and doing so doesn't require these hints, e.g.,
#[bounded_int(start, end)]
struct MyBoundedInt(i32);
creates a type called MyBoundedInt that never will take a value outside the range [start, end); this can be proved formally, i.e., there is no safe Rust program that can violate that. No need to run any kind of verification on top.The place where Rust differs from Ada, and the reason I use these attributes, is to enable the layout optimizations that Rust performs (e.g. Option<NonZero> having the same size in bytes as NonZero, and many others).
From the Rust reference (https://doc.rust-lang.org/reference/influences.html):
> Rust is not a particularly original language, with design elements coming from a wide range of sources. Some of these are listed below ...
Oh, I did not mean to suggest that Rust does, just some people do, because they are not familiar with other programming languages and whatnot.