Add support for options in CRuby, JRuby and FFI (#14594)

Rewrrte and extension of #12828, with additional work for JRuby. Partially fixes #1198 by adding support for custom options. Handling of extensions will be handled in a follow up.

Also includes these unrelated fixes:
* Removes code echo between `google/protobuf/repeated_field.rb` and `google/protobuf/ffi/repeated_field.rb` by `require`'ing the former in the latter.
* Adds missing calles to `testFrozen()` from methods of `RepeatedField` under JRuby that mutate.
* Various typos in comments.

Closes #14594

COPYBARA_INTEGRATE_REVIEW=https://github.com/protocolbuffers/protobuf/pull/14594 from protocolbuffers:add-support-for-options-in-ruby 16cc9e35b8635989af28962f4a54444a176a0559
PiperOrigin-RevId: 580848874
diff --git a/ruby/ext/google/protobuf_c/defs.c b/ruby/ext/google/protobuf_c/defs.c
index ed1b9e1..4e80547 100644
--- a/ruby/ext/google/protobuf_c/defs.c
+++ b/ruby/ext/google/protobuf_c/defs.c
@@ -44,7 +44,7 @@
   }
   return rb_str_new2(s);
 }
-
+static ID options_instancevar_interned;
 // -----------------------------------------------------------------------------
 // DescriptorPool.
 // -----------------------------------------------------------------------------
@@ -192,6 +192,7 @@
 
   rb_gc_register_address(&generated_pool);
   generated_pool = rb_class_new_instance(0, NULL, klass);
+  options_instancevar_interned = rb_intern("options");
 }
 
 // -----------------------------------------------------------------------------
@@ -226,6 +227,35 @@
   return ret;
 }
 
+// Decode and return a frozen instance of a Descriptor Option for the given pool
+static VALUE decode_options(VALUE self, const char* option_type, int size,
+                            const char* bytes, VALUE descriptor_pool) {
+  VALUE options_rb = rb_ivar_get(self, options_instancevar_interned);
+  if (options_rb != Qnil) {
+    return options_rb;
+  }
+
+  static const char* prefix = "google.protobuf.";
+  char fullname
+      [/*strlen(prefix)*/ 16 +
+       /*strln(longest option type supported e.g. "MessageOptions")*/ 14 +
+       /*null terminator*/ 1];
+
+  snprintf(fullname, sizeof(fullname), "%s%s", prefix, option_type);
+  const upb_MessageDef* msgdef = upb_DefPool_FindMessageByName(
+      ruby_to_DescriptorPool(descriptor_pool)->symtab, fullname);
+  if (!msgdef) {
+    rb_raise(rb_eRuntimeError, "Cannot find %s in DescriptorPool", option_type);
+  }
+
+  VALUE desc_rb = get_msgdef_obj(descriptor_pool, msgdef);
+  const Descriptor* desc = ruby_to_Descriptor(desc_rb);
+
+  options_rb = Message_decode_bytes(size, bytes, 0, desc->klass, true);
+  rb_ivar_set(self, options_instancevar_interned, options_rb);
+  return options_rb;
+}
+
 /*
  * call-seq:
  *     Descriptor.new => descriptor
@@ -374,6 +404,26 @@
   return self->klass;
 }
 
+/*
+ * call-seq:
+ *     Descriptor.options => options
+ *
+ * Returns the `MessageOptions` for this `Descriptor`.
+ */
+static VALUE Descriptor_options(VALUE _self) {
+  Descriptor* self = ruby_to_Descriptor(_self);
+  const google_protobuf_MessageOptions* opts =
+      upb_MessageDef_Options(self->msgdef);
+  upb_Arena* arena = upb_Arena_New();
+  size_t size;
+  char* serialized =
+      google_protobuf_MessageOptions_serialize(opts, arena, &size);
+  VALUE message_options = decode_options(_self, "MessageOptions", size,
+                                         serialized, self->descriptor_pool);
+  upb_Arena_Free(arena);
+  return message_options;
+}
+
 static void Descriptor_register(VALUE module) {
   VALUE klass = rb_define_class_under(module, "Descriptor", rb_cObject);
   rb_define_alloc_func(klass, Descriptor_alloc);
@@ -385,6 +435,7 @@
   rb_define_method(klass, "msgclass", Descriptor_msgclass, 0);
   rb_define_method(klass, "name", Descriptor_name, 0);
   rb_define_method(klass, "file_descriptor", Descriptor_file_descriptor, 0);
+  rb_define_method(klass, "options", Descriptor_options, 0);
   rb_include_module(klass, rb_mEnumerable);
   rb_gc_register_address(&cDescriptor);
   cDescriptor = klass;
@@ -484,12 +535,31 @@
   }
 }
 
+/*
+ * call-seq:
+ *     FileDescriptor.options => options
+ *
+ * Returns the `FileOptions` for this `FileDescriptor`.
+ */
+static VALUE FileDescriptor_options(VALUE _self) {
+  FileDescriptor* self = ruby_to_FileDescriptor(_self);
+  const google_protobuf_FileOptions* opts = upb_FileDef_Options(self->filedef);
+  upb_Arena* arena = upb_Arena_New();
+  size_t size;
+  char* serialized = google_protobuf_FileOptions_serialize(opts, arena, &size);
+  VALUE file_options = decode_options(_self, "FileOptions", size, serialized,
+                                      self->descriptor_pool);
+  upb_Arena_Free(arena);
+  return file_options;
+}
+
 static void FileDescriptor_register(VALUE module) {
   VALUE klass = rb_define_class_under(module, "FileDescriptor", rb_cObject);
   rb_define_alloc_func(klass, FileDescriptor_alloc);
   rb_define_method(klass, "initialize", FileDescriptor_initialize, 3);
   rb_define_method(klass, "name", FileDescriptor_name, 0);
   rb_define_method(klass, "syntax", FileDescriptor_syntax, 0);
+  rb_define_method(klass, "options", FileDescriptor_options, 0);
   rb_gc_register_address(&cFileDescriptor);
   cFileDescriptor = klass;
 }
@@ -540,7 +610,7 @@
 
 /*
  * call-seq:
- *    EnumDescriptor.new(c_only_cookie, pool, ptr) => EnumDescriptor
+ *    FieldDescriptor.new(c_only_cookie, pool, ptr) => FieldDescriptor
  *
  * Creates a descriptor wrapper object.  May only be called from C.
  */
@@ -841,6 +911,25 @@
   return Qnil;
 }
 
+/*
+ * call-seq:
+ *     FieldDescriptor.options => options
+ *
+ * Returns the `FieldOptions` for this `FieldDescriptor`.
+ */
+static VALUE FieldDescriptor_options(VALUE _self) {
+  FieldDescriptor* self = ruby_to_FieldDescriptor(_self);
+  const google_protobuf_FieldOptions* opts =
+      upb_FieldDef_Options(self->fielddef);
+  upb_Arena* arena = upb_Arena_New();
+  size_t size;
+  char* serialized = google_protobuf_FieldOptions_serialize(opts, arena, &size);
+  VALUE field_options = decode_options(_self, "FieldOptions", size, serialized,
+                                       self->descriptor_pool);
+  upb_Arena_Free(arena);
+  return field_options;
+}
+
 static void FieldDescriptor_register(VALUE module) {
   VALUE klass = rb_define_class_under(module, "FieldDescriptor", rb_cObject);
   rb_define_alloc_func(klass, FieldDescriptor_alloc);
@@ -857,6 +946,7 @@
   rb_define_method(klass, "clear", FieldDescriptor_clear, 1);
   rb_define_method(klass, "get", FieldDescriptor_get, 1);
   rb_define_method(klass, "set", FieldDescriptor_set, 2);
+  rb_define_method(klass, "options", FieldDescriptor_options, 0);
   rb_gc_register_address(&cFieldDescriptor);
   cFieldDescriptor = klass;
 }
@@ -956,12 +1046,32 @@
   return Qnil;
 }
 
