Weird behaviour reading from a Linux device event stream

Hi all,

First of all, I'm new to Rust. 20-plus years ago I was an embedded software engineer in assembly and C, but after a long time in the wilderness with Java (and management), I'm using Rust to re-learn the joy of programming (rather than the joy of Powerpoint, which is more the day job.) I say that to state that it may not just be that I'm new to Rust, it may be that my programming skills are, err, rusty also.

Secondly - the last time I was asking programming questions on a forum, Usenet was still a thing. So apologies if this is the wrong place to ask for help! Do let me know if I'd be better placed elsewhere.

So, all disclaimers aside, I have something going on in Rust that is confusing the hell out of me, and I'd love to know if anyone knows where I'm going wrong...

I have a simple little project that involves reading (on Linux) a USB event stream, direct from the /dev/input event streams. Basically, I just need to open a file (which happens to be a device special), and read the bytes in.

Opening a file and reading bytes is kind of the simplest program after Hello, World, so I've impressed myself that I've managed to make a mess of it.

The problem is this: I have written the code, and it works perfectly opening a "regular" file. But if I instead open the device, let's say /dev/input/event8, I get an "Invalid argument" (OS error 22) when I try to read() anything. Not when the file is opened, but rather when I try to read.

Because 'endianness', I'm using the read_u32 (etc.) methods, which I gather under-the-bonnet uses read_exact - and I can't see how Invalid argument is something that can return.

I thought maybe it was something to do with me not understanding blocking/non-blocking reads, given it's so long since I wrote real code - so I spent a lot of time looking at OpenOptions and the like, to no avail. I also tried a little experiment - I created a FIFO special file with mkfifo, used cat to just pipe the device data into the fifo and then pointed my Rust program at the fifo instead of directly at the device file - and that worked absolutely fine. So it seems to be something peculiarly unique to opening device specials from Rust.

If anyone has any idea what I am doing wrong, I'd love to know!

All the best and thanks in advance,
Tim

TL;DR version:

This code would fail with an OS Error 22, on the read. On Linux. Obviously, point it at a device file that actually exists, and with permission to open the file (i.e. sudo).

fn main() {
  let mut file_options = OpenOptions::new();
  file_options.read(true);
  file_options.write(false);

  let dev_file = file_options.open("/dev/input/event8").unwrap();

  // This next line will fail on a device special, but NOT on a FIFO, or
  // a regular file
  let anything = dev_file.read_u32::<LittleEndian>()?;  
}

I am SURE there must be something incredibly obvious that I'm doing wrong - but I honestly cannot work it out...

This is a very good place for questions!


Do you have an example of a C program that can successfully read the file?

Now THAT is an excellent question. And one which I regret not having thought to actually ask myself first..

It seems the behaviour is weird when I write a minimal C program as well. Sigh. So I guess this is something I don't understand about Linux device specials rather than something I don't understand about Rust... I just assumed it was my bad Rust programming at fault, rather than my bad Linux knowledge.

Sigh; OK, apologies for wasting your time, but thank you for prodding me in the right direction! I don't know why I didn't think of trying that myself...

You're welcome. It does seem somewhat surprising that those files act weird, and I couldn't find a description on those rules with a few searches myself, so if you find a resource that explains it, please post a link to it here.

1 Like

I think you're supposed to always read a multiple of sizeof(struct input_event) (as defined in linux/input.h).

2 Likes

Mmm, that is what I was trying to do, but doing so entry-by-entry, as a somewhat naive implementation - looking something like:

    let tv_sec  = rdr.read_u32::<NativeEndian>()?;
    let tv_usec = rdr.read_u32::<NativeEndian>()?;
    let evtype  = rdr.read_u16::<LittleEndian>()?;
    let code    = rdr.read_u16::<LittleEndian>()?;
    let value   = rdr.read_i32::<LittleEndian>()?;

But maybe you are right and there's something about Linux device specials that don't allow partial reads... I rewrote my code now to use read_exact and a buffer for the whole message and I still get the same problem though :frowning:

Thank you for the tips, anyway! I shall investigate more in the morning...

Strange. Does it happen with the other event devices as well? Maybe this particular one is broken somehow. Does evtest work on it?

