Module: FFI::Accessors

Overview

Syntax sugar for FFI::Struct

Modules that include Accessors are automatically extended by ClassMethods which provides for defining reader and writer methods over struct field members.

Although designed around needs of FFI::Struct, eg the ability to map natural ruby names to struct field names, this module can be used over anything that stores attributes in a Hash like structure. It provides equivalent method definitions to Module#attr_(reader|writer|accessor) except using the index methods :[], and :[]= instead of managing instance variables.

Additionally it supports boolean attributes with '?' aliases for reader methods, and keeps track of attribute definitions to support #fill,#to_h etc.

Standard instance variable based attributes defined through #attr_(reader|writer|accessor) also get these features.

Examples:

class MyStruct < FFI::Struct
  include FFI::Accessors

  layout(
    a: :int,
    b: :int,
    s_one: :string,
    enabled: :bool,
    t: TimeSpec,
    p: :pointer
  )

  ## Attribute reader, writer, accessor over struct fields

  # @!attribute [r] a
  #   @return [Integer]
  ffi_attr_reader :a

  # @!attribute [w] b
  #   @return [Integer]
  ffi_attr_writer :b

  # @!attribute [rw] one
  #   @return [String]
  ffi_attr_accessor({ one: :s_one }) # => [:one, :one=] reads/writes field :s_one

  ## Boolean attributes!

  # @!attribute [rw] enabled?
  #   @return [Boolean]
  ffi_attr_accessor(:enabled?) # => [:enabled, :enabled?, :enabled=]

  ## Simple block converters

  # @!attribute [rw] time
  #    @return [Time]
  ffi_attr_reader(time: :t) do |timespec|
    Time.at(timespec.tv_sec, timespec.tv_nsec) # convert TimeSpec struct to ruby Time
  end

  ## Complex attribute methods

  # writer for :time needs additional attributes
  ffi_attr_writer_method(time: :t) do |sec, nsec=0|
    sec, nsec = [sec.sec, sec.nsec] if sec.is_a?(Time)
    self[:t][tv_sec] = sec
    self[:t][tv_nsec] = nsec
    time
  end

  # safe readers handling a NULL struct
  safe_attrs = %i[a b].to_h { |m| [:"#{m}_safe", m] } # =>{ a_safe: :a, b_safe: b }
  ffi_attr_reader_method(**safe_attrs) do |default: nil|
     next default if null?

     _attr, member = ffi_reader(__method__)
     self[member]
  end

  ## Standard accessors over for instance variables, still supports boolean, to_h, fill

  # @!attribute [rw] debug?
  #   @return [Boolean]
  attr_accessor :debug?

  ## Private accessors

  private

  ffi_attr_accessor(pointer: :p)
end

# Fill from another MyStruct (or anything that quacks like a MyStruct with readers matching our writers)
s = MyStruct.new.fill(other)

# Fill from hash...
s = MyStruct.new.fill(b:2, one: 'str', time: Time.now, enabled: true, debug: false) # => s
s.values #=> (FFI::Struct method) [ 0, 2, 'str', true, <TimeSpec>, FFI::Pointer::NULL ]

# Struct instance to hash
s.to_h # => { a: 0, one: 'str', time: <Time>, enabled: true, debug: false }

# Attribute methods
s.a                             # => 0
s.b = 3                         # => 3
s.enabled                       # => true
s.enabled?                      # => true
s.time= 0,50                    # => Time<50 nanoseconds after epoch>
s.time= Time.now                # => Time<now>
s.debug?                        # => false
s.pointer                       # => NoMethodError, private method 'pointer' called for MyStruct
s.send(:pointer=, some_pointer) # => some_pointer
s.send(:pointer)                # => some_pointer

null_s = MyStruct.new(FFI::Pointer::NULL)
null_s.b_safe(default: 10)      # => 10

See Also:

Defined Under Namespace

Modules: ClassMethods

Private Accessor helpers collapse

Instance Method Summary collapse

Methods included from ClassMethods

attr_accessor, attr_reader, attr_writer, ffi_attr_accessor, ffi_attr_reader, ffi_attr_reader_method, ffi_attr_readers, ffi_attr_writer, ffi_attr_writer_method, ffi_attr_writers, ffi_bitflag_accessor, ffi_bitflag_reader, ffi_bitflag_writer, ffi_public_attr_readers, ffi_public_attr_writers