+/*
+ * call-seq:
+ *     OneofDescriptor.options => options
+ *
+ * Returns the `OneofOptions` for this `OneofDescriptor`.
+ */
+static VALUE OneOfDescriptor_options(VALUE _self) {
+  OneofDescriptor* self = ruby_to_OneofDescriptor(_self);
+  const google_protobuf_OneofOptions* opts =
+      upb_OneofDef_Options(self->oneofdef);
+  upb_Arena* arena = upb_Arena_New();
+  size_t size;
+  char* serialized = google_protobuf_OneofOptions_serialize(opts, arena, &size);
+  VALUE oneof_options = decode_options(_self, "OneofOptions", size, serialized,
+                                       self->descriptor_pool);
+  upb_Arena_Free(arena);
+  return oneof_options;
+}
+
 static void OneofDescriptor_register(VALUE module) {
   VALUE klass = rb_define_class_under(module, "OneofDescriptor", rb_cObject);
   rb_define_alloc_func(klass, OneofDescriptor_alloc);
   rb_define_method(klass, "initialize", OneofDescriptor_initialize, 3);
   rb_define_method(klass, "name", OneofDescriptor_name, 0);
   rb_define_method(klass, "each", OneofDescriptor_each, 0);
+  rb_define_method(klass, "options", OneOfDescriptor_options, 0);
   rb_include_module(klass, rb_mEnumerable);
   rb_gc_register_address(&cOneofDescriptor);
   cOneofDescriptor = klass;
@@ -1131,6 +1241,24 @@
   return self->module;
 }
 
+/*
+ * call-seq:
+ *     EnumDescriptor.options => options
+ *
+ * Returns the `EnumOptions` for this `EnumDescriptor`.
+ */
+static VALUE EnumDescriptor_options(VALUE _self) {
+  EnumDescriptor* self = ruby_to_EnumDescriptor(_self);
+  const google_protobuf_EnumOptions* opts = upb_EnumDef_Options(self->enumdef);
+  upb_Arena* arena = upb_Arena_New();
+  size_t size;
+  char* serialized = google_protobuf_EnumOptions_serialize(opts, arena, &size);
+  VALUE enum_options = decode_options(_self, "EnumOptions", size, serialized,
+                                      self->descriptor_pool);
+  upb_Arena_Free(arena);
+  return enum_options;
+}
+
 static void EnumDescriptor_register(VALUE module) {
   VALUE klass = rb_define_class_under(module, "EnumDescriptor", rb_cObject);
   rb_define_alloc_func(klass, EnumDescriptor_alloc);
@@ -1141,6 +1269,7 @@
   rb_define_method(klass, "each", EnumDescriptor_each, 0);
   rb_define_method(klass, "enummodule", EnumDescriptor_enummodule, 0);
   rb_define_method(klass, "file_descriptor", EnumDescriptor_file_descriptor, 0);
+  rb_define_method(klass, "options", EnumDescriptor_options, 0);
   rb_include_module(klass, rb_mEnumerable);
   rb_gc_register_address(&cEnumDescriptor);
   cEnumDescriptor = klass;
diff --git a/ruby/ext/google/protobuf_c/glue.c b/ruby/ext/google/protobuf_c/glue.c
index 3505437..e51e364 100644
--- a/ruby/ext/google/protobuf_c/glue.c
+++ b/ruby/ext/google/protobuf_c/glue.c
@@ -14,8 +14,43 @@
 upb_Arena* Arena_create() { return upb_Arena_Init(NULL, 0, &upb_alloc_global); }
 
 google_protobuf_FileDescriptorProto* FileDescriptorProto_parse(
-    const char* serialized_file_proto, size_t length) {
-  upb_Arena* arena = Arena_create();
+    const char* serialized_file_proto, size_t length, upb_Arena* arena) {
   return google_protobuf_FileDescriptorProto_parse(serialized_file_proto,
                                                    length, arena);
 }
+
+char* EnumDescriptor_serialized_options(const upb_EnumDef* enumdef,
+                                        size_t* size, upb_Arena* arena) {
+  const google_protobuf_EnumOptions* opts = upb_EnumDef_Options(enumdef);
+  char* serialized = google_protobuf_EnumOptions_serialize(opts, arena, size);
+  return serialized;
+}
+
+char* FileDescriptor_serialized_options(const upb_FileDef* filedef,
+                                        size_t* size, upb_Arena* arena) {
+  const google_protobuf_FileOptions* opts = upb_FileDef_Options(filedef);
+  char* serialized = google_protobuf_FileOptions_serialize(opts, arena, size);
+  return serialized;
+}
+
+char* Descriptor_serialized_options(const upb_MessageDef* msgdef, size_t* size,
+                                    upb_Arena* arena) {
+  const google_protobuf_MessageOptions* opts = upb_MessageDef_Options(msgdef);
+  char* serialized =
+      google_protobuf_MessageOptions_serialize(opts, arena, size);
+  return serialized;
+}
+
+char* OneOfDescriptor_serialized_options(const upb_OneofDef* oneofdef,
+                                         size_t* size, upb_Arena* arena) {
+  const google_protobuf_OneofOptions* opts = upb_OneofDef_Options(oneofdef);
+  char* serialized = google_protobuf_OneofOptions_serialize(opts, arena, size);
+  return serialized;
+}
+
+char* FieldDescriptor_serialized_options(const upb_FieldDef* fielddef,
+                                         size_t* size, upb_Arena* arena) {
+  const google_protobuf_FieldOptions* opts = upb_FieldDef_Options(fielddef);
+  char* serialized = google_protobuf_FieldOptions_serialize(opts, arena, size);
+  return serialized;
+}
diff --git a/ruby/ext/google/protobuf_c/map.c b/ruby/ext/google/protobuf_c/map.c
index 98ee489..e3bd80c 100644
--- a/ruby/ext/google/protobuf_c/map.c
+++ b/ruby/ext/google/protobuf_c/map.c
@@ -573,6 +573,26 @@
 }
 
 /*
+ * Deep freezes the map and values recursively.
+ * Internal use only.
+ */
+VALUE Map_internal_deep_freeze(VALUE _self) {
+  Map* self = ruby_to_Map(_self);
+  Map_freeze(_self);
+  if (self->value_type_info.type == kUpb_CType_Message) {
+    size_t iter = kUpb_Map_Begin;
+    upb_MessageValue key, val;
+
+    while (upb_Map_Next(self->map, &key, &val, &iter)) {
+      VALUE val_val =
+          Convert_UpbToRuby(val, self->value_type_info, self->arena);
+      Message_internal_deep_freeze(val_val);
+    }
+  }
+  return _self;
+}
+
+/*
  * call-seq:
  *     Map.hash => hash_value
  *
diff --git a/ruby/ext/google/protobuf_c/map.h b/ruby/ext/google/protobuf_c/map.h
index 016a50c..d3cebc6 100644
--- a/ruby/ext/google/protobuf_c/map.h
+++ b/ruby/ext/google/protobuf_c/map.h
@@ -38,4 +38,7 @@
 // Call at startup to register all types in this module.
 void Map_register(VALUE module);
 
+// Recursively freeze map
+VALUE Map_internal_deep_freeze(VALUE _self);
+
 #endif  // RUBY_PROTOBUF_MAP_H_
diff --git a/ruby/ext/google/protobuf_c/message.c b/ruby/ext/google/protobuf_c/message.c
index a15e0fa..2dec31a 100644
--- a/ruby/ext/google/protobuf_c/message.c
+++ b/ruby/ext/google/protobuf_c/message.c
@@ -860,6 +860,32 @@
 }
 
 /*
+ * Deep freezes the message object recursively.
+ * Internal use only.
+ */
+VALUE Message_internal_deep_freeze(VALUE _self) {
+  Message* self = ruby_to_Message(_self);
+  Message_freeze(_self);
+
+  int n = upb_MessageDef_FieldCount(self->msgdef);
+  for (int i = 0; i < n; i++) {
+    const upb_FieldDef* f = upb_MessageDef_Field(self->msgdef, i);
+    VALUE field = Message_getfield(_self, f);
+
+    if (field != Qnil) {
+      if (upb_FieldDef_IsMap(f)) {
+        Map_internal_deep_freeze(field);
+      } else if (upb_FieldDef_IsRepeated(f)) {
+        RepeatedField_internal_deep_freeze(field);
+      } else if (upb_FieldDef_IsSubMessage(f)) {
+        Message_internal_deep_freeze(field);
+      }
+    }
+  }
+  return _self;
+}
+
+/*
  * call-seq:
  *     Message.[](index) => value
  *
@@ -911,7 +937,7 @@
  *     MessageClass.decode(data, options) => message
  *
  * Decodes the given data (as a string containing bytes in protocol buffers wire
- * format) under the interpretration given by this message class's definition
+ * format) under the interpretation given by this message class's definition
  * and returns a message object with the corresponding field values.
  * @param options [Hash] options for the decoder
  *  recursion_limit: set to maximum decoding depth for message (default is 64)
@@ -942,18 +968,24 @@
     rb_raise(rb_eArgError, "Expected string for binary protobuf data.");
   }
 
+  return Message_decode_bytes(RSTRING_LEN(data), RSTRING_PTR(data), options,
+                              klass, /*freeze*/ false);
+}
+
+VALUE Message_decode_bytes(int size, const char* bytes, int options,
+                           VALUE klass, bool freeze) {
   VALUE msg_rb = initialize_rb_class_with_no_args(klass);
   Message* msg = ruby_to_Message(msg_rb);
 
-  upb_DecodeStatus status =
-      upb_Decode(RSTRING_PTR(data), RSTRING_LEN(data), (upb_Message*)msg->msg,
-                 upb_MessageDef_MiniTable(msg->msgdef), NULL, options,
-                 Arena_get(msg->arena));
-
+  upb_DecodeStatus status = upb_Decode(bytes, size, (upb_Message*)msg->msg,
+                                       upb_MessageDef_MiniTable(msg->msgdef),
+                                       NULL, options, Arena_get(msg->arena));
   if (status != kUpb_DecodeStatus_Ok) {
     rb_raise(cParseError, "Error occurred during parsing");
   }
-
+  if (freeze) {
+    Message_internal_deep_freeze(msg_rb);
+  }
   return msg_rb;
 }
 
