Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does a generic function replicating C's fread for unsigned integers always return zero?

Tags:

rust

I am trying to read in binary 16-bit machine instructions from a 16-bit architecture (the exact nature of that is irrelevant here), and print them back out as hexadecimal values. In C, I found this simple by using the fread function to read 16 bits into a uint16_t.

I figured that I would try to replicate fread in Rust. It seems to be reasonably trivial if I can know ahead-of-time the exact size of the variable that is being read into, and I had that working specifically for 16 bits.

I decided that I wanted to try to make the fread function generic over the various built-in unsigned integer types. For that I came up with the below function, using some traits from the Num crate:

fn fread<T>(
    buffer: &mut T,
    element_count: usize,
    stream: &mut BufReader<File>,
) -> Result<usize, std::io::Error>
where
    T: num::PrimInt + num::Unsigned,
{
    let type_size = std::mem::size_of::<T>();
    let mut buf = Vec::with_capacity(element_count * type_size);
    let buf_slice = buf.as_mut_slice();

    let bytes_read = match stream.read_exact(buf_slice) {
        Ok(()) => element_count * type_size,
        Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => 0,
        Err(e) => panic!("{}", e),
    };

    *buffer = buf_slice
        .iter()
        .enumerate()
        .map(|(i, &b)| {
            let mut holder2: T = num::zero();
            holder2 = holder2 | T::from(b).expect("Casting from u8 to T failed");
            holder2 << ((type_size - i) * 8)
        })
        .fold(num::zero(), |acc, h| acc | h);
    Ok(bytes_read)
}

The issue is that when I call it in the main function, I seem to always get 0x00 back out, but the number of bytes read that is returned by the function is always 2, so that the program enters an infinite loop:

extern crate num;

use std::fs::File;
use std::io::BufReader;
use std::io::prelude::Read;

fn main() -> Result<(), std::io::Error> {
    let cmd_line_args = std::env::args().collect::<Vec<_>>();

    let f = File::open(&cmd_line_args[1])?;
    let mut reader = BufReader::new(f);
    let mut instructions: Vec<u16> = Vec::new();

    let mut next_instruction: u16 = 0;
    fread(&mut next_instruction, 1, &mut reader)?;

    let base_address = next_instruction;

    while fread(&mut next_instruction, 1, &mut reader)? > 0 {
        instructions.push(next_instruction);
    }

    println!("{:#04x}", base_address);

    for i in instructions {
        println!("0x{:04x}", i);
    }

    Ok(())
}

It appears to me that I'm somehow never reading anything from the file, so the function always just returns the number of bytes it was supposed to read. I'm clearly not using something correctly here, but I'm honestly unsure what I'm doing wrong.

This is compiled on Rust 1.26 stable for Windows if that matters.

What am I doing wrong, and what should I do differently to replicate fread? I realise that this is probably a case of the XY problem (in that there's almost certainly a better Rust way to repeatedly read some bytes from a file and pack them into one unsigned integer), but I'm really curious as to what I'm doing wrong here.

like image 730
Jarak Avatar asked Jan 01 '26 20:01

Jarak


2 Answers

Your problem is that this line:

let mut buf = Vec::with_capacity(element_count * type_size);

creates a zero-length vector, even though it allocates memory for element_count * type_size bytes. Therefore you are asking stream.read_exact to read zero bytes. One way to fix this is to replace the above line with:

let mut buf = vec![0; element_count * type_size];

Side note: when the read succeeds, bytes_read receives the number of bytes you expected to read, not the number of bytes you actually read. You should probably use std::mem::size_of_val (buf_slice) to get the true byte count.

like image 114
Jmb Avatar answered Jan 03 '26 08:01

Jmb


in that there's almost certainly a better Rust way to repeatedly read some bytes from a file and pack them into one unsigned integer

Yes, use the byteorder crate. This requires no unneeded heap allocation (the Vec in the original code):

extern crate byteorder;

use byteorder::{LittleEndian, ReadBytesExt};
use std::{
    fs::File, io::{self, BufReader, Read},
};

fn read_instructions_to_end<R>(mut rdr: R) -> io::Result<Vec<u16>>
where
    R: Read,
{
    let mut instructions = Vec::new();
    loop {
        match rdr.read_u16::<LittleEndian>() {
            Ok(instruction) => instructions.push(instruction),
            Err(e) => {
                return if e.kind() == std::io::ErrorKind::UnexpectedEof {
                    Ok(instructions)
                } else {
                    Err(e)
                }
            }
        }
    }
}

fn main() -> Result<(), std::io::Error> {
    let name = std::env::args().skip(1).next().expect("no file name");

    let f = File::open(name)?;
    let mut f = BufReader::new(f);

    let base_address = f.read_u16::<LittleEndian>()?;
    let instructions = read_instructions_to_end(f)?;

    println!("{:#04x}", base_address);

    for i in &instructions {
        println!("0x{:04x}", i);
    }

    Ok(())
}
like image 36
Shepmaster Avatar answered Jan 03 '26 10:01

Shepmaster



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!