Module: Google::Protobuf::Internal::Convert

Included in:
Descriptor, FieldDescriptor, Map, Map, RepeatedField, RepeatedField
Defined in:
lib/google/protobuf/ffi/internal/convert.rb

Instance Method Summary collapse

Instance Method Details

#convert_ruby_to_upb(value, arena, c_type, msg_or_enum_def) ⇒ Google::Protobuf::FFI::MessageValue

Arena should be the

Parameters:

  • value (Object)

    Value to convert

  • arena (Arena)

    Arena that owns the Message where the MessageValue will be set

Returns:



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/google/protobuf/ffi/internal/convert.rb', line 21

def convert_ruby_to_upb(value, arena, c_type, msg_or_enum_def)
  raise ArgumentError.new "Expected Descriptor or EnumDescriptor, instead got #{msg_or_enum_def.class}" unless [NilClass, Descriptor, EnumDescriptor].include? msg_or_enum_def.class
  return_value = Google::Protobuf::FFI::MessageValue.new
  case c_type
  when :float
    raise TypeError.new "Expected number type for float field '#{name}' (given #{value.class})." unless value.respond_to? :to_f
    return_value[:float_val] = value.to_f
  when :double
    raise TypeError.new "Expected number type for double field '#{name}' (given #{value.class})." unless value.respond_to? :to_f
    return_value[:double_val] = value.to_f
  when :bool
    raise TypeError.new "Invalid argument for boolean field '#{name}' (given #{value.class})." unless [TrueClass, FalseClass].include? value.class
    return_value[:bool_val] = value
  when :string
    raise TypeError.new "Invalid argument for string field '#{name}' (given #{value.class})." unless [Symbol, String].include? value.class
    begin
      string_value = value.to_s.encode("UTF-8")
    rescue Encoding::UndefinedConversionError
      # TODO - why not include the field name here?
      raise Encoding::UndefinedConversionError.new "String is invalid UTF-8"
    end
    return_value[:str_val][:size] = string_value.bytesize
    return_value[:str_val][:data] = Google::Protobuf::FFI.arena_malloc(arena, string_value.bytesize)
    # TODO - how important is it to still use arena malloc, versus the following?
    # buffer = ::FFI::MemoryPointer.new(:char, string_value.bytesize)
    # buffer.put_bytes(0, string_value)
    # return_value[:str_val][:data] = buffer
    raise NoMemoryError.new "Cannot allocate #{string_value.bytesize} bytes for string on Arena" if return_value[:str_val][:data].nil? || return_value[:str_val][:data].null?
    return_value[:str_val][:data].write_string(string_value)
  when :bytes
    raise TypeError.new "Invalid argument for bytes field '#{name}' (given #{value.class})." unless value.is_a? String
    string_value = value.encode("ASCII-8BIT")
    return_value[:str_val][:size] = string_value.bytesize
    return_value[:str_val][:data] = Google::Protobuf::FFI.arena_malloc(arena, string_value.bytesize)
    raise NoMemoryError.new "Cannot allocate #{string_value.bytesize} bytes for bytes on Arena" if return_value[:str_val][:data].nil? || return_value[:str_val][:data].null?
    return_value[:str_val][:data].write_string_length(string_value, string_value.bytesize)
  when :message
    raise TypeError.new "nil message not allowed here." if value.nil?
    if value.is_a? Hash
      raise RuntimeError.new "Attempted to initialize message from Hash for field #{name} but have no definition" if msg_or_enum_def.nil?
      new_message = msg_or_enum_def.msgclass.
        send(:private_constructor, arena, initial_value: value)
      return_value[:msg_val] = new_message.instance_variable_get(:@msg)
      return return_value
    end

    descriptor = value.class.respond_to?(:descriptor) ? value.class.descriptor : nil
    if descriptor != msg_or_enum_def
      wkt = Google::Protobuf::FFI.get_well_known_type(msg_or_enum_def)
      case wkt
      when :Timestamp
        raise TypeError.new "Invalid type #{value.class} to assign to submessage field '#{name}'." unless value.kind_of? Time
        new_message = Google::Protobuf::FFI.new_message_from_def msg_or_enum_def, arena
        sec = Google::Protobuf::FFI::MessageValue.new
        sec[:int64_val] = value.tv_sec
        sec_field_def = Google::Protobuf::FFI.get_field_by_number msg_or_enum_def, 1
        raise "Should be impossible" unless Google::Protobuf::FFI.set_message_field new_message, sec_field_def, sec, arena
        nsec_field_def = Google::Protobuf::FFI.get_field_by_number msg_or_enum_def, 2
        nsec = Google::Protobuf::FFI::MessageValue.new
        nsec[:int32_val] = value.tv_nsec
        raise "Should be impossible" unless Google::Protobuf::FFI.set_message_field new_message, nsec_field_def, nsec, arena
        return_value[:msg_val] = new_message
      when :Duration
        raise TypeError.new "Invalid type #{value.class} to assign to submessage field '#{name}'." unless value.kind_of? Numeric
        new_message = Google::Protobuf::FFI.new_message_from_def msg_or_enum_def, arena
        sec = Google::Protobuf::FFI::MessageValue.new
        sec[:int64_val] = value
        sec_field_def = Google::Protobuf::FFI.get_field_by_number msg_or_enum_def, 1
        raise "Should be impossible" unless Google::Protobuf::FFI.set_message_field new_message, sec_field_def, sec, arena
        nsec_field_def = Google::Protobuf::FFI.get_field_by_number msg_or_enum_def, 2
        nsec = Google::Protobuf::FFI::MessageValue.new
        nsec[:int32_val] = ((value.to_f - value.to_i) * 1000000000).round
        raise "Should be impossible" unless Google::Protobuf::FFI.set_message_field new_message, nsec_field_def, nsec, arena
        return_value[:msg_val] = new_message
      else
        raise TypeError.new "Invalid type #{value.class} to assign to submessage field '#{name}'."
      end
    else
      arena.fuse(value.instance_variable_get(:@arena))
      return_value[:msg_val] = value.instance_variable_get :@msg
    end
  when :enum
    return_value[:int32_val] = case value
      when Numeric
        value.to_i
      when String, Symbol
        enum_number = EnumDescriptor.send(:lookup_name, msg_or_enum_def, value.to_s)
        #TODO add the bad value to the error message after tests pass
        raise RangeError.new "Unknown symbol value for enum field '#{name}'." if enum_number.nil?
        enum_number
      else
        raise TypeError.new "Expected number or symbol type for enum field '#{name}'."
      end
  #TODO After all tests pass, improve error message across integer type by including actual offending value
  when :int32
    raise TypeError.new "Expected number type for integral field '#{name}' (given #{value.class})." unless value.is_a? Numeric
    raise RangeError.new "Non-integral floating point value assigned to integer field '#{name}' (given #{value.class})." if value.floor != value
    raise RangeError.new "Value assigned to int32 field '#{name}' (given #{value.class}) with more than 32-bits." unless value.to_i.bit_length < 32
    return_value[:int32_val] = value.to_i
  when :uint32
    raise TypeError.new "Expected number type for integral field '#{name}' (given #{value.class})." unless value.is_a? Numeric
    raise RangeError.new "Non-integral floating point value assigned to integer field '#{name}' (given #{value.class})." if value.floor != value
    raise RangeError.new "Assigning negative value to unsigned integer field '#{name}' (given #{value.class})." if value < 0
    raise RangeError.new "Value assigned to uint32 field '#{name}' (given #{value.class}) with more than 32-bits." unless value.to_i.bit_length < 33
    return_value[:uint32_val] = value.to_i
  when :int64
    raise TypeError.new "Expected number type for integral field '#{name}' (given #{value.class})." unless value.is_a? Numeric
    raise RangeError.new "Non-integral floating point value assigned to integer field '#{name}' (given #{value.class})." if value.floor != value
    raise RangeError.new "Value assigned to int64 field '#{name}' (given #{value.class}) with more than 64-bits." unless value.to_i.bit_length < 64
    return_value[:int64_val] = value.to_i
  when :uint64
    raise TypeError.new "Expected number type for integral field '#{name}' (given #{value.class})." unless value.is_a? Numeric
    raise RangeError.new "Non-integral floating point value assigned to integer field '#{name}' (given #{value.class})." if value.floor != value
    raise RangeError.new "Assigning negative value to unsigned integer field '#{name}' (given #{value.class})." if value < 0
    raise RangeError.new "Value assigned to uint64 field '#{name}' (given #{value.class}) with more than 64-bits." unless value.to_i.bit_length < 65
    return_value[:uint64_val] = value.to_i
  else
    raise RuntimeError.new "Unsupported type #{c_type}"
  end
  return_value
