Строим биндинги из Ruby к C-библиотеке

в 11:42, , рубрики: ffi, ftdi, ruby, метки: , ,

На днях нужно было построить биндинги к библиотеке 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js