Blog Platform 8 min read

hidapi vs native gamepad drivers — picking the right one

hidapi vs native gamepad APIs (GameController, GameInput, evdev) for a MIDI bridge. Cross-platform shortcut vs full feature coverage — when each wins.

By Aidxn Design

Building a cross-platform gamepad MIDI bridge means picking a level of abstraction. Go too low and you reimplement the Mac, Windows, and Linux input stacks three times. Go too high (SDL3, libusb wrappers) and you pay 8 MB of runtime and lose adaptive triggers. The middle path is hidapi — a tiny C library that talks to USB and Bluetooth HID devices on all three OSes with the same eleven functions. It is not perfect, but for ninety percent of gamepad projects, hidapi gamepad code is the right choice for v1.

TL;DR
  • hidapi is a ~150 KB C library that wraps HIDClass on Windows, IOHID on macOS, and hidraw/libusb on Linux.
  • Use hidapi for prototype, MVP, and cross-platform v1. You will ship in an afternoon.
  • Use native APIs (GameController, GameInput, evdev) when you need adaptive triggers, advertised capabilities, or sub-millisecond latency.
  • Mixed approach: hidapi for input read, native call for feature-specific write (e.g. DualSense vendor report 0x05 for adaptive triggers).

What hidapi is, exactly

hidapi is maintained by the libusb organisation. The entire public API fits on one screen:

hid_init();
hid_enumerate(vendor_id, product_id);
hid_open(vid, pid, serial);
hid_read(handle, buf, len);
hid_write(handle, buf, len);
hid_send_feature_report(handle, buf, len);
hid_get_feature_report(handle, buf, len);
hid_close(handle);
hid_exit();

Eleven functions. ~150 KB compiled. Zero runtime dependencies on macOS and Linux. On Windows it links against HID.dll which is part of the OS. The licence is permissive (BSD-3 or GPL-3, your choice). It just works.

A complete DualSense reader in 40 lines

#include <hidapi.h>
#include <stdio.h>
#include <string.h>

#define SONY_VID   0x054C
#define DS_PID     0x0CE6   // DualSense

int main(void) {
    if (hid_init() != 0) return 1;
    hid_device *dev = hid_open(SONY_VID, DS_PID, NULL);
    if (!dev) { printf("DualSense not found\n"); return 1; }

    unsigned char buf[64];
    while (1) {
        int n = hid_read_timeout(dev, buf, sizeof buf, 100);
        if (n <= 0) continue;

        // USB report 0x01: byte offsets are stable
        unsigned lx = buf[1], ly = buf[2];
        unsigned rx = buf[3], ry = buf[4];
        unsigned l2 = buf[5], r2 = buf[6];
        unsigned buttons = buf[8] | (buf[9] << 8);

        // Scale & emit MIDI (CC + Note On/Off)
        emit_cc(1, lx * 127 / 255);
        emit_cc(2, ly * 127 / 255);
        if (buttons & 0x0001) emit_note_on(60, 127);
    }
}

That is a working bridge for the input direction. Forty lines, builds on Mac, Windows, Linux unchanged. The native equivalent is forty lines of Swift and forty lines of C++ and forty lines of C. We covered each in the GameController, GameInput, and evdev deep dives.

Where hidapi falls short

Three places.

Adaptive triggers. The DualSense uses vendor-specific output reports (0x02 on USB, 0x31 on BT) to control adaptive triggers. hidapi can send these — hid_write works fine — but you have to know the byte layout, including the magic flag bits and the per-mode parameter packing. Native APIs (GCDualSenseAdaptiveTrigger on macOS) typed-check this for you.

BT report-format differences. On USB the DualSense reports byte 1 onwards. On BT, the report starts with a different header and uses byte 2 onwards. hidapi gives you raw bytes; you write the BT-vs-USB branching yourself. Native APIs normalise this.

Capability discovery. Native APIs tell you "this device has adaptive triggers, this one does not". hidapi gives you VID/PID and trust your lookup table. For shipping a product that supports DualSense, DualShock 4, Xbox Series, Switch Pro, and the long tail of clones, that lookup table grows fast.

Comparison matrix

ConcernhidapiNative APIs
Cross-platform codeOne fileThree implementations
Input latency (USB)~1.5 ms~1.5 ms (parity)
Input latency (BT)~5 ms~4 ms (slight win)
Adaptive triggersManual byte packingTyped API
TouchpadRaw report parsingSurfaced as struct fields
RumbleManualBuilt-in
Device hot-plugPoll hid_enumerateOS-level events
Binary size impact~150 KB0 (system framework)
Permission needed (Linux)udev rule for hidrawSame (evdev)

The hybrid approach we actually ship

Universal Controller MIDI v1 was 100% hidapi. It worked. We shipped on macOS, Windows, and Linux from the same C codebase, latency was within spec, and we cared about getting to a paying user rather than nailing every platform feature.

v2 added native input on macOS (GameController) and Windows (GameInput) but kept hidapi as the fallback for everything else. The adaptive-trigger and touchpad code paths are native; the input-read for an off-brand Switch Pro clone or a Stadia controller still goes through hidapi. The result is best-of-both: feature-rich on the controllers that matter, broad coverage on the long tail. The cross-platform UX of this approach is documented in our Mac + Windows setup walkthrough.

When hidapi is wrong

  • You are shipping on one OS only. Skip it, use the native API, win on every axis.
  • You need to brag about "< 1 ms latency". hidapi is fast but the kernel HID layer adds a context switch. Native APIs occasionally dodge that on macOS via direct IOKit routes.
  • Your device has a vendor SDK. Steam Controller, Razer peripherals, some flight sticks ship vendor SDKs with capabilities that hidapi cannot reach.
  • You need report-descriptor parsing. hidapi exposes raw bytes — it does not parse the HID report descriptor for you. Some pads need that parsing to be useful.

Practical advice

  • Start with hidapi. Always. The afternoon-long prototype removes more risk than weeks of native code research.
  • Keep a VID/PID + offsets table in JSON, not in code. Adding a new controller becomes a config change.
  • Wrap hidapi behind your own interface. When you swap in native APIs later, the call sites do not change.
  • Test on Bluetooth early. The USB/BT report-format gap is where hidapi projects break first.
  • Pair with a kernel driver only if you have a reason. Custom drivers are a maintenance burden across OS updates — three years from now, you will regret it.

hidapi is the kind of library that wins quietly. It does one thing, the API has not changed meaningfully in a decade, and shipping a v1 on top of it is a half-day's work. Native APIs are the right destination eventually, but they are rarely the right starting point. For our money, the right answer is "hidapi today, native APIs tomorrow when a feature demands it". That has shipped a working product. Which is the only metric that matters.

Keep reading

More setup walkthroughs