7
\$\begingroup\$

My computer has been having this issue where when I press a key, two key presses get registered. The goal of this program is to detect the time delta between the two keys so that I can set an effective debounce threshold. I also thought that it would be a good idea to try out Rust, so here it is:

use std::{mem, ptr, env, process, io};
use std::io::Read;
use std::io::Write;
use std::fs::File;
use std::collections::HashMap;

const EV_SYN: u16 = 0x00;
const EV_KEY: u16 = 0x01;

#[repr(C)]
struct LinuxTimeval {
    tv_sec: isize,
    tv_usec: isize,
}

impl LinuxTimeval {
    fn to_microseconds(&self) -> u64 {
        self.tv_sec as u64 * 1000000u64 + self.tv_usec as u64
    }
}

#[repr(C)]
struct LinuxInputEvent {
    time: LinuxTimeval,
    type_: u16,
    code: u16,
    value: i32,
}

fn read_struct_from_file<T: Sized>(file: &mut File) -> T {
    let mut buf = vec![0u8; mem::size_of::<T>()];
    match file.read(&mut buf) {
        Ok(sz) => assert!(sz == buf.len()),
        Err(err) => panic!("Unable to read from device: {}", err),
    }
    let ptr = &buf as &[u8] as *const [u8] as *const T;
    unsafe { ptr::read(ptr) }
}

fn read_events_from_file(file: &mut File) -> Vec<LinuxInputEvent> {
    let mut vec = Vec::new();
    loop {
        let event: LinuxInputEvent = read_struct_from_file(file);
        if event.type_ == EV_SYN {
            if vec.len() > 0 {
                break;
            } else {
                continue;
            }
        } else {
            vec.push(event);
        }
    }
    vec
}

fn process_key_event(event: &LinuxInputEvent,
                     last_occurence_per_key: &mut HashMap<u16, u64>,
                     threshold: u64) {
    let key = event.code;
    let micros = event.time.to_microseconds();
    match last_occurence_per_key.get(&key) {
        None => (),
        Some(last_micros) => {
            let delta = micros - last_micros;
            if delta < threshold {
                println!("Key code {:3}: event delta: {} us",
                         key, delta);
            }
        }
    }
    last_occurence_per_key.insert(key, micros);
}

fn print_usage_and_exit(program_name: &str) {
    writeln!(&mut io::stderr(), "Usage: {} <microseconds-delta>",
             program_name).unwrap();
    process::exit(1);
}

fn main() {
    let args: Vec<_> = env::args().collect();
    if args.len() != 2 { print_usage_and_exit(&args[0]); }
    let threshold: u64 = args[1].parse().unwrap_or(0);
    if threshold == 0 { print_usage_and_exit(&args[0]); }
    let mut file;
    match File::open("/dev/input/by-path/platform-i8042-serio-0-event-kbd") {
        Ok(f) => { file = f },
        Err(err) => panic!("Unable to open file: {}", err),
    }
    let mut last_occurence_per_key = HashMap::new();
    loop {
        match read_events_from_file(&mut file)
                .iter().find(|&event| event.type_ == EV_KEY && event.value == 1) {
            None => continue,
            Some(event) => process_key_event(event,
                                             &mut last_occurence_per_key,
                                             threshold),
        }
    }
}

Please let me know if any improvements can be made.

\$\endgroup\$
  • \$\begingroup\$ An interesting problem! I'm having a hard time generating keyboard events in a VM with Docker. I assume that's because none of it is going through the keyboard device! Do you have any suggestions on how I could generate those events to see that any suggested changes still work? \$\endgroup\$ – Shepmaster Jul 12 '15 at 20:00
  • \$\begingroup\$ Try evtest, which simply dumps all the events from the device: packages.ubuntu.com/vivid/evtest \$\endgroup\$ – gvl Jul 12 '15 at 20:31
  • \$\begingroup\$ Sorry, I think I was confusing — my problem is that nothing will ever be read from /dev/input/by-path/platform-i8042-serio-0-event-kbd because there is no real keyboard attached. I'm looking for a way to generate fake input events. \$\endgroup\$ – Shepmaster Jul 12 '15 at 20:38
  • 1
    \$\begingroup\$ Apparently you can write "struct input_event" structures into the evdev device and it'll inject them as if they came from the device. lxr.free-electrons.com/source/drivers/input/evdev.c#L507 \$\endgroup\$ – gvl Jul 12 '15 at 21:02