diff --git a/ruby/ext/google/protobuf_c/message.h b/ruby/ext/google/protobuf_c/message.h
index 5e354b0..cb6897f 100644
--- a/ruby/ext/google/protobuf_c/message.h
+++ b/ruby/ext/google/protobuf_c/message.h
@@ -73,6 +73,13 @@
 // module.
 VALUE MessageOrEnum_GetDescriptor(VALUE klass);
 
+// Decodes a Message from a byte sequence.
+VALUE Message_decode_bytes(int size, const char* bytes, int options,
+                           VALUE klass, bool freeze);
+
+// Recursively freeze message
+VALUE Message_internal_deep_freeze(VALUE _self);
+
 // Call at startup to register all types in this module.
 void Message_register(VALUE protobuf);
 
diff --git a/ruby/ext/google/protobuf_c/repeated_field.c b/ruby/ext/google/protobuf_c/repeated_field.c
index f5ca3ca..1960126 100644
--- a/ruby/ext/google/protobuf_c/repeated_field.c
+++ b/ruby/ext/google/protobuf_c/repeated_field.c
@@ -488,6 +488,25 @@
 }
 
 /*
+ * Deep freezes the repeated field and values recursively.
+ * Internal use only.
+ */
+VALUE RepeatedField_internal_deep_freeze(VALUE _self) {
+  RepeatedField* self = ruby_to_RepeatedField(_self);
+  RepeatedField_freeze(_self);
+  if (self->type_info.type == kUpb_CType_Message) {
+    int size = upb_Array_Size(self->array);
+    int i;
+    for (i = 0; i < size; i++) {
+      upb_MessageValue msgval = upb_Array_Get(self->array, i);
+      VALUE val = Convert_UpbToRuby(msgval, self->type_info, self->arena);
+      Message_internal_deep_freeze(val);
+    }
+  }
+  return _self;
+}
+
+/*
  * call-seq:
  *     RepeatedField.hash => hash_value
  *
diff --git a/ruby/ext/google/protobuf_c/repeated_field.h b/ruby/ext/google/protobuf_c/repeated_field.h
index 97a908e..f3f7a50 100644
--- a/ruby/ext/google/protobuf_c/repeated_field.h
+++ b/ruby/ext/google/protobuf_c/repeated_field.h
@@ -35,4 +35,7 @@
 // Call at startup to register all types in this module.
 void RepeatedField_register(VALUE module);
 
+// Recursively freeze RepeatedField.
+VALUE RepeatedField_internal_deep_freeze(VALUE _self);
+
 #endif  // RUBY_PROTOBUF_REPEATED_FIELD_H_
diff --git a/ruby/lib/google/protobuf/ffi/descriptor.rb b/ruby/lib/google/protobuf/ffi/descriptor.rb
index c878eb5..25d226a 100644
--- a/ruby/lib/google/protobuf/ffi/descriptor.rb
+++ b/ruby/lib/google/protobuf/ffi/descriptor.rb
@@ -95,6 +95,15 @@
         @msg_class ||= build_message_class
       end
 
+      def options
+        @options ||= begin
+          size_ptr = ::FFI::MemoryPointer.new(:size_t, 1)
+          temporary_arena = Google::Protobuf::FFI.create_arena
+          buffer = Google::Protobuf::FFI.message_options(self, size_ptr, temporary_arena)
+          Google::Protobuf::MessageOptions.decode(buffer.read_string_length(size_ptr.read(:size_t)).force_encoding("ASCII-8BIT").freeze).send(:internal_deep_freeze)
+        end
+      end
+
       private
 
       extend Google::Protobuf::Internal::Convert
@@ -129,6 +138,7 @@
         message = OBJECT_CACHE.get(msg.address)
         if message.nil?
           message = descriptor.msgclass.send(:private_constructor, arena, msg: msg)
+          message.send :internal_deep_freeze if frozen?
         end
         message
       end
@@ -146,6 +156,7 @@
       attach_function :get_message_fullname, :upb_MessageDef_FullName,                [Descriptor], :string
       attach_function :get_mini_table,       :upb_MessageDef_MiniTable,               [Descriptor], MiniTable.ptr
       attach_function :oneof_count,          :upb_MessageDef_OneofCount,              [Descriptor], :int
+      attach_function :message_options,      :Descriptor_serialized_options,          [Descriptor, :pointer, Internal::Arena], :pointer
       attach_function :get_well_known_type,  :upb_MessageDef_WellKnownType,           [Descriptor], WellKnown
       attach_function :message_def_syntax,   :upb_MessageDef_Syntax,                  [Descriptor], Syntax
       attach_function :find_msg_def_by_name, :upb_MessageDef_FindByNameWithSize,      [Descriptor, :string, :size_t, :FieldDefPointer, :OneofDefPointer], :bool
diff --git a/ruby/lib/google/protobuf/ffi/descriptor_pool.rb b/ruby/lib/google/protobuf/ffi/descriptor_pool.rb
index bd9c96d..f0543ad 100644
--- a/ruby/lib/google/protobuf/ffi/descriptor_pool.rb
+++ b/ruby/lib/google/protobuf/ffi/descriptor_pool.rb
@@ -15,7 +15,7 @@
       attach_function :lookup_enum,           :upb_DefPool_FindEnumByName,    [:DefPool, :string], EnumDescriptor
       attach_function :lookup_msg,            :upb_DefPool_FindMessageByName, [:DefPool, :string], Descriptor
       # FileDescriptorProto
-      attach_function :parse,                 :FileDescriptorProto_parse, [:binary_string, :size_t], :FileDescriptorProto
+      attach_function :parse,                 :FileDescriptorProto_parse,     [:binary_string, :size_t, Internal::Arena], :FileDescriptorProto
     end
     class DescriptorPool
       attr :descriptor_pool
@@ -35,7 +35,8 @@
         memBuf = ::FFI::MemoryPointer.new(:char, file_contents.bytesize)
         # Insert the data
         memBuf.put_bytes(0, file_contents)
-        file_descriptor_proto = Google::Protobuf::FFI.parse memBuf, file_contents.bytesize
+        temporary_arena = Google::Protobuf::FFI.create_arena
+        file_descriptor_proto = Google::Protobuf::FFI.parse memBuf, file_contents.bytesize, temporary_arena
         raise ArgumentError.new("Unable to parse FileDescriptorProto") if file_descriptor_proto.null?
 
         status = Google::Protobuf::FFI::Status.new
diff --git a/ruby/lib/google/protobuf/ffi/enum_descriptor.rb b/ruby/lib/google/protobuf/ffi/enum_descriptor.rb
index 773cf73..a1f4fef 100644
--- a/ruby/lib/google/protobuf/ffi/enum_descriptor.rb
+++ b/ruby/lib/google/protobuf/ffi/enum_descriptor.rb
@@ -19,7 +19,7 @@
         prepend Google::Protobuf::Internal::TypeSafety
         include Google::Protobuf::Internal::PointerHelper
 
-        # @param value [Arena] Arena to convert to an FFI native type
+        # @param value [EnumDescriptor] EnumDescriptor to convert to an FFI native type
         # @param _ [Object] Unused
         def to_native(value, _)
           value.instance_variable_get(:@enum_def) || ::FFI::Pointer::NULL
@@ -79,6 +79,15 @@
         @module
       end
 
+      def options
+        @options ||= begin
+          size_ptr = ::FFI::MemoryPointer.new(:size_t, 1)
+          temporary_arena = Google::Protobuf::FFI.create_arena
+          buffer = Google::Protobuf::FFI.enum_options(self, size_ptr, temporary_arena)
+          Google::Protobuf::EnumOptions.decode(buffer.read_string_length(size_ptr.read(:size_t)).force_encoding("ASCII-8BIT").freeze).send(:internal_deep_freeze)
+        end
+      end
+
       private
 
       def initialize(enum_def, descriptor_pool)
@@ -152,6 +161,7 @@
       attach_function :enum_value_by_name,        :upb_EnumDef_FindValueByNameWithSize,[EnumDescriptor, :string, :size_t], :EnumValueDef
       attach_function :enum_value_by_number,      :upb_EnumDef_FindValueByNumber,      [EnumDescriptor, :int], :EnumValueDef
       attach_function :get_enum_fullname,         :upb_EnumDef_FullName,               [EnumDescriptor], :string
+      attach_function :enum_options,              :EnumDescriptor_serialized_options,  [EnumDescriptor, :pointer, Internal::Arena], :pointer
       attach_function :enum_value_by_index,       :upb_EnumDef_Value,                  [EnumDescriptor, :int], :EnumValueDef
       attach_function :enum_value_count,          :upb_EnumDef_ValueCount,             [EnumDescriptor], :int
       attach_function :enum_name,                 :upb_EnumValueDef_Name,              [:EnumValueDef], :string
diff --git a/ruby/lib/google/protobuf/ffi/field_descriptor.rb b/ruby/lib/google/protobuf/ffi/field_descriptor.rb
index 800661b..d7c45da 100644
--- a/ruby/lib/google/protobuf/ffi/field_descriptor.rb
+++ b/ruby/lib/google/protobuf/ffi/field_descriptor.rb
@@ -206,6 +206,15 @@
         end
       end
 
+      def options
+        @options ||= begin
+          size_ptr = ::FFI::MemoryPointer.new(:size_t, 1)
+          temporary_arena = Google::Protobuf::FFI.create_arena
+          buffer = Google::Protobuf::FFI.field_options(self, size_ptr, temporary_arena)
+          Google::Protobuf::FieldOptions.decode(buffer.read_string_length(size_ptr.read(:size_t)).force_encoding("ASCII-8BIT").freeze).send(:internal_deep_freeze)
+        end
+      end
+
       private
 
       def initialize(field_def, descriptor_pool)
@@ -289,21 +298,22 @@
       attach_function :get_field_by_number,  :upb_MessageDef_FindFieldByNumber,      [Descriptor, :uint32_t], FieldDescriptor
 
       # FieldDescriptor
-      attach_function :get_containing_message_def, :upb_FieldDef_ContainingType,     [FieldDescriptor], Descriptor
-      attach_function :get_c_type,                 :upb_FieldDef_CType,              [FieldDescriptor], CType
-      attach_function :get_default,                :upb_FieldDef_Default,            [FieldDescriptor], MessageValue.by_value
-      attach_function :get_subtype_as_enum,        :upb_FieldDef_EnumSubDef,         [FieldDescriptor], EnumDescriptor
-      attach_function :get_has_presence,           :upb_FieldDef_HasPresence,        [FieldDescriptor], :bool
-      attach_function :is_map,                     :upb_FieldDef_IsMap,              [FieldDescriptor], :bool
-      attach_function :is_repeated,                :upb_FieldDef_IsRepeated,         [FieldDescriptor], :bool
-      attach_function :is_sub_message,             :upb_FieldDef_IsSubMessage,       [FieldDescriptor], :bool
-      attach_function :get_json_name,              :upb_FieldDef_JsonName,           [FieldDescriptor], :string
-      attach_function :get_label,                  :upb_FieldDef_Label,              [FieldDescriptor], Label
-      attach_function :get_subtype_as_message,     :upb_FieldDef_MessageSubDef,      [FieldDescriptor], Descriptor
-      attach_function :get_full_name,              :upb_FieldDef_Name,               [FieldDescriptor], :string
-      attach_function :get_number,                 :upb_FieldDef_Number,             [FieldDescriptor], :uint32_t
-      attach_function :get_type,                   :upb_FieldDef_Type,               [FieldDescriptor], FieldType
-      attach_function :file_def_by_raw_field_def,  :upb_FieldDef_File,               [:pointer], :FileDef
+      attach_function :field_options,              :FieldDescriptor_serialized_options, [FieldDescriptor, :pointer, Internal::Arena], :pointer
+      attach_function :get_containing_message_def, :upb_FieldDef_ContainingType,        [FieldDescriptor], Descriptor
+      attach_function :get_c_type,                 :upb_FieldDef_CType,                 [FieldDescriptor], CType
+      attach_function :get_default,                :upb_FieldDef_Default,               [FieldDescriptor], MessageValue.by_value
+      attach_function :get_subtype_as_enum,        :upb_FieldDef_EnumSubDef,            [FieldDescriptor], EnumDescriptor
+      attach_function :get_has_presence,           :upb_FieldDef_HasPresence,           [FieldDescriptor], :bool
+      attach_function :is_map,                     :upb_FieldDef_IsMap,                 [FieldDescriptor], :bool
+      attach_function :is_repeated,                :upb_FieldDef_IsRepeated,            [FieldDescriptor], :bool
+      attach_function :is_sub_message,             :upb_FieldDef_IsSubMessage,          [FieldDescriptor], :bool
+      attach_function :get_json_name,              :upb_FieldDef_JsonName,              [FieldDescriptor], :string
+      attach_function :get_label,                  :upb_FieldDef_Label,                 [FieldDescriptor], Label
+      attach_function :get_subtype_as_message,     :upb_FieldDef_MessageSubDef,         [FieldDescriptor], Descriptor
+      attach_function :get_full_name,              :upb_FieldDef_Name,                  [FieldDescriptor], :string
+      attach_function :get_number,                 :upb_FieldDef_Number,                [FieldDescriptor], :uint32_t
+      attach_function :get_type,                   :upb_FieldDef_Type,                  [FieldDescriptor], FieldType
+      attach_function :file_def_by_raw_field_def,  :upb_FieldDef_File,                  [:pointer], :FileDef
     end
   end
 end
diff --git a/ruby/lib/google/protobuf/ffi/file_descriptor.rb b/ruby/lib/google/protobuf/ffi/file_descriptor.rb
index 0a465ec..291ac4f 100644
--- a/ruby/lib/google/protobuf/ffi/file_descriptor.rb
+++ b/ruby/lib/google/protobuf/ffi/file_descriptor.rb
@@ -12,7 +12,9 @@
       attach_function :file_def_name,   :upb_FileDef_Name,   [:FileDef], :string
       attach_function :file_def_syntax, :upb_FileDef_Syntax, [:FileDef], Syntax
       attach_function :file_def_pool,   :upb_FileDef_Pool,   [:FileDef], :DefPool
+      attach_function :file_options,    :FileDescriptor_serialized_options,  [:FileDef, :pointer, Internal::Arena], :pointer
     end
+
     class FileDescriptor
       attr :descriptor_pool, :file_def
 
@@ -43,6 +45,15 @@
       def name
         Google::Protobuf::FFI.file_def_name(@file_def)
       end
+
+      def options
+        @options ||= begin
+          size_ptr = ::FFI::MemoryPointer.new(:size_t, 1)
+          temporary_arena = Google::Protobuf::FFI.create_arena
+          buffer = Google::Protobuf::FFI.file_options(@file_def, size_ptr, temporary_arena)
+          Google::Protobuf::FileOptions.decode(buffer.read_string_length(size_ptr.read(:size_t)).force_encoding("ASCII-8BIT").freeze).send(:internal_deep_freeze)
+        end
+      end
     end
   end
 end
diff --git a/ruby/lib/google/protobuf/ffi/map.rb b/ruby/lib/google/protobuf/ffi/map.rb
index 50c917e..61abbe5 100644
--- a/ruby/lib/google/protobuf/ffi/map.rb
+++ b/ruby/lib/google/protobuf/ffi/map.rb
@@ -269,6 +269,17 @@
 
       include Google::Protobuf::Internal::Convert
 
+      def internal_deep_freeze
+        freeze
+        if value_type == :message
+          internal_iterator do |iterator|
+            value_message_value = Google::Protobuf::FFI.map_value(@map_ptr, iterator)
+            convert_upb_to_ruby(value_message_value, value_type, descriptor, arena).send :internal_deep_freeze
+          end
+        end
+        self
+      end
+
       def internal_iterator
         iter = ::FFI::MemoryPointer.new(:size_t, 1)
         iter.write(:size_t, Google::Protobuf::FFI::Upb_Map_Begin)
diff --git a/ruby/lib/google/protobuf/ffi/message.rb b/ruby/lib/google/protobuf/ffi/message.rb
index d53d437..045f67f 100644
--- a/ruby/lib/google/protobuf/ffi/message.rb
+++ b/ruby/lib/google/protobuf/ffi/message.rb
@@ -19,7 +19,7 @@
       attach_function :encode_message,          :upb_Encode,                  [:Message, MiniTable.by_ref, :size_t, Internal::Arena, :pointer, :pointer], EncodeStatus
       attach_function :json_decode_message,     :upb_JsonDecode,              [:binary_string, :size_t, :Message, Descriptor, :DefPool, :int, Internal::Arena, Status.by_ref], :bool
       attach_function :json_encode_message,     :upb_JsonEncode,              [:Message, Descriptor, :DefPool, :int, :binary_string, :size_t, Status.by_ref], :size_t
-      attach_function :decode_message,          :upb_Decode,                              [:binary_string, :size_t, :Message, MiniTable.by_ref, :ExtensionRegistry, :int, Internal::Arena], DecodeStatus
+      attach_function :decode_message,          :upb_Decode,                  [:binary_string, :size_t, :Message, MiniTable.by_ref, :ExtensionRegistry, :int, Internal::Arena], DecodeStatus
       attach_function :get_mutable_message,     :upb_Message_Mutable,         [:Message, FieldDescriptor, Internal::Arena], MutableMessageValue.by_value
       attach_function :get_message_which_oneof, :upb_Message_WhichOneof,      [:Message, OneofDescriptor], FieldDescriptor
       attach_function :message_discard_unknown, :upb_Message_DiscardUnknown,  [:Message, Descriptor, :int], :bool
@@ -293,6 +293,17 @@
 
           include Google::Protobuf::Internal::Convert
 
+          def internal_deep_freeze
+            freeze
+            self.class.descriptor.each do |field_descriptor|
+              next if field_descriptor.has_presence? && !Google::Protobuf::FFI.get_message_has(@msg, field_descriptor)
+              if field_descriptor.map? or field_descriptor.repeated? or field_descriptor.sub_message?
+                get_field(field_descriptor).send :internal_deep_freeze
+              end
+            end
+            self
+          end
+
           def self.setup_accessors!
             @descriptor.each do |field_descriptor|
               field_name = field_descriptor.name
@@ -619,6 +630,7 @@
             repeated_field = OBJECT_CACHE.get(array.address)
             if repeated_field.nil?
               repeated_field = RepeatedField.send(:construct_for_field, field, @arena, array: array)
+              repeated_field.send :internal_deep_freeze if frozen?
             end
             repeated_field
           end
@@ -631,6 +643,7 @@
             map_field = OBJECT_CACHE.get(map.address)
             if map_field.nil?
               map_field = Google::Protobuf::Map.send(:construct_for_field, field, @arena, map: map)
+              map_field.send :internal_deep_freeze if frozen?
             end
             map_field
           end
diff --git a/ruby/lib/google/protobuf/ffi/oneof_descriptor.rb b/ruby/lib/google/protobuf/ffi/oneof_descriptor.rb
index 00d1d04..00acc99 100644
--- a/ruby/lib/google/protobuf/ffi/oneof_descriptor.rb
+++ b/ruby/lib/google/protobuf/ffi/oneof_descriptor.rb
@@ -22,10 +22,7 @@
         # @param value [OneofDescriptor] FieldDescriptor to convert to an FFI native type
         # @param _ [Object] Unused
         def to_native(value, _ = nil)
-          oneof_def_ptr = value.instance_variable_get(:@oneof_def)
-          warn "Underlying oneof_def was nil!" if oneof_def_ptr.nil?
-          raise "Underlying oneof_def was null!" if !oneof_def_ptr.nil? and oneof_def_ptr.null?
-          oneof_def_ptr
+          value.instance_variable_get(:@oneof_def) || ::FFI::Pointer::NULL
         end
 
         ##
@@ -56,6 +53,15 @@
         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.oneof_options(self, size_ptr, temporary_arena)
+          Google::Protobuf::OneofOptions.decode(buffer.read_string_length(size_ptr.read(:size_t)).force_encoding("ASCII-8BIT").freeze).send(:internal_deep_freeze)
+       end
+      end
+
       private
 
       def initialize(oneof_def, descriptor_pool)
@@ -72,17 +78,18 @@
 
     class FFI
       # MessageDef
-      attach_function :get_oneof_by_name,    :upb_MessageDef_FindOneofByNameWithSize, [Descriptor, :string, :size_t], OneofDescriptor
-      attach_function :get_oneof_by_index,   :upb_MessageDef_Oneof,                   [Descriptor, :int], OneofDescriptor
+      attach_function :get_oneof_by_name,        :upb_MessageDef_FindOneofByNameWithSize, [Descriptor, :string, :size_t], OneofDescriptor
+      attach_function :get_oneof_by_index,       :upb_MessageDef_Oneof,                   [Descriptor, :int], OneofDescriptor
 
       # OneofDescriptor
-      attach_function :get_oneof_name,           :upb_OneofDef_Name,          [OneofDescriptor], :string
-      attach_function :get_oneof_field_count,    :upb_OneofDef_FieldCount,    [OneofDescriptor], :int
-      attach_function :get_oneof_field_by_index, :upb_OneofDef_Field,         [OneofDescriptor, :int], FieldDescriptor
-      attach_function :get_oneof_containing_type,:upb_OneofDef_ContainingType,[:pointer], Descriptor
+      attach_function :get_oneof_name,           :upb_OneofDef_Name,                      [OneofDescriptor], :string
+      attach_function :get_oneof_field_count,    :upb_OneofDef_FieldCount,                [OneofDescriptor], :int
+      attach_function :get_oneof_field_by_index, :upb_OneofDef_Field,                     [OneofDescriptor, :int], FieldDescriptor
+      attach_function :get_oneof_containing_type,:upb_OneofDef_ContainingType,            [:pointer], Descriptor
+      attach_function :oneof_options,            :OneOfDescriptor_serialized_options,     [OneofDescriptor, :pointer, Internal::Arena], :pointer
 
       # FieldDescriptor
-      attach_function :real_containing_oneof,      :upb_FieldDef_RealContainingOneof,[FieldDescriptor], OneofDescriptor
+      attach_function :real_containing_oneof,    :upb_FieldDef_RealContainingOneof,       [FieldDescriptor], OneofDescriptor
     end
   end
 end
diff --git a/ruby/lib/google/protobuf/ffi/repeated_field.rb b/ruby/lib/google/protobuf/ffi/repeated_field.rb
index c75ea17..ccc95ad 100644
--- a/ruby/lib/google/protobuf/ffi/repeated_field.rb
+++ b/ruby/lib/google/protobuf/ffi/repeated_field.rb
@@ -5,8 +5,6 @@
 # license that can be found in the LICENSE file or at
 # https://developers.google.com/open-source/licenses/bsd
 
-require 'forwardable'
-
 #
 # This class makes RepeatedField act (almost-) like a Ruby Array.
 # It has convenience methods that extend the core C or Java based
@@ -15,7 +13,7 @@
 # This is a best-effort to mirror Array behavior.  Two comments:
 #  1) patches always welcome :)
 #  2) if performance is an issue, feel free to rewrite the method
-#     in jruby and C.  The source code has plenty of examples
+#     in C.  The source code has plenty of examples
 #
 # KNOWN ISSUES
 #   - #[]= doesn't allow less used approaches such as `arr[1, 2] = 'fizz'`
@@ -35,19 +33,6 @@
     end
 
     class RepeatedField
-      extend Forwardable
-      # NOTE:  using delegators rather than method_missing to make the
-      #        relationship explicit instead of implicit
-      def_delegators :to_ary,
-        :&, :*, :-, :'<=>',
-        :assoc, :bsearch, :bsearch_index, :combination, :compact, :count,
-        :cycle, :dig, :drop, :drop_while, :eql?, :fetch, :find_index, :flatten,
-        :include?, :index, :inspect, :join,
-        :pack, :permutation, :product, :pretty_print, :pretty_print_cycle,
-        :rassoc, :repeated_combination, :repeated_permutation, :reverse,
-        :rindex, :rotate, :sample, :shuffle, :shelljoin,
-        :to_s, :transpose, :uniq, :|
-
       include Enumerable
 
       ##
@@ -262,128 +247,21 @@
         push(*other.to_a)
       end
 
-      def first(n=nil)
-        if n.nil?
-          return self[0]
-        elsif n < 0
-          raise ArgumentError, "negative array size"
-        else
-          return self[0...n]
-        end
-      end
-
-
-      def last(n=nil)
-        if n.nil?
-          return self[-1]
-        elsif n < 0
-          raise ArgumentError, "negative array size"
-        else
-          start = [self.size-n, 0].max
-          return self[start...self.size]
-        end
-      end
-
-
-      def pop(n=nil)
-        if n
-          results = []
-          n.times{ results << pop_one }
-          return results
-        else
-          return pop_one
-        end
-      end
-
-
-      def empty?
-        self.size == 0
-      end
-
-      # array aliases into enumerable
-      alias_method :each_index, :each_with_index
-      alias_method :slice, :[]
-      alias_method :values_at, :select
-      alias_method :map, :collect
-
-
-      class << self
-        def define_array_wrapper_method(method_name)
-          define_method(method_name) do |*args, &block|
-            arr = self.to_a
-            result = arr.send(method_name, *args)
-            self.replace(arr)
-            return result if result
-            return block ? block.call : result
-          end
-        end
-        private :define_array_wrapper_method
-
-
-        def define_array_wrapper_with_result_method(method_name)
-          define_method(method_name) do |*args, &block|
-            # result can be an Enumerator, Array, or nil
-            # Enumerator can sometimes be returned if a block is an optional argument and it is not passed in
-            # nil usually specifies that no change was made
-            result = self.to_a.send(method_name, *args, &block)
-            if result
-              new_arr = result.to_a
-              self.replace(new_arr)
-              if result.is_a?(Enumerator)
-                # generate a fresh enum; rewinding the exiting one, in Ruby 2.2, will
-                # reset the enum with the same length, but all the #next calls will
-                # return nil
-                result = new_arr.to_enum
-                # generate a wrapper enum so any changes which occur by a chained
-                # enum can be captured
-                ie = ProxyingEnumerator.new(self, result)
-                result = ie.to_enum
-              end
-            end
-            result
-          end
-        end
-        private :define_array_wrapper_with_result_method
-      end
-
-
-      %w(delete delete_at shift slice! unshift).each do |method_name|
-        define_array_wrapper_method(method_name)
-      end
-
-
-      %w(collect! compact! delete_if fill flatten! insert reverse!
-        rotate! select! shuffle! sort! sort_by! uniq!).each do |method_name|
-        define_array_wrapper_with_result_method(method_name)
-      end
-      alias_method :keep_if, :select!
-      alias_method :map!, :collect!
-      alias_method :reject!, :delete_if
-
-
-      # propagates changes made by user of enumerator back to the original repeated field.
-      # This only applies in cases where the calling function which created the enumerator,
-      # such as #sort!, modifies itself rather than a new array, such as #sort
-      class ProxyingEnumerator < Struct.new(:repeated_field, :external_enumerator)
-        def each(*args, &block)
-          results = []
-          external_enumerator.each_with_index do |val, i|
-            result = yield(val)
-            results << result
-            #nil means no change occurred from yield; usually occurs when #to_a is called
-            if result
-              repeated_field[i] = result if result != val
-            end
-          end
-          results
-        end
-      end
-
       private
       include Google::Protobuf::Internal::Convert
 
       attr :name, :arena, :array, :type, :descriptor
 
+      def internal_deep_freeze
+        freeze
+        if type == :message
+          each do |element|
+            element.send :internal_deep_freeze
+          end
+        end
+        self
+      end
+
       def internal_push(*elements)
         elements.each do |element|
           append_msg_val convert_ruby_to_upb(element, arena, type, descriptor)
@@ -501,3 +379,5 @@
     end
   end
 end
+
+require 'google/protobuf/repeated_field'
diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyDescriptor.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyDescriptor.java
index b809253..aa64d1a 100644
--- a/ruby/src/main/java/com/google/protobuf/jruby/RubyDescriptor.java
+++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyDescriptor.java
@@ -32,6 +32,7 @@
 
 package com.google.protobuf.jruby;
 
+import com.google.protobuf.CodedInputStream;
 import com.google.protobuf.Descriptors.Descriptor;
 import com.google.protobuf.Descriptors.FieldDescriptor;
 import com.google.protobuf.Descriptors.OneofDescriptor;
@@ -158,6 +159,22 @@
     return Helpers.nullToNil(oneofDescriptors.get(Utils.symToString(name)), context.nil);
   }
 
+  @JRubyMethod
+  public IRubyObject options(ThreadContext context) {
+    RubyDescriptorPool pool = (RubyDescriptorPool) RubyDescriptorPool.generatedPool(null, null);
+    RubyDescriptor messageOptionsDescriptor =
+        (RubyDescriptor)
+            pool.lookup(context, context.runtime.newString("google.protobuf.MessageOptions"));
+    RubyClass messageOptionsClass = (RubyClass) messageOptionsDescriptor.msgclass(context);
+    RubyMessage msg = (RubyMessage) messageOptionsClass.newInstance(context, Block.NULL_BLOCK);
+    return msg.decodeBytes(
+        context,
+        msg,
+        CodedInputStream.newInstance(
+            descriptor.getOptions().toByteString().toByteArray()), /*freeze*/
+        true);
+  }
+
   protected FieldDescriptor getField(String name) {
     return descriptor.findFieldByName(name);
   }
diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyEnumDescriptor.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyEnumDescriptor.java
index 24c03cd..7e8247c 100644
--- a/ruby/src/main/java/com/google/protobuf/jruby/RubyEnumDescriptor.java
+++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyEnumDescriptor.java
@@ -32,6 +32,7 @@
 
 package com.google.protobuf.jruby;
 
+import com.google.protobuf.CodedInputStream;
 import com.google.protobuf.DescriptorProtos.EnumDescriptorProto;
 import com.google.protobuf.Descriptors.EnumDescriptor;
 import com.google.protobuf.Descriptors.EnumValueDescriptor;
@@ -124,6 +125,22 @@
     return RubyFileDescriptor.getRubyFileDescriptor(context, descriptor);
   }
 
+  @JRubyMethod
+  public IRubyObject options(ThreadContext context) {
+    RubyDescriptorPool pool = (RubyDescriptorPool) RubyDescriptorPool.generatedPool(null, null);
+    RubyDescriptor enumOptionsDescriptor =
+        (RubyDescriptor)
+            pool.lookup(context, context.runtime.newString("google.protobuf.EnumOptions"));
+    RubyClass enumOptionsClass = (RubyClass) enumOptionsDescriptor.msgclass(context);
+    RubyMessage msg = (RubyMessage) enumOptionsClass.newInstance(context, Block.NULL_BLOCK);
+    return msg.decodeBytes(
+        context,
+        msg,
+        CodedInputStream.newInstance(
+            descriptor.getOptions().toByteString().toByteArray()), /*freeze*/
+        true);
+  }
+
   public boolean isValidValue(ThreadContext context, IRubyObject value) {
     EnumValueDescriptor enumValue;
 
@@ -198,9 +215,9 @@
         // always start with uppercase letters. We tolerate this case by capitalizing
         // the first character if possible.
         return new StringBuilder()
-                .appendCodePoint(Character.toUpperCase(ch))
-                .append(name.substring(1))
-                .toString();
+            .appendCodePoint(Character.toUpperCase(ch))
+            .append(name.substring(1))
+            .toString();
       }
     }
     return name;
diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyFieldDescriptor.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyFieldDescriptor.java
index 50f42cf..5ba86ef 100644
--- a/ruby/src/main/java/com/google/protobuf/jruby/RubyFieldDescriptor.java
+++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyFieldDescriptor.java
@@ -32,11 +32,13 @@
 
 package com.google.protobuf.jruby;
 
+import com.google.protobuf.CodedInputStream;
 import com.google.protobuf.Descriptors.FieldDescriptor;
 import com.google.protobuf.LegacyDescriptorsUtil.LegacyFileDescriptor;
 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;
@@ -231,6 +233,21 @@
     return context.nil;
   }
 
+  @JRubyMethod
+  public IRubyObject options(ThreadContext context) {
+    RubyDescriptor fieldOptionsDescriptor =
+        (RubyDescriptor)
+            pool.lookup(context, context.runtime.newString("google.protobuf.FieldOptions"));
+    RubyClass fieldOptionsClass = (RubyClass) fieldOptionsDescriptor.msgclass(context);
+    RubyMessage msg = (RubyMessage) fieldOptionsClass.newInstance(context, Block.NULL_BLOCK);
+    return msg.decodeBytes(
+        context,
+        msg,
+        CodedInputStream.newInstance(
+            descriptor.getOptions().toByteString().toByteArray()), /*freeze*/
+        true);
+  }
+
   protected void setDescriptor(
       ThreadContext context, FieldDescriptor descriptor, RubyDescriptorPool pool) {
     if (descriptor.isRequired()
diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyFileDescriptor.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyFileDescriptor.java
index 6504806..db9580d 100644
--- a/ruby/src/main/java/com/google/protobuf/jruby/RubyFileDescriptor.java
+++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyFileDescriptor.java
@@ -32,6 +32,7 @@
 
 package com.google.protobuf.jruby;
 
+import com.google.protobuf.CodedInputStream;
 import com.google.protobuf.Descriptors.FileDescriptor;
 import com.google.protobuf.Descriptors.GenericDescriptor;
 import com.google.protobuf.LegacyDescriptorsUtil.LegacyFileDescriptor;
@@ -106,6 +107,22 @@
     }
   }
 
