Class: CStruct

Inherits:
Object
  • Object
show all
Defined in:
lib/cstruct.rb

Overview

Struct does some trickery with custom allocators so we can’t subclass it without writing C. Instead we define a CStruct class that does something similar enough for our purpose. It is subclassed just like any other class. A nice side-effect of this syntax is that it is always clear that a CStruct is just a class and instances of the struct are objects.

Some light metaprogramming is used to make the following syntax possible:

class MachHeader < CStruct

uint :magic
int  :cputype
int  :cpusubtype
 ...
int  :flags

end

Inheritance works as you would expect.

class LoadCommand < CStruct

uint32 :cmd
uint32 :cmdsize

end

# inherits cmd and cmdsize as the first 2 fields class SegmentCommand < LoadCommand

string :segname, 16
uint32 :vmaddr
uint32

end

Nothing tricky or confusing there. Members of a CStruct class are declared in the class definition. A different definition using a more static approach probably wouldn’t be very hard… if performance is critical … but then why are you using Ruby? ;-)

TODO support bit fields

Bit fields should be supported by passing the number of bits a field should occupy. Perhaps we could use the size ‘pack’ for the rest of the field.

class RelocationInfo < CStruct

int32  :address
uint32 :symbolnum, 24
pack   :pcrel,      1
pack   :length,     2
pack   :extern,     1
pack   :type,       4

end

Constant Summary collapse

SizeMap =

Size in bytes.

{
  :int8   => 1,
  :uint8  => 1,
  :int16  => 2,
  :uint16 => 2,
  :int32  => 4,
  :uint32 => 4,
  :int64  => 8,
  :uint64 => 8,
  :string => lambda { |*opts| opts.first }, # first opt is size
  # the last 3 are to make the language more C-like
  :int    => 4,
  :uint   => 4,
  :char   => 1
}
PackMap =

32-bit

{
  :int8   => 'c',
  :uint8  => 'C',
  :int16  => 's',
  :uint16 => 'S',
  :int32  => 'i',
  :uint32 => 'I',
  :int64  => 'q',
  :uint64 => 'Q',
  :string => lambda do |str, *opts|
                      len = opts.first
                      str.ljust(len, "\0")[0, len]
                    end,
  # a few C-like names
  :int    => 'i',
  :uint   => 'I',
  :char   => 'C'
}
UnpackMap =

Only needed when unpacking is different from packing, i.e. strings w/ lambdas in PackMap.

{
  :string => lambda do |str, *opts|
                      len = opts.first
                      val = str[0, len-1].sub(/\0*$/, '')
                      str.slice!((len-1)..-1)
                      val
                    end
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args) ⇒ CStruct

Returns a new instance of CStruct.



193
194
195
# File 'lib/cstruct.rb', line 193

def initialize(*args)
  @values = args
end

Instance Attribute Details

#valuesObject (readonly) Also known as: to_a

Instance Methods #



191
192
193
# File 'lib/cstruct.rb', line 191

def values
  @values
end

Class Method Details

.bytesizeObject

Return the number of bytes occupied in memory or on disk.



170
171
172
# File 'lib/cstruct.rb', line 170

def bytesize
  const_get(:Members).inject(0) { |size, name| size + sizeof(name) }
end

.inherited(subclass) ⇒ Object



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/cstruct.rb', line 122

def inherited(subclass)
  subclass.instance_eval do
    
    # These "constants" are only constant references.  Structs can
    # be modified.  After the struct is defined it is still open,
    # but good practice would be not to change a struct after it
    # has been defined.
    # 
    # To support inheritance properly we try to get these
    # constants from the enclosing scope (and clone them before
    # modifying them!), and default to empty, er, defaults.
    
    members = const_get(:Members).clone rescue []
    member_index = const_get(:MemberIndex).clone rescue {}
    member_sizes = const_get(:MemberSizes).clone rescue {}
    member_opts = const_get(:MemberOptions).clone rescue {}
    
    const_set(:Members, members)
    const_set(:MemberIndex, member_index)
    const_set(:MemberSizes, member_sizes)
    const_set(:MemberOptions, member_opts)
    
  end
end

.lengthObject

Return the number of members.



167
168
169
# File 'lib/cstruct.rb', line 167

def size
  const_get(:Members).size
