Ruby FFI for Quick Prototyping

I recently found myself trying to answer the question “Is there a quick way to talk to a USB HID device from Windows via Ruby?”. The short answer is “yes, via HID API and FFI,” but that’s much too short a story. Let’s look at the long answer:

I consulted my trusty friends Github, Rubygems, and Google. After looking around for a bit, nothing met my needs. There were libraries that did USB via libusb. There were even one or two that had a nice clean HID interface, but they hadn’t been touched in four years and no longer worked in modern versions of Ruby.

Along the way I was guided to a C library called HID API. The interface looked clean and simple. Only one problem, no Ruby. Then I remembered how easy Ruby’s foreign function interface (FFI) was to use. I’ve used it in the past and John Croisant wrote a great introduction to it last year. Here’s the code I needed to finish up my quick spike:

require 'ffi'
 
module HidApi
  extend FFI::Library
  ffi_lib 'hidapi'
 
  attach_function :hid_open, [:int, :int, :int], :pointer
  attach_function :hid_write, [:pointer, :pointer, :int], :int
  attach_function :hid_read_timeout, [:pointer, :pointer, :int, :int], :int
  attach_function :hid_close, [:pointer], :void
 
  REPORT_SIZE = 65 # 64 bytes + 1 byte for report type
  def self.pad_to_report_size(bytes)
    (bytes+[0]*(REPORT_SIZE-bytes.size)).pack("C*")
  end
end
 
# USAGE
 
# CONNECT
vendor_id = 1234
product_id = 65432
serial_number = 0
device = HidApi.hid_open(vendor_id, product_id, serial_number)
 
# SEND
command_to_send = HidApi.pad_to_report_size([0]+ARGV.map(&:hex)).pack("C*")
res = HidApi.hid_write dev, command_to_send, HidApi::REPORT_SIZE
raise "command write failed" if res <= 0
 
# READ
buffer = FFI::Buffer.new(:char, HidApi::REPORT_SIZE)
res = HidApi.hid_read_timeout device, buffer, HidApi::REPORT_SIZE, 1000
raise "command read failed" if res <= 0
p buffer.read_bytes(HidApi::REPORT_SIZE).unpack("C*")

To be clear, this was a quick spike to test feasibility of an idea. A clean tested API would need to sit on top of the FFI layer, but I was able to get Ruby speaking to a USB HID device on Windows with just a handful of lines. An entire finished and functioning wrapper wasn’t needed, so I just wrapped the basics. FFI allows you the speed of Ruby prototyping with access to fully mature C libraries. I’ll definitely keep FFI in my tool-belt for quick prototyping in the future.