6
\$\begingroup\$

Highlights

  • Using the libc crate for C data types
  • No heap allocation of read struct
  • No (new) panics — better error handling
  • Event iterator instead of vectors
  • New types and traits to collect related functionality

Beware - I still haven't gotten this to actually read any events on my machine, but it does compile. That should be good enough, right? ^_^

libc crate

The libc crate should always be used when interoperating with C code. This crate is guaranteed to have the correct sizes for platform-specific types.

Additionally, it already has a definition for timeval, so that was nice.

Less heap allocation

I used mem::uninitialized to create an on-stack version of the struct with no values. I then got a reference to that area as bytes and read directly to that.

Note that I marked that function as unsafe - depending on what T resolves to, it could produce very bad results.

Error handling

Everywhere that had a panic! or assert! was changed into a Result. This allows the caller to decide how to deal with the error. The most awkward place left is in the call to read_c_struct, as it just stops the iterator on failure. Instead, it could return the Result directly and allow its caller to see the failure.

Embrace the Iterator

Rust loves iterators. This seemed like a perfect case for it - you have a stream of events and you want to do something based on them. I reworked your method into an Iterator implementation. This allows the caller to decide how much memory to allocate; in the final code, I believe it's very small. It also allows using the filter method and other iterator adaptors.

New types and traits

Iterators need a type to exist, and I also felt that your hashmap and threshold needed to live together. I also moved the read-specific methods into a new trait that is then implemented for all types that implement Read.

Complete code

There's a few comments scattered about for smaller things, but here it is:

extern crate libc;

use std::{mem, env, process, io, slice};
use std::io::{Read, Write};
use std::fs::File;
use std::collections::HashMap;

// Some helper methods for reading things
trait ReadExtensions : Read {
    /// Completely fill the supplied buffer
    fn read_complete(&mut self, buf: &mut [u8]) -> io::Result<()> {
        let mut total_read = 0;
        while total_read < buf.len() {
            match self.read(&mut buf[total_read..]) {
                // Unable to read anything
                Ok(0) => return Err(io::Error::new(io::ErrorKind::Other, "Unable to read")),
                // Read some amount
                Ok(n) => total_read += n,
                // Might have been interrupted by something
                Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {},
                Err(e) => return Err(From::from(e))
            }
        }
        Ok(())
    }

    /// Directly read a C struct.
    ///
    /// Unsafe because only the concrete type of `T` can determine if
    /// the result is safe. Should only be used to read structs
    /// composed of primitive types without any pointers.
    unsafe fn read_c_struct<T>(&mut self) -> io::Result<T> {
        // avoid allocating on the heap
        let mut val = mem::uninitialized();
        let ptr = &mut val as *mut T as *mut u8;
        let mut buf = slice::from_raw_parts_mut(ptr, mem::size_of::<T>());

        try!(self.read_complete(buf));
        Ok(val)
    }
}

impl<R> ReadExtensions for R where R: io::Read {}



const EV_KEY: u16 = 0x01;

fn to_microseconds(tv: libc::timeval) -> u64 {
    // u64 suffix unneeded, adding _ for readability
    tv.tv_sec as u64 * 1_000_000 + tv.tv_usec as u64
}

// Using libc
#[derive(Copy,Clone)]
#[repr(C)]
struct LinuxInputEvent {
    time: libc::timeval,
    type_: libc::uint16_t,
    code: libc::uint16_t,
    value: libc::int32_t,
}

/// An iterator of Linux events
struct InputSource {
    file: File,
}

impl InputSource {
    fn new(path: &str) -> io::Result<InputSource> {
        let f = try!(File::open(path));
        Ok(InputSource { file: f })
    }
}

