lib/ffi/accessors.rb in ffi-libfuse-0.4.0 vs lib/ffi/accessors.rb in ffi-libfuse-0.4.1

- old
+ new

@@ -1,159 +1,471 @@ # frozen_string_literal: true require 'ffi' module FFI - # Syntax sugar for FFI::Struct + # 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 + # *:[<member>]*, and *:[<member>]=* 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. + # @example + # 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 ClassMethods module Accessors - # DSL methods for defining struct member accessors + # Class methods for defining struct member accessors module ClassMethods - # Define both a reader and a writer for members - # @param [Array<Symbol>] attrs the attribute names - # @param [String] format - # A format string containing a single %s to convert attr symbol to struct member - # @return [void] - def ffi_attr_accessor(*attrs, format: '%s') - ffi_attr_reader(*attrs, format: format) - ffi_attr_writer(*attrs, format: format) + # Keep track of default visibility since define_method doesn't do this itself + # @visibility private + %i[public private protected].each do |visibility| + define_method(visibility) do |*args| + @default_visibility = visibility if args.empty? + super(*args) + end end + # @visibility private + def default_visibility + @default_visibility ||= :public + end + + # Standard instance variable based reader with support for boolean and integration with *to_h*, *inspect* etc.. # - # Define a struct attribute reader for members - # @param [Array<Symbol>] - # attrs the attribute names used as the reader method name + # The *member* registered for each attribute will be its instance variable symbol (ie with a leading '@') + # @return [Array<Symbol] + def attr_reader(*args) + super(*args.map { |a| a[-1] == '?' ? a[0..-2] : a }) + ffi_attr_reader_method(**args.to_h { |a| [a, :"@#{a[-1] == '?' ? a[0..-2] : a}"] }) + end + + # Standard instance variable based writer with support for booleans and integration with *fill* etc.. # - # a trailing '?' will be stripped from attribute names for primary reader method name, and cause an - # boolean alias method to be created. - # @param [Proc|String] format - # A Proc, or format string containing a single %s, to convert each attribute name to the corresponding - # struct member name - # @param [Boolean] simple - # Controls how writer methods are defined using block - # @param [Proc] block - # An optional block to convert the struct field value into something more useful - # - # If simple is true then block takes the struct field value, otherwise method is defined directly from the block - # and should use __method__ to get the attribute name. and self.class.ffi_attr_readers[__method__] to get the - # member name if these are not available from enclosed variables. - # @return [void] - def ffi_attr_reader(*attrs, format: '%s', simple: true, &block) - attrs.each do |attr| - bool, attr = attr[-1] == '?' ? [true, attr[..-2]] : [false, attr] + # The *member* registered for each attribute will be its instance variable symbol (ie with a leading '@') + def attr_writer(*args) + super(*args.map { |a| a[-1] == '?' ? a[0..-2] : a }) + ffi_attr_writer_method(**args.to_h { |a| [a, :"@#{a[-1] == '?' ? a[0..-2] : a}"] }) + end - member = (format.respond_to?(:call) ? format.call(attr) : format % attr).to_sym - ffi_attr_readers[attr.to_sym] = member - if !block - define_method(attr) { self[member] } - elsif simple - define_method(attr) { block.call(self[member]) } - else - define_method(attr, &block) - end + # Override instance variable based accessor to build our enhanced readers and writers + def attr_accessor(*args) + attr_reader(*args) + attr_writer(*args) + end - alias_method "#{attr}?", attr if bool + # @!group Accessor Definition + + # Define both reader and writer + # @return [Array<Symbol] list of methods defined + def ffi_attr_accessor(*attrs, **attrs_map) + ffi_attr_reader(*attrs, **attrs_map) + ffi_attr_writer(*attrs, **attrs_map) + end + + # Define reader methods for the given attributes + # + # @param [Array<Symbol>] attrs + # List of struct field members to treat as attributes + # + # a trailing '?' in an attribute name indicates a boolean reader. + # eg. :debug? will define the reader method :debug and an alias method :debug? => :debug, + # + # String values are converted to Symbol + # + # @param [Hash<Symbol,Symbol>] attrs_map + # Map of attribute name to struct field name - where field names don't fit natural ruby methods etc... + # + # A Hash value in *attrs* is also treated as an *attrs_map*. String keys/values are transformed to Symbol. + # + # @param [Proc] block + # An optional block taking a single argument (the struct field value) to convert into something more useful. + # + # This block is evaluated within the method using :instance_exec + # @return [Array<Symbol>] list of methods defined + def ffi_attr_reader(*attrs, **attrs_map, &block) + ffi_attr_reader_method(*attrs, **attrs_map) do + _attr, member = ffi_attr_reader_member(__method__) + val = self[member] + block ? instance_exec(val, &block) : val end end - # Define a struct attribute writer - # @param [Array<Symbol>] attrs the attribute names - # trailing '?' will be stripped from attribute names - # @param [String|Proc] format - # A format string containing a single %s to convert attr symbol to struct member - # @param [Boolean] simple - # Controls how writer methods are defined using block + # Define reader methods directly from a block + # + # @param [Array<Symbol>] attrs see {ffi_attr_reader} + # @param [Hash<Symbol,Symbol>] attrs_map # @param [Proc] block - # An optional block to set the input value into the struct field. + # must allow zero arity, but can have additional optional arguments or keyword arguments. # - # If simple is true then the struct field is set to the result of calling block with the input value, - # otherwise the method is defined directly from the block. Use __method__[0..-1] to get the attribute name - # and self.class.ffi_attr_writers[__method__[0..-1]] to get the struct member name - # @return [void] - def ffi_attr_writer(*attrs, format: '%s', simple: true, &block) - attrs.each do |attr| - attr = attr[..-2] if attr[-1] == '?' + # the block is evaluated using :instance_exec + # + # within block the attribute name is always the method name (`__method__`) and the associated struct field + # member name is from any attribute maps supplied; ie *attrs_map* or Hash value in *attrs*. + # They can be retrieved using {ffi_attr_reader_member} + # + # `attr, member = ffi_attr_reader_member(__method__)` + # + # if not supplied a reader will still be registered for each attribute and a boolean alias created if required + # @return [Array<Symbol>] list of methods defined + # @example Related struct members + # # uid/gid are only meaningful if corresponding set_ field is true + # layout(set_uid: :bool, uid: :uint, set_gid: :bool, gid: :uint) + # + # # @!attribute [r] uid + # # @return [Integer] the user id + # # @return [nil] if uid has not been explicitly set + # + # # @!attribute [r] gid + # # @return [Integer] the group id + # # @return [nil] if gid has not been explicitly set + # + # ffi_attr_reader_method(:uid, :gid) do + # attr, member = ffi_attr_reader_member(__method__) + # setter = :"set_#{attr}" + # self[setter] ? self[:attr] : nil + # end # => [:uid :gid] + def ffi_attr_reader_method(*attrs, **attrs_map, &block) + attr_methods = map_attributes(attrs, attrs_map).flat_map do |attr, member, bool| + ffi_attr_readers_map[attr] = member + define_method(attr, &block) if block + next attr unless bool - member = (format % attr).to_sym - ffi_attr_writers[attr.to_sym] = member - if !block - define_method("#{attr}=") { |val| self[member] = val } - elsif simple - define_method("#{attr}=") { |val| self[member] = block.call(val) } - else - define_method("#{attr}=", &block) - end + bool_alias = :"#{attr}?" + alias_method(bool_alias, attr) + [attr, bool_alias] end + send(default_visibility, *attr_methods) + attr_methods end - # All defined readers - # @return [Hash<Symbol,Symbol>] map of attr names to member names for which readers exist - def ffi_attr_readers - @ffi_attr_readers ||= {} + # Define struct attribute writers for the given attributes + # @param [Array<Symbol>] attrs see {ffi_attr_reader} + # @param [Hash<Symbol,Symbol>] attrs_map + # @param [Proc<Object>] block + # An optional block taking a single argument to convert input value into a value to be placed in the underlying + # struct field + # + # This block is evaluated within the method using :instance_exec + # @return [Array<Symbol>] list of methods defined + def ffi_attr_writer(*attrs, **attrs_map, &block) + ffi_attr_writer_method(*attrs, **attrs_map) do |val| + _attr, member = ffi_attr_writer_member(__method__) + self[member] = block ? instance_exec(val, &block) : val + end end - # All defined writers - # @return [Hash<Symbol,Symbol>] map of attr names to member names for which writers exist - def ffi_attr_writers - @ffi_attr_writers ||= {} + # Define writer methods directly from a block + # @param [Array<Symbol>] attrs see {ffi_attr_reader} + # @param [Hash<Symbol,Symbol>] attrs_map + # @param [Proc] block + # must allow arity = 1, but can have additional optional arguments or keyword arguments. + # + # the block is evaluated using :instance_exec + # + # within block the attribute name is always the method name stripped of its trailing '=' + # (`:"#{__method__[0..-2]}"`) and the associated struct field member name is from any attribute maps + # supplied. ie *attrs_map* or Hash value in *attrs*. They can be retrieved using {ffi_attr_writer_member} + # + # `attr, member = ffi_attr_writer_member(__method__)` + # + # if not supplied a writer method is still registered for each attribute name + # @return [Array<Symbol>] list of methods defined + def ffi_attr_writer_method(*attrs, **attrs_map, &block) + writer_methods = map_attributes(attrs, attrs_map) do |attr, member, _bool| + ffi_attr_writers_map[attr] = member + block ? define_method("#{attr}=", &block) : attr + end + send(default_visibility, *writer_methods) + writer_methods end # Define individual flag accessors over a bitmask field - def ffi_bitflag_accessor(attr, *flags) - ffi_bitflag_reader(attr, *flags) - ffi_bitflag_writer(attr, *flags) + # @return [Array<Symbol>] list of methods defined + def ffi_bitflag_accessor(member, *flags) + ffi_bitflag_reader(member, *flags) + ffi_bitflag_writer(member, *flags) end # Define individual flag readers over a bitmask field - # @param [Symbol] attr the bitmask member - # @param [Array<Symbol>] flags list of flags - # @return [void] - def ffi_bitflag_reader(attr, *flags) - flags.each do |f| - ffi_attr_reader(:"#{f}?", simple: false) { self[attr].include?(f) } + # @param [Symbol] member the bitmask member + # @param [Array<Symbol>] flags list of flags to define methods for. Each flag also gets a :flag? boolean alias + # @return [Array<Symbol>] list of methods defined + def ffi_bitflag_reader(member, *flags) + bool_attrs = flags.to_h { |f| [:"#{f}?", member] } + ffi_attr_reader_method(**bool_attrs) do + flag_attr, member = ffi_attr_reader_member(__method__) + self[member].include?(flag_attr) end end # Define individual flag writers over a bitmask field - # @param [Symbol] attr the bitmask member - # @param [Array<Symbol>] flags list of flags - # @return [void] - def ffi_bitflag_writer(attr, *flags) - flags.each do |f| - ffi_attr_writer(f, simple: false) do |v| - v ? self[attr] += [f] : self[attr] -= [f] - v + # @param [Symbol] member the bitmask member + # @param [Array<Symbol>] flags list of flag attributes + # @return [Array<Symbol>] list of methods defined + def ffi_bitflag_writer(member, *flags) + writers = flags.to_h { |f| [f, member] } + ffi_attr_writer_method(**writers) do |v| + flag_attr, member = ffi_attr_writer_member(__method__) + v ? self[member] += [flag_attr] : self[member] -= flag + v + end + end + + # @!endgroup + # @!group Accessor Information + + # @return [Array<Symbol>] + # list of public attr accessor reader methods + def ffi_public_attr_readers + ffi_attr_readers & public_instance_methods + end + + # @return [Array<Symbol>] + # list of accessor reader methods defined. (excludes boolean aliases) + def ffi_attr_readers + ffi_attr_readers_map.keys + end + + # @return [Array<Symbol>] + # list of accessor writer methods (ie ending in '=') + def ffi_attr_writers + ffi_attr_writers_map.keys.map { |a| :"#{a}=" } + end + + # @return [Array<Symbol>] + # list of public accessor writer methods (ie ending in '=') + def ffi_public_attr_writers + ffi_attr_writers & public_instance_methods + end + + # @!endgroup + + # @!visibility private + def ffi_attr_readers_map + @ffi_attr_readers_map ||= {} + end + + # @!visibility private + def ffi_attr_writers_map + @ffi_attr_writers_map ||= {} + end + + private + + def map_attributes(attrs, attrs_map) + return enum_for(__method__, attrs, attrs_map) unless block_given? + + attrs << attrs_map unless attrs_map.empty? + + attrs.flat_map do |attr_entry| + case attr_entry + when Symbol, String + bool, attr = bool_attr(attr_entry) + + yield attr, attr, bool + when Hash + attr_entry.flat_map do |attr, member| + bool, attr = bool_attr(attr) + yield attr, member.to_sym, bool + end + else + raise ArgumentError end end end + + def bool_attr(attr) + attr[-1] == '?' ? [true, attr[..-2].to_sym] : [false, attr.to_sym] + end end + # @!parse extend ClassMethods + # @!visibility private def self.included(mod) mod.extend(ClassMethods) end - # Fill the native struct from another object or list of properties + # Fill struct from another object or list of properties # @param [Object] from - # for each attribute we call self.attr=(from.attr) + # if from is a Hash then its is merged with args, otherwise look for corresponding readers on from, for our + # public writer attributes # @param [Hash<Symbol,Object>] args # for each entry <attr,val> we call self.attr=(val) + # @raise [ArgumentError] if args contains properties that do not have public writers # @return [self] def fill(from = nil, **args) + ffi_attr_fill(from, writers: self.class.ffi_public_attr_writers, **args) + end + + # Inspect attributes + # @param [Array<Symbol>] readers list of attribute names to include in inspect, defaults to all readers + # @return [String] + def inspect(readers: self.class.ffi_public_attr_readers) + "#{self.class.name} {#{readers.map { |r| "#{r}: #{send(r)} " }.join(',')}" + end + + # Convert struct to hash + # @param [Array<Symbol>] readers list of attribute names to include in hash, defaults to all public readers. + # @return [Hash<Symbol,Object>] map of attribute name to value + def to_h(readers: self.class.ffi_public_attr_readers) + readers.to_h { |r| [r, send(r)] } + end + + private + + # @!visibility public + # *(private)* Fill struct from another object or list of properties + # @param [Object] from + # @param [Hash<Symbol>] args + # @param [Array<Symbol>] writers list of allowed writer methods + # @raise [ArgumentError] if args contains properties not included in writers list + # @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) + def ffi_attr_fill(from, writers: self.class.ffi_attr_writers, **args) if from.is_a?(Hash) args.merge!(from) else - self.class.ffi_attr_writers.each_key { |v| send("#{v}=", from.send(v)) if from.respond_to?(v) } + writers.each do |w| + r = w[0..-2] # strip trailing = + send(w, from.public_send(r)) if from.respond_to?(r) + end end - args.each_pair { |k, v| send("#{k}=", v) } + args.transform_keys! { |k| :"#{k}=" } + + args.each_pair { |k, v| send(k, v) } self end - def inspect - "#{self.class.name} {#{self.class.ffi_attr_readers.keys.map { |r| "#{r}: #{send(r)} " }.join(',')}" + def ffi_attr(method) + %w[? =].include?(method[-1]) ? :"#{method[0..-2]}" : method end - # Convert struct to hash - # @return [Hash<Symbol,Object>] map of reader attribute name to value - def to_h - self.class.ffi_attr_readers.keys.each_with_object({}) { |r, h| h[r] = send(r) } + # @!group Private Accessor helpers + + # @!visibility public + # *(private)* Takes `__method__` and returns the corresponding attr and struct member names + # @param [Symbol] attr_method typically `__method__` (or `__callee__`) + # @param [Symbol] default default if method is not a reader method + # @return [Array<Symbol,Symbol>] attr,member + # @raise [KeyError] if method has not been defined as a reader and no default is supplied + def ffi_attr_reader_member(attr_method, *default) + attr = ffi_attr(attr_method) + [attr, self.class.ffi_attr_readers_map.fetch(attr, *default)] end + + # @!visibility public + # *(private)* Takes `__method__` and returns the corresponding attr and struct member names + # @param [Symbol] attr_method typically `__method__` (or `__callee__`) + # @param [Symbol|nil] default default if method is not a writer method + # @return [Array<Symbol,Symbol>] attr,member + # @raise [KeyError] if method has not been defined as a writer and no default is supplied + def ffi_attr_writer_member(attr_method, *default) + attr = ffi_attr(attr_method) + [attr, self.class.ffi_attr_writers_map.fetch(attr, *default)] + end + + # @!endgroup end end