+  @JRubyMethod
+  public IRubyObject options(ThreadContext context) {
+    RubyDescriptorPool pool = (RubyDescriptorPool) RubyDescriptorPool.generatedPool(null, null);
+    RubyDescriptor fileOptionsDescriptor =
+        (RubyDescriptor)
+            pool.lookup(context, context.runtime.newString("google.protobuf.FileOptions"));
+    RubyClass fileOptionsClass = (RubyClass) fileOptionsDescriptor.msgclass(context);
+    RubyMessage msg = (RubyMessage) fileOptionsClass.newInstance(context, Block.NULL_BLOCK);
+    return msg.decodeBytes(
+        context,
+        msg,
+        CodedInputStream.newInstance(
+            fileDescriptor.getOptions().toByteString().toByteArray()), /*freeze*/
+        true);
+  }
+
   private static RubyClass cFileDescriptor;
 
   private FileDescriptor fileDescriptor;
diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyMap.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyMap.java
index 8727b13..f3849b1 100644
--- a/ruby/src/main/java/com/google/protobuf/jruby/RubyMap.java
+++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyMap.java
@@ -372,6 +372,16 @@
     return RubyHash.newHash(context.runtime, mapForHash, context.nil);
   }
 
+  protected IRubyObject deepFreeze(ThreadContext context) {
+    setFrozen(true);
+    if (valueType == FieldDescriptor.Type.MESSAGE) {
+      for (IRubyObject key : table.keySet()) {
+        ((RubyMessage) table.get(key)).deepFreeze(context);
+      }
+    }
+    return this;
+  }
+
   // Used by Google::Protobuf.deep_copy but not exposed directly.
   protected IRubyObject deepCopy(ThreadContext context) {
     RubyMap newMap = newThisType(context);
diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyMessage.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyMessage.java
index 209f35e..25f9dca 100644
--- a/ruby/src/main/java/com/google/protobuf/jruby/RubyMessage.java
+++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyMessage.java
@@ -628,7 +628,11 @@
         input.setRecursionLimit(((RubyNumeric) recursionLimit).getIntValue());
       }
     }
