#!/usr/bin/env ruby require 'pp' # A proxy that defines as little as possible, other than the specified methods # (which should probably include method_missing to make this useful). This lets # us e.g. define fields with names of Ruby methods. def make_proxy_class(methods) proxy_class = Class.new methods.each {|name,b| proxy_class.send(:define_method, name, &b) } pesky_methods = [proxy_class.instance_methods, proxy_class.public_instance_methods, proxy_class.protected_instance_methods, proxy_class.private_instance_methods].flatten \ - [:method_missing, :instance_eval, :define_method, :object_id, :__send__, :initialize, methods.keys].flatten pesky_methods.uniq! pesky_methods.each {|m| proxy_class.class_eval "undef #{m.inspect}" } proxy_class end def yielding_proc(&orig) proc {|*args, &b| orig.yield(*args, &b) } end class Structish class Field attr_accessor :type attr_reader :name attr_reader :dimensions attr_accessor :value def initialize(name) @type = nil @name = name @dimensions = [] end # Become an array (returns self to handle multidimensional arrays). def [](n) @dimensions << n self end def ptrify @dimensions << :ptr end # Become a pointer (this happens when given as "multiple argument", # i.e. "f *a") def to_a ptrify [self] end def inspect ret = "#{@type}" name = @name.to_s @dimensions.each {|d| if d == :ptr name = "*" + name else name = name + "[#{d}]" end } ret = "#{@type} #{name}" ret += " = #{@value}" if defined?(@value) ret end end class Bitfield attr_reader :type attr_reader :name attr_reader :bits attr_accessor :value def initialize(type, name, bits) @type = type @name = name @bits = bits end def inspect "%s %s:%s%s" % [@type, @name, @bits, defined?(@value) ? " = #{@value}" : ""] end end class Type attr_reader :name def initialize(name) @name = name @ptr_handler = nil end def inspect @name end def to_s inspect.to_s end def on_ptr(&b) @ptr_handler = b end def call_ptr_handler(field) raise "ptr_handler called but not provided" if @ptr_handler.nil? @ptr_handler[field] @ptr_handler = nil end def *(field) field.ptrify call_ptr_handler(field) end def **(field) field.ptrify field.ptrify call_ptr_handler(field) end # process_field() and process_bitfield() optionally return a value that # the field will be replaced with when it's seen again. def process_field(field) # ... end def process_bitfield(bitfield) # ... end end def initialize @fields = {} @known_types = {} end def run(&b) methods = @known_types.merge({method_missing: method(:called_method_missing)}) @proxy_class = make_proxy_class(methods) @proxy = @proxy_class.new @proxy.instance_eval(&b) @fields.values end def called_method_missing(meth, *args) return @fields[meth].value if @fields.include? meth field = Field.new(meth) if args.length > 0 if args.length == 1 && Array === args[0] # char arr [8]; # Sadly doesn't work for multidimensional arrays... Unless we # temporarily redefine Array#[], but that's going a bit far. field[args[0][0]] return field end raise "field name used as function " + \ "with non-Array argument (missing comma?)" \ end field end def add_fields(type, fields, bitfields) #raise "type `#{type}' declares no values" \ # if fields.empty? && bitfields.empty? if fields.empty? && bitfields.empty? # Being used as pointer using * or **? type.on_ptr {|f| f.type = type @fields[f.name] = f } return type end fields.each {|f| raise "field `#{f.name}' already defined" if @fields.include? f.name f.type = type @fields[f.name] = f type.process_field(f) } bitfields.each {|name,bits| # "Hashes enumerate their values in the order that the # corresponding keys were inserted." # Sadly all bitfields have to come after all non-bitfields, and # we have some whitspace restrictions to make : work. But other # than that this is relatively straightforward -- we get a symbol # => bitsize hash, we add each of them to the struct. raise "field `#{name}' already defined" if @fields.include? name bf = Bitfield.new(type, name, bits) @fields[name] = bf type.process_bitfield(bf) } end def add_types(*types) # We want the block to be called in the Struct's context, not the # proxy's. types.each {|type| @known_types[type.name] = yielding_proc {|*args| bitfields = (Hash === args[-1] ? args.pop : {}) add_fields(type, args, bitfields) } } end end def struct(&b) s = Structish.new mktype = ->n { Structish::Type.new(n) } s.add_types(mktype[:uint32_t], mktype[:char], mktype[:bool]) sdef = s.run(&b) pp sdef end struct { char sectname[16]; char segname[16]; uint32_t addr; uint32_t size; uint32_t offset; uint32_t align; uint32_t reloff; uint32_t nreloc; uint32_t flags; uint32_t reserved1; uint32_t reserved2; }; struct { uint32_t multiple, values; char string[12], val; uint32_t bit:1, fields:7; char matrix[256][256]; char *ptrs[5], byte, *ptr_matrix[8][8]; bool **ptr_to_ptr; uint32_t * ptr_array_with_whitespace[256]; char byte_arr_with_other_whitespace [256]; }; __END__ Output: [char sectname[16], char segname[16], uint32_t addr, uint32_t size, uint32_t offset, uint32_t align, uint32_t reloff, uint32_t nreloc, uint32_t flags, uint32_t reserved1, uint32_t reserved2] [uint32_t multiple, uint32_t values, char string[12], char val, uint32_t bit:1, uint32_t fields:7, char matrix[256][256], char *ptrs[5], char byte, char *ptr_matrix[8][8], bool **ptr_to_ptr, uint32_t *ptr_array_with_whitespace[256], char byte_arr_with_other_whitespace[256]]