From 54d72184310d7d9bfc06d4a58956e3871170c43d Mon Sep 17 00:00:00 2001 From: KJ Tsanaktsidis Date: Tue, 13 Feb 2024 21:43:01 +1100 Subject: [PATCH] Implement support for looking up services descriptors in Ruby This commit implements suppot for looking up Service descriptors in the global descriptor pool, and from there looking up the associated methods. This is implemented in each of the C extension, JRuby extension, and the FFI implementation. The descriptors can be used to look up the options on the service/methods, and also the request/response types and streaming flags of methods. --- .gitignore | 1 + ruby/Rakefile | 1 + ruby/ext/google/protobuf_c/defs.c | 308 ++++++++++++++++++ ruby/ext/google/protobuf_c/glue.c | 14 + .../google/protobuf/ffi/descriptor_pool.rb | 4 +- .../google/protobuf/ffi/method_descriptor.rb | 114 +++++++ .../google/protobuf/ffi/service_descriptor.rb | 107 ++++++ ruby/lib/google/protobuf_ffi.rb | 2 + .../protobuf/jruby/RubyDescriptorPool.java | 20 ++ .../protobuf/jruby/RubyMethodDescriptor.java | 156 +++++++++ .../protobuf/jruby/RubyServiceDescriptor.java | 157 +++++++++ .../main/java/google/ProtobufJavaService.java | 2 + ruby/tests/service_test.proto | 50 +++ ruby/tests/service_test.rb | 61 ++++ 14 files changed, 996 insertions(+), 1 deletion(-) create mode 100644 ruby/lib/google/protobuf/ffi/method_descriptor.rb create mode 100644 ruby/lib/google/protobuf/ffi/service_descriptor.rb create mode 100644 ruby/src/main/java/com/google/protobuf/jruby/RubyMethodDescriptor.java create mode 100644 ruby/src/main/java/com/google/protobuf/jruby/RubyServiceDescriptor.java create mode 100644 ruby/tests/service_test.proto create mode 100644 ruby/tests/service_test.rb 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