Using `funty::Numeric` Trait to Convert Numeric Types to Little-Endian Bytes

Hi everyone,

I'm working on a Rust crate and need help with allowing users to pass a numeric parameter to a function. This parameter should implement the to_le_bytes() method. I discovered that the funty crate provides a Numeric trait which seems to fit my needs. However, I'm having trouble understanding how to use it correctly.

My goal is to use the Numeric trait to convert any numeric value to its little-endian byte representation (to_le_bytes()) and obtain a Vec<u8> from it.

Here is my current implementation:

/// Sample trait.
pub trait Sample: funty::Numeric + Send + 'static {
    fn to_vec(self) -> Vec<u8>;
}

macro_rules! impl_to_le_bytes {
    ($($t:ty),*) => {
        $(
            impl Sample for $t {
                fn to_vec(self) -> Vec<u8> {
                    <$t>::to_le_bytes(self).to_vec()
                }
            }
        )*
    };
}

impl_to_le_bytes!(u8, i8, u16, i16, u32, i32, f32, u64, i64, f64, u128, i128, usize, isize);

pub struct Source<T: Sample> {
    receiver: tokio::sync::mpsc::Receiver<Vec<T>>,
}

impl<T: Sample> Source<T> {
    pub fn new() -> (Self, tokio::sync::mpsc::Sender<Vec<T>>) {
        let (sender, receiver) = tokio::sync::mpsc::channel::<Vec<T>>(1);
        (Self { receiver }, sender)
    }

    pub async fn next(&mut self) -> Option<Vec<T>> {
        self.receiver.recv().await
    }
}

async fn test_importing_source<T: Sample>(mut source: Source<T>) {
    let x = source.next().await.unwrap();

    assert_eq!(x[0].to_vec(), vec![1, 0, 0, 0]);
    assert_eq!(x[1].to_vec(), vec![2, 0, 0, 0]);
    assert_eq!(x[2].to_vec(), vec![3, 0, 0, 0]);
}

#[cfg(test)]
mod tests {
    #[tokio::test]
    async fn it_works() {
        let (source, sender) = super::Source::new();
        sender.send(vec![1, 2, 3]).await.unwrap();

        super::test_importing_source(source).await;
    }
}

My main issue is that I want to avoid defining my own to_vec() function for each numeric type. Instead, I'd like to directly use funty::Numeric's to_le_bytes() method, which already exists, but the resulting self::Bytes type doesn't support to_vec() directly.

How can I use the funty::Numeric trait to achieve this without writing extra code for each numeric type? Any guidance or examples would be greatly appreciated.

Thank you!

By adding an appropriate trait bound, you can implement it for all types that return an array of u8:

pub trait Sample: funty::Numeric {
    fn to_vec(self) -> Vec<u8>;
}

impl<T: funty::Numeric> Sample for T where T::Bytes: Into<Vec<u8>> {
    fn to_vec(self) -> Vec<u8> {
        self.to_le_bytes().into()
    }
}
1 Like

This is working. Thanks, @jendrikw!

Now I can use the to_vec() function, but I would like to use any of the numeric functions provided by Rust.

To be more specific, how can I make my Sample trait more generic so that I can use any of the functions provided for numeric types?

For example:

async fn test_importing_source<T: Sample>(mut source: Source<T>) {
    let x = source.next().await.unwrap();

    assert_eq!(x[0].to_le_bytes(), [1, 0, 0, 0]);
    assert_eq!(x[1].to_be_bytes().to_vec(), vec![0, 0, 0, 2]);
    assert_eq!(x[3].to_be_bytes().as_slice(), [0, 0, 0, 3]);
    // etc...
    // like using the number directly:
    assert_eq!(3_i32.to_le_bytes(), [3, 0, 0, 0]);
    assert_eq!(3_i32.to_le_bytes().to_vec(), vec![3, 0, 0, 0]);
    assert_eq!(3_i32.to_be_bytes().as_slice(), [0, 0, 0, 3]);
}

How can I extend my Sample trait so that it supports using any of these numeric functions directly? Any advice or examples would be greatly appreciated.

Thank you!

use std::fmt::Debug;

pub struct Source<T> {
    receiver: tokio::sync::mpsc::Receiver<Vec<T>>,
}

impl<T> Source<T> {
    pub fn new() -> (Self, tokio::sync::mpsc::Sender<Vec<T>>) {
        let (sender, receiver) = tokio::sync::mpsc::channel::<Vec<T>>(1);
        (Self { receiver }, sender)
    }

    pub async fn next(&mut self) -> Option<Vec<T>> {
        self.receiver.recv().await
    }
}

async fn test_importing_source<T>(mut source: Source<T>)
where
    T: funty::Numeric,
    T::Bytes: PartialEq<[u8; 4]> + Debug + Into<Vec<u8>>,
{
    let x = source.next().await.unwrap();

    assert_eq!(x[0].to_le_bytes(), [1, 0, 0, 0]);
    assert_eq!(x[1].to_be_bytes().into(), vec![0, 0, 0, 2]);
    assert_eq!(3_i32.to_be_bytes().as_slice(), [0, 0, 0, 3]);
    // etc...
    // like using the number directly:
    assert_eq!(3_i32.to_le_bytes(), [3, 0, 0, 0]);
    assert_eq!(3_i32.to_le_bytes().to_vec(), vec![3, 0, 0, 0]);
    assert_eq!(3_i32.to_be_bytes().as_slice(), [0, 0, 0, 3]);
}

