📜 ⬆️ ⬇️

Build Ruby Binding C-Library

The other day, it was necessary to build bindings to the libftdi library, which provides interaction with FTDI chips (chips for converting serial data transmission via RS-232 or TTL levels into USB bus signals, in order to enable modern computers to use outdated devices © Wikipedia).

To create binders, I chose the FFI extension, which allows you to load dynamic libraries and build binders for them.

FFI has several virtues that have played in its favor:

Binding Repository for Ruby .
')

Start


Create a binding module that loads the libftdi library:
require 'ffi' # Represents libftdi ruby bindings. # End-user API represented by {Ftdi::Context} class. module Ftdi extend FFI::Library ffi_lib "libftdi" end 

Managing Unmanaged Resources


The main essence of libftdi is its context, which needs to be highlighted when you start working with it, and then, accordingly, release it. The class FFI::ManagedStruct is responsible for the automatic collection of unmanaged resources:

  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 

The FFI :: ManagedStruct constructor takes a pointer to the structure that needs to be marshaled by the specified layout (the map converting the structure from the native view to the FFI view). In our constructor, we get a pointer through a call to ftdi_new (basically using malloc) and pass it to the superclass.

When collecting garbage, a release class method will be called with a pointer to the native structure, in which we will release it.

We form API


Since all library calls work with the context, we will make all API context methods and create a ctx method that returns a pointer to the libftdi context, to simplify invoking these calls.

Most libftdi functions return a signed integer that indicates an error if the result is less than zero. Therefore, it is convenient to write a helper for parsing the result of calling functions and throwing an exception in case of problems:
  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 

Here, error_string , is the method that receives the error message from the libftdi context.

Now, for example, we form an enumeration of port options and a binding to the function call ftdi_set_interface . From what we dance:
 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); 

And what we get:
  # 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 


Work with byte arrays


While working with ASCIIZ strings is trivial (type :string ), trying to use them to transfer an array of bytes is doomed to failure, since the FFI marshaller stumbles on the first zero byte.

To transfer an array of bytes, we will use the type :pointer , which we will form via FFI :: MemoryPointer (allocating and filling the corresponding buffer in memory).

  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 


As you can see, building bindings turned out to be a trivial task.

For those who would like to automate their construction, I recommend to look towards SWIG .

Source: https://habr.com/ru/post/142172/


All Articles