impl Iterator for InputSource {
    type Item = LinuxInputEvent;

    fn next(&mut self) -> Option<LinuxInputEvent> {
        unsafe { self.file.read_c_struct() }.ok()
    }
}

/// Tracks when multiple presses of the same key occurs and calls the
/// callback when duplicate events are triggered within a threshold.
struct KeyEventSink {
    last_occurences: HashMap<u16, u64>,
    threshold: u64,
}

impl KeyEventSink {
    fn new(threshold: u64) -> KeyEventSink {
        KeyEventSink {
            last_occurences: HashMap::new(),
            threshold: threshold,
        }
    }

    // Take a closure so we don't have to worry about user interface here
    fn process_event<F>(&mut self, event: LinuxInputEvent, f: F)
        where F: FnOnce(u16, u64)
    {
        let key = event.code;
        let micros = to_microseconds(event.time);

        // if-let for one useful match arm
        if let Some(last_micros) = self.last_occurences.get(&key) {
            let delta = micros - last_micros;
            if delta < self.threshold {
                f(key, delta)
            }
        }
        self.last_occurences.insert(key, micros);
    }
}

fn print_usage_and_exit(program_name: &str) {
    writeln!(&mut io::stderr(), "Usage: {} <microseconds-delta>", program_name).unwrap();
    process::exit(1);
}

fn main() {
    let args: Vec<_> = env::args().collect();
    if args.len() != 2 { print_usage_and_exit(&args[0]); }
    let threshold: u64 = args[1].parse().unwrap_or(0);
    if threshold == 0 { print_usage_and_exit(&args[0]); }

    // just use value of match
    let input = match InputSource::new("/dev/input/event2") {
        Ok(f) => f,
        Err(err) => panic!("Unable to open file: {}", err),
    };

    let mut sink = KeyEventSink::new(threshold);

    let key_events = input.filter(|&event| event.type_ == EV_KEY && event.value == 1);
    for key_event in key_events {
        sink.process_event(key_event, |key, delta| {
            println!("Key code {:3}: event delta: {} μs", key, delta);
        })
    }
}

Followups

is there any particular reason you added the Copy and Clone traits to LinuxInputEvent?

Basically, LinuxInputEvent is a simple type comprised of Copyable values, and it's also fairly small in the grand scheme of things. Making it Copy simply improves the ergonomics of using it. If you implement Copy, you need to implement Clone.

Seeing as the function is used at most once, FnOnce would also be better than Fn

Absolutely correct - I updated to use this.

I also updated the read_complete method a bit based on feedback from this Stack Overflow question.

\$\endgroup\$
  • \$\begingroup\$ Thanks for your improvements! I know how to use a lot more of the idioms now. I didn't expect partial reads due to the nature of the device but I guess it's good to include it anyways. I had no idea that trait extensions were a thing either. One question though: is there any particular reason you added the Copy and Clone traits to LinuxInputEvent? \$\endgroup\$ – gvl Jul 13 '15 at 4:11
  • 1
    \$\begingroup\$ The convention for simple generic bounds seems to be leaning back away from using where, yielding fn process_event<F: Fn(u16, u64)>(… (actually, with closures it may well be heading to using trait objects, but we’re not there yet). Seeing as the function is used at most once, FnOnce would also be better than Fn. \$\endgroup\$ – Chris Morgan Jul 13 '15 at 10:59
  • \$\begingroup\$ @gvl updated to address comments \$\endgroup\$ – Shepmaster Jul 16 '15 at 2:21
  • \$\begingroup\$ @ChrisMorgan I don't know if I can get behind that shift. Once there's a second set of <> or () in the bounds, I don't think it's simple. Even if there are two different bounds, I don't think it's simple. I basically always use where now, because that lets me grow it without reorganizing my code. I basically only use ?Sized in the declaration <>. The trait object part is surprising - those won't be optimized as aggressively, which makes me sad. \$\endgroup\$ – Shepmaster Jul 16 '15 at 2:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.