Class: Tem::Builders::Abi

Inherits:
Object
  • Object
show all
Defined in:
lib/tem/builders/abi.rb

Overview

Builder class for the ABI (Abstract Binary Interface) builder.

Direct Known Subclasses

Crypto

Defined Under Namespace

Modules: Impl

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(target) ⇒ Abi

Creates a builder targeting a module / class.



362
363
364
# File 'lib/tem/builders/abi.rb', line 362

def initialize(target)
  @target = target
end

Instance Attribute Details

#targetObject (readonly)

The module / class impacted by the builder.



359
360
361
# File 'lib/tem/builders/abi.rb', line 359

def target
  @target
end

Class Method Details

.define_abi(class_or_module) {|new(class_or_module)| ... } ⇒ Object

Creates a builder targeting a module / class.

The given parameter should be a class or module.

Yields:

  • (new(class_or_module))


12
13
14
# File 'lib/tem/builders/abi.rb', line 12

def self.define_abi(class_or_module)  # :yields: abi
  yield new(class_or_module)
end

Instance Method Details

#conditional_wrapper(name, tag_length, rules) ⇒ Object

Defines methods for handling an ‘enum’-like ABI type whose type is determined by a fixed-length tag that is prefixed to the data.

tag_length indicates the tag’s length, in bytes. The mapping between tags and lower-level ABI types is expressed as an array of rules. Each rule is a hash, and the following attributes are supported.

tag:: an array of numbers; the tag must match this array (de-serializing)
type:: the lower-level ABI type used for serialization/de-serialization
class:: a ruby Class; if present, the value must be a kind of the given
    class to match the rule (serializing)
predicate:: a Proc; if present, the Proc is given the value, and must
    return a true value for the value to match the rule (serializing)


320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/tem/builders/abi.rb', line 320

def conditional_wrapper(name, tag_length, rules)
  impl = Tem::Builders::Abi::Impl
  
  defines = Proc.new do 
    define_method :"read_#{name}" do |array, offset|      
      tag = array[offset, tag_length]
      matching_rule = rules.find { |rule| rule[:tag] == tag }
      raise "Rules don't cover tag #{tag.inspect}" unless matching_rule
      self.send :"read_#{matching_rule[:type]}", array, offset + tag_length
    end
    define_method :"read_#{name}_length" do |array, offset|      
      tag = array[offset, tag_length]
      matching_rule = rules.find { |rule| rule[:tag] == tag }
      raise "Rules don't cover tag #{tag.inspect}" unless matching_rule
      if self.respond_to? :"#{matching_rule[:type]}_length"
        tag_length + self.send(:"#{matching_rule[:type]}_length")
      else
        tag_length + self.send(:"read_#{matching_rule[:type]}_length", array,
                               offset + tag_length)
      end
    end
    define_method :"to_#{name}" do |value|
      matching_rule = rules.find do |rule|
        next false if rule[:class] && !value.kind_of?(rule[:class])
        next false if rule[:predicate] && !rule[:predicate].call(value)
        true
      end

      raise "Rules don't cover #{value.inspect}" unless matching_rule
      matching_rule[:tag] + self.send(:"to_#{matching_rule[:type]}", value)
    end
    define_method(:"#{name}_length") { bytes }
  end
  
  @target.class_eval &defines
  (class << @target; self; end).module_eval &defines    
end

#fixed_length_number(name, bytes, options = {}) ⇒ Object

Defines the methods for handling a fixed-length number type in an ABI.

The |options| hash supports the following keys:

signed:: if false, the value type cannot hold negative numbers;
         signed values are stored using 2s-complement; defaults to true
big_endian:: if true, bytes are sent over the wire in big-endian order

The following methods are defined for a type named ‘name’:

* read_name(array, offset) -> number
* to_name(number) -> array
* signed_to_name(number) -> array  # takes signed inputs on unsigned types
* name_length -> number  # the number of bytes in the number type


28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/tem/builders/abi.rb', line 28

def fixed_length_number(name, bytes, options = {})
  impl = Tem::Builders::Abi::Impl
  signed = options.fetch :signed, true
  big_endian = options.fetch :big_endian, true    
  
  defines = Proc.new do 
    define_method :"read_#{name}" do |array, offset|
      impl.number_from_array array, offset, bytes, signed, big_endian
    end
    define_method :"to_#{name}" do |n|
      impl.check_number_range n, bytes, signed
      impl.number_to_array n, bytes, signed, big_endian
    end
    define_method :"signed_to_#{name}" do |n|
      impl.number_to_array n, bytes, signed, big_endian
    end
    define_method(:"#{name}_length") { bytes }
  end
  
  @target.class_eval &defines
  (class << @target; self; end).module_eval &defines
end

#fixed_length_string(name, bytes, options = {}) ⇒ Object

Defines the methods for handling a fixed-length string type in an ABI.

