На днях нужно было построить биндинги к библиотеке libftdi, которая обеспечивает взаимодействие с чипами FTDI, которые позволяют создавать различные USB-устройства.
Для создания биндингов я выбрал расширение FFI, которое позволяет загружать динамические библиотеки и строить биндинги к ним.
У FFI есть несколько достоинств, которые сыграли в его пользу:
- Поддержка интерпретаторов MRI Ruby 1.9, MRI Ruby 1.8, JRuby, поддержка платформы Windows, ограниченная поддержка Rubinius;
- Отсутствие необходимости компиляции биндингов;
- Удобный язык описания биндингов.
Репозиторий биндингов для Ruby.
Начало
Создаем модуль биндингов, который подгружает библиотеку libftdi:
require 'ffi'
# Represents libftdi ruby bindings.
# End-user API represented by {Ftdi::Context} class.
module Ftdi
extend FFI::Library
ffi_lib "libftdi"
end
Управление неуправляемыми ресурсами
Основной сущностью libftdi является её контекст, который нужно выделить при начале работы с ней, и затем, соответственно, освободить. За автоматический сбор неуправляемых ресурсов отвечает класс FFI::ManagedStruct
:
attach_function :ftdi_new, [ ], :pointer
attach_function :ftdi_free, [ :pointer ], :void
# Represents libftdi context and end-user API.
# @example Open USB device
# ctx = Ftdi::Context.new
# begin
# ctx.usb_open(0x0403, 0x6001)
# begin
# ctx.baudrate = 250000
# ensure
# ctx.usb_close
# end
# rescue Ftdi::Error => e
# $stderr.puts e.to_s
# end
class Context < FFI::ManagedStruct
# layout skipped...
# Initializes new libftdi context.
# @raise [CannotInitializeContextError] libftdi cannot be initialized.
def initialize
ptr = Ftdi.ftdi_new
raise CannotInitializeContextError.new if ptr.nil?
super(ptr)
end
# Deinitialize and free an ftdi context.
# @return [NilClass] nil
def self.release(p)
Ftdi.ftdi_free(p)
nil
end
end
Конструктор FFI::ManagedStruct принимает указатель на структуру, которую нужно нужно маршалить по указанному layout (карта преобразования структуры из нативного представления в представление FFI). В своём конструкторе мы получаем указатель через вызов ftdi_new (в основе своей использующей malloc) и передаём его в суперкласс.
При сборе мусора будет вызван метод класса release с параметром-указателем на нативную структуру, в котором мы освободим её.
Формируем API
Поскольку все вызовы библиотеки работают с контекстом, мы сделаем все API методами контекста и создадим метод ctx, возвращающий указатель на контекст libftdi, для упрощения вызова этих вызовов.
Большинство функций libftdi возвращают целое число со знаком, которое указывает на наличие ошибки, если результат меньше нуля. Поэтому удобно написать хэлпер для парсинга результата вызова функций и выброса исключения в случае проблем:
private
def ctx
self.to_ptr
end
def check_result(status_code)
if status_code < 0
raise StatusCodeError.new(status_code, error_string)
end
nil
end
Здесь error_string
, — это метод, получающий сообщение об ошибке из контекста libftdi.
Теперь, к примеру, формируем перечисление вариантов портов и биндинг к вызову функции ftdi_set_interface
. От чего пляшем:
enum ftdi_interface
{
INTERFACE_ANY = 0,
INTERFACE_A = 1,
INTERFACE_B = 2,
INTERFACE_C = 3,
INTERFACE_D = 4
};
int ftdi_set_interface(struct ftdi_context *ftdi, enum ftdi_interface interface);
И что получаем:
# Port interface for chips with multiple interfaces.
# @see Ftdi::Context#interface=
Interface = enum(:interface_any, :interface_a, :interface_b, :interface_c, :interface_d)
attach_function :ftdi_set_interface, [ :pointer, Interface ], :int
class Context # ...
# Open selected channels on a chip, otherwise use first channel.
# @param [Interface] new_interface Interface to use for FT2232C/2232H/4232H chips.
# @raise [StatusCodeError] libftdi reports error.
# @return [Interface] New interface.
def interface=(new_interface)
check_result(Ftdi.ftdi_set_interface(ctx, new_interface))
new_interface
end
...
end
Работа с массивами байтов
В то время, как работа с ASCIIZ-строками тривиальна (тип :string
), попытка использовать их для передачи массива байт обречена на неудачу, так как маршалер FFI спотыкнется на первом же нулевом байте.
Для передачи массива байт будем использовать тип :pointer
, который будем формировать через FFI::MemoryPointer (выделяя и заполняя соответствующий буфер в памяти).
attach_function :ftdi_write_data, [ :pointer, :pointer, :int ], :int
class Context # ...
# Writes data.
# @param [String, Array] bytes String or array of integers that will be interpreted as bytes using pack('c*').
# @return [Fixnum] Number of written bytes.
# @raise [StatusCodeError] libftdi reports error.
def write_data(bytes)
bytes = bytes.pack('c*') if bytes.respond_to?(:pack)
size = bytes.respond_to?(:bytesize) ? bytes.bytesize : bytes.size
mem_buf = FFI::MemoryPointer.new(:char, size)
mem_buf.put_bytes(0, bytes)
bytes_written = Ftdi.ftdi_write_data(ctx, mem_buf, size)
check_result(bytes_written)
bytes_written
end
end
Как видите, построение биндингов оказалось тривиальной задачей. Для тех, кто хотел бы автоматизировать их построение, рекомендую посмотреть в сторону SWIG.
Автор: akzhan