end

#convert_upb_to_ruby(message_value, c_type, msg_or_enum_def = nil, arena = nil) ⇒ Object

Safe to call without an arena if the caller has checked that c_type is not :message.

Parameters:



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/google/protobuf/ffi/internal/convert.rb', line 150

def convert_upb_to_ruby(message_value, c_type, msg_or_enum_def = nil, arena = nil)
  throw TypeError.new "Expected MessageValue but got #{message_value.class}" unless message_value.is_a? Google::Protobuf::FFI::MessageValue

  case c_type
  when :bool
    message_value[:bool_val]
  when :int32
    message_value[:int32_val]
  when :uint32
    message_value[:uint32_val]
  when :double
    message_value[:double_val]
  when :int64
    message_value[:int64_val]
  when :uint64
    message_value[:uint64_val]
  when :string
    if message_value[:str_val][:size].zero?
      ""
    else
      message_value[:str_val][:data].read_string_length(message_value[:str_val][:size]).force_encoding("UTF-8").freeze
    end
  when :bytes
    if message_value[:str_val][:size].zero?
      ""
    else
      message_value[:str_val][:data].read_string_length(message_value[:str_val][:size]).force_encoding("ASCII-8BIT").freeze
    end
  when :float
    message_value[:float_val]
  when :enum
    EnumDescriptor.send(:lookup_value, msg_or_enum_def, message_value[:int32_val]) || message_value[:int32_val]
  when :message
    raise "Null Arena for message" if arena.nil?
    Descriptor.send(:get_message, message_value[:msg_val], msg_or_enum_def, arena)
  else
    raise RuntimeError.new "Unexpected type #{c_type}"
  end
