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

    На днях нужно было построить биндинги к библиотеке libftdi, которая обеспечивает взаимодействие с чипами FTDI (чипы для преобразования последовательной передачи данных по RS-232 или уровней TTL в сигналы шины USB, для того чтобы дать возможность современным компьютерам использовать устаревшие устройства © Wikipedia).

    Для создания биндингов я выбрал расширение FFI, которое позволяет загружать динамические библиотеки и строить биндинги к ним.

    У FFI есть несколько достоинств, которые сыграли в его пользу:
    • Поддержка интерпретаторов MRI Ruby 1.9, MRI Ruby 1.8, JRuby, ограниченная поддержка Rubinius;
    • Поддержка платформы Windows (наряду с другими);
    • Отсутствие необходимости компиляции биндингов;
    • Удобный язык описания биндингов.

    Репозиторий биндингов для 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.
    • +13
    • 2.5k
    • 3
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 3

      +1
      Добавлю два слова о железках.

      Этот модуль управлял линейками RGB светодиодов с помощью DMX контроллеров. DMX512 — американский стандарт управления театральным оборудованием. С точки зрения программиста можно рассматривать «вселенную» оборудования (DMX-universe), как простой 512-байтовый массив.

      Каждый байт в этом массиве управляет соотвествующим каналом устройства (fixture), в данном случае R, G, и B каналами каждой из светодиодных линеек. Индекс первого канала контроллера задается перемычками на его плате

      Сами DMX-контроллеры управляются с помощью USB-DMX bridge-a, на котором собственно и установлен FTDI чип, о котором идет речь в статье.
        0
        Акжан, спасибо за статью.

        Может подскажешь быстро: как на FT2232C зажечь одну ножку? Просто что бы там был не ноль. Нужно для сброса питания в реле.

      Only users with full accounts can post comments. Log in, please.