The |options| hash supports the following keys:

signed:: if false, the value type cannot hold negative numbers;
         signed values are stored using 2s-complement; defaults to true
big_endian:: if true, bytes are sent over the wire in big-endian order

The following methods are defined for a type named ‘name’:

* read_name(array, offset) -> string
* to_name(string or array) -> array
* name_length -> number  # the number of bytes in the string type


178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/tem/builders/abi.rb', line 178

def fixed_length_string(name, bytes, options = {})
  impl = Tem::Builders::Abi::Impl
  signed = options.fetch :signed, true
  big_endian = options.fetch :big_endian, true    
  
  defines = Proc.new do 
    define_method :"read_#{name}" do |array, offset|      
      impl.string_from_array array, offset, bytes
    end
    define_method :"to_#{name}" do |n|
      impl.string_to_array n, bytes
    end
    define_method(:"#{name}_length") { bytes }
  end
  
  @target.class_eval &defines
  (class << @target; self; end).module_eval &defines
end

#object_wrapper(name, object_class, schema, hooks = {}) ⇒ Object

Defines methods for handling a complex ABI structure wrapped into an object.

Objects are assumed to be of the object_class type. The objects are serialized according to a schema, which is an array of 2-element directives. The first element in a directive indicates the lower-level ABI type to be serialized, and the 2nd element indicates the mapping between the object and the higher level ABI. The mapping can be:

* a symbol - the lower level ABI output is assigned to an object property
* a hash - the keys in the lower level ABI output are assigned to the
           object properties indicated by the values
* nil - the components of the lower level ABI type (which should act like
        packed_variable_length_numbers types) are mapped to identically
        named object properties

The following methods are defined for a type named ‘name’:

* read_name(array, offset) -> object
* read_name_length(array, offset) -> number
* to_name(object) -> array
* name_class -> Class

The following hooks (Procs in the hooks argument) are supported:

new(object_class) -> object:: called to instantiate a new object in read_;
    if the hook is not present, object_class.new is used instead
read(object) -> object:: called after the object is de-serialized using
    the lower-level ABI; if the hook is present, its value is returned
    from the read_ method
to(object) -> object:: called before the object is serialized using the
    lower-level ABI; if the hook is present, its value is used for
    serialization


226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/tem/builders/abi.rb', line 226

def object_wrapper(name, object_class, schema, hooks = {})
  if hooks[:new]
    read_body = "r = #{name}_newhook(#{name}_class);"
  else
    read_body = "r = #{name}_class.new;"
  end    

  to_body = "r = [];"
  to_body << "value = #{name}_tohook(value);" if hooks[:to]
  
  readlen_body = "old_offset = offset;"
  
  0.upto schema.length / 2 - 1 do |i|
    abi_type = schema[i * 2]
    type_mapping = schema[i * 2 + 1]
    
    # Set up the translation table.
    if type_mapping.nil?
      type_mapping = {}
      components = @target.send :"#{abi_type}_components"
      components.each { |c| type_mapping[c] = c }
    end
    
    # Set up the read_ and read_name_length methods.
    if abi_type.kind_of? Symbol
      read_body << "v = read_#{abi_type}(array,offset);"
    else
      read_body << "v = #{abi_type.inspect};"
    end    
    case type_mapping
    when Symbol
      read_body << "r.#{type_mapping} = v;"
    when Hash, nil        
      type_mapping.each do |k, v|
        read_body << "r.#{v} = v[:#{k}];"
      end
    end
    if abi_type.kind_of? Symbol
      if @target.respond_to? :"#{abi_type}_length"
        read_body << "offset += #{@target.send :"#{abi_type}_length"};"
        readlen_body << "offset += #{@target.send :"#{abi_type}_length"};"
      elsif @target.respond_to? :"read_#{abi_type}_length"
        read_body << "offset += read_#{abi_type}_length(array,offset);"        
        readlen_body << "offset += read_#{abi_type}_length(array,offset);"        
      else
        raise "#{abi_type} doesn't support _length or read_#{abi_type}_length"
      end
    end
    
    # Set up the to_ method.
    next unless abi_type.kind_of? Symbol
    to_body << "r += to_#{abi_type}("
    case type_mapping
    when Symbol
      to_body << "value.#{type_mapping}"
    when Hash
      to_body << type_mapping.map { |k, v| ":#{k} => value.#{v}" }.join(', ')
    end
    to_body << ");"
  end
  read_body << "r = self.#{name}_readhook(r);" if hooks[:read]
  read_body << "r;"
  to_body << "r;"
  readlen_body << "offset - old_offset;"

  define_str = "def read_#{name}(array,offset);#{read_body}end;"
  define_str << "def read_#{name}_length(array,offset);#{readlen_body}end;"
  define_str << "def to_#{name}(value);#{to_body}end;"
  
  defines = Proc.new do 
    define_method(:"#{name}_class") { object_class }
    define_method(:"#{name}_newhook", &hooks[:new]) if hooks[:new]      
    define_method(:"#{name}_readhook", &hooks[:read]) if hooks[:read]
    define_method(:"#{name}_tohook", &hooks[:to]) if hooks[:to]      
  end
  
  @target.class_eval &defines
  @target.class_eval define_str
  (class << @target; self; end).module_eval &defines
  (class << @target; self; end).module_eval define_str
