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.



368
369
370
# File 'lib/tem/builders/abi.rb', line 368

def initialize(target)
  @target = target
end

Instance Attribute Details

#targetObject (readonly)

The module / class impacted by the builder.



365
366
367
# File 'lib/tem/builders/abi.rb', line 365

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))


18
19
20
# File 'lib/tem/builders/abi.rb', line 18

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)


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
357
358
359
360
361
362
# File 'lib/tem/builders/abi.rb', line 326

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


34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/tem/builders/abi.rb', line 34

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


184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/tem/builders/abi.rb', line 184

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


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
307
308
309
310
311
312
# File 'lib/tem/builders/abi.rb', line 232

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


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
166
167
168
169
170
171
# File 'lib/tem/builders/abi.rb', line 124

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


71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/tem/builders/abi.rb', line 71

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