end

#map_create_hash(map_ptr, field_descriptor) ⇒ Object



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/google/protobuf/ffi/internal/convert.rb', line 215

def map_create_hash(map_ptr, field_descriptor)
  return {} if map_ptr.nil? or map_ptr.null?
  return_value = {}

  message_descriptor = field_descriptor.send(:subtype)
  key_field_def = Google::Protobuf::FFI.get_field_by_number(message_descriptor, 1)
  key_field_type = Google::Protobuf::FFI.get_type(key_field_def)

  value_field_def = Google::Protobuf::FFI.get_field_by_number(message_descriptor, 2)
  value_field_type = Google::Protobuf::FFI.get_type(value_field_def)

  iter = ::FFI::MemoryPointer.new(:size_t, 1)
  iter.write(:size_t, Google::Protobuf::FFI::Upb_Map_Begin)
  while Google::Protobuf::FFI.map_next(map_ptr, iter) do
    iter_size_t = iter.read(:size_t)
    key_message_value = Google::Protobuf::FFI.map_key(map_ptr, iter_size_t)
    value_message_value = Google::Protobuf::FFI.map_value(map_ptr, iter_size_t)
    hash_key = convert_upb_to_ruby(key_message_value, key_field_type)
    hash_value = scalar_create_hash(value_message_value, value_field_type, msg_or_enum_descriptor: value_field_def.subtype)
    return_value[hash_key] = hash_value
  end
  return_value
end

#message_value_deep_copy(message_value, type, descriptor, arena) ⇒ Object



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/google/protobuf/ffi/internal/convert.rb', line 269

