Sugar and helpers for Ruby-FFI
Ruby
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
lib
samples
spec
.document
.gitignore
History.txt
LICENSE
README.rdoc
Rakefile
VERSION
ffi_dry.gemspec

README.rdoc

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.dsl_metadata 
[{:type=>:uint16, :name=>:field1, :desc=>"this is field 1"},
 {:type=>:uint16, :name=>:field2, :desc=>"this is field 2"}]
# => nil

pp SomeStruct.dsl_metadata
#...

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}"
    dsl_metadata().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.