Implement service & method descriptor lookup in Ruby (#15817)

This PR implements lookup of service descriptor and method descriptor objects in Ruby as described in issue https://github.com/protocolbuffers/protobuf/issues/14891.

It contains three implementations - one for the CRuby extension API, one for JRuby, and one for FFI.

With this patch,

* `DescriptorPool#lookup('fully.qualified.service.name')` works and returns a `Google::Protobuf::ServiceDescriptor` object
* You can call `#options` on that to get the service options
* You can call `#methods` on that to get the services' methods as `Google::Protobuf::MethodDescriptor` objects,
* You can call `MethodDescriptor#options` to get method options
* You can also get the streaming flags & input/output types of the method with `#input_type`, `#output_type`, `#client_streaming`, and `#server_streaming`.

In order to make the FFI implementation work, I had to mark some more methods in the UPB header as exported - I guess that's something which will have to be done on the UPB side, like this 01fed1cc1b

CC @dazuma & @haberman from the original issue, and @JasonLunn (since you work on protobuf it seems - small world!)

I apologies for the large volume of copy-pasta'd code from the existing descriptor class implementations into the new ones - I felt this was probably better than designing new abstractions to reduce it off the bat though; this feels like it "fits in" with the existing implementation.

Closes #15817

COPYBARA_INTEGRATE_REVIEW=https://github.com/protocolbuffers/protobuf/pull/15817 from KJTsanaktsidis:ktsanaktsidis/add_service_method_descriptors 54d7218431
PiperOrigin-RevId: 618221016
pull/16263/head
KJ Tsanaktsidis 2024-03-22 10:32:08 -07:00 committed by Copybara-Service
parent d8791df72e
commit 2082ee8bb5
17 changed files with 1025 additions and 15 deletions

1
.gitignore vendored
View File

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

View File

@ -29,6 +29,7 @@ test_protos = %w[
tests/generated_code_editions.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

View File

@ -23,6 +23,9 @@ 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 +156,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 +173,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;
}
@ -1307,6 +1316,298 @@ 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);
@ -1348,6 +1649,16 @@ 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
// -----------------------------------------------------------------------------
@ -1423,6 +1734,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);

View File

@ -54,3 +54,19 @@ 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;
}

View File

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

View File

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

View File

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

View File

@ -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'

View File

@ -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,9 @@ 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 +161,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 +213,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 +232,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<FileDescriptor> fileDescriptors;

View File

@ -0,0 +1,160 @@
/*
* 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;
}

View File

@ -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 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<IRubyObject, RubyMethodDescriptor> 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<IRubyObject, RubyMethodDescriptor>();
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<IRubyObject, RubyMethodDescriptor> methodDescriptors;
private IRubyObject name;
}

View File

@ -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;
}

View File

@ -0,0 +1,52 @@
syntax = "proto3";
package service_test_protos;
import "google/protobuf/descriptor.proto";
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;
}

View File

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

View File

@ -66,8 +66,8 @@ const upb_FieldDef* upb_DefPool_FindExtensionByNumber(const upb_DefPool* s,
const upb_MessageDef* m,
int32_t fieldnum);
const upb_ServiceDef* upb_DefPool_FindServiceByName(const upb_DefPool* s,
const char* name);
UPB_API const upb_ServiceDef* upb_DefPool_FindServiceByName(
const upb_DefPool* s, const char* name);
const upb_ServiceDef* upb_DefPool_FindServiceByNameWithSize(
const upb_DefPool* s, const char* name, size_t size);

View File

@ -19,18 +19,19 @@
extern "C" {
#endif
bool upb_MethodDef_ClientStreaming(const upb_MethodDef* m);
UPB_API bool upb_MethodDef_ClientStreaming(const upb_MethodDef* m);
const char* upb_MethodDef_FullName(const upb_MethodDef* m);
bool upb_MethodDef_HasOptions(const upb_MethodDef* m);
int upb_MethodDef_Index(const upb_MethodDef* m);
const upb_MessageDef* upb_MethodDef_InputType(const upb_MethodDef* m);
const char* upb_MethodDef_Name(const upb_MethodDef* m);
const UPB_DESC(MethodOptions) * upb_MethodDef_Options(const upb_MethodDef* m);
UPB_API const upb_MessageDef* upb_MethodDef_InputType(const upb_MethodDef* m);
UPB_API const char* upb_MethodDef_Name(const upb_MethodDef* m);
UPB_API const UPB_DESC(MethodOptions) *
upb_MethodDef_Options(const upb_MethodDef* m);
const UPB_DESC(FeatureSet) *
upb_MethodDef_ResolvedFeatures(const upb_MethodDef* m);
const upb_MessageDef* upb_MethodDef_OutputType(const upb_MethodDef* m);
bool upb_MethodDef_ServerStreaming(const upb_MethodDef* m);
const upb_ServiceDef* upb_MethodDef_Service(const upb_MethodDef* m);
UPB_API const upb_MessageDef* upb_MethodDef_OutputType(const upb_MethodDef* m);
UPB_API bool upb_MethodDef_ServerStreaming(const upb_MethodDef* m);
UPB_API const upb_ServiceDef* upb_MethodDef_Service(const upb_MethodDef* m);
#ifdef __cplusplus
} /* extern "C" */

View File

@ -19,16 +19,17 @@
extern "C" {
#endif
const upb_FileDef* upb_ServiceDef_File(const upb_ServiceDef* s);
UPB_API const upb_FileDef* upb_ServiceDef_File(const upb_ServiceDef* s);
const upb_MethodDef* upb_ServiceDef_FindMethodByName(const upb_ServiceDef* s,
const char* name);
const char* upb_ServiceDef_FullName(const upb_ServiceDef* s);
UPB_API const char* upb_ServiceDef_FullName(const upb_ServiceDef* s);
bool upb_ServiceDef_HasOptions(const upb_ServiceDef* s);
int upb_ServiceDef_Index(const upb_ServiceDef* s);
const upb_MethodDef* upb_ServiceDef_Method(const upb_ServiceDef* s, int i);
int upb_ServiceDef_MethodCount(const upb_ServiceDef* s);
UPB_API const upb_MethodDef* upb_ServiceDef_Method(const upb_ServiceDef* s,
int i);
UPB_API int upb_ServiceDef_MethodCount(const upb_ServiceDef* s);
const char* upb_ServiceDef_Name(const upb_ServiceDef* s);
const UPB_DESC(ServiceOptions) *
UPB_API const UPB_DESC(ServiceOptions) *
upb_ServiceDef_Options(const upb_ServiceDef* s);
const UPB_DESC(FeatureSet) *
upb_ServiceDef_ResolvedFeatures(const upb_ServiceDef* s);