end

.new_from_bin(bin) ⇒ Object



179
180
181
182
# File 'lib/cstruct.rb', line 179

def new_from_bin(bin)
  new_struct = new
  new_struct.unserialize(bin)
end

.sizeObject

Return the number of members.



164
165
166
# File 'lib/cstruct.rb', line 164

def size
  const_get(:Members).size
end

.sizeof(name) ⇒ Object



174
175
176
177
# File 'lib/cstruct.rb', line 174

def sizeof(name)
  value = SizeMap[const_get(:MemberSizes)[name]]
  value.respond_to?(:call) ? value.call(*const_get(:MemberOptions)[name]) : value
end

Instance Method Details

#==(other) ⇒ Object



266
267
268
269
270
# File 'lib/cstruct.rb', line 266

def ==(other)
  puts @values.inspect
  puts other.values.inspect
  other.is_a?(self.class) && other.values == @values
end

#[](name_or_idx) ⇒ Object



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/cstruct.rb', line 234

def [](name_or_idx)
  case name_or_idx
    
  when Numeric
    idx = name_or_idx
    @values[idx]
    
  when String, Symbol
    name = name_or_idx.to_sym
    @values[member_index[name]]
    
  else
    raise ArgumentError, "expected name or index, got #{name_or_idx.inspect}"
  end
end

#[]=(name_or_idx, value) ⇒ Object



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/cstruct.rb', line 250

def []=(name_or_idx, value)
  case name_or_idx
    
  when Numeric
    idx = name_or_idx
    @values[idx] = value
    
  when String, Symbol
    name = name_or_idx.to_sym
    @values[member_index[name]] = value
    
  else
    raise ArgumentError, "expected name or index, got #{name_or_idx.inspect}"
  end
end

#bytesizeObject



291
292
293
# File 'lib/cstruct.rb', line 291

def bytesize
  self.class.bytesize
end

#each(&block) ⇒ Object

Some of these are just to quack like Ruby’s built-in Struct. YAGNI, but can’t hurt either.



274
275
276
# File 'lib/cstruct.rb', line 274

def each(&block)
  @values.each(&block)
end

#each_pair(&block) ⇒ Object



278
279
280
# File 'lib/cstruct.rb', line 278

def each_pair(&block)
  members.zip(@values).each(&block)
end

#member_indexObject



304
305
306
# File 'lib/cstruct.rb', line 304

def member_index
  self.class::MemberIndex
end

#member_optionsObject



312
313
314
# File 'lib/cstruct.rb', line 312

def member_options
  self.class::MemberOptions
end

#member_sizesObject



308
309
310
# File 'lib/cstruct.rb', line 308

def member_sizes
  self.class::MemberSizes
end

#membersObject

A few convenience methods.



300
301
302
# File 'lib/cstruct.rb', line 300

def members
  self.class::Members
end

#pack_patternObject



226
227
228
# File 'lib/cstruct.rb', line 226

def pack_pattern
  members.map { |name| PackMap[member_sizes[name]] }
end

#serializeObject



197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/cstruct.rb', line 197

def serialize
  vals = @values.clone
  membs = members.clone
  pack_pattern.map do |patt|
    name = membs.shift
    if patt.is_a?(String)
      [vals.shift].pack(patt)
    else
      patt.call(vals.shift, *member_options[name])
    end      
  end.join
end

#sizeObject Also known as: length



282
283
284
# File 'lib/cstruct.rb', line 282

def size
  members.size
end

#sizeof(name) ⇒ Object



287
288
289
# File 'lib/cstruct.rb', line 287

def sizeof(name)
  self.class.sizeof(name)
end

#unpack_patternObject



230
231
232
# File 'lib/cstruct.rb', line 230

def unpack_pattern
  members.map { |name| UnpackMap[member_sizes[name]] || PackMap[member_sizes[name]] }
end

#unserialize(bin) ⇒ Object



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/cstruct.rb', line 210

def unserialize(bin)
  bin = bin.clone
  @values = []
  membs = members.clone
  unpack_pattern.each do |patt|
    name = membs.shift
    if patt.is_a?(String)
      @values += bin.unpack(patt)
      bin.slice!(0, sizeof(name))
    else
      @values << patt.call(bin, *member_options[name])
    end
  end
  self
end