FWIW There's a evdev crate that provides a rust implementation of the evdev code, might be useful to compare against. Though if it doesn't work in C either that is likely no help.

FWIW it also fails here with dev_file.read_u32 directly from the socket, but this works fine for me:

use std::fs::OpenOptions;
use std::io::{Cursor, Read};
use byteorder::{NativeEndian, ReadBytesExt};

fn main() {
    let mut file_options = OpenOptions::new();
    file_options.read(true);
    file_options.write(false);

    let mut dev_file = file_options.open("/dev/input/event0").unwrap();

    let mut packet = [0u8; 24];
    dev_file.read_exact(&mut packet).unwrap();

    let mut rdr = Cursor::new(packet);
    let tv_sec  = rdr.read_u64::<NativeEndian>().unwrap();
    let tv_usec = rdr.read_u64::<NativeEndian>().unwrap();
    let evtype  = rdr.read_u16::<NativeEndian>().unwrap();
    let code    = rdr.read_u16::<NativeEndian>().unwrap();
    let value   = rdr.read_i32::<NativeEndian>().unwrap();

    println!("{} {} {} {} {}", tv_sec, tv_usec, evtype, code, value);
}

Edit: note this will likely only work on x86_64, on other platforms the fields in the timeval structure will be differently-sized.
(for example on armv7-unknown-linux-gnueabihf the fields are 32 bits, and sizeof(struct input_event)==16)

1 Like

Aha! You're my hero :-). Thank you...

So; my "read_exact" version didn't work because I was using the wrong type for tv_sec/tv_usec (32bit instead of 64) - so I was actually reading too small a buffer still. Now I fixed the types, it works. (My actual eventual target is indeed Arm, so dumb me was looking at the Arm declaration of timeval rather than the x86 ones I'm currently building on. (Actually, I'm building on a Mac and cross-compiling for both Arm and Linux-x86, but that's another story. That's one of the reasons I chose this project to learn Rust with, because I know the pain of cross-compiling C... Getting working cross compilation working with Rust has been an absolute joy by comparison.)

For my next trick, I need to work out the "correct" way to use the correct timeval type for the target I'm compiling for...

For what it's worth, my - now working - implementation looked like this:

#[repr(C)]
#[derive(Debug)]
pub struct InputEvent {
  tv_sec: cty::uint64_t,
  tv_usec: cty::uint64_t,
  evtype: cty::uint16_t,
  code: cty::uint16_t,
  value: cty::int32_t
}

impl InputEvent {
  pub fn from_reader(mut rdr: impl Read) -> io::Result<Self> {
    let struct_size = mem::size_of::<InputEvent>();
    unsafe {
      let mut event: InputEvent = mem::zeroed();
      let buffer = slice::from_raw_parts_mut(&mut event as *mut _ as *mut u8, struct_size);

      rdr.read_exact(buffer).unwrap();

      Ok(event)
    }
}

...which I am sure is horrible in all sorts of other ways, but does at least now work without that error.

But, I much prefer your solution with the Cursor. It seems 'safer', and also allows me to use the Endian conversions easily - so I'll be switching to your version :smiley:.

Anyway, enough rambling - super thanks for your help. I learned something I don't much like about those device specials on Linux - if they're exposed as character specials they should allow reading by character, and I don't like that now I am dependent on the implementation of read_exact mapping to a single read() at OS level or things will break. That seems like bad encapsulation at the Linux level, but that's not Rust's problem :slight_smile:.

Thanks again,
Tim

Happy it helped! :smile:

Yea—interfacing to the Linux kernel without the specified user-space libraries is always a portability nightmare.

If you're not working in no_std context, you could use libc's timeval type libc::timeval.

Even then I'm not sure in how far this is guaranteed to remain the case in the future (there's 2038-compatibility work going on to extend all time fields to 64 bit).

This made me realize that you don't need read_exact at all. A simple read() will work just as well and guarantees that one syscall is used.

Yes. Maybe. It's a scalability compromise. An ev device is a single producer multi-consumer queue, so it's desirable for the kernel side to keep as little state as possible per consumer. And to support partial reads they'd have to do buffering. Clients tend to want to minimize latency (no one likes input lag!) so tend to want to read entire packets with a single syscall.

1 Like

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.