+    return decodeBytes(context, ret, input, /*freeze*/ false);
+  }
 
+  public static IRubyObject decodeBytes(
+      ThreadContext context, RubyMessage ret, CodedInputStream input, boolean freeze) {
     try {
       ret.builder.mergeFrom(input);
     } catch (Exception e) {
@@ -658,7 +662,9 @@
                 }
               });
     }
-
+    if (freeze) {
+      ret.deepFreeze(context);
+    }
     return ret;
   }
 
@@ -811,6 +817,22 @@
     return ret;
   }
 
+  protected IRubyObject deepFreeze(ThreadContext context) {
+    setFrozen(true);
+    for (FieldDescriptor fdef : descriptor.getFields()) {
+      if (fdef.isMapField()) {
+        ((RubyMap) fields.get(fdef)).deepFreeze(context);
+      } else if (fdef.isRepeated()) {
+        this.getRepeatedField(context, fdef).deepFreeze(context);
+      } else if (fields.containsKey(fdef)) {
+        if (fdef.getType() == FieldDescriptor.Type.MESSAGE) {
+          ((RubyMessage) fields.get(fdef)).deepFreeze(context);
+        }
+      }
+    }
+    return this;
+  }
+
   protected DynamicMessage build(ThreadContext context, int depth, int recursionLimit) {
     if (depth >= recursionLimit) {
       throw context.runtime.newRuntimeError("Recursion limit exceeded during encoding.");
diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyOneofDescriptor.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyOneofDescriptor.java
index 5ade98b..36fe2d0 100644
--- a/ruby/src/main/java/com/google/protobuf/jruby/RubyOneofDescriptor.java
+++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyOneofDescriptor.java
@@ -1,5 +1,6 @@
 package com.google.protobuf.jruby;
 
+import com.google.protobuf.CodedInputStream;
 import com.google.protobuf.Descriptors.FieldDescriptor;
 import com.google.protobuf.Descriptors.OneofDescriptor;
 import java.util.ArrayList;
@@ -66,6 +67,22 @@
     return context.nil;
   }
 
+  @JRubyMethod
+  public IRubyObject options(ThreadContext context) {
+    RubyDescriptorPool pool = (RubyDescriptorPool) RubyDescriptorPool.generatedPool(null, null);
+    RubyDescriptor oneofOptionsDescriptor =
+        (RubyDescriptor)
+            pool.lookup(context, context.runtime.newString("google.protobuf.OneofOptions"));
+    RubyClass oneofOptionsClass = (RubyClass) oneofOptionsDescriptor.msgclass(context);
+    RubyMessage msg = (RubyMessage) oneofOptionsClass.newInstance(context, Block.NULL_BLOCK);
+    return msg.decodeBytes(
+        context,
+        msg,
+        CodedInputStream.newInstance(
+            descriptor.getOptions().toByteString().toByteArray()), /*freeze*/
+        true);
+  }
+
   protected Collection<RubyFieldDescriptor> getFields() {
     return fields;
   }
diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyRepeatedField.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyRepeatedField.java
index 90ab541..085dd1c 100644
--- a/ruby/src/main/java/com/google/protobuf/jruby/RubyRepeatedField.java
+++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyRepeatedField.java
@@ -111,6 +111,7 @@
    */
   @JRubyMethod(name = "[]=")
   public IRubyObject indexSet(ThreadContext context, IRubyObject index, IRubyObject value) {
+    testFrozen("Can't set index in frozen repeated field");
     int arrIndex = normalizeArrayIndex(index);
     value = Utils.checkType(context, fieldType, name, value, (RubyModule) typeClass);
     IRubyObject defaultValue = defaultValue(context);
@@ -183,6 +184,7 @@
       required = 1,
       rest = true)
   public IRubyObject push(ThreadContext context, IRubyObject[] args) {
+    testFrozen("Can't push frozen repeated field");
     for (int i = 0; i < args.length; i++) {
       IRubyObject val = args[i];
       if (fieldType != FieldDescriptor.Type.MESSAGE || !val.isNil()) {
@@ -199,6 +201,7 @@
    */
   @JRubyMethod(visibility = org.jruby.runtime.Visibility.PRIVATE)
   public IRubyObject pop_one(ThreadContext context) {
+    testFrozen("Can't pop frozen repeated field");
     IRubyObject ret = this.storage.last();
     this.storage.remove(ret);
     return ret;
@@ -212,6 +215,7 @@
    */
   @JRubyMethod
   public IRubyObject replace(ThreadContext context, IRubyObject list) {
+    testFrozen("Can't replace frozen repeated field");
     RubyArray arr = (RubyArray) list;
     checkArrayElementType(context, arr);
     this.storage = arr;
@@ -226,6 +230,7 @@
    */
   @JRubyMethod
   public IRubyObject clear(ThreadContext context) {
+    testFrozen("Can't clear frozen repeated field");
     this.storage.clear();
     return this;
   }
@@ -274,6 +279,7 @@
    */
   @JRubyMethod
   public IRubyObject concat(ThreadContext context, IRubyObject list) {
+    testFrozen("Can't concat frozen repeated field");
     if (list instanceof RubyArray) {
       checkArrayElementType(context, (RubyArray) list);
       this.storage.addAll((RubyArray) list);
@@ -352,6 +358,16 @@
     return storage.inspect();
   }
 
+  protected IRubyObject deepFreeze(ThreadContext context) {
+    setFrozen(true);
+    if (fieldType == FieldDescriptor.Type.MESSAGE) {
+      for (int i = 0; i < size(); i++) {
+        ((RubyMessage) storage.eltInternal(i)).deepFreeze(context);
+      }
+    }
+    return this;
+  }
+
   // Java API
   protected IRubyObject get(int index) {
     return this.storage.eltInternal(index);
diff --git a/ruby/tests/basic.rb b/ruby/tests/basic.rb
index b70f630..9cd2d70 100755
--- a/ruby/tests/basic.rb
+++ b/ruby/tests/basic.rb
@@ -695,6 +695,50 @@
         msg.map_string_int32_as_value = :boom
       end
     end
+
+    def test_file_descriptor_options
+      file_descriptor = TestMessage.descriptor.file_descriptor
+
+      assert_instance_of Google::Protobuf::FileOptions, file_descriptor.options
+      assert file_descriptor.options.deprecated
+    end
+
+    def test_field_descriptor_options
+      field_descriptor = TestDeprecatedMessage.descriptor.lookup("foo")
+
+      assert_instance_of Google::Protobuf::FieldOptions, field_descriptor.options
+      assert field_descriptor.options.deprecated
+    end
+
+    def test_descriptor_options
+      descriptor = TestDeprecatedMessage.descriptor
+
+      assert_instance_of Google::Protobuf::MessageOptions, descriptor.options
+      assert descriptor.options.deprecated
+    end
+
+    def test_enum_descriptor_options
+      enum_descriptor = TestDeprecatedEnum.descriptor
+
+      assert_instance_of Google::Protobuf::EnumOptions, enum_descriptor.options
+      assert enum_descriptor.options.deprecated
+    end
+
+    def test_oneof_descriptor_options
+      descriptor = TestDeprecatedMessage.descriptor
+      oneof_descriptor = descriptor.lookup_oneof("test_deprecated_message_oneof")
+
+      assert_instance_of Google::Protobuf::OneofOptions, oneof_descriptor.options
+    end
+
+    def test_options_deep_freeze
+      descriptor = TestDeprecatedMessage.descriptor
+
+      assert_raise FrozenError do
+        descriptor.options.uninterpreted_option.push \
+          Google::Protobuf::UninterpretedOption.new
+      end
+    end
   end
 
   def test_oneof_fields_respond_to? # regression test for issue 9202
diff --git a/ruby/tests/basic_test.proto b/ruby/tests/basic_test.proto
index d480d48..89eb9cc 100644
--- a/ruby/tests/basic_test.proto
+++ b/ruby/tests/basic_test.proto
@@ -2,12 +2,14 @@
 
 package basic_test;
 
-import "google/protobuf/wrappers.proto";
-import "google/protobuf/timestamp.proto";
 import "google/protobuf/duration.proto";
 import "google/protobuf/struct.proto";
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/wrappers.proto";
 import "test_import_proto2.proto";
 
+option deprecated = true;
+
 message Foo {
   Bar bar = 1;
   repeated Baz baz = 2;
@@ -68,6 +70,20 @@
   optional int32 foo = 1;
 }
 
+message TestDeprecatedMessage {
+  option deprecated = true;
+
+  optional int32 foo = 1 [deprecated = true];
+
+  oneof test_deprecated_message_oneof {
+    string a = 2;
+    int32 b = 3;
+  }
+
+  map<string, TestMessage2> map_string_msg = 4;
+  repeated TestMessage2 repeated_msg = 5;
+}
+
 enum TestEnum {
   Default = 0;
   A = 1;
@@ -76,6 +92,13 @@
   v0 = 4;
 }
 
+enum TestDeprecatedEnum {
+  option deprecated = true;
+
+  DefaultA = 0;
+  AA = 1 [deprecated = true];
+}
+
 message TestEmbeddedMessageParent {
   TestEmbeddedMessageChild child_msg = 1;
   int32 number = 2;
@@ -130,8 +153,7 @@
   map<int32, Inner> items = 1;
 }
 
-message Inner {
-}
+message Inner {}
 
 message Wrapper {
   google.protobuf.DoubleValue double = 1;
@@ -213,8 +235,8 @@
 }
 
 message WithJsonName {
-  optional int32 foo_bar = 1 [json_name="jsonFooBar"];
-  repeated WithJsonName baz = 2 [json_name="jsonBaz"];
+  optional int32 foo_bar = 1 [json_name = "jsonFooBar"];
+  repeated WithJsonName baz = 2 [json_name = "jsonBaz"];
 }
 
 message HelloRequest {