Instance Method Details

#ffi_attr_fill(from, writers: self.class.ffi_attr_writers, **args) ⇒ Object

Note:

This private method allows an including classes' instance method to fill attributes through any writer method (vs ##fill which only sets attributes with public writers)

(private) Fill struct from another object or list of properties

Parameters:

  • from (Object)
  • args (Hash<Symbol>)
  • writers (Array<Symbol>) (defaults to: self.class.ffi_attr_writers)

    list of allowed writer methods

Raises:

  • (ArgumentError)

    if args contains properties not included in writers list



426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'lib/ffi/accessors.rb', line 426

def ffi_attr_fill(from, writers: self.class.ffi_attr_writers, **args)
  if from.is_a?(Hash)
    args.merge!(from)
  else
    writers.each do |w|
      r = w[0..-2] # strip trailing =
      send(w, from.public_send(r)) if from.respond_to?(r)
    end
  end
  args.transform_keys! { |k| :"#{k}=" }

  args.each_pair { |k, v| send(k, v) }
  self
end

#ffi_attr_reader_member(attr_method, *default) ⇒ Array<Symbol,Symbol>

(private) Takes __method__ and returns the corresponding attr and struct member names

Parameters:

  • attr_method (Symbol)

    typically __method__ (or __callee__)

  • default (Symbol)

    default if method is not a reader method

Returns:

  • (Array<Symbol,Symbol>)

    attr,member

Raises:

  • (KeyError)

    if method has not been defined as a reader and no default is supplied



453
454
455
456
# File 'lib/ffi/accessors.rb', line 453

def ffi_attr_reader_member(attr_method, *default)
  attr = ffi_attr(attr_method)
  [attr, self.class.ffi_attr_readers_map.fetch(attr, *default)]
end

#ffi_attr_writer_member(attr_method, *default) ⇒ Array<Symbol,Symbol>

(private) Takes __method__ and returns the corresponding attr and struct member names

Parameters:

  • attr_method (Symbol)

    typically __method__ (or __callee__)

  • default (Symbol|nil)

    default if method is not a writer method

Returns:

  • (Array<Symbol,Symbol>)

    attr,member

Raises:

  • (KeyError)

    if method has not been defined as a writer and no default is supplied



464
465
466
467
# File 'lib/ffi/accessors.rb', line 464

def ffi_attr_writer_member(attr_method, *default)
  attr = ffi_attr(attr_method)
  [attr, self.class.ffi_attr_writers_map.fetch(attr, *default)]
end

#fill(from = nil, **args) ⇒ self

Fill struct from another object or list of properties

Parameters:

  • from (Object) (defaults to: nil)

    if from is a Hash then its is merged with args, otherwise look for corresponding readers on from, for our public writer attributes

  • args (Hash<Symbol,Object>)

    for each entry we call self.attr=(val)

Returns:

  • (self)

Raises:

  • (ArgumentError)

    if args contains properties that do not have public writers



398
399
400
# File 'lib/ffi/accessors.rb', line 398

def fill(from = nil, **args)
  ffi_attr_fill(from, writers: self.class.ffi_public_attr_writers, **args)
end

#inspect(readers: self.class.ffi_public_attr_readers) ⇒ String

Inspect attributes

Parameters:

  • readers (Array<Symbol>) (defaults to: self.class.ffi_public_attr_readers)

    list of attribute names to include in inspect, defaults to all readers

Returns:

  • (String)


405
406
407
# File 'lib/ffi/accessors.rb', line 405

def inspect(readers: self.class.ffi_public_attr_readers)
  "#{self.class.name} {#{readers.map { |r| "#{r}: #{send(r)} " }.join(',')}"
end

#to_h(readers: self.class.ffi_public_attr_readers) ⇒ Hash<Symbol,Object>

Convert struct to hash

Parameters:

  • readers (Array<Symbol>) (defaults to: self.class.ffi_public_attr_readers)

    list of attribute names to include in hash, defaults to all public readers.

Returns:

  • (Hash<Symbol,Object>)

    map of attribute name to value



412
413
414
# File 'lib/ffi/accessors.rb', line 412

def to_h(readers: self.class.ffi_public_attr_readers)
  readers.to_h { |r| [r, send(r)] }
end