I'm attempting to write Rust bindings for a C collection library (Judy Arrays [1]) which only provides itself room to store a pointer-width value. My company has a fair amount of existing code which uses this space to directly store non-pointer values such as pointer-width integers and small structs. I'd like my Rust bindings to allow type-safe access to such collections using generics, but am having trouble getting the pointer-stashing semantics working correctly.
I have a basic interface working using std::mem::transmute_copy()
to store the value, but that function explicitly does nothing to ensure the source and destination types are the same size. I'm able to verify that collection type parameter is of a compatible size at run-time via an assertion, but I'd really like the check to somehow be at compile-time.
Example code:
pub struct Example<T> {
v: usize,
t: PhantomData<T>,
}
impl<T> Example<T> {
pub fn new() -> Example<T> {
assert!(mem::size_of::<usize>() == mem::size_of::<T>());
Example { v: 0, t: PhantomData }
}
pub fn insert(&mut self, val: T) {
unsafe {
self.v = mem::transmute_copy(&val);
mem::forget(val);
}
}
}
Is there a better way to do this, or is this run-time check the best Rust 1.0 supports?
(Related question, explaining why I'm not using mem::transmute()
.)
[1] I'm aware of the existing rust-judy project, but it doesn't support the pointer-stashing I want, and I'm writing these new bindings largely as a learning exercise anyway.
Contrary to the accepted answer, you can check at compile-time!
The trick is to insert, when compiling with optimizations, a call to an undefined C function in the dead-code path. You will get a linker error if your assertion would fail.
Compile-time check?
In general, there are some hacky solutions to do some kind of compile time testing of arbitrary conditions. For example, there is the
static_assertions
crate which offers some useful macros (including one macro similar to C++'sstatic_assert
). However, this is hacky and very limited.In your particular situation, I haven't found a way to perform the check at compile time. The root problem here is that you can't use
mem::size_of
ormem::transmute
on a generic type. Related issues: #43408 and #47966. For this reason, thestatic_assertions
crate doesn't work either.If you think about it, this would also allow a kind of error very unfamiliar to Rust programmers: an error when instantiating a generic function with a specific type. This is well known to C++ programmers -- Rust's trait bounds are used to fix those often very bad and unhelpful error messages. In the Rust world, one would need to specify your requirement as trait bound: something like
where size_of::<T> == size_of::<usize>()
.However, this is currently not possible. There once was a fairly famous "const-dependent type system" RFC which would allow these kinds of bounds, but got rejected for now. Support for these kinds of features are slowly but steadily progressing. "Miri" was merged into the compiler some time ago, allowing much more powerful constant evaluation. This is an enabler for many things, including the "Const Generics" RFC, which was actually merged. It is not yet implemented, but it is expected to land in 2018 or 2019.
Unfortunately, it still doesn't enable the kind of bound you need. Comparing two const expressions for equality, was purposefully left out of the main RFC to be resolved in a future RFC.
So it is to be expected that a bound similar to
where size_of::<T> == size_of::<usize>()
will eventually be possible. But this shouldn't be expected in the near future!Workaround
In your situation, I would probably introduce an unsafe trait
AsBigAsUsize
. To implement it, you could write a macroimpl_as_big_as_usize
which performs a size check and implements the trait. Maybe something like this:This uses basically the same trickery as
static_assertions
is using. This works, because we never usesize_of
on a generic type, but only on concrete types of the macro invocation.So... this is obviously far from perfect. The user of your library has to invoke
impl_as_big_as_usize
once for every type they want to use in your data structure. But at least it's safe: as long as programmers only use the macro to impl the trait, the trait is in fact only implemented for types that have the same size asusize
. Also, the error "trait boundAsBigAsUsize
is not satisfied" is very understandable.What about the run-time check?
As bluss said in the comments, in your
assert!
code, there is no run-time check, because the optimizer constant-folds the check. Let's test that statement with this code:The crazy
asm!()
expressions serve two purposes:t
from LLVM, such that LLVM can't perform optimizations we don't want (like removing the whole function)Compile it with a nightly compiler (in a 64 bit environment!):
As usual, the resulting assembly code is hard to read; here are the important spots (with some cleanup):
The
#APP
-#NO_APP
pair is ourasm!()
expression.foo<bool>
case: you can see that our firstasm!()
instruction is compiled, then an unconditioned call topanic!()
is made and afterwards comes nothing (ud2
just says “the program can never reach this spot,panic!()
diverges”).foo<u64>
case: you can see both#APP
-#NO_APP
pairs (bothasm!()
expressions) without anything in between.So yes: the compiler removes the check completely.
It would be way better if the compiler would just refuse to compile the code. But this way we at least know, that there's no run-time overhead.