Gamepad Input with Rust

jeikabu profile image jeikabu Originally published at rendered-obsolete.github.io on ・1 min read

Part of our platform includes a user-space device layer that abstracts input to gamepads supporting DirectInput, XInput, or HID. It uses dll injection to attach to games (via Evolve’s tech) and provide input. It’s a fairly large chunk of worrisome C++ with dubious multi-threading that keeps me up at night.

I’ve been toying with the idea of gradually replacing it with Rust. To do that I decided to look into accessing HID gamepads from Rust on both Windows and OSX. Specifically, these devices (cross-reference with this online list):

Gamepad Vendor Id Product Id Notes
Microsoft Xbox One 045e 02d1
Ruyi Wireless 0483 5751 Not yet available
Sony PS4 054c 09cc
Nintendo Switch Pro 057e 2009

Thus far have found two crates that wrap native libraries:

Cargo libusb

Add the following to Cargo.toml:

libusb = "0.3.0"

Run cargo build:

process didn't exit successfully: `/Users/jake/projects/input/target/debug/build/libusb-sys-a53f7746ba781f88/build-script-build` (exit code: 101)
--- stderr
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "Failed to run `\"pkg-config\" \"--libs\" \"--cflags\" \"libusb-1.0\"`: No such file or directory (os error 2)"', libcore/result.rs:945:5

Oops, no pkg-config. Found instructions how to build it on OSX:

LDFLAGS="-framework CoreFoundation -framework Carbon" ./configure --with-internal-glib
sudo make install

Again, cargo build:

   Compiling libusb-sys v0.2.3
   Compiling libusb v0.3.0
   Compiling input v0.1.0 (file:///Users/jake/projects/input)
    Finished dev [unoptimized + debuginfo] target(s) in 1.33s

libusb Sample

From github and docs enumerate USB devices and get identifying strings:

let timeout = std::time::Duration::from_secs(1);
let mut context = libusb::Context::new().unwrap();
for mut device in context.devices().unwrap().iter() {

    let mut handle = device.open().unwrap();
    let langs = handle.read_languages(timeout);
    if let Ok(langs) = langs {
        for lang in langs.iter() {

            let manufacturer = handle.read_manufacturer_string(*lang, &device_desc, timeout).unwrap();
            let product = handle.read_product_string(*lang, &device_desc, timeout).unwrap();
            println!("Lang {:?} manufacturer={} product={}", lang.primary_language(), manufacturer, product);

Works for all the devices I had on-hand.

Tried reading values in a loop but it fails with LIBUSB_ERROR_NOT_FOUND:

loop {
    let mut buffer: [u8; 128] = [0; 128];
    let read = handle.read_bulk(libusb_sys::LIBUSB_ENDPOINT_IN, &mut buffer, timeout);
    match read {
        Ok(count) => println!("{}", count),
        Err(error) => println!("{}", error)

Looks like you need to call claim_interface() before using the device:

if let Err(error) = handle.claim_interface(0) {
    println!("claim_interface failed: {}", error);

But on OSX it fails with: Access denied (insufficient permissions).

This seems to be a common problem with libusb on OSX (libusb FAQ, github issue). Tried calling detach_kernel_driver() but it returns Operation not supported or unimplemented on this platform.

Switched over to a Windows 10 machine and then I was back to not having pkg-config


A second option is hidapi. Just by looking at the API it’s clearly simplier than libusb.

In Cargo.toml:

hidapi = "0.5.0"

Simple read loop:

fn main() {
    let api = hidapi::HidApi::new().unwrap();
    // Print out information about all connected devices
    for device in api.devices() {
        println!("{:?}", device);

    // Connect to device using its VID and PID (Ruyi controller)
    let (VID, PID) = (0x0483, 0x5751);
    let device = api.open(VID, PID).unwrap();
    let manufacturer = device.get_manufacturer_string().unwrap();
    let product = device.get_product_string().unwrap();
    println!("Product {:?} Device {:?}", manufacturer, product);

    loop {
        // Read data from device
        let mut buf = [0u8; 8];
        let res = device.read_timeout(&mut buf[..], 1000).unwrap();
        println!("Read: {:?}", &buf[..res]);

When I first tested this at home with a Nintendo Switch Pro controller, read_timeout() always times out and returns no data. As I was writing this up as a failed/unsucessful experiment and double-checking error codes and so on, I found out it works correctly with our “Ruyi” controller!

For our controller the read returns 10 bytes:

use std::fmt;
use std::mem;

#[derive(Copy, Clone)]
enum Buttons {
    DPadLeft = 0x0001,
    DPadRight = 0x0002,
    DPadUp = 0x0004,
    DPadDown = 0x0008,
    Start = 0x0010,
    Opt = 0x0020,
    Home = 0x0040,
    Share = 0x0080,
    X = 0x0400,
    B = 0x0800,
    Y = 0x1000,
    A = 0x2000,
    L1 = 0x4000,
    R1 = 0x8000,

struct RuyiInput {
    header: u16, // First byte is report number?
    buttons: Buttons,
    left_trigger: u8,
    right_trigger: u8,
    left_stick_x: u8,
    left_stick_y: u8,
    right_stick_x: u8,
    right_stick_y: u8,

impl RuyiInput {
    fn new(data: [u8; 10]) -> RuyiInput {
        unsafe {

Button presses are bit-flags set in two bytes. enum Buttons represents flags returned by the device, and #[repr(u16)] makes it a u16 (see this SO).

As per this SO, use mem::transmute() to convert the bytes into a struct RuyiInput.

I originally had #[derive(Debug)] on the enum and struct and println!("{:?}", input) to print it out. But, on the first loop iteration the program exits with Illegal instruction: 4. Turns out it was because there was no enum value for 0 (or other bit-flag combinations).

Instead, implement fmt::Display:

impl fmt::Display for RuyiInput {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:04x} LT {} RT {} LS x{}y{} RS x{}y{}", self.buttons as u16, 
            self.left_trigger, self.right_trigger, self.left_stick_x, self.left_stick_y, 
            self.right_stick_x, self.right_stick_y)

Copy (and Clone) are needed on enum Buttons otherwise self.buttons as u16 fails to compile with cannot move out of borrowed content.

Next up, sending it somewhere.

Posted on Sep 17 '18 by:


markdown guide