In other words, [][..]
should be of type [_;0]
instead of type [_]
.
I need to manipulate mac6 addresses stored as u64
in my database. The eponymous crate macaddr seems rather adequate as a model of mac addresses, including parsing and printing.
However, converting from and into u64
proved to be a challenge, and smells both less pretty and less efficient than it should be.
In rust, I'd expect stuffs containing the word as
, implying some sort of zero copy, or at least some easily implemented From
/TryFrom
. Since it doesn't exist, I did it myself.
Ideally, I would like something like :
fn mac6_to_u64(addr: &macaddr::addr6::MacAddr6) -> u64 {
u64::from_bytes(addr.as_bytes())
}
pub fn mac6_from_u64(mac: u64) -> macaddr::addr6::MacAddr6 {
MacAddr6::from(mac.to_bytes())
}
Rust and its ecosystem being pedantic, it doesn't work, and for good reasons. Indeed, a mac6 address is 6 bytes long, versus the 8 bytes of an u64. What's more, endianness matters.
With that in mind, I'd expect to be able to write something along the lines of:
fn mac6_to_u64(addr: &macaddr::addr6::MacAddr6) -> u64 {
let mut mac = [0; 8];
addr.as_be_bytes().clone_into(&mut mac[2..]);
u64::from_be_bytes(mac)
}
pub fn mac6_from_u64(mac: u64) -> macaddr::addr6::MacAddr6 {
MacAddr6::from(mac.to_be_bytes()[2..])
}
plus some unit testing to check the correctness of the offset and endianness.
There are a couple issues however, and I expect them to be faces of the same coin:
addr.as_be_bytes()
is of type[u8]
and not[u8; 6]
, making it impossible to clone into anything other than aVec
.
On the surface, it may look like an oversight from the writers ofmacaddr
, but I suspect it is actually very hard to output a[u8; 6]
without ugly copies and expect, or crazy unsafe techniques.
Nowmac.to_be_bytes()
is of type[u8, 8]
, so it doesn't seem impossible to output constant sized array.mac[2..]
(ormac[2..8]
for what matters) is of type[u8]
, and not[u8; 6]
, another reason why cloning into this slice is impossible.MacAddr6
implementsFrom<[u8; 6]>
, as it should, butmac.to_be_bytes()[2..]
is of type[u8]
.
So the code I settled for is as follows:
fn mac6_to_u64(addr: &macaddr::addr6::MacAddr6) -> u64 {
let mac = addr.as_bytes();
u64::from_be_bytes([0, 0, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]])
}
pub fn mac6_from_u64(mac: u64) -> macaddr::addr6::MacAddr6 {
let mac = mac.to_be_bytes();
MacAddr6::new(mac[2], mac[3], mac[4], mac[5], mac[6], mac[7])
}
Which feels ugly and wasteful.
In C++, I'd expect Macaddr6 could be modeled as an adapter over char[6]
instead of being its own class/type.
Adapters are ugly with regard to encapsulation, but they still feel better than Rust's mental gymnastics when in comes to doing absolutely nothing, like is almost the case here.
Now with that being said, in C++, endianness and size would be foot-guns, not compile error and conscious explicit choice forced by the language or the library.
Still, if Rust is going to be a PITA (Paramount In Theoretical Analysis) about types modelisation, at least it would be nice to have the model be accurate, which I don't think is currently possible in safe rust.
If slices whose bounds are known at compile time where typed with accounting their known size, the second solution would be possible.
I'm not entirely convinced it could spare a clone in this particular example, but I believe it would open opportunities for more Zero Copy stuff, and more things to live in the stack or out of boxes (due to being Sized), on top of allowing more faithful representations.
Here is an example without dependencies illustrating the current state of affairs:
Minimal playground
fn main() {
let names = ["toto", "titi"];
// Compile time error
println!("{}", names[3]);
// Trying to cast a slice [T] of known constant size as a [T; SIZE] is impossible
//let slice: [&str;0] = names[0..0];
// Runtime panic, even though in theory all the information is there to deny an unconditional panic
println!("{:?}", &names[0..6]);
}
The kind of static analysis I'm asking for already exists for indexing, in the form of denying unconditional panics (making a distinction between indexing with a variable and a constant)
Now there are open philosophical questions such as: what should be the type of [][8..3]
? Never
? not the most consistent; [_;-5]
? -5
is not a usize
; [_]
? That would be a particular case. But so would be indexing with an variable.
The last solution is my preferred one. More generally, while I'm sure implementing slice size inference is no easy task, I don't think degenerate cases which should be denied as unconditional panics should dictate how we think about design consistency.
Now maybe I overlooked something very simple for my particular problem, maybe there are reasons NOT to infer slice sizes, or maybe it is already an RFC in the works.
I haven't had much luck finding info about the subject, hence I submit it here.