#[tokio::main]
async fn main() {
    let (source, sender) = Source::<i32>::new();
    sender.send(vec![1, 2, 3]).await.unwrap();
    test_importing_source(source).await;
}

1 Like

Thanks for the reply!

The complexity lies in the fact that I don't know which type will be used. It could be i32, u8, i16, f32, etc.

I've rewritten the example to let more clear it:

pub trait Sample: funty::Numeric {
    fn to_vec(self) -> Vec<u8>;
}

impl<T: funty::Numeric> Sample for T where T::Bytes: Into<Vec<u8>> {
    fn to_vec(self) -> Vec<u8> {
        self.to_le_bytes().into()
    }
}

pub struct Source<T: Sample> {
    receiver: tokio::sync::mpsc::Receiver<Vec<T>>,
}

impl<T: Sample> Source<T> {
    pub fn new() -> (Self, tokio::sync::mpsc::Sender<Vec<T>>) {
        let (sender, receiver) = tokio::sync::mpsc::channel::<Vec<T>>(1);
        (Self { receiver }, sender)
    }

    pub async fn next(&mut self) -> Option<Vec<T>> {
        self.receiver.recv().await
    }
}


async fn test_importing_source<T: Sample>(mut source: Source<T>, expected: Vec<Vec<u8>>) {
    let x = source.next().await.unwrap();
    
    for (i, el) in x.iter().enumerate() {
        assert_eq!(el.to_vec(), expected[i]);
    }
    

    for (i, el) in x.iter().enumerate() {
        // But this is not working:
        //assert_eq!(el.to_le_bytes().to_vec(), expected[i]);
    }
}


#[cfg(test)]
mod tests {
    #[tokio::test]
    async fn using_i32() {
        let (source, sender) = super::Source::new();
        sender.send(vec![1, 2, 3]).await.unwrap();

        super::test_importing_source(source, vec![
            vec![1, 0, 0, 0],
            vec![2, 0, 0, 0],
            vec![3, 0, 0, 0],
        ]).await;
    }

    #[tokio::test]
    async fn using_f32() {
        let (source, sender) = super::Source::new();
        sender.send(vec![1_f32, 2_f32, 3_f32]).await.unwrap();

        super::test_importing_source(source, vec![
            vec![0, 0, 128, 63],
            vec![0, 0, 0, 64],
            vec![0, 0, 64, 64]
        ]).await;
    }
    
}


That's just looks like a mistake. Change:

                assert_eq!(el.to_le_bytes().to_vec(), expected[i]);

to

                assert_eq!(el.to_vec(), expected[i]);

It is to_vec that calls to_le_bytes, so I don't know why you would expect that to work.

1 Like

Hi, thanks for your reply.

I'm expecting this behaviour because I consider el to be a numeric value like 3_i32, 3_i16, or 3.45_f32. Therefore, I expect to be able to use any of the to_XX_bytes functions on it.

The issue is that when using funty::Numeric, the return type of to_le_bytes is self::Bytes, not the expected &[u8; X] as when calling to_le_bytes directly on a numeric value like x_i32 or y_f32.

The first suggested solution works well, but I want to be able to use any combination to create the Vec<u8>, not just the one implemented (to_le_bytes().to_vec()).

If you need more complex things, it may be best to write your own trait has the functionality you need and implement if for all numeric types, as you've done in your first post.

1 Like

Ok, so the to_vec in your example is not the same to_vec implemented, you're just saying you want to convert any numeric to its byte array, and that byte array to a vec.

I don't see a way to do that with funty because the Bytes type is too opaque and I haven't been able to convert it to a &[u8]. Maybe there is something tricky with generics that would do it, but I don't know what it is.

I was looking at num-traits as an alternative and it may work better because it's Bytes type implements NumBytes, which implements several conversions to &[u8]. I haven't had time to try num-traits yet in your example.


Update: If you use Num + ToBytes (from num_traits) instead of the Sample trait, you can call to_le_bytes and convert the result to &[u8]. With that change, this does now compile:

                assert_eq!(el.to_le_bytes().as_ref().to_vec(), expected[i]);

I did not manage to create a trait with a generic to_vec method, but here is a free fn that works:

        fn to_vec<T: Num + ToBytes>(el: &T) -> Vec<u8> {
            el.to_le_bytes().as_ref().into()
        }

Update #2: You can remove the Num bound if you don't otherwise need it, all of the above works with just the ToBytes bound.

2 Likes

Unfortunately funty::Numeric cannot require/guarantee that Self::Bytes is actually [u8; N], because it's not (yet?) allowed to write a signature like -> [u8; Self::BYTES] (where BYTES is an associated const: usize item on the trait). The general bound you probably want to place on Self::Bytes is probably AsRef<[u8]>; the trait is implemented for all [T; N] and allows you to call .as_ref() to get &[T].

The trait you actually want for T, though, is probably bytemuck::Pod (for "plain ol' data"). This will allow you to use cast_slice to directly go from &[T] to &[u8] without any copying.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.