blob: 4a4881302166fc5b92900638b93843992091708d [file]
# Protocol Buffers - Google's data interchange format
# Copyright 2023 Google Inc. All rights reserved.
#
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd
# Decorates Descriptor with the `build_message_class` method that defines
# Message classes.
module Google
module Protobuf
class FFI
# Message
attach_function :clear_message_field, :upb_Message_ClearFieldByDef, [:Message, FieldDescriptor], :void
attach_function :get_message_value, :upb_Message_GetFieldByDef, [:Message, FieldDescriptor], MessageValue.by_value
attach_function :get_message_has, :upb_Message_HasFieldByDef, [:Message, FieldDescriptor], :bool
attach_function :set_message_field, :upb_Message_SetFieldByDef, [:Message, FieldDescriptor, MessageValue.by_value, Internal::Arena], :bool
attach_function :encode_message, :upb_Encode, [:Message, MiniTable.by_ref, :size_t, Internal::Arena, :pointer, :pointer], EncodeStatus
attach_function :json_decode_message_detecting_nonconformance, :upb_JsonDecodeDetectingNonconformance, [:binary_string, :size_t, :Message, Descriptor, :DefPool, :int, Internal::Arena, Status.by_ref], :int
attach_function :json_encode_message, :upb_JsonEncode, [:Message, Descriptor, :DefPool, :int, :binary_string, :size_t, Status.by_ref], :size_t
attach_function :decode_message, :upb_Decode, [:binary_string, :size_t, :Message, MiniTable.by_ref, :ExtensionRegistry, :int, Internal::Arena], DecodeStatus
attach_function :get_mutable_message, :upb_Message_Mutable, [:Message, FieldDescriptor, Internal::Arena], MutableMessageValue.by_value
attach_function :get_message_which_oneof, :upb_Message_WhichOneofByDef, [:Message, OneofDescriptor], FieldDescriptor
attach_function :message_discard_unknown, :upb_Message_DiscardUnknown, [:Message, Descriptor, :DefPool, :int], :bool
attach_function :message_next, :upb_Message_Next, [:Message, Descriptor, :DefPool, :FieldDefPointer, MessageValue.by_ref, :pointer], :bool
attach_function :message_freeze, :upb_Message_Freeze, [:Message, MiniTable.by_ref], :void
attach_function :message_frozen?, :upb_Message_IsFrozen, [:Message], :bool
# MessageValue
attach_function :message_value_equal, :shared_Msgval_IsEqual, [MessageValue.by_value, MessageValue.by_value, CType, Descriptor], :bool
attach_function :message_value_hash, :shared_Msgval_GetHash, [MessageValue.by_value, CType, Descriptor, :uint64_t], :uint64_t
end
class Descriptor
def build_message_class
descriptor = self
Class.new(Google::Protobuf::const_get(:AbstractMessage)) do
@descriptor = descriptor
class << self
attr_accessor :descriptor
private
attr_accessor :oneof_field_names
include ::Google::Protobuf::Internal::Convert
end
alias original_method_missing method_missing
def method_missing(method_name, *args)
method_missing_internal method_name, *args, mode: :method_missing
end
def respond_to_missing?(method_name, include_private = false)
method_missing_internal(method_name, mode: :respond_to_missing?) || super
end
##
# Public constructor. Automatically allocates from a new Arena.
def self.new(initial_value = nil)
instance = allocate
instance.send(:initialize, initial_value)
instance
end
##
# Is this object frozen?
# Returns true if either this Ruby wrapper or the underlying
# representation are frozen. Freezes the wrapper if the underlying
# representation is already frozen but this wrapper isn't.
def frozen?
unless Google::Protobuf::FFI.message_frozen? @msg
raise RuntimeError.new "Ruby frozen Message with mutable representation" if super
return false
end
method(:freeze).super_method.call unless super
true
end
##
# Freezes the map object. We have to intercept this so we can freeze the
# underlying representation, not just the Ruby wrapper. Returns self.
def freeze
if method(:frozen?).super_method.call
unless Google::Protobuf::FFI.message_frozen? @msg
raise RuntimeError.new "Underlying representation of message still mutable despite frozen wrapper"
end
return self
end
unless Google::Protobuf::FFI.message_frozen? @msg
Google::Protobuf::FFI.message_freeze(@msg, Google::Protobuf::FFI.get_mini_table(self.class.descriptor))
end
super
end
def dup
duplicate = self.class.private_constructor(@arena)
mini_table = Google::Protobuf::FFI.get_mini_table(self.class.descriptor)
size = mini_table[:size]
duplicate.instance_variable_get(:@msg).write_string_length(@msg.read_string_length(size), size)
duplicate
end
alias clone dup
def eql?(other)
return false unless self.class === other
encoding_options = Google::Protobuf::FFI::Upb_Encode_Deterministic | Google::Protobuf::FFI::Upb_Encode_SkipUnknown
temporary_arena = Google::Protobuf::FFI.create_arena
mini_table = Google::Protobuf::FFI.get_mini_table(self.class.descriptor)
size_one = ::FFI::MemoryPointer.new(:size_t, 1)
encoding_one = ::FFI::MemoryPointer.new(:pointer, 1)
encoding_status = Google::Protobuf::FFI.encode_message(@msg, mini_table, encoding_options, temporary_arena, encoding_one.to_ptr, size_one)
raise ParseError.new "Error comparing messages due to #{encoding_status} while encoding LHS of `eql?()`" unless encoding_status == :Ok
size_two = ::FFI::MemoryPointer.new(:size_t, 1)
encoding_two = ::FFI::MemoryPointer.new(:pointer, 1)
encoding_status = Google::Protobuf::FFI.encode_message(other.instance_variable_get(:@msg), mini_table, encoding_options, temporary_arena, encoding_two.to_ptr, size_two)
raise ParseError.new "Error comparing messages due to #{encoding_status} while encoding RHS of `eql?()`" unless encoding_status == :Ok
if encoding_one.null? or encoding_two.null?
raise ParseError.new "Error comparing messages"
end
size_one.read(:size_t) == size_two.read(:size_t) and Google::Protobuf::FFI.memcmp(encoding_one.read(:pointer), encoding_two.read(:pointer), size_one.read(:size_t)).zero?
end
alias == eql?
def hash
encoding_options = Google::Protobuf::FFI::Upb_Encode_Deterministic | Google::Protobuf::FFI::Upb_Encode_SkipUnknown
temporary_arena = Google::Protobuf::FFI.create_arena
mini_table_ptr = Google::Protobuf::FFI.get_mini_table(self.class.descriptor)
size_ptr = ::FFI::MemoryPointer.new(:size_t, 1)
encoding = ::FFI::MemoryPointer.new(:pointer, 1)
encoding_status = Google::Protobuf::FFI.encode_message(@msg, mini_table_ptr, encoding_options, temporary_arena, encoding.to_ptr, size_ptr)
if encoding_status != :Ok or encoding.null?
raise ParseError.new "Error calculating hash"
end
encoding.read(:pointer).read_string(size_ptr.read(:size_t)).hash
end
def to_h
to_h_internal @msg, self.class.descriptor
end
##
# call-seq:
# Message.inspect => string
#
# Returns a human-readable string representing this message. It will be
# formatted as "<MessageType: field1: value1, field2: value2, ...>". Each
# field's value is represented according to its own #inspect method.
def inspect
self.class.inspect_internal @msg
end
def to_s
self.inspect
end
##
# call-seq:
# Message.[](index) => value
# Accesses a field's value by field name. The provided field name
# should be a string.
def [](name)
raise TypeError.new "Expected String for name but got #{name.class}" unless name.is_a? String
index_internal name
end
##
# call-seq:
# Message.[]=(index, value)
# Sets a field's value by field name. The provided field name should
# be a string.
# @param name [String] Name of the field to be set
# @param value [Object] Value to set the field to
def []=(name, value)
raise TypeError.new "Expected String for name but got #{name.class}" unless name.is_a? String
index_assign_internal(value, name: name)
end
##
# call-seq:
# MessageClass.decode(data, options) => message
#
# Decodes the given data (as a string containing bytes in protocol buffers wire
# format) under the interpretation given by this message class's definition
# and returns a message object with the corresponding field values.
# @param data [String] Binary string in Protobuf wire format to decode
# @param options [Hash] options for the decoder
# @option options [Integer] :recursion_limit Set to maximum decoding depth for message (default is 64)
def self.decode(data, options = {})
raise ArgumentError.new "Expected hash arguments." unless options.is_a? Hash
raise ArgumentError.new "Expected string for binary protobuf data." unless data.is_a? String
decoding_options = 0
depth = options[:recursion_limit]
if depth.is_a? Numeric
decoding_options |= Google::Protobuf::FFI.decode_max_depth(depth.to_i)
end
message = new
mini_table_ptr = Google::Protobuf::FFI.get_mini_table(message.class.descriptor)
status = Google::Protobuf::FFI.decode_message(
data,
data.bytesize,
message.instance_variable_get(:@msg),
mini_table_ptr,
Google::Protobuf::FFI.get_extension_registry(message.class.descriptor.send(:pool).descriptor_pool),
decoding_options,
message.instance_variable_get(:@arena)
)
raise ParseError.new "Error occurred during parsing" unless status == :Ok
message
end
##
# call-seq:
# MessageClass.encode(msg, options) => bytes
#
# Encodes the given message object to its serialized form in protocol buffers
# wire format.
# @param options [Hash] options for the encoder
# @option options [Integer] :recursion_limit Set to maximum encoding depth for message (default is 64)
def self.encode(message, options = {})
raise ArgumentError.new "Message of wrong type." unless message.is_a? self
raise ArgumentError.new "Expected hash arguments." unless options.is_a? Hash
encoding_options = 0
depth = options[:recursion_limit]
if depth.is_a? Numeric
encoding_options |= Google::Protobuf::FFI.decode_max_depth(depth.to_i)
end
encode_internal(message.instance_variable_get(:@msg), encoding_options) do |encoding, size, _|
if encoding.nil? or encoding.null?
raise RuntimeError.new "Exceeded maximum depth (possibly cycle)"
else
encoding.read_string_length(size).force_encoding("ASCII-8BIT").freeze
end
end
end
##
# all-seq:
# MessageClass.decode_json(data, options = {}) => message
#
# Decodes the given data (as a string containing bytes in protocol buffers wire
# format) under the interpretation given by this message class's definition
# and returns a message object with the corresponding field values.
#
# @param options [Hash] options for the decoder
# @option options [Boolean] :ignore_unknown_fields Set true to ignore unknown fields (default is to raise an error)
# @return [Message]
def self.decode_json(data, options = {})
decoding_options = 0
unless options.is_a? Hash
if options.respond_to? :to_h
options options.to_h
else
#TODO can this error message be improve to include what was received?
raise ArgumentError.new "Expected hash arguments"
end
end
raise ArgumentError.new "Expected string for JSON data." unless data.is_a? String
raise RuntimeError.new "Cannot parse a wrapper directly" if descriptor.send(:wrapper?)
if options[:ignore_unknown_fields]
decoding_options |= Google::Protobuf::FFI::Upb_JsonDecode_IgnoreUnknown
end
message = new
pool_def = message.class.descriptor.instance_variable_get(:@descriptor_pool).descriptor_pool
status = Google::Protobuf::FFI::Status.new
result = Google::Protobuf::FFI.json_decode_message_detecting_nonconformance(data, data.bytesize, message.instance_variable_get(:@msg), message.class.descriptor, pool_def, decoding_options, message.instance_variable_get(:@arena), status)
case result
when Google::Protobuf::FFI::Upb_JsonDecodeResult_Error
raise ParseError.new "Error occurred during parsing: #{Google::Protobuf::FFI.error_message(status)}"
end
message
end
def self.encode_json(message, options = {})
encoding_options = 0
unless options.is_a? Hash
if options.respond_to? :to_h
options = options.to_h
else
#TODO can this error message be improve to include what was received?
raise ArgumentError.new "Expected hash arguments"
end
end
if options[:preserve_proto_fieldnames]
encoding_options |= Google::Protobuf::FFI::Upb_JsonEncode_UseProtoNames
end
if options[:emit_defaults]
encoding_options |= Google::Protobuf::FFI::Upb_JsonEncode_EmitDefaults
end
if options[:format_enums_as_integers]
encoding_options |= Google::Protobuf::FFI::Upb_JsonEncode_FormatEnumsAsIntegers
end
buffer_size = 1024
buffer = ::FFI::MemoryPointer.new(:char, buffer_size)
status = Google::Protobuf::FFI::Status.new
msg = message.instance_variable_get(:@msg)
pool_def = message.class.descriptor.instance_variable_get(:@descriptor_pool).descriptor_pool
size = Google::Protobuf::FFI::json_encode_message(msg, message.class.descriptor, pool_def, encoding_options, buffer, buffer_size, status)
unless status[:ok]
raise ParseError.new "Error occurred during encoding: #{Google::Protobuf::FFI.error_message(status)}"
end
if size >= buffer_size
buffer_size = size + 1
buffer = ::FFI::MemoryPointer.new(:char, buffer_size)
status.clear
size = Google::Protobuf::FFI::json_encode_message(msg, message.class.descriptor, pool_def, encoding_options, buffer, buffer_size, status)
unless status[:ok]
raise ParseError.new "Error occurred during encoding: #{Google::Protobuf::FFI.error_message(status)}"
end
if size >= buffer_size
raise ParseError.new "Inconsistent JSON encoding sizes - was #{buffer_size - 1}, now #{size}"
end
end
buffer.read_string_length(size).force_encoding("UTF-8").freeze
end
private
# Implementation details below are subject to breaking changes without
# warning and are intended for use only within the gem.
include Google::Protobuf::Internal::Convert
##
# Checks ObjectCache for a sentinel empty frozen Map of the key and
# value types matching the field descriptor's MessageDef and returns
# the cache entry. If an entry is not found, one is created and added
# to the cache keyed by the MessageDef pointer first.
# @param field_descriptor [FieldDescriptor] Field to retrieve.
def empty_frozen_map(field_descriptor)
sub_message_def = Google::Protobuf::FFI.get_subtype_as_message field_descriptor
frozen_map = OBJECT_CACHE.get sub_message_def
if frozen_map.nil?
frozen_map = Google::Protobuf::Map.send(:construct_for_field, field_descriptor)
OBJECT_CACHE.try_add(sub_message_def, frozen_map.freeze)
end
raise "Empty Frozen Map is not frozen" unless frozen_map.frozen?
frozen_map
end
##
# Returns a frozen Map instance for the given field. If the message
# already has a value for that field, it is used. If not, a sentinel
# (per FieldDescriptor) empty frozen Map is returned instead.
# @param field_descriptor [FieldDescriptor] Field to retrieve.
def frozen_map_from_field_descriptor(field_descriptor)
message_value = Google::Protobuf::FFI.get_message_value @msg, field_descriptor
return empty_frozen_map field_descriptor if message_value[:map_val].null?
get_map_field(message_value[:map_val], field_descriptor).freeze
end
##
# Returns a Map instance for the given field. If the message is frozen
# the return value is also frozen. If not, a mutable instance is
# returned instead.
# @param field_descriptor [FieldDescriptor] Field to retrieve.
def map_from_field_descriptor(field_descriptor)
return frozen_map_from_field_descriptor field_descriptor if frozen?
mutable_message_value = Google::Protobuf::FFI.get_mutable_message @msg, field_descriptor, @arena
get_map_field(mutable_message_value[:map], field_descriptor)
end
##
# Checks ObjectCache for a sentinel empty frozen RepeatedField of the
# value type matching the field descriptor's MessageDef and returns
# the cache entry. If an entry is not found, one is created and added
# to the cache keyed by the MessageDef pointer first.
# @param field_descriptor [FieldDescriptor] Field to retrieve.
def empty_frozen_repeated_field(field_descriptor)
sub_message_def = Google::Protobuf::FFI.get_subtype_as_message field_descriptor
frozen_repeated_field = OBJECT_CACHE.get sub_message_def
if frozen_repeated_field.nil?
frozen_repeated_field = Google::Protobuf::RepeatedField.send(:construct_for_field, field_descriptor)
OBJECT_CACHE.try_add(sub_message_def, frozen_repeated_field.freeze)
end
raise "Empty frozen RepeatedField is not frozen" unless frozen_repeated_field.frozen?
frozen_repeated_field
end
##
# Returns a frozen RepeatedField instance for the given field. If the
# message already has a value for that field, it is used. If not, a
# sentinel (per FieldDescriptor) empty frozen RepeatedField is
# returned instead.
# @param field_descriptor [FieldDescriptor] Field to retrieve.
def frozen_repeated_field_from_field_descriptor(field_descriptor)
message_value = Google::Protobuf::FFI.get_message_value @msg, field_descriptor
return empty_frozen_repeated_field field_descriptor if message_value[:array_val].null?
get_repeated_field(message_value[:array_val], field_descriptor).freeze
end
##
# Returns a RepeatedField instance for the given field. If the message
# is frozen the return value is also frozen. If not, a mutable
# instance is returned instead.
# @param field_descriptor [FieldDescriptor] Field to retrieve.
def repeated_field_from_field_descriptor(field_descriptor)
return frozen_repeated_field_from_field_descriptor field_descriptor if frozen?
mutable_message_value = Google::Protobuf::FFI.get_mutable_message @msg, field_descriptor, @arena
get_repeated_field(mutable_message_value[:array], field_descriptor)
end
##
# Returns a Message instance for the given field. If the message
# is frozen nil is always returned. Otherwise, a mutable instance is
# returned instead.
# @param field_descriptor [FieldDescriptor] Field to retrieve.
def message_from_field_descriptor(field_descriptor)
return nil if frozen?
return nil unless Google::Protobuf::FFI.get_message_has @msg, field_descriptor
mutable_message = Google::Protobuf::FFI.get_mutable_message @msg, field_descriptor, @arena
sub_message = mutable_message[:msg]
sub_message_def = Google::Protobuf::FFI.get_subtype_as_message field_descriptor
Descriptor.send(:get_message, sub_message, sub_message_def, @arena)
end
##
# Returns a scalar value for the given field. If the message
# is frozen the return value is also frozen.
# @param field_descriptor [FieldDescriptor] Field to retrieve.
def scalar_from_field_descriptor(field_descriptor)
c_type = field_descriptor.send(:c_type)
message_value = Google::Protobuf::FFI.get_message_value @msg, field_descriptor
msg_or_enum_def = c_type == :enum ? Google::Protobuf::FFI.get_subtype_as_enum(field_descriptor) : nil
return_value = convert_upb_to_ruby message_value, c_type, msg_or_enum_def
frozen? ? return_value.freeze : return_value
end
##
# Dynamically define accessors methods for every field of @descriptor.
# Methods with names that conflict with existing methods are skipped.
def self.setup_accessors!
@descriptor.each do |field_descriptor|
field_name = field_descriptor.name
unless instance_methods(true).include?(field_name.to_sym)
# Dispatching to either index_internal or get_field is logically
# correct, but slightly slower due to having to perform extra
# lookups on each invocation rather than doing it once here.
if field_descriptor.map?
define_method(field_name) do
map_from_field_descriptor field_descriptor
end
elsif field_descriptor.repeated?
define_method(field_name) do
repeated_field_from_field_descriptor field_descriptor
end
elsif field_descriptor.sub_message?
define_method(field_name) do
message_from_field_descriptor field_descriptor
end
else
define_method(field_name) do
scalar_from_field_descriptor field_descriptor
end
end
define_method("#{field_name}=") do |value|
index_assign_internal(value, field_descriptor: field_descriptor)
end
define_method("clear_#{field_name}") do
clear_internal(field_descriptor)
end
if field_descriptor.type == :enum
if field_descriptor.repeated?
define_method("#{field_name}_const") do
return_value = []
repeated_field_from_field_descriptor(field_descriptor).send(:each_msg_val) do |msg_val|
return_value << msg_val[:int32_val]
end
return_value
end
else
define_method("#{field_name}_const") do
message_value = Google::Protobuf::FFI.get_message_value @msg, field_descriptor
message_value[:int32_val]
end
end
end
if !field_descriptor.repeated? and field_descriptor.wrapper?
define_method("#{field_name}_as_value") do
get_field(field_descriptor, unwrap: true)
end
define_method("#{field_name}_as_value=") do |value|
if value.nil?
clear_internal(field_descriptor)
else
index_assign_internal(value, field_descriptor: field_descriptor, wrap: true)
end
end
end
if field_descriptor.has_presence?
define_method("has_#{field_name}?") do
Google::Protobuf::FFI.get_message_has(@msg, field_descriptor)
end
end
end
end
end
##
# Dynamically define accessors methods for every OneOf field of
# @descriptor.
def self.setup_oneof_accessors!
@oneof_field_names = []
@descriptor.each_oneof do |oneof_descriptor|
self.add_oneof_accessors_for! oneof_descriptor
end
end
##
# Dynamically define accessors methods for the given OneOf field.
# Methods with names that conflict with existing methods are skipped.
# @param oneof_descriptor [OneofDescriptor] Field to create accessors
# for.
def self.add_oneof_accessors_for!(oneof_descriptor)
field_name = oneof_descriptor.name.to_sym
@oneof_field_names << field_name
unless instance_methods(true).include?(field_name)
define_method(field_name) do
field_descriptor = Google::Protobuf::FFI.get_message_which_oneof(@msg, oneof_descriptor)
if field_descriptor.nil?
return
else
return field_descriptor.name.to_sym
end
end
define_method("clear_#{field_name}") do
field_descriptor = Google::Protobuf::FFI.get_message_which_oneof(@msg, oneof_descriptor)
unless field_descriptor.nil?
clear_internal(field_descriptor)
end
end
define_method("has_#{field_name}?") do
!Google::Protobuf::FFI.get_message_which_oneof(@msg, oneof_descriptor).nil?
end
end
end
setup_accessors!
setup_oneof_accessors!
def self.private_constructor(arena, msg: nil, initial_value: nil)
instance = allocate
instance.send(:initialize, initial_value, arena, msg)
instance
end
def self.inspect_field(field_descriptor, c_type, message_value)
if field_descriptor.sub_message?
sub_msg_descriptor = Google::Protobuf::FFI.get_subtype_as_message(field_descriptor)
sub_msg_descriptor.msgclass.send(:inspect_internal, message_value[:msg_val])
else
convert_upb_to_ruby(message_value, c_type, field_descriptor.subtype).inspect
end
end
# @param msg [::FFI::Pointer] Pointer to the Message
def self.inspect_internal(msg)
field_output = []
descriptor.each do |field_descriptor|
next if field_descriptor.has_presence? && !Google::Protobuf::FFI.get_message_has(msg, field_descriptor)
if field_descriptor.map?
# TODO Adapted - from map#each_msg_val and map#inspect- can this be refactored to reduce echo without introducing a arena allocation?
message_descriptor = field_descriptor.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)
message_value = Google::Protobuf::FFI.get_message_value(msg, field_descriptor)
iter = ::FFI::MemoryPointer.new(:size_t, 1)
iter.write(:size_t, Google::Protobuf::FFI::Upb_Map_Begin)
key_value_pairs = []
while Google::Protobuf::FFI.map_next(message_value[:map_val], iter) do
iter_size_t = iter.read(:size_t)
key_message_value = Google::Protobuf::FFI.map_key(message_value[:map_val], iter_size_t)
value_message_value = Google::Protobuf::FFI.map_value(message_value[:map_val], iter_size_t)
key_string = convert_upb_to_ruby(key_message_value, key_field_type).inspect
value_string = inspect_field(value_field_def, value_field_type, value_message_value)
key_value_pairs << "#{key_string}=>#{value_string}"
end
field_output << "#{field_descriptor.name}: {#{key_value_pairs.join(", ")}}"
elsif field_descriptor.repeated?
# TODO Adapted - from repeated_field#each - can this be refactored to reduce echo?
repeated_field_output = []
message_value = Google::Protobuf::FFI.get_message_value(msg, field_descriptor)
array = message_value[:array_val]
n = array.null? ? 0 : Google::Protobuf::FFI.array_size(array)
0.upto(n - 1) do |i|
element = Google::Protobuf::FFI.get_msgval_at(array, i)
repeated_field_output << inspect_field(field_descriptor, field_descriptor.send(:c_type), element)
end
field_output << "#{field_descriptor.name}: [#{repeated_field_output.join(", ")}]"
else
message_value = Google::Protobuf::FFI.get_message_value msg, field_descriptor
rendered_value = inspect_field(field_descriptor, field_descriptor.send(:c_type), message_value)
field_output << "#{field_descriptor.name}: #{rendered_value}"
end
end
"<#{name}: #{field_output.join(', ')}>"
end
def self.deep_copy(msg, arena = nil)
arena ||= Google::Protobuf::FFI.create_arena
encode_internal(msg) do |encoding, size, mini_table_ptr|
message = private_constructor(arena)
if encoding.nil? or encoding.null? or Google::Protobuf::FFI.decode_message(encoding, size, message.instance_variable_get(:@msg), mini_table_ptr, nil, 0, arena) != :Ok
raise ParseError.new "Error occurred copying proto"
end
message
end
end
def self.encode_internal(msg, encoding_options = 0)
temporary_arena = Google::Protobuf::FFI.create_arena
mini_table_ptr = Google::Protobuf::FFI.get_mini_table(descriptor)
size_ptr = ::FFI::MemoryPointer.new(:size_t, 1)
pointer_ptr = ::FFI::MemoryPointer.new(:pointer, 1)
encoding_status = Google::Protobuf::FFI.encode_message(msg, mini_table_ptr, encoding_options, temporary_arena, pointer_ptr.to_ptr, size_ptr)
raise "Encoding failed due to #{encoding_status}" unless encoding_status == :Ok
yield pointer_ptr.read(:pointer), size_ptr.read(:size_t), mini_table_ptr
end
def method_missing_internal(method_name, *args, mode: nil)
raise ArgumentError.new "method_missing_internal called with invalid mode #{mode.inspect}" unless [:respond_to_missing?, :method_missing].include? mode
#TODO not being allowed is not the same thing as not responding, but this is needed to pass tests
if method_name.to_s.end_with? '='
if self.class.send(:oneof_field_names).include? method_name.to_s[0..-2].to_sym
return false if mode == :respond_to_missing?
raise RuntimeError.new "Oneof accessors are read-only."
end
end
original_method_missing(method_name, *args) if mode == :method_missing
end
def clear_internal(field_def)
raise FrozenError.new "can't modify frozen #{self.class}" if frozen?
Google::Protobuf::FFI.clear_message_field(@msg, field_def)
end
# Accessor for field by name. Does not delegate to methods setup by
# self.setup_accessors! in order to avoid conflicts with bad field
# names e.g. `dup` or `class` which are perfectly valid for proto
# fields.
def index_internal(name)
field_descriptor = self.class.descriptor.lookup(name)
get_field field_descriptor unless field_descriptor.nil?
end
#TODO - well known types keeps us on our toes by overloading methods.
# How much of the public API needs to be defended?
def index_assign_internal(value, name: nil, field_descriptor: nil, wrap: false)
raise FrozenError.new "can't modify frozen #{self.class}" if frozen?
if field_descriptor.nil?
field_descriptor = self.class.descriptor.lookup(name)
if field_descriptor.nil?
raise ArgumentError.new "Unknown field: #{name}"
end
end
unless field_descriptor.send :set_value_on_message, value, @msg, @arena, wrap: wrap
raise RuntimeError.new "allocation failed"
end
end
##
# @param initial_value [Object] initial value of this Message
# @param arena [Arena] Optional; Arena where this message will be allocated
# @param msg [::FFI::Pointer] Optional; Message to initialize; creates
# one if omitted or nil.
def initialize(initial_value = nil, arena = nil, msg = nil)
@arena = arena || Google::Protobuf::FFI.create_arena
@msg = msg || Google::Protobuf::FFI.new_message_from_def(Google::Protobuf::FFI.get_mini_table(self.class.descriptor), @arena)
unless initial_value.nil?
raise ArgumentError.new "Expected hash arguments or message, not #{initial_value.class}" unless initial_value.respond_to? :each
field_def_ptr = ::FFI::MemoryPointer.new :pointer
oneof_def_ptr = ::FFI::MemoryPointer.new :pointer
initial_value.each do |key, value|
raise ArgumentError.new "Expected string or symbols as hash keys when initializing proto from hash." unless [String, Symbol].include? key.class
unless Google::Protobuf::FFI.find_msg_def_by_name self.class.descriptor, key.to_s, key.to_s.bytesize, field_def_ptr, oneof_def_ptr
raise ArgumentError.new "Unknown field name '#{key}' in initialization map entry."
end
raise NotImplementedError.new "Haven't added oneofsupport yet" unless oneof_def_ptr.get_pointer(0).null?
raise NotImplementedError.new "Expected a field def" if field_def_ptr.get_pointer(0).null?
field_descriptor = FieldDescriptor.from_native field_def_ptr.get_pointer(0)
next if value.nil?
if field_descriptor.map?
index_assign_internal(Google::Protobuf::Map.send(:construct_for_field, field_descriptor, arena: @arena, value: value), name: key.to_s)
elsif field_descriptor.repeated?
index_assign_internal(RepeatedField.send(:construct_for_field, field_descriptor, arena: @arena, values: value), name: key.to_s)
else
index_assign_internal(value, name: key.to_s)
end
end
end
# Should always be the last expression of the initializer to avoid
# leaking references to this object before construction is complete.
Google::Protobuf::OBJECT_CACHE.try_add @msg.address, self
end
##
# Gets a field of this message identified by FieldDescriptor.
#
# @param field [FieldDescriptor] Field to retrieve.
# @param unwrap [Boolean](false) If true, unwraps wrappers.
def get_field(field, unwrap: false)
if field.map?
map_from_field_descriptor field
elsif field.repeated?
repeated_field_from_field_descriptor field
elsif field.sub_message?
return nil unless Google::Protobuf::FFI.get_message_has @msg, field
if unwrap
if field.has?(self)
sub_message_def = Google::Protobuf::FFI.get_subtype_as_message field
wrapper_message_value = Google::Protobuf::FFI.get_message_value @msg, field
fields = Google::Protobuf::FFI.field_count(sub_message_def)
raise "Sub message has #{fields} fields! Expected exactly 1." unless fields == 1
value_field_def = Google::Protobuf::FFI.get_field_by_number sub_message_def, 1
message_value = Google::Protobuf::FFI.get_message_value wrapper_message_value[:msg_val], value_field_def
convert_upb_to_ruby message_value, Google::Protobuf::FFI.get_c_type(value_field_def)
else
nil
end
else
message_from_field_descriptor field
end
else
scalar_from_field_descriptor field
end
end
##
# Gets a RepeatedField from the ObjectCache or creates a new one.
# @param array [::FFI::Pointer] Pointer to the upb_Array
# @param field [Google::Protobuf::FieldDescriptor] Type of the repeated field
def get_repeated_field(array, field)
return nil if array.nil? or array.null?
repeated_field = OBJECT_CACHE.get(array.address)
if repeated_field.nil?
repeated_field = RepeatedField.send(:construct_for_field, field, arena: @arena, array: array)
repeated_field.freeze if frozen?
end
repeated_field
end
##
# Gets a Map from the ObjectCache or creates a new one.
# @param map [::FFI::Pointer] Pointer to the upb_Map
# @param field [Google::Protobuf::FieldDescriptor] Type of the map field
def get_map_field(map, field)
return nil if map.nil? or map.null?
map_field = OBJECT_CACHE.get(map.address)
if map_field.nil?
map_field = Google::Protobuf::Map.send(:construct_for_field, field, arena: @arena, map: map)
map_field.freeze if frozen?
end
map_field
end
end
end
end
end
end