ffi_dry

Helpers, sugar methods, and new features over Ruby FFI to do some common things and add support for some uncommon ones.

Requirements

  • ffi (>= 0.5.0) - github.com/ffi/ffi

Synopsis

(samples/ in the package for code)

One major feature is a DSL“-like” syntax for declaring structure members in FFI::Struct or FFI::ManagedStruct definitions.

require 'rubygems'
require 'ffi'
require 'ffi/dry'

class SomeStruct < FFI::Struct
  include FFI::DRY::StructHelper

  # we get a new way of specifying layouts with a 'dsl'-like syntax
  # The hash containing {:desc => ... } can contain arbitrary keys which 
  # can be used however we like. dsl_metadata will contain all these
  # in the class and instance.
  dsl_layout do
    field   :field1,  :uint16, :desc => 'this is field 1'
    field   :field2,  :uint16, :desc => 'this is field 2'
  end
end

ss0=SomeStruct.new

With the declarations above, we specified :desc hash value in metadata. Extra metadata can have arbitrary keys and is accessible in every instance and class.

pp ss0. 
[{:type=>:uint16, :name=>:field1, :desc=>"this is field 1"},
 {:type=>:uint16, :name=>:field2, :desc=>"this is field 2"}]
# => nil

pp SomeStruct.
#...

We get some additional ways of instantiating and declaring values for free during initialization. (The FFI standard ways still work too)

raw_data = "\x00\x00\xff\xff"

ss1=SomeStruct.new :raw => raw_data
ss2=SomeStruct.new :raw => raw_data, :field1 => 1, :field2 => 2
ss3=SomeStruct.new {|x| x.field1=1 }
ss4=SomeStruct.new(:raw => raw_data) {|x| x.field1=1 }

[ ss0, ss1, ss2, ss3, ss4 
].each_with_index {|x,i| p ["ss#{i}",[x.field1, x.field2]]}

# which produces...
# ["ss0", [0, 0]]
# ["ss1", [0, 65535]]
# ["ss2", [1, 2]]
# ["ss3", [1, 0]]
# ["ss4", [1, 65535]]

Here’s a broader example which utilizes that arbitrary ‘:desc’ parameter in a “neighborly” way. This also demonstrates using superclasses to add common struct features, declaring array fields, as well as nesting other structs.

require 'rubygems'
require 'ffi'
require 'ffi/dry'

class NeighborlyStruct < ::FFI::Struct
  include ::FFI::DRY::StructHelper

  def self.describe
    print "Struct: #{self.name}"
    ().each_with_index do |spec, i|
      print "  Field #{i}\n"
      print "    name:  #{spec[:name].inspect}\n"
      print "    type:  #{spec[:type].inspect}\n"
      print "    desc:  #{spec[:desc]}\n\n"
    end
    print "\n"
  end
  def describe;  self.class.describe;  end
end

class TestStruct < NeighborlyStruct
  dsl_layout do
    field   :field1,  :uint8,  :desc => "test field 1"
    field   :field2,  :uint8,  :desc => "test field 2"
  end
end

class SomeStruct < NeighborlyStruct
  dsl_layout do
    field  :kind, :uint8,      :desc => "a type identifier"
    struct :tst,  TestStruct,  :desc => "a nested TestStruct"
    field  :len,  :uint8,      :desc => "8-bit size value (>= self.size+2)"
    array  :str,  [:char,255], 
        :desc => "a string up to 255 bytes bound by :len"
  end

  # override kind getter method with our own
  # resolves kind to some kind of type array for example...
  def kind
    [:default, :bar, :baz][ self[:kind] ]
  end
end

s1=TestStruct.new
s2=SomeStruct.new

# check out that 'kind' override:
s2.kind
# => :default

# oh and the regular FFI way is always intact
s2[:kind]
# => 0

s2[:kind]=1
s2.kind
# => :bar

s2.kind=3
s2.kind
# => :baz

puts "*"*70
s1.describe
## we get a dump of metadata
# **********************************************************************
# Struct: TestStruct  
# Field 0
#   name:  :field1
#   type:  :uint8
#   desc:  test field 1
#
# Field 1
#   name:  :field2
#   type:  :uint8
#   desc:  test field 2

puts "*"*70
s2.describe
## we get a dump of metadata
# Struct: SomeStruct  Field 0
#     name:  :kind
#     type:  :uint8
#     desc:  a type identifier
#
#   Field 1
#     name:  :tst
#     type:  TestStruct
#     desc:  a nested TestStruct
#
#   Field 2
#     name:  :len
#     type:  :uint8
#     desc:  8-bit size value (>= self.size+2)
#
#   Field 3
#     name:  :str
#     type:  [:char, 255]
#     desc:  a string up to 255 bytes bound by :len

puts "*"*70
s2.tst.describe
## same as s1.describe
# **********************************************************************
# Struct: TestStruct  
# Field 0
#   name:  :field1
#   type:  :uint8
#   desc:  test field 1
#
# Field 1
#   name:  :field2
#   type:  :uint8
#   desc:  test field 2

There’s also some helper modules for collecting lookup maps for constants, a common and handy thing when porting various libraries. We use the Ruby Socket socket namespace here for demonstration purposes. You can ‘slurp’ constants from any namespace this way.

require 'ffi/dry'
require 'socket'

module AddressFamily
  include FFI::DRY::ConstMap
  slurp_constants ::Socket, "AF_"
  def list ; @@list ||= super() ; end  # only generate the hash once
end

AddressFamily now has all the constants it found for Socket::AF_* minus the prefix.

AddressFamily::INET
AddressFamily::LINK
AddressFamily::INET6

etc…

We can do type or value lookups using []

AddressFamily[2]      # => "INET"
AddressFamily["INET"] # => 2

We can get a hash of all constant->value pairs with .list

AddressFamily.list
# => {"NATM"=>31, "DLI"=>13, "UNIX"=>1, "NETBIOS"=>33,  ...}

… and invert for a reverse mapping

AddressFamily.list.invert
# => {16=>"APPLETALK", 5=>"CHAOS", 27=>"NDRV", 0=>"UNSPEC", ...}

License

Copyright © 2009 Eric Monti. See LICENSE for details.