end

#packed_variable_length_numbers(name, length_type, components, options = {}) ⇒ Object

Defines the methods for handling a group of packed variable-length numbers in the ABI.

When serializing a group of variable-length numbers, it’s desirable to have the lengths of all the numbers grouped before the number data. This makes reading and writing easier & faster for embedded code. The optimization is important enough that it’s made its way into the API.

All the numbers’ lengths are assumed to be represented by the same fixed-length type, whose name is given as the length_type parameter.

The numbers are de-serialized into a hash, where each number is associated with a key. The components argument specifies the names of the keys, in the order that the numbers are serialized in.

The |options| hash supports the following keys:

signed:: if false, the value type cannot hold negative numbers;
         signed values are stored using 2s-complement; defaults to true
big_endian:: if true, bytes are sent over the wire in big-endian order

The following methods are defined for a type named ‘name’:

* read_name(array, offset) -> hash
* read_name_length(array, offset) -> number
* to_name(hash) -> array
* name_components -> array


118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/tem/builders/abi.rb', line 118

def packed_variable_length_numbers(name, length_type, components,
                                   options = {})
  impl = Tem::Builders::Abi::Impl
  sub_names = components.freeze
  signed = options.fetch :signed, true
  big_endian = options.fetch :big_endian, true
  length_bytes = @target.send :"#{length_type}_length"
  read_length_msg = :"read_#{length_type}"
  write_length_msg = :"to_#{length_type}"
  
  defines = Proc.new do 
    define_method :"read_#{name}" do |array, offset|
      response = {}
      data_offset = offset + length_bytes * sub_names.length
      sub_names.each_with_index do |sub_name, i|
        length = self.send read_length_msg, array, offset + i * length_bytes
        response[sub_name] =
            impl.number_from_array array, data_offset, length, signed,
                                   big_endian
        data_offset += length
      end
      response
    end
    define_method :"to_#{name}" do |numbers|
      number_data = sub_names.map do |sub_name|
        impl.number_to_array numbers[sub_name], nil, signed, big_endian
      end
      length_data = number_data.map do |number|
        self.send write_length_msg, number.length
      end
      # Concatenate all the arrays without using flatten.
      lengths = length_data.inject([]) { |acc, i| acc += i }
      number_data.inject(lengths) { |acc, i| acc += i }
    end
    define_method :"read_#{name}_length" do |array, offset|
      response = sub_names.length * length_bytes
      0.upto(sub_names.length - 1) do |i|
        response += self.send read_length_msg, array,
                              offset + i * length_bytes
      end
      response
    end
    define_method(:"#{name}_components") { sub_names }
  end
  
  @target.class_eval &defines
  (class << @target; self; end).module_eval &defines    
end

#variable_length_number(name, length_type, options = {}) ⇒ Object

Defines the methods for handling a variable-length number type in an ABI.

The length_type argument holds the name of a fixed-length number type that will be used to store the length of hte variable-length number.

The |options| hash supports the following keys:

signed:: if false, the value type cannot hold negative numbers;
         signed values are stored using 2s-complement; defaults to true
big_endian:: if true, bytes are sent over the wire in big-endian order

The following methods are defined for a type named ‘name’:

* read_name(array, offset) -> number
* read_name_length(array, offset) -> number
* to_name(number) -> array


65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/tem/builders/abi.rb', line 65

def variable_length_number(name, length_type, options = {})
  impl = Tem::Builders::Abi::Impl
  signed = options.fetch :signed, true
  big_endian = options.fetch :big_endian, true
  length_bytes = @target.send :"#{length_type}_length"
  read_length_msg = :"read_#{length_type}"
  write_length_msg = :"to_#{length_type}"
  
  defines = Proc.new do 
    define_method :"read_#{name}" do |array, offset|
      length = self.send read_length_msg, array, offset
      impl.number_from_array array, offset + length_bytes, length, signed,
                             big_endian
    end
    define_method :"to_#{name}" do |n|
      number_data = impl.number_to_array n, nil, signed, big_endian
      length_data = self.send write_length_msg, number_data.length
      length_data + number_data
    end
    define_method :"read_#{name}_length" do |array, offset|
      length_bytes + self.send(read_length_msg, array, offset)
    end
  end
  
  @target.class_eval &defines
  (class << @target; self; end).module_eval &defines    
end