Guidance adjusting a Rust function to handle PrinInt instead of i64

Viewed 86

I have a simple function that encodes an i64 to a byte stream.

fn write_integer(out: &mut Cursor<Vec<u8>>, value: i64, width: u8) -> Result<()> {
    let mut value = value;

    // while there's stuff outside the 7 LSBs
    while value & !0x7F != 0 {
        // write the 7 LSBs, with the 8th bit set
        out.write_u8((value as u8) & 0x7F | 0x80);

        value >>= 7;
    }

    out.write_u8(value as u8);
    Ok(())
}

Instead of reimplementing this to handle all integer sizes, I've attempted to make this generic...

use num_traits::PrimInt;
fn write_integer_g<T: PrimInt>(out: &mut Cursor<Vec<u8>>, value: T, width: u8) -> Result<()> {
    let mut value = value;

    // while there's stuff outside the 7 LSBs
    while value & !0x7F != 0 {
        // write the bottom 7 bits, with the 8th bit set
        out.write_u8((value as u8) & 0x7F | 0x80);

        value >>= 7;
    }

    out.write_u8(value as u8);
    Ok(())
}

however with less than perfect results..

error[E0308]: mismatched types
  --> src/encoded_data.rs:31:23
   |
27 |     fn write_integer_g<T: PrimInt>(out: &mut Cursor<Vec<u8>>, value: T, width: u8) -> Result<()> {
   |                        - this type parameter
...
31 |         while value & !0x7F != 0 {
   |                       ^^^^^ expected type parameter `T`, found integer
   |
   = note: expected type parameter `T`
                        found type `{integer}`

Clearly my integer literals need converting to values of type T. How can I do this?

2 Answers

You can use the num crate / num-traits crate traits, such as FromPrimitive, ToPrimitive, and AsPrimitive to convert literals to T. Note that the methods in FromPrimitive and ToPrimitive returns Options, so you might want to handle that better than just unwrapping.

Assuming that write_u8() comes from the byteorder crate's WriteBytesExt trait, then you can rewrite it like this:

use std::io::{self, Cursor, Write};
use std::ops::ShrAssign;
// byteorder = "1.4"
use byteorder::WriteBytesExt;
// num-traits = "0.2"
use num_traits::{AsPrimitive, FromPrimitive, PrimInt, Zero};

fn write_integer_g<T>(out: &mut Cursor<Vec<u8>>, value: T, _width: u8) -> io::Result<()>
where
    T: PrimInt,
    T: FromPrimitive,
    T: AsPrimitive<u8>,
    T: ShrAssign,
{
    let mut value = value;

    let int_127 = T::from_u8(0x7F).unwrap();

    while value & !int_127 != Zero::zero() {
        out.write_u8(value.as_() & 0x7F | 0x80)?;

        value >>= T::from_u8(7).unwrap();
    }

    out.write_u8(value.as_())?;

    Ok(())
}

Note that the PrimInt requires the Num trait, which in turn requires the Zero trait. Which is why we're able to use the methods and associated functions from Zero.

So instead of value & !int_127 != Zero::zero() you could also do !(value & !int_127).is_zero().

Note that your original code results in an infinite loop if provided with a negative input.

One easy solution is to use AsPrimitive to convert your input into u64, then output that:

use num_traits::AsPrimitive;
fn write_integer<T: AsPrimitive<u64>>(out: &mut Cursor<Vec<u8>>, value: T, width: u8) -> Result<()> {
    let mut value: u64 = value.as_();

    // while there's stuff outside the 7 LSBs
    while value & !0x7F != 0 {
        // write the 7 LSBs, with the 8th bit set
        out.write_u8((value as u8) & 0x7F | 0x80);

        value >>= 7;
    }

    out.write_u8(value as u8);
    Ok(())
}

Playground

Related