def message_value_deep_copy(message_value, type, descriptor, arena)
  raise unless message_value.is_a? Google::Protobuf::FFI::MessageValue
  new_message_value = Google::Protobuf::FFI::MessageValue.new
  case type
  when :string, :bytes
    # TODO - how important is it to still use arena malloc, versus using FFI MemoryPointers?
    new_message_value[:str_val][:size] = message_value[:str_val][:size]
    new_message_value[:str_val][:data] = Google::Protobuf::FFI.arena_malloc(arena, message_value[:str_val][:size])
    raise NoMemoryError.new "Allocation failed" if  new_message_value[:str_val][:data].nil? or new_message_value[:str_val][:data].null?
    Google::Protobuf::FFI.memcpy(new_message_value[:str_val][:data], message_value[:str_val][:data], message_value[:str_val][:size])
  when :message
    new_message_value[:msg_val] = descriptor.msgclass.send(:deep_copy, message_value[:msg_val], arena).instance_variable_get(:@msg)
  else
    Google::Protobuf::FFI.memcpy(new_message_value.to_ptr, message_value.to_ptr, Google::Protobuf::FFI::MessageValue.size)
  end
  new_message_value
end

#repeated_field_create_array(array, field_descriptor, type) ⇒ Object



239
240
241
242
243
244
245
246
247
# File 'lib/google/protobuf/ffi/internal/convert.rb', line 239

def repeated_field_create_array(array, field_descriptor, type)
  return_value = []
  n = (array.nil? || array.null?) ? 0 : Google::Protobuf::FFI.array_size(array)
  0.upto(n - 1) do |i|
    message_value = Google::Protobuf::FFI.get_msgval_at(array, i)
    return_value << scalar_create_hash(message_value, type, field_descriptor: field_descriptor)
  end
  return_value
end

#scalar_create_hash(message_value, type, field_descriptor: nil, msg_or_enum_descriptor: nil) ⇒ Object

Parameters:

  • field_descriptor (FieldDescriptor) (defaults to: nil)

    Descriptor of the field to convert to a hash.



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/google/protobuf/ffi/internal/convert.rb', line 250

def scalar_create_hash(message_value, type, field_descriptor: nil, msg_or_enum_descriptor: nil)
  if [:message, :enum].include? type
    if field_descriptor.nil?
      if msg_or_enum_descriptor.nil?
        raise "scalar_create_hash requires either a FieldDescriptor, MessageDescriptor, or EnumDescriptor as an argument, but received only nil"
      end
    else
      msg_or_enum_descriptor = field_descriptor.subtype
    end
    if type == :message
      to_h_internal(message_value[:msg_val], msg_or_enum_descriptor)
    elsif type == :enum
      convert_upb_to_ruby message_value, type, msg_or_enum_descriptor
    end
  else
    convert_upb_to_ruby message_value, type
  end
end

#to_h_internal(msg, message_descriptor) ⇒ Object



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/google/protobuf/ffi/internal/convert.rb', line 190

def to_h_internal(msg, message_descriptor)
  return nil if msg.nil? or msg.null?
  hash = {}
  iter = ::FFI::MemoryPointer.new(:size_t, 1)
  iter.write(:size_t, Google::Protobuf::FFI::Upb_Message_Begin)
  message_value = Google::Protobuf::FFI::MessageValue.new
  field_def_ptr = ::FFI::MemoryPointer.new :pointer

  while Google::Protobuf::FFI::message_next(msg, message_descriptor, nil, field_def_ptr, message_value, iter) do
    field_descriptor = FieldDescriptor.from_native field_def_ptr.get_pointer(0)

    if field_descriptor.map?
      hash_entry = map_create_hash(message_value[:map_val], field_descriptor)
    elsif field_descriptor.repeated?
      hash_entry = repeated_field_create_array(message_value[:array_val], field_descriptor, field_descriptor.type)
    else
      hash_entry = scalar_create_hash(message_value, field_descriptor.type, field_descriptor: field_descriptor)
    end

    hash[field_descriptor.name.to_sym] = hash_entry
  end

  hash
end