diff --git a/.gitignore b/.gitignore index f25b4f7cdf..37d7aebc74 100644 --- a/.gitignore +++ b/.gitignore @@ -173,6 +173,7 @@ ruby/tests/test_import_pb.rb ruby/tests/test_ruby_package_pb.rb ruby/tests/generated_code_proto2_pb.rb ruby/tests/multi_level_nesting_test_pb.rb +ruby/tests/service_test_pb.rb ruby/tests/test_import_proto2_pb.rb ruby/tests/test_ruby_package_proto2_pb.rb ruby/compatibility_tests/v3.0.0/protoc diff --git a/ruby/Rakefile b/ruby/Rakefile index 6d842917c3..43b181b529 100644 --- a/ruby/Rakefile +++ b/ruby/Rakefile @@ -27,6 +27,7 @@ test_protos = %w[ tests/generated_code_proto2.proto tests/multi_level_nesting_test.proto tests/repeated_field_test.proto + tests/service_test.proto tests/stress.proto tests/test_import.proto tests/test_import_proto2.proto diff --git a/ruby/ext/google/protobuf_c/defs.c b/ruby/ext/google/protobuf_c/defs.c index 9141c7bd08..69eeacae31 100644 --- a/ruby/ext/google/protobuf_c/defs.c +++ b/ruby/ext/google/protobuf_c/defs.c @@ -23,6 +23,8 @@ static VALUE get_enumdef_obj(VALUE descriptor_pool, const upb_EnumDef* def); static VALUE get_fielddef_obj(VALUE descriptor_pool, const upb_FieldDef* def); static VALUE get_filedef_obj(VALUE descriptor_pool, const upb_FileDef* def); static VALUE get_oneofdef_obj(VALUE descriptor_pool, const upb_OneofDef* def); +static VALUE get_servicedef_obj(VALUE descriptor_pool, const upb_ServiceDef* def); +static VALUE get_methoddef_obj(VALUE descriptor_pool, const upb_MethodDef* def); // A distinct object that is not accessible from Ruby. We use this as a // constructor argument to enforce that certain objects cannot be created from @@ -153,6 +155,7 @@ static VALUE DescriptorPool_lookup(VALUE _self, VALUE name) { const upb_MessageDef* msgdef; const upb_EnumDef* enumdef; const upb_FieldDef* fielddef; + const upb_ServiceDef* servicedef; msgdef = upb_DefPool_FindMessageByName(self->symtab, name_str); if (msgdef) { @@ -169,6 +172,11 @@ static VALUE DescriptorPool_lookup(VALUE _self, VALUE name) { return get_enumdef_obj(_self, enumdef); } + servicedef = upb_DefPool_FindServiceByName(self->symtab, name_str); + if (servicedef) { + return get_servicedef_obj(_self, servicedef); + } + return Qnil; } @@ -1294,6 +1302,296 @@ static void EnumDescriptor_register(VALUE module) { cEnumDescriptor = klass; } +// ----------------------------------------------------------------------------- +// ServiceDescriptor +// ----------------------------------------------------------------------------- + +typedef struct { + const upb_ServiceDef* servicedef; + // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() + // macro to update VALUE references, as to trigger write barriers. + VALUE module; // begins as nil + VALUE descriptor_pool; // Owns the upb_ServiceDef. +} ServiceDescriptor; + +static VALUE cServiceDescriptor = Qnil; + +static void ServiceDescriptor_mark(void* _self) { + ServiceDescriptor* self = _self; + rb_gc_mark(self->module); + rb_gc_mark(self->descriptor_pool); +} + +static const rb_data_type_t ServiceDescriptor_type = { + "Google::Protobuf::ServicDescriptor", + {ServiceDescriptor_mark, RUBY_DEFAULT_FREE, NULL}, + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED, +}; + +static ServiceDescriptor* ruby_to_ServiceDescriptor(VALUE val) { + ServiceDescriptor* ret; + TypedData_Get_Struct(val, ServiceDescriptor, &ServiceDescriptor_type, ret); + return ret; +} + +static VALUE ServiceDescriptor_alloc(VALUE klass) { + ServiceDescriptor* self = ALLOC(ServiceDescriptor); + VALUE ret = TypedData_Wrap_Struct(klass, &ServiceDescriptor_type, self); + self->servicedef = NULL; + self->module = Qnil; + self->descriptor_pool = Qnil; + return ret; +} + +/* + * call-seq: + * ServiceDescriptor.new(c_only_cookie, ptr) => ServiceDescriptor + * + * Creates a descriptor wrapper object. May only be called from C. + */ +static VALUE ServiceDescriptor_initialize(VALUE _self, VALUE cookie, + VALUE descriptor_pool, VALUE ptr) { + ServiceDescriptor* self = ruby_to_ServiceDescriptor(_self); + + if (cookie != c_only_cookie) { + rb_raise(rb_eRuntimeError, + "Descriptor objects may not be created from Ruby."); + } + + RB_OBJ_WRITE(_self, &self->descriptor_pool, descriptor_pool); + self->servicedef = (const upb_ServiceDef*)NUM2ULL(ptr); + + return Qnil; +} + +/* + * call-seq: + * ServiceDescriptor.name => name + * + * Returns the name of this service. + */ +static VALUE ServiceDescriptor_name(VALUE _self) { + ServiceDescriptor* self = ruby_to_ServiceDescriptor(_self); + return rb_str_maybe_null(upb_ServiceDef_FullName(self->servicedef)); +} + +/* + * call-seq: + * ServiceDescriptor.file_descriptor + * + * Returns the FileDescriptor object this service belongs to. + */ +static VALUE ServiceDescriptor_file_descriptor(VALUE _self) { + ServiceDescriptor* self = ruby_to_ServiceDescriptor(_self); + return get_filedef_obj(self->descriptor_pool, + upb_ServiceDef_File(self->servicedef)); +} + + +/* + * call-seq: + * ServiceDescriptor.each(&block) + * + * Iterates over methods in this service, yielding to the block on each one. + */ +static VALUE ServiceDescriptor_each(VALUE _self) { + ServiceDescriptor* self = ruby_to_ServiceDescriptor(_self); + + int n = upb_ServiceDef_MethodCount(self->servicedef); + for (int i = 0; i < n; i++) { + const upb_MethodDef *method = upb_ServiceDef_Method(self->servicedef, i); + VALUE obj = get_methoddef_obj(self->descriptor_pool, method); + rb_yield(obj); + } + return Qnil; +} + +/* + * call-seq: + * ServiceDescriptor.options => options + * + * Returns the `ServiceOptions` for this `ServiceDescriptor`. + */ +static VALUE ServiceDescriptor_options(VALUE _self) { + ServiceDescriptor* self = ruby_to_ServiceDescriptor(_self); + const google_protobuf_ServiceOptions* opts = + upb_ServiceDef_Options(self->servicedef); + upb_Arena* arena = upb_Arena_New(); + size_t size; + char* serialized = + google_protobuf_ServiceOptions_serialize(opts, arena, &size); + VALUE service_options = decode_options(_self, "ServiceOptions", size, + serialized, self->descriptor_pool); + upb_Arena_Free(arena); + return service_options; +} + +static void ServiceDescriptor_register(VALUE module) { + VALUE klass = rb_define_class_under(module, "ServiceDescriptor", rb_cObject); + rb_define_alloc_func(klass, ServiceDescriptor_alloc); + rb_define_method(klass, "initialize", ServiceDescriptor_initialize, 3); + rb_define_method(klass, "name", ServiceDescriptor_name, 0); + rb_define_method(klass, "each", ServiceDescriptor_each, 0); + rb_define_method(klass, "file_descriptor", ServiceDescriptor_file_descriptor, 0); + rb_define_method(klass, "options", ServiceDescriptor_options, 0); + rb_include_module(klass, rb_mEnumerable); + rb_gc_register_address(&cServiceDescriptor); + cServiceDescriptor = klass; +} + +// ----------------------------------------------------------------------------- +// MethodDescriptor +// ----------------------------------------------------------------------------- + +typedef struct { + const upb_MethodDef* methoddef; + // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() + // macro to update VALUE references, as to trigger write barriers. + VALUE module; // begins as nil + VALUE descriptor_pool; // Owns the upb_MethodDef. +} MethodDescriptor; + +static VALUE cMethodDescriptor = Qnil; + +static void MethodDescriptor_mark(void* _self) { + MethodDescriptor* self = _self; + rb_gc_mark(self->module); + rb_gc_mark(self->descriptor_pool); +} + +static const rb_data_type_t MethodDescriptor_type = { + "Google::Protobuf::MethodDescriptor", + {MethodDescriptor_mark, RUBY_DEFAULT_FREE, NULL}, + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED, +}; + +static MethodDescriptor* ruby_to_MethodDescriptor(VALUE val) { + MethodDescriptor* ret; + TypedData_Get_Struct(val, MethodDescriptor, &MethodDescriptor_type, ret); + return ret; +} + +static VALUE MethodDescriptor_alloc(VALUE klass) { + MethodDescriptor* self = ALLOC(MethodDescriptor); + VALUE ret = TypedData_Wrap_Struct(klass, &MethodDescriptor_type, self); + self->methoddef = NULL; + self->module = Qnil; + self->descriptor_pool = Qnil; + return ret; +} + +/* + * call-seq: + * MethodDescriptor.new(c_only_cookie, ptr) => MethodDescriptor + * + * Creates a descriptor wrapper object. May only be called from C. + */ +static VALUE MethodDescriptor_initialize(VALUE _self, VALUE cookie, + VALUE descriptor_pool, VALUE ptr) { + MethodDescriptor* self = ruby_to_MethodDescriptor(_self); + + if (cookie != c_only_cookie) { + rb_raise(rb_eRuntimeError, + "Descriptor objects may not be created from Ruby."); + } + + RB_OBJ_WRITE(_self, &self->descriptor_pool, descriptor_pool); + self->methoddef = (const upb_ServiceDef*)NUM2ULL(ptr); + + return Qnil; +} + +/* + * call-seq: + * MethodDescriptor.name => name + * + * Returns the name of this method + */ +static VALUE MethodDescriptor_name(VALUE _self) { + MethodDescriptor* self = ruby_to_MethodDescriptor(_self); + return rb_str_maybe_null(upb_MethodDef_Name(self->methoddef)); +} + +/* + * call-seq: + * MethodDescriptor.options => options + * + * Returns the `MethodOptions` for this `MethodDescriptor`. + */ +static VALUE MethodDescriptor_options(VALUE _self) { + MethodDescriptor* self = ruby_to_MethodDescriptor(_self); + const google_protobuf_MethodOptions* opts = + upb_MethodDef_Options(self->methoddef); + upb_Arena* arena = upb_Arena_New(); + size_t size; + char* serialized = + google_protobuf_MethodOptions_serialize(opts, arena, &size); + VALUE method_options = decode_options(_self, "MethodOptions", size, + serialized, self->descriptor_pool); + upb_Arena_Free(arena); + return method_options; +} + +/* + * call-seq: + * MethodDescriptor.input_type => Descriptor + * + * Returns the `Descriptor` for the request message type of this method + */ +static VALUE MethodDescriptor_input_type(VALUE _self) { + MethodDescriptor* self = ruby_to_MethodDescriptor(_self); + const upb_MessageDef* type = upb_MethodDef_InputType(self->methoddef); + return get_msgdef_obj(self->descriptor_pool, type); +} + +/* + * call-seq: + * MethodDescriptor.output_type => Descriptor + * + * Returns the `Descriptor` for the response message type of this method + */ +static VALUE MethodDescriptor_output_type(VALUE _self) { + MethodDescriptor* self = ruby_to_MethodDescriptor(_self); + const upb_MessageDef* type = upb_MethodDef_OutputType(self->methoddef); + return get_msgdef_obj(self->descriptor_pool, type); +} + +/* + * call-seq: + * MethodDescriptor.client_streaming => bool + * + * Returns whether or not this is a streaming request method + */ +static VALUE MethodDescriptor_client_streaming(VALUE _self) { + MethodDescriptor* self = ruby_to_MethodDescriptor(_self); + return upb_MethodDef_ClientStreaming(self->methoddef) ? Qtrue : Qfalse; +} + +/* + * call-seq: + * MethodDescriptor.server_streaming => bool + * + * Returns whether or not this is a streaming response method + */ +static VALUE MethodDescriptor_server_streaming(VALUE _self) { + MethodDescriptor* self = ruby_to_MethodDescriptor(_self); + return upb_MethodDef_ServerStreaming(self->methoddef) ? Qtrue : Qfalse; +} + +static void MethodDescriptor_register(VALUE module) { + VALUE klass = rb_define_class_under(module, "MethodDescriptor", rb_cObject); + rb_define_alloc_func(klass, MethodDescriptor_alloc); + rb_define_method(klass, "initialize", MethodDescriptor_initialize, 3); + rb_define_method(klass, "name", MethodDescriptor_name, 0); + rb_define_method(klass, "options", MethodDescriptor_options, 0); + rb_define_method(klass, "input_type", MethodDescriptor_input_type, 0); + rb_define_method(klass, "output_type", MethodDescriptor_output_type, 0); + rb_define_method(klass, "client_streaming", MethodDescriptor_client_streaming, 0); + rb_define_method(klass, "server_streaming", MethodDescriptor_server_streaming, 0); + rb_gc_register_address(&cMethodDescriptor); + cMethodDescriptor = klass; +} + static VALUE get_def_obj(VALUE _descriptor_pool, const void* ptr, VALUE klass) { DescriptorPool* descriptor_pool = ruby_to_DescriptorPool(_descriptor_pool); VALUE key = ULL2NUM((intptr_t)ptr); @@ -1335,6 +1633,14 @@ static VALUE get_oneofdef_obj(VALUE descriptor_pool, const upb_OneofDef* def) { return get_def_obj(descriptor_pool, def, cOneofDescriptor); } +static VALUE get_servicedef_obj(VALUE descriptor_pool, const upb_ServiceDef* def) { + return get_def_obj(descriptor_pool, def, cServiceDescriptor); +} + +static VALUE get_methoddef_obj(VALUE descriptor_pool, const upb_MethodDef* def) { + return get_def_obj(descriptor_pool, def, cMethodDescriptor); +} + // ----------------------------------------------------------------------------- // Shared functions // ----------------------------------------------------------------------------- @@ -1410,6 +1716,8 @@ void Defs_register(VALUE module) { FieldDescriptor_register(module); OneofDescriptor_register(module); EnumDescriptor_register(module); + ServiceDescriptor_register(module); + MethodDescriptor_register(module); rb_gc_register_address(&c_only_cookie); c_only_cookie = rb_class_new_instance(0, NULL, rb_cObject); diff --git a/ruby/ext/google/protobuf_c/glue.c b/ruby/ext/google/protobuf_c/glue.c index e51e364279..831ba91907 100644 --- a/ruby/ext/google/protobuf_c/glue.c +++ b/ruby/ext/google/protobuf_c/glue.c @@ -54,3 +54,17 @@ char* FieldDescriptor_serialized_options(const upb_FieldDef* fielddef, char* serialized = google_protobuf_FieldOptions_serialize(opts, arena, size); return serialized; } + +char *ServiceDescriptor_serialized_options(const upb_ServiceDef* servicedef, + size_t* size, upb_Arena* arena) { + const google_protobuf_ServiceOptions* opts = upb_ServiceDef_Options(servicedef); + char* serialized = google_protobuf_ServiceOptions_serialize(opts, arena, size); + return serialized; +} + +char *MethodDescriptor_serialized_options(const upb_MethodDef* methoddef, + size_t* size, upb_Arena* arena) { + const google_protobuf_MethodOptions* opts = upb_MethodDef_Options(methoddef); + char* serialized = google_protobuf_MethodOptions_serialize(opts, arena, size); + return serialized; +} diff --git a/ruby/lib/google/protobuf/ffi/descriptor_pool.rb b/ruby/lib/google/protobuf/ffi/descriptor_pool.rb index 96c7d09417..4dc177d582 100644 --- a/ruby/lib/google/protobuf/ffi/descriptor_pool.rb +++ b/ruby/lib/google/protobuf/ffi/descriptor_pool.rb @@ -16,6 +16,7 @@ module Google attach_function :lookup_enum, :upb_DefPool_FindEnumByName, [:DefPool, :string], EnumDescriptor attach_function :lookup_extension, :upb_DefPool_FindExtensionByName,[:DefPool, :string], FieldDescriptor attach_function :lookup_msg, :upb_DefPool_FindMessageByName, [:DefPool, :string], Descriptor + attach_function :lookup_service, :upb_DefPool_FindServiceByName, [:DefPool, :string], ServiceDescriptor # FileDescriptorProto attach_function :parse, :FileDescriptorProto_parse, [:binary_string, :size_t, Internal::Arena], :FileDescriptorProto @@ -54,7 +55,8 @@ module Google def lookup name Google::Protobuf::FFI.lookup_msg(@descriptor_pool, name) || Google::Protobuf::FFI.lookup_enum(@descriptor_pool, name) || - Google::Protobuf::FFI.lookup_extension(@descriptor_pool, name) + Google::Protobuf::FFI.lookup_extension(@descriptor_pool, name) || + Google::Protobuf::FFI.lookup_service(@descriptor_pool, name) end def self.generated_pool diff --git a/ruby/lib/google/protobuf/ffi/method_descriptor.rb b/ruby/lib/google/protobuf/ffi/method_descriptor.rb new file mode 100644 index 0000000000..b7bbf0af08 --- /dev/null +++ b/ruby/lib/google/protobuf/ffi/method_descriptor.rb @@ -0,0 +1,114 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2024 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 + +module Google + module Protobuf + class MethodDescriptor + attr :method_def, :descriptor_pool + + include Google::Protobuf::Internal::Convert + + # FFI Interface methods and setup + extend ::FFI::DataConverter + native_type ::FFI::Type::POINTER + + class << self + prepend Google::Protobuf::Internal::TypeSafety + include Google::Protobuf::Internal::PointerHelper + + # @param value [MethodDescriptor] MethodDescriptor to convert to an FFI native type + # @param _ [Object] Unused + def to_native(value, _) + method_def_ptr = value.nil? ? nil : value.instance_variable_get(:@method_def) + return ::FFI::Pointer::NULL if method_def_ptr.nil? + raise "Underlying method_def was null!" if method_def_ptr.null? + method_def_ptr + end + + ## + # @param service_def [::FFI::Pointer] MethodDef pointer to be wrapped + # @param _ [Object] Unused + def from_native(method_def, _ = nil) + return nil if method_def.nil? or method_def.null? + service_def = Google::Protobuf::FFI.raw_service_def_by_raw_method_def(method_def) + file_def = Google::Protobuf::FFI.file_def_by_raw_service_def(service_def) + descriptor_from_file_def(file_def, method_def) + end + end + + def self.new(*arguments, &block) + raise "Descriptor objects may not be created from Ruby." + end + + def to_s + inspect + end + + def inspect + "#{self.class.name}: #{name}" + end + + def name + @name ||= Google::Protobuf::FFI.get_method_name(self) + end + + def options + @options ||= begin + size_ptr = ::FFI::MemoryPointer.new(:size_t, 1) + temporary_arena = Google::Protobuf::FFI.create_arena + buffer = Google::Protobuf::FFI.method_options(self, size_ptr, temporary_arena) + Google::Protobuf::MethodOptions.decode(buffer.read_string_length(size_ptr.read(:size_t)).force_encoding("ASCII-8BIT").freeze).freeze + end + end + + def input_type + @input_type ||= Google::Protobuf::FFI.method_input_type(self) + end + + def output_type + @output_type ||= Google::Protobuf::FFI.method_output_type(self) + end + + def client_streaming + @client_streaming ||= Google::Protobuf::FFI.method_client_streaming(self) + end + + def server_streaming + @server_streaming ||= Google::Protobuf::FFI.method_server_streaming(self) + end + + private + + def initialize(method_def, descriptor_pool) + @method_def = method_def + @descriptor_pool = descriptor_pool + end + + def self.private_constructor(method_def, descriptor_pool) + instance = allocate + instance.send(:initialize, method_def, descriptor_pool) + instance + end + + def c_type + @c_type ||= Google::Protobuf::FFI.get_c_type(self) + end + end + + class FFI + # MethodDef + attach_function :raw_service_def_by_raw_method_def, :upb_MethodDef_Service, [:pointer], :pointer + attach_function :get_method_name, :upb_MethodDef_Name, [MethodDescriptor], :string + attach_function :method_options, :MethodDescriptor_serialized_options, [MethodDescriptor, :pointer, Internal::Arena], :pointer + attach_function :method_input_type, :upb_MethodDef_InputType, [MethodDescriptor], Descriptor + attach_function :method_output_type, :upb_MethodDef_OutputType, [MethodDescriptor], Descriptor + attach_function :method_client_streaming, :upb_MethodDef_ClientStreaming, [MethodDescriptor], :bool + attach_function :method_server_streaming, :upb_MethodDef_ServerStreaming, [MethodDescriptor], :bool + end + end +end + diff --git a/ruby/lib/google/protobuf/ffi/service_descriptor.rb b/ruby/lib/google/protobuf/ffi/service_descriptor.rb new file mode 100644 index 0000000000..d8554d379d --- /dev/null +++ b/ruby/lib/google/protobuf/ffi/service_descriptor.rb @@ -0,0 +1,107 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2024 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 + +module Google + module Protobuf + class ServiceDescriptor + attr :service_def, :descriptor_pool + include Enumerable + + include Google::Protobuf::Internal::Convert + + # FFI Interface methods and setup + extend ::FFI::DataConverter + native_type ::FFI::Type::POINTER + + class << self + prepend Google::Protobuf::Internal::TypeSafety + include Google::Protobuf::Internal::PointerHelper + + # @param value [ServiceDescriptor] ServiceDescriptor to convert to an FFI native type + # @param _ [Object] Unused + def to_native(value, _) + service_def_ptr = value.nil? ? nil : value.instance_variable_get(:@service_def) + return ::FFI::Pointer::NULL if service_def_ptr.nil? + raise "Underlying service_def was null!" if service_def_ptr.null? + service_def_ptr + end + + ## + # @param service_def [::FFI::Pointer] ServiceDef pointer to be wrapped + # @param _ [Object] Unused + def from_native(service_def, _ = nil) + return nil if service_def.nil? or service_def.null? + file_def = Google::Protobuf::FFI.file_def_by_raw_service_def(service_def) + descriptor_from_file_def(file_def, service_def) + end + end + + def self.new(*arguments, &block) + raise "Descriptor objects may not be created from Ruby." + end + + def to_s + inspect + end + + def inspect + "#{self.class.name}: #{name}" + end + + def name + @name ||= Google::Protobuf::FFI.get_service_full_name(self) + end + + def file_descriptor + @descriptor_pool.send(:get_file_descriptor, Google::Protobuf::FFI.file_def_by_raw_service_def(@service_def)) + end + + def each &block + n = Google::Protobuf::FFI.method_count(self) + 0.upto(n-1) do |i| + yield(Google::Protobuf::FFI.get_method_by_index(self, i)) + end + nil + end + + def options + @options ||= begin + size_ptr = ::FFI::MemoryPointer.new(:size_t, 1) + temporary_arena = Google::Protobuf::FFI.create_arena + buffer = Google::Protobuf::FFI.service_options(self, size_ptr, temporary_arena) + Google::Protobuf::ServiceOptions.decode(buffer.read_string_length(size_ptr.read(:size_t)).force_encoding("ASCII-8BIT").freeze).freeze + end + end + + private + + def initialize(service_def, descriptor_pool) + @service_def = service_def + @descriptor_pool = descriptor_pool + end + + def self.private_constructor(service_def, descriptor_pool) + instance = allocate + instance.send(:initialize, service_def, descriptor_pool) + instance + end + + def c_type + @c_type ||= Google::Protobuf::FFI.get_c_type(self) + end + end + + class FFI + # ServiceDef + attach_function :file_def_by_raw_service_def, :upb_ServiceDef_File, [:pointer], :FileDef + attach_function :get_service_full_name, :upb_ServiceDef_FullName, [ServiceDescriptor], :string + attach_function :method_count, :upb_ServiceDef_MethodCount, [ServiceDescriptor], :int + attach_function :get_method_by_index, :upb_ServiceDef_Method, [ServiceDescriptor, :int], MethodDescriptor + attach_function :service_options, :ServiceDescriptor_serialized_options, [ServiceDescriptor, :pointer, Internal::Arena], :pointer + end + end +end diff --git a/ruby/lib/google/protobuf_ffi.rb b/ruby/lib/google/protobuf_ffi.rb index 0839b36d3f..6580df49a1 100644 --- a/ruby/lib/google/protobuf_ffi.rb +++ b/ruby/lib/google/protobuf_ffi.rb @@ -15,6 +15,8 @@ require 'google/protobuf/ffi/descriptor' require 'google/protobuf/ffi/enum_descriptor' require 'google/protobuf/ffi/field_descriptor' require 'google/protobuf/ffi/oneof_descriptor' +require 'google/protobuf/ffi/method_descriptor' +require 'google/protobuf/ffi/service_descriptor' require 'google/protobuf/ffi/descriptor_pool' require 'google/protobuf/ffi/file_descriptor' require 'google/protobuf/ffi/map' diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyDescriptorPool.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyDescriptorPool.java index 7c01eb9270..43912ae804 100644 --- a/ruby/src/main/java/com/google/protobuf/jruby/RubyDescriptorPool.java +++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyDescriptorPool.java @@ -38,6 +38,8 @@ import com.google.protobuf.Descriptors.DescriptorValidationException; import com.google.protobuf.Descriptors.EnumDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.Descriptors.ServiceDescriptor; +import com.google.protobuf.Descriptors.MethodDescriptor; import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.InvalidProtocolBufferException; import java.util.ArrayList; @@ -73,6 +75,8 @@ public class RubyDescriptorPool extends RubyObject { cDescriptor = (RubyClass) runtime.getClassFromPath("Google::Protobuf::Descriptor"); cEnumDescriptor = (RubyClass) runtime.getClassFromPath("Google::Protobuf::EnumDescriptor"); cFieldDescriptor = (RubyClass) runtime.getClassFromPath("Google::Protobuf::FieldDescriptor"); + cServiceDescriptor = (RubyClass) runtime.getClassFromPath("Google::Protobuf::ServiceDescriptor"); + cMethodDescriptor = (RubyClass) runtime.getClassFromPath("Google::Protobuf::MethodDescriptor"); } public RubyDescriptorPool(Ruby runtime, RubyClass klazz) { @@ -156,6 +160,8 @@ public class RubyDescriptorPool extends RubyObject { registerDescriptor(context, message, packageName); for (FieldDescriptor fieldDescriptor : fd.getExtensions()) registerExtension(context, fieldDescriptor, packageName); + for (ServiceDescriptor serviceDescriptor : fd.getServices()) + registerService(context, serviceDescriptor, packageName); // Mark this as a loaded file fileDescriptors.add(fd); @@ -206,6 +212,18 @@ public class RubyDescriptorPool extends RubyObject { symtab.put(name, des); } + private void registerService( + ThreadContext context, ServiceDescriptor descriptor, String parentPath) { + String fullName = parentPath + descriptor.getName(); + RubyString name = context.runtime.newString(fullName); + RubyServiceDescriptor des = + (RubyServiceDescriptor) cServiceDescriptor.newInstance(context, Block.NULL_BLOCK); + des.setName(name); + // n.b. this will also construct the descriptors for the service's methods. + des.setDescriptor(context, descriptor, this); + symtab.putIfAbsent(name, des); + } + private FileDescriptor[] existingFileDescriptors() { return fileDescriptors.toArray(new FileDescriptor[fileDescriptors.size()]); } @@ -213,6 +231,8 @@ public class RubyDescriptorPool extends RubyObject { private static RubyClass cDescriptor; private static RubyClass cEnumDescriptor; private static RubyClass cFieldDescriptor; + private static RubyClass cServiceDescriptor; + private static RubyClass cMethodDescriptor; private static RubyDescriptorPool descriptorPool; private List fileDescriptors; diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyMethodDescriptor.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyMethodDescriptor.java new file mode 100644 index 0000000000..62ce7c07b8 --- /dev/null +++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyMethodDescriptor.java @@ -0,0 +1,156 @@ +/* + * Protocol Buffers - Google's data interchange format + * Copyright 2024 Google Inc. All rights reserved. + * https://developers.google.com/protocol-buffers/ + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.protobuf.jruby; + + +import com.google.protobuf.CodedInputStream; +import com.google.protobuf.Descriptors.MethodDescriptor; +import org.jruby.*; +import org.jruby.anno.JRubyClass; +import org.jruby.anno.JRubyMethod; +import org.jruby.runtime.Block; +import org.jruby.runtime.ObjectAllocator; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.anno.JRubyClass; +import org.jruby.anno.JRubyMethod; + +@JRubyClass(name = "MethoDescriptor") +public class RubyMethodDescriptor extends RubyObject { + public static void createRubyMethodDescriptor(Ruby runtime) { + RubyModule protobuf = runtime.getClassFromPath("Google::Protobuf"); + RubyClass cMethodDescriptor = + protobuf.defineClassUnder( + "MethodDescriptor", + runtime.getObject(), + new ObjectAllocator() { + @Override + public IRubyObject allocate(Ruby runtime, RubyClass klazz) { + return new RubyMethodDescriptor(runtime, klazz); + } + }); + cMethodDescriptor.defineAnnotatedMethods(RubyMethodDescriptor.class); + } + + public RubyMethodDescriptor(Ruby runtime, RubyClass klazz) { + super(runtime, klazz); + } + + /* + * call-seq: + * MethodDescriptor.name => name + * + * Returns the name of this method + */ + @JRubyMethod(name = "name") + public IRubyObject getName(ThreadContext context) { + return context.runtime.newString(this.descriptor.getName()); + } + + /* + * call-seq: + * MethodDescriptor.options + * + * Returns the options set on this protobuf rpc method + */ + @JRubyMethod + public IRubyObject options(ThreadContext context) { + RubyDescriptorPool pool = (RubyDescriptorPool) RubyDescriptorPool.generatedPool(null, null); + RubyDescriptor methodOptionsDescriptor = + (RubyDescriptor) + pool.lookup(context, context.runtime.newString("google.protobuf.MethodOptions")); + RubyClass methodOptionsClass = (RubyClass) methodOptionsDescriptor.msgclass(context); + RubyMessage msg = (RubyMessage) methodOptionsClass.newInstance(context, Block.NULL_BLOCK); + return msg.decodeBytes( + context, + msg, + CodedInputStream.newInstance( + descriptor.getOptions().toByteString().toByteArray()), /*freeze*/ + true); + } + + /* + * call-seq: + * MethodDescriptor.input_type => Descriptor + * + * Returns the `Descriptor` for the request message type of this method + */ + @JRubyMethod(name = "input_type") + public IRubyObject getInputType(ThreadContext context) { + return this.pool.lookup(context, context.runtime.newString(this.descriptor.getInputType().getFullName())); + } + + /* + * call-seq: + * MethodDescriptor.output_type => Descriptor + * + * Returns the `Descriptor` for the response message type of this method + */ + @JRubyMethod(name = "output_type") + public IRubyObject getOutputType(ThreadContext context) { + return this.pool.lookup(context, context.runtime.newString(this.descriptor.getOutputType().getFullName())); + } + + + /* + * call-seq: + * MethodDescriptor.client_streaming => bool + * + * Returns whether or not this is a streaming request method + */ + @JRubyMethod(name = "client_streaming") + public IRubyObject getClientStreaming(ThreadContext context) { + return this.descriptor.isClientStreaming() ? context.runtime.getTrue() : context.runtime.getFalse(); + } + + /* + * call-seq: + * MethodDescriptor.server_streaming => bool + * + * Returns whether or not this is a streaming response method + */ + @JRubyMethod(name = "server_streaming") + public IRubyObject getServerStreaming(ThreadContext context) { + return this.descriptor.isServerStreaming() ? context.runtime.getTrue() : context.runtime.getFalse(); + } + + protected void setDescriptor( + ThreadContext context, MethodDescriptor descriptor, RubyDescriptorPool pool) { + this.descriptor = descriptor; + this.pool = pool; + } + + private MethodDescriptor descriptor; + private IRubyObject name; + private RubyDescriptorPool pool; +} diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyServiceDescriptor.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyServiceDescriptor.java new file mode 100644 index 0000000000..5556f01b70 --- /dev/null +++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyServiceDescriptor.java @@ -0,0 +1,157 @@ +/* + * Protocol Buffers - Google's data interchange format + * Copyright 2024 Google Inc. All rights reserved. + * https://developers.google.com/protocol-buffers/ + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.protobuf.jruby; + + +import com.google.protobuf.CodedInputStream; +import com.google.protobuf.Descriptors.MethodDescriptor; +import com.google.protobuf.Descriptors.ServiceDescriptor; +import java.util.LinkedHashMap; +import java.util.Map; +import org.jruby.*; +import org.jruby.anno.JRubyClass; +import org.jruby.anno.JRubyMethod; +import org.jruby.runtime.Block; +import org.jruby.runtime.ObjectAllocator; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.anno.JRubyClass; +import org.jruby.anno.JRubyMethod; + +@JRubyClass(name = "ServiceDescriptor") +public class RubyServiceDescriptor extends RubyObject { + public static void createRubyServiceDescriptor(Ruby runtime) { + RubyModule protobuf = runtime.getClassFromPath("Google::Protobuf"); + RubyClass cServiceDescriptor = + protobuf.defineClassUnder( + "ServiceDescriptor", + runtime.getObject(), + new ObjectAllocator() { + @Override + public IRubyObject allocate(Ruby runtime, RubyClass klazz) { + return new RubyServiceDescriptor(runtime, klazz); + } + }); + cServiceDescriptor.includeModule(runtime.getEnumerable()); + cServiceDescriptor.defineAnnotatedMethods(RubyServiceDescriptor.class); + cMethodDescriptor = (RubyClass) runtime.getClassFromPath("Google::Protobuf::MethodDescriptor"); + } + + public RubyServiceDescriptor(Ruby runtime, RubyClass klazz) { + super(runtime, klazz); + } + + /* + * call-seq: + * ServiceDescriptor.name => name + * + * Returns the name of this service type as a fully-qualified string (e.g., + * My.Package.Service). + */ + @JRubyMethod(name = "name") + public IRubyObject getName(ThreadContext context) { + return name; + } + + /* + * call-seq: + * ServiceDescriptor.file_descriptor + * + * Returns the FileDescriptor object this service belongs to. + */ + @JRubyMethod(name = "file_descriptor") + public IRubyObject getFileDescriptor(ThreadContext context) { + return RubyFileDescriptor.getRubyFileDescriptor(context, descriptor); + } + + /* + * call-seq: + * ServiceDescriptor.options + * + * Returns the options set on this protobuf service + */ + @JRubyMethod + public IRubyObject options(ThreadContext context) { + RubyDescriptorPool pool = (RubyDescriptorPool) RubyDescriptorPool.generatedPool(null, null); + RubyDescriptor serviceOptionsDescriptor = + (RubyDescriptor) + pool.lookup(context, context.runtime.newString("google.protobuf.ServiceOptions")); + RubyClass serviceOptionsClass = (RubyClass) serviceOptionsDescriptor.msgclass(context); + RubyMessage msg = (RubyMessage) serviceOptionsClass.newInstance(context, Block.NULL_BLOCK); + return msg.decodeBytes( + context, + msg, + CodedInputStream.newInstance( + descriptor.getOptions().toByteString().toByteArray()), /*freeze*/ + true); + } + + /* + * call-seq: + * ServiceDescriptor.each(&block) + * + * Iterates over methods in this service, yielding to the block on each one. + */ + @JRubyMethod(name = "each") + public IRubyObject each(ThreadContext context, Block block) { + for (Map.Entry entry : methodDescriptors.entrySet()) { + block.yield(context, entry.getValue()); + } + return context.nil; + } + + protected void setDescriptor( + ThreadContext context, ServiceDescriptor descriptor, RubyDescriptorPool pool) { + this.descriptor = descriptor; + + // Populate the methods (and preserve the order by using LinkedHashMap) + methodDescriptors = new LinkedHashMap(); + + for (MethodDescriptor methodDescriptor : descriptor.getMethods()) { + RubyMethodDescriptor md = + (RubyMethodDescriptor) cMethodDescriptor.newInstance(context, Block.NULL_BLOCK); + md.setDescriptor(context, methodDescriptor, pool); + methodDescriptors.put(context.runtime.newString(methodDescriptor.getName()), md); + } + } + + protected void setName(IRubyObject name) { + this.name = name; + } + + private static RubyClass cMethodDescriptor; + + private ServiceDescriptor descriptor; + private Map methodDescriptors; + private IRubyObject name; +} diff --git a/ruby/src/main/java/google/ProtobufJavaService.java b/ruby/src/main/java/google/ProtobufJavaService.java index 00d60a1498..c9b7c4e522 100644 --- a/ruby/src/main/java/google/ProtobufJavaService.java +++ b/ruby/src/main/java/google/ProtobufJavaService.java @@ -55,6 +55,8 @@ public class ProtobufJavaService implements BasicLibraryService { RubyMap.createRubyMap(ruby); RubyOneofDescriptor.createRubyOneofDescriptor(ruby); RubyDescriptor.createRubyDescriptor(ruby); + RubyMethodDescriptor.createRubyMethodDescriptor(ruby); + RubyServiceDescriptor.createRubyServiceDescriptor(ruby); RubyDescriptorPool.createRubyDescriptorPool(ruby); return true; } diff --git a/ruby/tests/service_test.proto b/ruby/tests/service_test.proto new file mode 100644 index 0000000000..e0ac273754 --- /dev/null +++ b/ruby/tests/service_test.proto @@ -0,0 +1,50 @@ +syntax = "proto3"; +import "google/protobuf/descriptor.proto"; + +package service_test_protos; + +message UnaryRequestType { + string ping = 1; +} + +message UnaryResponseType { + string pong = 1; +} + +message StreamRequestType { + string ping = 1; + uint32 sequence = 2; +} + +message StreamResponseType { + string pong = 1; + uint32 sequence = 2; +} + +message TestOptionsType { + uint32 int_option_value = 1; +} + +extend google.protobuf.ServiceOptions { + optional TestOptionsType test_options = 50000; +} + +service TestService { + option(test_options).int_option_value = 8325; + + rpc UnaryOne(UnaryRequestType) returns (UnaryResponseType); + rpc UnaryTwo(UnaryRequestType) returns (UnaryResponseType); + + rpc IdempotentMethod(UnaryRequestType) returns (UnaryResponseType) { + option idempotency_level = IDEMPOTENT; + } + rpc PureMethod(UnaryRequestType) returns (UnaryResponseType) { + option idempotency_level = NO_SIDE_EFFECTS; + } + + rpc StreamingMethod(stream StreamRequestType) returns (stream StreamResponseType); +} + +service DeprecatedService { + option deprecated = true; +} diff --git a/ruby/tests/service_test.rb b/ruby/tests/service_test.rb new file mode 100644 index 0000000000..729b799599 --- /dev/null +++ b/ruby/tests/service_test.rb @@ -0,0 +1,61 @@ +#!/usr/bin/ruby + +require 'google/protobuf' +require 'service_test_pb' +require 'test/unit' +require 'json' + +class ServiceTest < Test::Unit::TestCase + def setup + @test_service = Google::Protobuf::DescriptorPool.generated_pool.lookup('service_test_protos.TestService') + @deprecated_service = Google::Protobuf::DescriptorPool.generated_pool.lookup('service_test_protos.DeprecatedService') + @test_service_methods = @test_service.to_h { |method| [method.name, method] } + end + + def test_lookup_service_descriptor + assert_kind_of Google::Protobuf::ServiceDescriptor, @test_service + assert_equal 'service_test_protos.TestService', @test_service.name + end + + def test_file_descriptor + assert_kind_of Google::Protobuf::FileDescriptor, @test_service.file_descriptor + assert_equal 'service_test.proto', @test_service.file_descriptor.name + end + + def test_method_iteration + @test_service.each { |method| assert_kind_of Google::Protobuf::MethodDescriptor, method } + assert_equal %w(UnaryOne UnaryTwo), @test_service.map { |method| method.name }.first(2) + end + + def test_service_options + assert @deprecated_service.options.deprecated + refute @test_service.options.deprecated + end + + def test_service_options_extensions + extension_field = Google::Protobuf::DescriptorPool.generated_pool.lookup('service_test_protos.test_options') + assert_equal 8325, extension_field.get(@test_service.options).int_option_value + end + + def test_method_options + assert_equal :IDEMPOTENT, @test_service_methods['IdempotentMethod'].options.idempotency_level + assert_equal :NO_SIDE_EFFECTS, @test_service_methods['PureMethod'].options.idempotency_level + end + + def test_method_input_type + unary_request_type = Google::Protobuf::DescriptorPool.generated_pool.lookup('service_test_protos.UnaryRequestType') + assert_same unary_request_type, @test_service_methods['UnaryOne'].input_type + end + + def test_method_output_type + unary_response_type = Google::Protobuf::DescriptorPool.generated_pool.lookup('service_test_protos.UnaryResponseType') + assert_same unary_response_type, @test_service_methods['UnaryOne'].output_type + end + + def test_method_streaming_flags + refute @test_service_methods['UnaryOne'].client_streaming + refute @test_service_methods['UnaryOne'].server_streaming + assert @test_service_methods['StreamingMethod'].client_streaming + assert @test_service_methods['StreamingMethod'].server_streaming + end +end