From 7309e8882cb81a92cee4b0be708a61c8f4707224 Mon Sep 17 00:00:00 2001
From: Jean Boussier <jean.boussier@gmail.com>
Date: Wed, 20 Jul 2022 10:37:09 +0200
Subject: [PATCH] Ruby: Use class inheritance to save memory

The only difference between all the `Message` classes is just
their `descriptor` instance variable.

So rather than create an entirely new class from scratch every time
we can simply inherit from an abstract class.

This shink each `Message` class from `1456` bytes to `944` bytes, and
the singleton class of each from `960` to `792`, for a total of
`680` bytes saved per message class, so a ~28% reduction.
---
 ruby/ext/google/protobuf_c/message.c     | 61 +++++++++++++-----------
 ruby/lib/google/protobuf/message_exts.rb |  5 ++
 2 files changed, 37 insertions(+), 29 deletions(-)

diff --git a/ruby/ext/google/protobuf_c/message.c b/ruby/ext/google/protobuf_c/message.c
index e430b79490..b11878545f 100644
--- a/ruby/ext/google/protobuf_c/message.c
+++ b/ruby/ext/google/protobuf_c/message.c
@@ -37,6 +37,7 @@
 #include "repeated_field.h"
 
 static VALUE cParseError = Qnil;
+static VALUE cAbstractMessage = Qnil;
 static ID descriptor_instancevar_interned;
 
 static VALUE initialize_rb_class_with_no_args(VALUE klass) {
@@ -1201,36 +1202,8 @@ VALUE build_class_from_descriptor(VALUE descriptor) {
   klass = rb_define_class_id(
       // Docs say this parameter is ignored. User will assign return value to
       // their own toplevel constant class name.
-      rb_intern("Message"), rb_cObject);
+      rb_intern("Message"), cAbstractMessage);
   rb_ivar_set(klass, descriptor_instancevar_interned, descriptor);
-  rb_define_alloc_func(klass, Message_alloc);
-  rb_require("google/protobuf/message_exts");
-  rb_include_module(klass, rb_eval_string("::Google::Protobuf::MessageExts"));
-  rb_extend_object(
-      klass, rb_eval_string("::Google::Protobuf::MessageExts::ClassMethods"));
-
-  rb_define_method(klass, "method_missing", Message_method_missing, -1);
-  rb_define_method(klass, "respond_to_missing?", Message_respond_to_missing,
-                   -1);
-  rb_define_method(klass, "initialize", Message_initialize, -1);
-  rb_define_method(klass, "dup", Message_dup, 0);
-  // Also define #clone so that we don't inherit Object#clone.
-  rb_define_method(klass, "clone", Message_dup, 0);
-  rb_define_method(klass, "==", Message_eq, 1);
-  rb_define_method(klass, "eql?", Message_eq, 1);
-  rb_define_method(klass, "freeze", Message_freeze, 0);
-  rb_define_method(klass, "hash", Message_hash, 0);
-  rb_define_method(klass, "to_h", Message_to_h, 0);
-  rb_define_method(klass, "inspect", Message_inspect, 0);
-  rb_define_method(klass, "to_s", Message_inspect, 0);
-  rb_define_method(klass, "[]", Message_index, 1);
-  rb_define_method(klass, "[]=", Message_index_set, 2);
-  rb_define_singleton_method(klass, "decode", Message_decode, -1);
-  rb_define_singleton_method(klass, "encode", Message_encode, -1);
-  rb_define_singleton_method(klass, "decode_json", Message_decode_json, -1);
-  rb_define_singleton_method(klass, "encode_json", Message_encode_json, -1);
-  rb_define_singleton_method(klass, "descriptor", Message_descriptor, 0);
-
   return klass;
 }
 
@@ -1392,8 +1365,38 @@ const upb_Message* Message_GetUpbMessage(VALUE value, const upb_MessageDef* m,
   return self->msg;
 }
 
+static void Message_define_class(VALUE klass) {
+  rb_define_alloc_func(klass, Message_alloc);
+
+  rb_require("google/protobuf/message_exts");
+  rb_define_method(klass, "method_missing", Message_method_missing, -1);
+  rb_define_method(klass, "respond_to_missing?", Message_respond_to_missing,
+                   -1);
+  rb_define_method(klass, "initialize", Message_initialize, -1);
+  rb_define_method(klass, "dup", Message_dup, 0);
+  // Also define #clone so that we don't inherit Object#clone.
+  rb_define_method(klass, "clone", Message_dup, 0);
+  rb_define_method(klass, "==", Message_eq, 1);
+  rb_define_method(klass, "eql?", Message_eq, 1);
+  rb_define_method(klass, "freeze", Message_freeze, 0);
+  rb_define_method(klass, "hash", Message_hash, 0);
+  rb_define_method(klass, "to_h", Message_to_h, 0);
+  rb_define_method(klass, "inspect", Message_inspect, 0);
+  rb_define_method(klass, "to_s", Message_inspect, 0);
+  rb_define_method(klass, "[]", Message_index, 1);
+  rb_define_method(klass, "[]=", Message_index_set, 2);
+  rb_define_singleton_method(klass, "decode", Message_decode, -1);
+  rb_define_singleton_method(klass, "encode", Message_encode, -1);
+  rb_define_singleton_method(klass, "decode_json", Message_decode_json, -1);
+  rb_define_singleton_method(klass, "encode_json", Message_encode_json, -1);
+  rb_define_singleton_method(klass, "descriptor", Message_descriptor, 0);
+}
+
 void Message_register(VALUE protobuf) {
   cParseError = rb_const_get(protobuf, rb_intern("ParseError"));
+  cAbstractMessage = rb_define_class_under(protobuf, "AbstractMessage", rb_cObject);
+  Message_define_class(cAbstractMessage);
+  rb_gc_register_address(&cAbstractMessage);
 
   // Ruby-interned string: "descriptor". We use this identifier to store an
   // instance variable on message classes we create in order to link them back
diff --git a/ruby/lib/google/protobuf/message_exts.rb b/ruby/lib/google/protobuf/message_exts.rb
index 6608521728..3cc1a08860 100644
--- a/ruby/lib/google/protobuf/message_exts.rb
+++ b/ruby/lib/google/protobuf/message_exts.rb
@@ -49,5 +49,10 @@ module Google
       end
 
     end
+    class AbstractMessage
+      include MessageExts
+      extend MessageExts::ClassMethods
+    end
+    private_constant :AbstractMessage
   end
 end