[gjs/ewlsh/wrap-es-class: 10/10] overrides: Support class fields in GObject.registerClass




commit e0410e4c34372fa5f4c9f2001b209216c1be12dc
Author: Evan Welsh <contact evanwelsh com>
Date:   Sat Jan 8 22:44:07 2022 -0800

    overrides: Support class fields in GObject.registerClass

 gi/gobject.cpp                         |  12 +
 gi/private.cpp                         | 185 ++++++++++++---
 gi/wrapperutils.h                      |  71 +++++-
 gjs/atoms.h                            |   1 +
 installed-tests/js/testGObjectClass.js | 273 +++++++++++++++++++++-
 installed-tests/js/testGtk3.js         |  17 +-
 installed-tests/js/testGtk4.js         |  17 +-
 modules/core/_common.js                |  25 ++-
 modules/core/overrides/GObject.js      | 399 ++++++++++++++++++++++++---------
 modules/core/overrides/Gtk.js          | 112 ++++-----
 10 files changed, 904 insertions(+), 208 deletions(-)
---
diff --git a/gi/gobject.cpp b/gi/gobject.cpp
index d6501624..f589e7b0 100644
--- a/gi/gobject.cpp
+++ b/gi/gobject.cpp
@@ -84,6 +84,18 @@ static bool jsobj_set_gproperty(JSContext* cx, JS::HandleObject object,
             if (jsprop.setter() &&
                 !JS_SetProperty(cx, object, pspec->name, jsvalue))
                 return false;
+
+            // If a getter is defined, redefine the property with that getter.
+            if (jsprop.getterObject()) {
+                unsigned getter_flags = GJS_MODULE_PROP_FLAGS | JSPROP_GETTER;
+                JS::RootedObject getter(cx, jsprop.getterObject());
+                return JS_DefineProperty(cx, object, underscore_name, getter,
+                                         nullptr, getter_flags) &&
+                       JS_DefineProperty(cx, object, camel_name, getter,
+                                         nullptr, getter_flags) &&
+                       JS_DefineProperty(cx, object, pspec->name, getter,
+                                         nullptr, getter_flags);
+            }
         }
 
         return JS_DefineProperty(cx, object, underscore_name, jsvalue, flags) &&
diff --git a/gi/private.cpp b/gi/private.cpp
index 4a07afb1..d859f50b 100644
--- a/gi/private.cpp
+++ b/gi/private.cpp
@@ -16,6 +16,7 @@
 #include <js/RootingAPI.h>
 #include <js/TypeDecls.h>
 #include <js/Utility.h>  // for UniqueChars
+#include <js/ValueArray.h>
 #include <jsapi.h>       // for JS_GetElement
 
 #include "gi/gobject.h"
@@ -175,17 +176,30 @@ static bool get_interface_gtypes(JSContext* cx, JS::HandleObject interfaces,
 }
 
 GJS_JSAPI_RETURN_CONVENTION
-static bool gjs_register_interface(JSContext* cx, unsigned argc,
-                                   JS::Value* vp) {
-    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+static bool create_wrapper_array(JSContext* cx, JS::HandleObject prototype,
+                                 GType type, JS::MutableHandleValue rval) {
+    JS::RootedObject gtype_wrapper(cx,
+                                   gjs_gtype_create_gtype_wrapper(cx, type));
+    if (!gtype_wrapper)
+        return false;
 
-    JS::UniqueChars name;
-    JS::RootedObject interfaces(cx), properties(cx);
-    if (!gjs_parse_call_args(cx, "register_interface", args, "soo", "name",
-                             &name, "interfaces", &interfaces, "properties",
-                             &properties))
+    JS::RootedValueArray<2> tuple(cx);
+    tuple[0].setObject(*prototype);
+    tuple[1].setObject(*gtype_wrapper);
+
+    JS::RootedObject array(cx, JS::NewArrayObject(cx, tuple));
+    if (!array)
         return false;
 
+    rval.setObject(*array);
+    return true;
+}
+
+GJS_JSAPI_RETURN_CONVENTION
+static bool gjs_register_interface_impl(JSContext* cx, const char* name,
+                                        JS::HandleObject interfaces,
+                                        JS::HandleObject properties,
+                                        GType* gtype) {
     uint32_t n_interfaces, n_properties;
     if (!validate_interfaces_and_properties_args(cx, interfaces, properties,
                                                  &n_interfaces, &n_properties))
@@ -198,13 +212,13 @@ static bool gjs_register_interface(JSContext* cx, unsigned argc,
     if (!get_interface_gtypes(cx, interfaces, n_interfaces, iface_types))
         return false;
 
-    if (g_type_from_name(name.get()) != G_TYPE_INVALID) {
-        gjs_throw(cx, "Type name %s is already registered", name.get());
+    if (g_type_from_name(name) != G_TYPE_INVALID) {
+        gjs_throw(cx, "Type name %s is already registered", name);
         return false;
     }
 
     GTypeInfo type_info = gjs_gobject_interface_info;
-    GType interface_type = g_type_register_static(G_TYPE_INTERFACE, name.get(),
+    GType interface_type = g_type_register_static(G_TYPE_INTERFACE, name,
                                                   &type_info, GTypeFlags(0));
 
     g_type_set_qdata(interface_type, ObjectBase::custom_type_quark(),
@@ -217,6 +231,27 @@ static bool gjs_register_interface(JSContext* cx, unsigned argc,
     for (uint32_t ix = 0; ix < n_interfaces; ix++)
         g_type_interface_add_prerequisite(interface_type, iface_types[ix]);
 
+    *gtype = interface_type;
+    return true;
+}
+
+GJS_JSAPI_RETURN_CONVENTION
+static bool gjs_register_interface(JSContext* cx, unsigned argc,
+                                   JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+    JS::UniqueChars name;
+    JS::RootedObject interfaces(cx), properties(cx);
+    if (!gjs_parse_call_args(cx, "register_interface", args, "soo", "name",
+                             &name, "interfaces", &interfaces, "properties",
+                             &properties))
+        return false;
+
+    GType interface_type;
+    if (!gjs_register_interface_impl(cx, name.get(), interfaces, properties,
+                                     &interface_type))
+        return false;
+
     /* create a custom JSClass */
     JS::RootedObject module(cx, gjs_lookup_private_namespace(cx));
     if (!module)
@@ -231,6 +266,36 @@ static bool gjs_register_interface(JSContext* cx, unsigned argc,
     return true;
 }
 
+GJS_JSAPI_RETURN_CONVENTION
+static bool gjs_register_interface_with_class(JSContext* cx, unsigned argc,
+                                              JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+    JS::UniqueChars name;
+    JS::RootedObject klass(cx), interfaces(cx), properties(cx);
+    if (!gjs_parse_call_args(cx, "register_interface_with_class", args, "osoo",
+                             "class", &klass, "name", &name, "interfaces",
+                             &interfaces, "properties", &properties))
+        return false;
+
+    GType interface_type;
+    if (!gjs_register_interface_impl(cx, name.get(), interfaces, properties,
+                                     &interface_type))
+        return false;
+
+    /* create a custom JSClass */
+    JS::RootedObject module(cx, gjs_lookup_private_namespace(cx));
+    if (!module)
+        return false;  // error will have been thrown already
+
+    JS::RootedObject prototype(cx);
+    if (!InterfacePrototype::wrap_class(cx, module, nullptr, interface_type,
+                                        klass, &prototype))
+        return false;
+
+    return create_wrapper_array(cx, prototype, interface_type, args.rval());
+}
+
 static inline void gjs_add_interface(GType instance_type,
                                      GType interface_type) {
     static GInterfaceInfo interface_vtable{nullptr, nullptr, nullptr};
@@ -239,18 +304,13 @@ static inline void gjs_add_interface(GType instance_type,
 }
 
 GJS_JSAPI_RETURN_CONVENTION
-static bool gjs_register_type(JSContext* cx, unsigned argc, JS::Value* vp) {
-    JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
-
-    JS::UniqueChars name;
-    GTypeFlags type_flags;
-    JS::RootedObject parent(cx), interfaces(cx), properties(cx);
-    if (!gjs_parse_call_args(cx, "register_type", argv, "osioo", "parent",
-                             &parent, "name", &name, "flags", &type_flags,
-                             "interfaces", &interfaces,
-                             "properties", &properties))
-        return false;
-
+static bool gjs_register_type_impl(JSContext* cx, const char* name,
+                                   GTypeFlags type_flags,
+                                   JS::HandleObject parent,
+                                   JS::HandleObject interfaces,
+                                   JS::HandleObject properties,
+                                   GType** iface_types_out,
+                                   uint32_t* n_interfaces_out, GType* gtype) {
     if (!parent)
         return false;
 
@@ -272,8 +332,8 @@ static bool gjs_register_type(JSContext* cx, unsigned argc, JS::Value* vp) {
     if (!get_interface_gtypes(cx, interfaces, n_interfaces, iface_types))
         return false;
 
-    if (g_type_from_name(name.get()) != G_TYPE_INVALID) {
-        gjs_throw(cx, "Type name %s is already registered", name.get());
+    if (g_type_from_name(name) != G_TYPE_INVALID) {
+        gjs_throw(cx, "Type name %s is already registered", name);
         return false;
     }
 
@@ -292,8 +352,8 @@ static bool gjs_register_type(JSContext* cx, unsigned argc, JS::Value* vp) {
     type_info.class_size = query.class_size;
     type_info.instance_size = query.instance_size;
 
-    GType instance_type = g_type_register_static(
-        parent_priv->gtype(), name.get(), &type_info, type_flags);
+    GType instance_type = g_type_register_static(parent_priv->gtype(), name,
+                                                 &type_info, type_flags);
 
     g_type_set_qdata(instance_type, ObjectBase::custom_type_quark(),
                      GINT_TO_POINTER(1));
@@ -305,6 +365,33 @@ static bool gjs_register_type(JSContext* cx, unsigned argc, JS::Value* vp) {
     for (uint32_t ix = 0; ix < n_interfaces; ix++)
         gjs_add_interface(instance_type, iface_types[ix]);
 
+    *gtype = instance_type;
+    *n_interfaces_out = n_interfaces;
+    *iface_types_out = iface_types.release();
+    return true;
+}
+
+GJS_JSAPI_RETURN_CONVENTION
+static bool gjs_register_type(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
+
+    JS::UniqueChars name;
+    GTypeFlags type_flags;
+    JS::RootedObject parent(cx), interfaces(cx), properties(cx);
+    if (!gjs_parse_call_args(cx, "register_type", argv, "osioo", "parent",
+                             &parent, "name", &name, "flags", &type_flags,
+                             "interfaces", &interfaces, "properties",
+                             &properties))
+        return false;
+
+    GType instance_type;
+    GjsAutoPointer<GType> iface_types;
+    uint32_t n_interfaces;
+    if (!gjs_register_type_impl(cx, name.get(), type_flags, parent, interfaces,
+                                properties, iface_types.out(), &n_interfaces,
+                                &instance_type))
+        return false;
+
     /* create a custom JSClass */
     JS::RootedObject module(cx, gjs_lookup_private_namespace(cx));
     JS::RootedObject constructor(cx), prototype(cx);
@@ -321,6 +408,44 @@ static bool gjs_register_type(JSContext* cx, unsigned argc, JS::Value* vp) {
     return true;
 }
 
+GJS_JSAPI_RETURN_CONVENTION
+static bool gjs_register_type_with_class(JSContext* cx, unsigned argc,
+                                         JS::Value* vp) {
+    JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
+
+    JS::UniqueChars name;
+    GTypeFlags type_flags;
+    JS::RootedObject klass(cx), parent(cx), interfaces(cx), properties(cx);
+    if (!gjs_parse_call_args(cx, "register_type_with_class", argv, "oosioo",
+                             "class", &klass, "parent", &parent, "name", &name,
+                             "flags", &type_flags, "interfaces", &interfaces,
+                             "properties", &properties))
+        return false;
+
+    GType instance_type;
+    uint32_t n_interfaces;
+    GjsAutoPointer<GType> iface_types;
+    if (!gjs_register_type_impl(cx, name.get(), type_flags, parent, interfaces,
+                                properties, iface_types.out(), &n_interfaces,
+                                &instance_type))
+        return false;
+
+    /* create a custom JSClass */
+    JS::RootedObject module(cx, gjs_lookup_private_namespace(cx));
+    JS::RootedObject prototype(cx);
+
+    auto* priv = ObjectPrototype::wrap_class(cx, module, nullptr, instance_type,
+                                             klass, &prototype);
+
+    if (!priv)
+        return false;
+
+    priv->set_interfaces(iface_types, n_interfaces);
+    priv->set_type_qdata();
+
+    return create_wrapper_array(cx, prototype, instance_type, argv.rval());
+}
+
 GJS_JSAPI_RETURN_CONVENTION
 static bool gjs_signal_new(JSContext* cx, unsigned argc, JS::Value* vp) {
     JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
@@ -407,12 +532,18 @@ static JSFunctionSpec private_module_funcs[] = {
     JS_FN("override_property", gjs_override_property, 2, GJS_MODULE_PROP_FLAGS),
     JS_FN("register_interface", gjs_register_interface, 3,
           GJS_MODULE_PROP_FLAGS),
+    JS_FN("register_interface_with_class", gjs_register_interface_with_class, 4,
+          GJS_MODULE_PROP_FLAGS),
     JS_FN("register_type", gjs_register_type, 4, GJS_MODULE_PROP_FLAGS),
+    JS_FN("register_type_with_class", gjs_register_type_with_class, 5,
+          GJS_MODULE_PROP_FLAGS),
     JS_FN("signal_new", gjs_signal_new, 6, GJS_MODULE_PROP_FLAGS),
     JS_FS_END,
 };
 
 static JSPropertySpec private_module_props[] = {
+    JS_PSG("gobject_prototype_symbol",
+           symbol_getter<&GjsAtoms::gobject_prototype>, GJS_MODULE_PROP_FLAGS),
     JS_PSG("hook_up_vfunc_symbol", symbol_getter<&GjsAtoms::hook_up_vfunc>,
            GJS_MODULE_PROP_FLAGS),
     JS_PSG("signal_find_symbol", symbol_getter<&GjsAtoms::signal_find>,
diff --git a/gi/wrapperutils.h b/gi/wrapperutils.h
index 1a20b1b5..cbfb1884 100644
--- a/gi/wrapperutils.h
+++ b/gi/wrapperutils.h
@@ -303,12 +303,38 @@ class GIWrapperBase : public CWrapperPointerOps<Base> {
      */
     [[nodiscard]] static Prototype* resolve_prototype(JSContext* cx,
                                                       JS::HandleObject proto) {
-        if (JS_GetClass(proto) != &Base::klass) {
-            gjs_throw(cx, "Tried to construct an object without a GType");
+        if (JS_GetClass(proto) == &Base::klass) {
+            return Prototype::for_js(cx, proto);
+        }
+
+        const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+
+        bool has_property = false;
+        if (!JS_HasOwnPropertyById(cx, proto, atoms.gobject_prototype(),
+                                   &has_property))
+            return nullptr;
+
+        if (!has_property) {
+            gjs_throw(cx, "Tried to construct an object without a GType!");
+            return nullptr;
+        }
+
+        JS::RootedValue gobject_proto(cx);
+        if (!JS_GetPropertyById(cx, proto, atoms.gobject_prototype(),
+                                &gobject_proto))
+            return nullptr;
+
+        if (!gobject_proto.isObject()) {
+            gjs_throw(cx, "Tried to construct an object without a GType!");
             return nullptr;
         }
 
-        return Prototype::for_js(cx, proto);
+        JS::RootedObject obj(cx, &gobject_proto.toObject());
+        // gobject_prototype is an internal symbol so we can assert that it is
+        // only assigned to objects with &Base::klass definitions
+        g_assert(JS_GetClass(obj) == &Base::klass);
+
+        return Prototype::for_js(cx, obj);
     }
 
     /*
@@ -897,6 +923,45 @@ class GIWrapperPrototype : public Base {
         return proto;
     }
 
+    GJS_JSAPI_RETURN_CONVENTION
+    static Prototype* wrap_class(JSContext* cx, JS::HandleObject in_object,
+                                 Info* info, GType gtype,
+                                 JS::HandleObject constructor,
+                                 JS::MutableHandleObject prototype) {
+        g_assert(in_object);
+
+        GjsAutoPrototype priv = create_prototype(info, gtype);
+        if (!priv->init(cx))
+            return nullptr;
+
+        JS::RootedObject parent_proto(cx);
+        if (!priv->get_parent_proto(cx, &parent_proto))
+            return nullptr;
+
+        if (parent_proto) {
+            prototype.set(
+                JS_NewObjectWithGivenProto(cx, &Base::klass, parent_proto));
+        } else {
+            prototype.set(JS_NewObject(cx, &Base::klass));
+        }
+
+        if (!prototype)
+            return nullptr;
+
+        Prototype* proto = priv.release();
+        JS_SetPrivate(prototype, proto);
+
+        if (!proto->define_static_methods(cx, constructor))
+            return nullptr;
+
+        GjsAutoChar class_name = g_strdup_printf("%s", proto->name());
+        if (!JS_DefineProperty(cx, in_object, class_name, constructor,
+                               GJS_MODULE_PROP_FLAGS))
+            return nullptr;
+
+        return proto;
+    }
+
     // Methods to get an existing Prototype
 
     /*
diff --git a/gjs/atoms.h b/gjs/atoms.h
index 2c6e7c6b..1e0b72fc 100644
--- a/gjs/atoms.h
+++ b/gjs/atoms.h
@@ -75,6 +75,7 @@ class JSTracer;
     macro(zone, "zone")
 
 #define FOR_EACH_SYMBOL_ATOM(macro) \
+    macro(gobject_prototype, "__GObject__prototype") \
     macro(hook_up_vfunc, "__GObject__hook_up_vfunc") \
     macro(private_ns_marker, "__gjsPrivateNS") \
     macro(signal_find, "__GObject__signal_find") \
diff --git a/installed-tests/js/testGObjectClass.js b/installed-tests/js/testGObjectClass.js
index 41e7a8e9..af3f760a 100644
--- a/installed-tests/js/testGObjectClass.js
+++ b/installed-tests/js/testGObjectClass.js
@@ -115,6 +115,112 @@ const MyObject = GObject.registerClass({
     }
 });
 
+const MyObjectWithCustomConstructor = GObject.registerClass({
+    Properties: {
+        'readwrite': GObject.ParamSpec.string('readwrite', 'ParamReadwrite',
+            'A read write parameter', GObject.ParamFlags.READWRITE, ''),
+        'readonly': GObject.ParamSpec.string('readonly', 'ParamReadonly',
+            'A readonly parameter', GObject.ParamFlags.READABLE, ''),
+        'construct': GObject.ParamSpec.string('construct', 'ParamConstructOnly',
+            'A readwrite construct-only parameter',
+            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
+            ''),
+    },
+    Signals: {
+        'empty': {},
+        'minimal': {param_types: [GObject.TYPE_INT, GObject.TYPE_INT]},
+        'full': {
+            flags: GObject.SignalFlags.RUN_LAST,
+            accumulator: GObject.AccumulatorType.FIRST_WINS,
+            return_type: GObject.TYPE_INT,
+            param_types: [],
+        },
+        'run-last': {flags: GObject.SignalFlags.RUN_LAST},
+        'detailed': {
+            flags: GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.DETAILED,
+            param_types: [GObject.TYPE_STRING],
+        },
+    },
+}, class MyObjectWithCustomConstructor extends GObject.Object {
+    _readwrite;
+    _readonly;
+    _constructProp;
+
+    constructor({readwrite = 'foo', readonly = 'bar', construct = 'default'} = {}) {
+        super();
+
+        this._constructProp = construct;
+        this._readwrite = readwrite;
+        this._readonly = readonly;
+    }
+
+    get readwrite() {
+        return this._readwrite;
+    }
+
+    set readwrite(val) {
+        if (val === 'ignore')
+            return;
+
+        this._readwrite = val;
+    }
+
+    get readonly() {
+        return this._readonly;
+    }
+
+    set readonly(val) {
+        // this should never be called
+        void val;
+        this._readonly = 'bogus';
+    }
+
+    get construct() {
+        return this._constructProp;
+    }
+
+    notifyProp() {
+        this._readonly = 'changed';
+
+        this.notify('readonly');
+    }
+
+    emitEmpty() {
+        this.emit('empty');
+    }
+
+    emitMinimal(one, two) {
+        this.emit('minimal', one, two);
+    }
+
+    emitFull() {
+        return this.emit('full');
+    }
+
+    emitDetailed() {
+        this.emit('detailed::one');
+        this.emit('detailed::two');
+    }
+
+    emitRunLast(callback) {
+        this._run_last_callback = callback;
+        this.emit('run-last');
+    }
+
+    on_run_last() {
+        this._run_last_callback();
+    }
+
+    on_empty() {
+        this.empty_called = true;
+    }
+
+    on_full() {
+        this.full_default_handler_called = true;
+        return 79;
+    }
+});
+
 const MyAbstractObject = GObject.registerClass({
     GTypeFlags: GObject.TypeFlags.ABSTRACT,
 }, class MyAbstractObject extends GObject.Object {
@@ -145,6 +251,16 @@ const Derived = GObject.registerClass(class Derived extends MyObject {
     }
 });
 
+const DerivedWithCustomConstructor = GObject.registerClass(class DerivedWithCustomConstructor extends 
MyObjectWithCustomConstructor {
+    constructor() {
+        super({readwrite: 'yes'});
+    }
+});
+
+const ObjectWithDefaultConstructor = GObject.registerClass(class ObjectWithDefaultConstructor extends 
GObject.Object {
+
+});
+
 const Cla$$ = GObject.registerClass(class Cla$$ extends MyObject {});
 
 const MyCustomInit = GObject.registerClass(class MyCustomInit extends GObject.Object {
@@ -187,7 +303,7 @@ describe('GObject class with decorator', function () {
         GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_MESSAGE,
             '*Too many arguments*');
 
-        new MyObject({readwrite: 'baz'}, 'this is ignored', 123);
+        new ObjectWithDefaultConstructor({}, 'this is ignored', 123);
 
         GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectClass.js', 0,
             'testGObjectClassTooManyArguments');
@@ -511,6 +627,161 @@ describe('GObject class with decorator', function () {
     });
 });
 
+describe('GObject class with custom constructor', function () {
+    let myInstance;
+    beforeEach(function () {
+        myInstance = new MyObjectWithCustomConstructor();
+    });
+
+    it('throws an error when not used with a GObject-derived class', function () {
+        class Foo {}
+        expect(() => GObject.registerClass(class Bar extends Foo {})).toThrow();
+    });
+
+    it('constructs with default values for properties', function () {
+        expect(myInstance.readwrite).toEqual('foo');
+        expect(myInstance.readonly).toEqual('bar');
+        expect(myInstance.construct).toEqual('default');
+    });
+
+    it('constructs with a hash of property values', function () {
+        let myInstance2 = new MyObjectWithCustomConstructor({readwrite: 'baz', construct: 'asdf'});
+        expect(myInstance2.readwrite).toEqual('baz');
+        expect(myInstance2.readonly).toEqual('bar');
+        console.log(Object.getOwnPropertyDescriptor(myInstance2, 'construct'));
+        expect(myInstance2.construct).toEqual('asdf');
+    });
+
+    it('accepts a property hash that is not a plain object', function () {
+        expect(() => new MyObjectWithCustomConstructor(new GObject.Object())).not.toThrow();
+    });
+
+    const ui = `<interface>
+                  <object class="Gjs_MyObject" id="MyObject">
+                    <property name="readwrite">baz</property>
+                    <property name="construct">quz</property>
+                  </object>
+                </interface>`;
+
+    it('constructs with property values from Gtk.Builder', function () {
+        let builder = Gtk.Builder.new_from_string(ui, -1);
+        let myInstance3 = builder.get_object('MyObject');
+        expect(myInstance3.readwrite).toEqual('baz');
+        expect(myInstance3.readonly).toEqual('bar');
+        expect(myInstance3.construct).toEqual('quz');
+    });
+
+    it('does not allow changing CONSTRUCT_ONLY properties', function () {
+        myInstance.construct = 'val';
+        expect(myInstance.construct).toEqual('default');
+    });
+
+    it('has a name', function () {
+        expect(MyObjectWithCustomConstructor.name).toEqual('MyObjectWithCustomConstructor');
+    });
+
+    it('has a notify signal', function () {
+        let notifySpy = jasmine.createSpy('notifySpy');
+        myInstance.connect('notify::readonly', notifySpy);
+
+        myInstance.notifyProp();
+        myInstance.notifyProp();
+
+        expect(notifySpy).toHaveBeenCalledTimes(2);
+    });
+
+    it('can define its own signals', function () {
+        let emptySpy = jasmine.createSpy('emptySpy');
+        myInstance.connect('empty', emptySpy);
+        myInstance.emitEmpty();
+
+        expect(emptySpy).toHaveBeenCalled();
+        expect(myInstance.empty_called).toBeTruthy();
+    });
+
+    it('passes emitted arguments to signal handlers', function () {
+        let minimalSpy = jasmine.createSpy('minimalSpy');
+        myInstance.connect('minimal', minimalSpy);
+        myInstance.emitMinimal(7, 5);
+
+        expect(minimalSpy).toHaveBeenCalledWith(myInstance, 7, 5);
+    });
+
+    it('can return values from signals', function () {
+        let fullSpy = jasmine.createSpy('fullSpy').and.returnValue(42);
+        myInstance.connect('full', fullSpy);
+        let result = myInstance.emitFull();
+
+        expect(fullSpy).toHaveBeenCalled();
+        expect(result).toEqual(42);
+    });
+
+    it('does not call first-wins signal handlers after one returns a value', function () {
+        let neverCalledSpy = jasmine.createSpy('neverCalledSpy');
+        myInstance.connect('full', () => 42);
+        myInstance.connect('full', neverCalledSpy);
+        myInstance.emitFull();
+
+        expect(neverCalledSpy).not.toHaveBeenCalled();
+        expect(myInstance.full_default_handler_called).toBeFalsy();
+    });
+
+    it('gets the return value of the default handler', function () {
+        let result = myInstance.emitFull();
+
+        expect(myInstance.full_default_handler_called).toBeTruthy();
+        expect(result).toEqual(79);
+    });
+
+    it('calls run-last default handler last', function () {
+        let stack = [];
+        let runLastSpy = jasmine.createSpy('runLastSpy')
+            .and.callFake(() => {
+                stack.push(1);
+            });
+        myInstance.connect('run-last', runLastSpy);
+        myInstance.emitRunLast(() => {
+            stack.push(2);
+        });
+
+        expect(stack).toEqual([1, 2]);
+    });
+
+
+
+
+    it('can be a subclass', function () {
+        let derived = new DerivedWithCustomConstructor();
+
+        expect(derived instanceof DerivedWithCustomConstructor).toBeTruthy();
+        expect(derived instanceof MyObjectWithCustomConstructor).toBeTruthy();
+
+        expect(derived.readwrite).toEqual('yes');
+    });
+
+
+
+
+    it('can override a property from the parent class', function () {
+        const OverrideObjectWithCustomConstructor = GObject.registerClass({
+            Properties: {
+                'readwrite': GObject.ParamSpec.override('readwrite', MyObjectWithCustomConstructor),
+            },
+        }, class OverrideObjectWithCustomConstructor extends MyObjectWithCustomConstructor {
+            get readwrite() {
+                return this._subclass_readwrite;
+            }
+
+            set readwrite(val) {
+                this._subclass_readwrite = `subclass${val}`;
+            }
+        });
+        let obj = new OverrideObjectWithCustomConstructor();
+        obj.readwrite = 'foo';
+        expect(obj.readwrite).toEqual('subclassfoo');
+    });
+});
+
 describe('GObject virtual function', function () {
     it('can have its property read', function () {
         expect(GObject.Object.prototype.vfunc_constructed).toBeTruthy();
diff --git a/installed-tests/js/testGtk3.js b/installed-tests/js/testGtk3.js
index 83ca8247..92a19858 100644
--- a/installed-tests/js/testGtk3.js
+++ b/installed-tests/js/testGtk3.js
@@ -54,18 +54,15 @@ const MyComplexGtkSubclass = GObject.registerClass({
     boundCallback(widget) {
         widget.callbackBoundTo = this;
     }
-});
 
-// Sadly, putting this in the body of the class will prevent calling
-// get_template_child, since MyComplexGtkSubclass will be bound to the ES6
-// class name without the GObject goodies in it
-MyComplexGtkSubclass.prototype.testChildrenExist = function () {
-    this._internalLabel = this.get_template_child(MyComplexGtkSubclass, 'label-child');
-    expect(this._internalLabel).toEqual(jasmine.anything());
+    testChildrenExist() {
+        this._internalLabel = this.get_template_child(MyComplexGtkSubclass, 'label-child');
+        expect(this._internalLabel).toEqual(jasmine.anything());
 
-    expect(this.label_child2).toEqual(jasmine.anything());
-    expect(this._internal_label_child).toEqual(jasmine.anything());
-};
+        expect(this.label_child2).toEqual(jasmine.anything());
+        expect(this._internal_label_child).toEqual(jasmine.anything());
+    }
+});
 
 const MyComplexGtkSubclassFromResource = GObject.registerClass({
     Template: 'resource:///org/gjs/jsunit/complex3.ui',
diff --git a/installed-tests/js/testGtk4.js b/installed-tests/js/testGtk4.js
index d7fc0028..c3444715 100644
--- a/installed-tests/js/testGtk4.js
+++ b/installed-tests/js/testGtk4.js
@@ -50,18 +50,15 @@ const MyComplexGtkSubclass = GObject.registerClass({
     boundCallback(widget) {
         widget.callbackBoundTo = this;
     }
-});
 
-// Sadly, putting this in the body of the class will prevent calling
-// get_template_child, since MyComplexGtkSubclass will be bound to the ES6
-// class name without the GObject goodies in it
-MyComplexGtkSubclass.prototype.testChildrenExist = function () {
-    this._internalLabel = this.get_template_child(MyComplexGtkSubclass, 'label-child');
-    expect(this._internalLabel).toEqual(jasmine.anything());
+    testChildrenExist() {
+        this._internalLabel = this.get_template_child(MyComplexGtkSubclass, 'label-child');
+        expect(this._internalLabel).toEqual(jasmine.anything());
 
-    expect(this.label_child2).toEqual(jasmine.anything());
-    expect(this._internal_label_child).toEqual(jasmine.anything());
-};
+        expect(this.label_child2).toEqual(jasmine.anything());
+        expect(this._internal_label_child).toEqual(jasmine.anything());
+    }
+});
 
 const MyComplexGtkSubclassFromResource = GObject.registerClass({
     Template: 'resource:///org/gjs/jsunit/complex4.ui',
diff --git a/modules/core/_common.js b/modules/core/_common.js
index edc70215..4e361e16 100644
--- a/modules/core/_common.js
+++ b/modules/core/_common.js
@@ -3,11 +3,13 @@
 // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
 // SPDX-FileCopyrightText: 2020 Philip Chimento <philip chimento gmail com>
 
-/* exported _checkAccessors */
+/* exported _checkAccessors, _registerType, definePublicProperties, definePrivateProperties */
 
 // This is a helper module in which to put code that is common between the
 // legacy GObject.Class system and the new GObject.registerClass system.
 
+var _registerType = Symbol('GObject register type hook');
+
 function _generateAccessors(pspec, propdesc, GObject) {
     const {name, flags} = pspec;
     const readable = flags & GObject.ParamFlags.READABLE;
@@ -90,3 +92,24 @@ function _checkAccessors(proto, pspec, GObject) {
             Object.defineProperty(proto, camelName, propdesc);
     }
 }
+
+function definePublicProperties(object, values) {
+    Object.defineProperties(object, Object.getOwnPropertyDescriptors(values));
+}
+
+function definePrivateProperties(object, values) {
+    const propertyKeys = [...Object.getOwnPropertyNames(values), ...Object.getOwnPropertySymbols(values)];
+
+    const privateDescriptors = Object.fromEntries(propertyKeys.map(key => {
+        const descriptor = Object.getOwnPropertyDescriptor(values, key);
+
+        return [key, {
+            ...descriptor,
+            ...'value' in descriptor ? {writable: false} : {},
+            configurable: false,
+            enumerable: false,
+        }];
+    }));
+
+    Object.defineProperties(object, privateDescriptors);
+}
diff --git a/modules/core/overrides/GObject.js b/modules/core/overrides/GObject.js
index 6bfaf144..f7817e80 100644
--- a/modules/core/overrides/GObject.js
+++ b/modules/core/overrides/GObject.js
@@ -5,7 +5,7 @@
 
 const Gi = imports._gi;
 const {GjsPrivate, GLib} = imports.gi;
-const {_checkAccessors} = imports._common;
+const {_checkAccessors, _registerType, definePublicProperties, definePrivateProperties} = imports._common;
 const Legacy = imports._legacy;
 
 let GObject;
@@ -23,6 +23,38 @@ var _gtkCssName = Symbol('GTK widget CSS name');
 var _gtkInternalChildren = Symbol('GTK widget template internal children');
 var _gtkTemplate = Symbol('GTK widget template');
 
+function _mapWidgetDefinitionToClass(klass, metaInfo) {
+    if ('CssName' in metaInfo)
+        klass[_gtkCssName] = metaInfo.CssName;
+    if ('Template' in metaInfo)
+        klass[_gtkTemplate] = metaInfo.Template;
+    if ('Children' in metaInfo)
+        klass[_gtkChildren] = metaInfo.Children;
+    if ('InternalChildren' in metaInfo)
+        klass[_gtkInternalChildren] = metaInfo.InternalChildren;
+}
+
+function _mapTypeDefinition(klass, metaInfo) {
+    if ('GTypeName' in metaInfo)
+        klass[GTypeName] = metaInfo.GTypeName;
+    if ('GTypeFlags' in metaInfo)
+        klass[GTypeFlags] = metaInfo.GTypeFlags;
+    if ('Properties' in metaInfo)
+        klass[properties] = metaInfo.Properties;
+    if ('Signals' in metaInfo)
+        klass[signals] = metaInfo.Signals;
+}
+
+function _mapClassDefinition(klass, metaInfo) {
+    if ('Implements' in metaInfo)
+        klass[interfaces] = metaInfo.Implements;
+}
+
+function _mapInterfaceDefinition(klass, metaInfo) {
+    if ('Requires' in metaInfo)
+        klass[requires] = metaInfo.Requires;
+}
+
 function registerClass(...args) {
     let klass = args[0];
     if (args.length === 2) {
@@ -42,40 +74,125 @@ function registerClass(...args) {
         // standard, this function can be used directly as a decorator.
         let metaInfo = args[0];
         klass = args[1];
-        if ('GTypeName' in metaInfo)
-            klass[GTypeName] = metaInfo.GTypeName;
-        if ('GTypeFlags' in metaInfo)
-            klass[GTypeFlags] = metaInfo.GTypeFlags;
-        if ('Implements' in metaInfo)
-            klass[interfaces] = metaInfo.Implements;
-        if ('Properties' in metaInfo)
-            klass[properties] = metaInfo.Properties;
-        if ('Signals' in metaInfo)
-            klass[signals] = metaInfo.Signals;
-        if ('Requires' in metaInfo)
-            klass[requires] = metaInfo.Requires;
-        if ('CssName' in metaInfo)
-            klass[_gtkCssName] = metaInfo.CssName;
-        if ('Template' in metaInfo)
-            klass[_gtkTemplate] = metaInfo.Template;
-        if ('Children' in metaInfo)
-            klass[_gtkChildren] = metaInfo.Children;
-        if ('InternalChildren' in metaInfo)
-            klass[_gtkInternalChildren] = metaInfo.InternalChildren;
+
+        _mapTypeDefinition(klass, metaInfo);
+        _mapClassDefinition(klass, metaInfo);
+        _mapInterfaceDefinition(klass, metaInfo);
+        _mapWidgetDefinitionToClass(klass, metaInfo);
+    }
+
+    _assertDerivesFromGObject(klass, [GObject.Object, GObject.Interface], 'registerClass');
+
+    if ('_classInit' in klass) {
+        klass = klass._classInit(klass);
+    } else {
+        // Lang.Class compatibility.
+        klass = _resolveLegacyClassFunction(klass, '_classInit')?.(klass);
     }
 
-    if (!(klass.prototype instanceof GObject.Object) &&
-        !(klass.prototype instanceof GObject.Interface)) {
-        throw new TypeError('GObject.registerClass() used with invalid base ' +
+    return klass;
+}
+
+function _assertDerivesFromGObject(klass, parents, functionName) {
+    if (parents.every(parent => !(klass.prototype instanceof parent))) {
+        throw new TypeError(`GObject.${functionName}() used with invalid base ` +
             `class (is ${Object.getPrototypeOf(klass).name})`);
     }
+}
 
+function _resolveLegacyClassFunction(klass, func) {
     // Find the "least derived" class with a _classInit static function; there
     // definitely is one, since this class must inherit from GObject
     let initclass = klass;
-    while (typeof initclass._classInit === 'undefined')
+    while (typeof initclass[func] === 'undefined')
         initclass = Object.getPrototypeOf(initclass.prototype).constructor;
-    return initclass._classInit(klass);
+    return initclass[func];
+}
+
+function _hookupVFuncs(prototype, gobject_prototype, gtype) {
+    Object.getOwnPropertyNames(prototype)
+        .filter(name => name.startsWith('vfunc_') || name.startsWith('on_'))
+        .forEach(name => {
+            let descr = Object.getOwnPropertyDescriptor(prototype, name);
+            if (typeof descr.value !== 'function')
+                return;
+
+            let func = prototype[name];
+
+            if (name.startsWith('vfunc_')) {
+                gobject_prototype[Gi.hook_up_vfunc_symbol](name.slice(6), func);
+            } else if (name.startsWith('on_')) {
+                let id = GObject.signal_lookup(name.slice(3).replace('_', '-'),
+                    gtype);
+                if (id !== 0) {
+                    GObject.signal_override_class_closure(id, gtype, function (...argArray) {
+                        let emitter = argArray.shift();
+
+                        return func.apply(emitter, argArray);
+                    });
+                }
+            }
+        });
+}
+
+function _defineGType(klass, giPrototype, registeredType) {
+    const config = {
+        enumerable: false,
+        configurable: false,
+    };
+
+    /**
+     * class Example {
+     *     // The JS object for this class' ObjectPrototype
+     *     static [Gi.gobject_prototype_symbol] = ...
+     *     static get $gtype () {
+     *         return ...;
+     *     }
+     *     static set $gtype (value) {}
+     * }
+     *
+     * // Equal to the same property on the constructor
+     * Example.prototype[Gi.gobject_prototype_symbol] = ...
+     */
+
+    Object.defineProperties(klass, {
+        $gtype: {
+            ...config,
+            set() {
+                // Setting $gtype is a no-op.
+            },
+            get() {
+                return registeredType;
+            },
+        },
+    });
+
+    Object.defineProperty(klass.prototype, Gi.gobject_prototype_symbol, {
+        ...config,
+        writable: false,
+        value: giPrototype,
+    });
+}
+
+function _createInterfaceGenerics(klass) {
+    Object.getOwnPropertyNames(klass.prototype)
+        .filter(key => key !== 'constructor')
+        .concat(Object.getOwnPropertySymbols(klass.prototype))
+        .forEach(key => {
+            let descr = Object.getOwnPropertyDescriptor(klass.prototype, key);
+
+            // Create wrappers on the interface object so that generics work (e.g.
+            // SomeInterface.some_function(this, blah) instead of
+            // SomeInterface.prototype.some_function.call(this, blah)
+            if (typeof descr.value === 'function') {
+                let interfaceProto = klass.prototype;  // capture in closure
+                klass[key] = function (thisObj, ...args) {
+                    return interfaceProto[key].call(thisObj, ...args);
+                };
+            }
+
+            Object.defineProperty(klass.prototype, key, descr);
+        });
 }
 
 // Some common functions between GObject.Class and GObject.Interface
@@ -166,14 +283,26 @@ function _propertiesAsArray(klass) {
     return propertiesArray;
 }
 
-function _copyAllDescriptors(target, source, filter) {
-    Object.getOwnPropertyNames(source)
-    .filter(key => !['prototype', 'constructor'].concat(filter).includes(key))
-    .concat(Object.getOwnPropertySymbols(source))
-    .forEach(key => {
-        let descriptor = Object.getOwnPropertyDescriptor(source, key);
-        Object.defineProperty(target, key, descriptor);
-    });
+function _copyInterfacePrototypeDescriptors(targetPrototype, sourceInterface) {
+    Object.entries(Object.getOwnPropertyDescriptors(sourceInterface))
+        .filter(([key, descriptor]) =>
+            // Don't attempt to copy the constructor or toString implementations
+            !['constructor', 'toString'].includes(key) &&
+            // Ignore properties starting with __
+            (
+                typeof key !== 'string' || !key.startsWith('__')
+            ) &&
+            // Don't override an implementation on the target
+            !targetPrototype.hasOwnProperty(key) &&
+            descriptor &&
+            // Only copy if the descriptor has a getter, is a function, or is enumerable.
+            (
+                typeof descriptor.value === 'function' || descriptor.get || descriptor.enumerable
+            )
+        )
+        .forEach(([key, descriptor]) => {
+            Object.defineProperty(targetPrototype, key, descriptor);
+        });
 }
 
 function _interfacePresent(required, klass) {
@@ -222,6 +351,14 @@ function _checkInterface(iface, proto) {
     }
 }
 
+function _checkProperties(klass) {
+    if (!klass.hasOwnProperty(properties))
+        return;
+
+    for (let pspec of Object.values(klass[properties]))
+        _checkAccessors(klass.prototype, pspec, GObject);
+}
+
 function _init() {
     GObject = this;
 
@@ -236,7 +373,7 @@ function _init() {
 
     GObject.gtypeNameBasedOnJSPath = false;
 
-    _makeDummyClass(GObject, 'VoidType', 'NONE', 'void', function () {});
+    _makeDummyClass(GObject, 'VoidType', 'NONE', 'void', function () { });
     _makeDummyClass(GObject, 'Char', 'CHAR', 'gchar', Number);
     _makeDummyClass(GObject, 'UChar', 'UCHAR', 'guchar', Number);
     _makeDummyClass(GObject, 'Unichar', 'UNICHAR', 'gint', String);
@@ -431,98 +568,150 @@ function _init() {
 
     GObject.registerClass = registerClass;
 
-    GObject.Object._classInit = function (klass) {
-        let gtypename = _createGTypeName(klass);
-        let gflags = klass.hasOwnProperty(GTypeFlags) ? klass[GTypeFlags] : 0;
-        let gobjectInterfaces = klass.hasOwnProperty(interfaces) ? klass[interfaces] : [];
-        let propertiesArray = _propertiesAsArray(klass);
-        let parent = Object.getPrototypeOf(klass);
-        let gobjectSignals = klass.hasOwnProperty(signals) ? klass[signals] : [];
+    const {toString} = GObject.Object.prototype;
 
-        propertiesArray.forEach(pspec => _checkAccessors(klass.prototype, pspec, GObject));
+    Object.assign(GObject.Object.prototype, {
+        toString() {
+            if (!this.constructor)
+                return Object.prototype.toString.call(this);
 
-        let newClass = Gi.register_type(parent.prototype, gtypename, gflags,
-            gobjectInterfaces, propertiesArray);
-        Object.setPrototypeOf(newClass, parent);
+            // Use the native toString if this is not a custom class.
+            if (!this.constructor.prototype[Gi.gobject_prototype_symbol])
+                return toString.call(this);
 
-        _createSignals(newClass.$gtype, gobjectSignals);
+            const isInstance = this !== this.constructor.prototype;
 
-        _copyAllDescriptors(newClass, klass);
-        gobjectInterfaces.forEach(iface =>
-            _copyAllDescriptors(newClass.prototype, iface.prototype,
-                ['toString']));
-        _copyAllDescriptors(newClass.prototype, klass.prototype);
+            let out = '';
+            if (isInstance)
+                out += '[object instance wrapper';
+            else
+                out += '[GObject prototype of';
 
-        Object.getOwnPropertyNames(newClass.prototype)
-        .filter(name => name.startsWith('vfunc_') || name.startsWith('on_'))
-        .forEach(name => {
-            let descr = Object.getOwnPropertyDescriptor(newClass.prototype, name);
-            if (typeof descr.value !== 'function')
-                return;
 
-            let func = newClass.prototype[name];
+            try {
+                let gtype = this.constructor.$gtype;
 
-            if (name.startsWith('vfunc_')) {
-                newClass.prototype[Gi.hook_up_vfunc_symbol](name.slice(6), func);
-            } else if (name.startsWith('on_')) {
-                let id = GObject.signal_lookup(name.slice(3).replace('_', '-'),
-                    newClass.$gtype);
-                if (id !== 0) {
-                    GObject.signal_override_class_closure(id, newClass.$gtype, function (...argArray) {
-                        let emitter = argArray.shift();
-
-                        return func.apply(emitter, argArray);
-                    });
-                }
+                out += ` GType:${GObject.type_name(gtype)}`;
+            } catch {
+                out += ' GType:unknown';
             }
-        });
 
-        gobjectInterfaces.forEach(iface =>
-            _checkInterface(iface, newClass.prototype));
+            try {
+                out += ` jsobj@${imports.system.addressOf(this)}`;
+            } catch { }
+
+            try {
+                if (isInstance)
+                    out += ` native@${imports.system.addressOfGObject(this)}`;
+            } catch { }
 
-        // For backwards compatibility only. Use instanceof instead.
-        newClass.implements = function (iface) {
+            out += ']';
+
+            return out;
+        },
+    });
+
+    definePublicProperties(GObject.Object, {
+        implements(iface) {
             if (iface.$gtype)
-                return GObject.type_is_a(newClass.$gtype, iface.$gtype);
+                return GObject.type_is_a(this, iface.$gtype);
             return false;
-        };
+        },
+    });
+
+    definePrivateProperties(GObject.Object, {
+        [_registerType]() {
+            let klass = this;
+
+            let gtypename = _createGTypeName(klass);
+            let gflags = klass.hasOwnProperty(GTypeFlags) ? klass[GTypeFlags] : 0;
+            let gobjectInterfaces = klass.hasOwnProperty(interfaces) ? klass[interfaces] : [];
+            let propertiesArray = _propertiesAsArray(klass);
+            let parent = Object.getPrototypeOf(klass);
+            let gobjectSignals = klass.hasOwnProperty(signals) ? klass[signals] : [];
+
+            // Default to the GObject-specific prototype, fallback on the JS prototype for GI native classes.
+            const parentPrototype = parent.prototype[Gi.gobject_prototype_symbol] ?? parent.prototype;
+
+            const [giPrototype, registeredType] = Gi.register_type_with_class(
+                klass,
+                parentPrototype,
+                gtypename,
+                gflags,
+                gobjectInterfaces,
+                propertiesArray
+            );
+
+            _defineGType(klass, giPrototype, registeredType);
+            _createSignals(klass.$gtype, gobjectSignals);
+
+            // Reverse the interface array to give the last required interface precedence over the first.
+            const requiredInterfaces = [...gobjectInterfaces].reverse();
+            requiredInterfaces.forEach(iface =>
+                _copyInterfacePrototypeDescriptors(klass.prototype, iface.prototype));
+
+            _hookupVFuncs(klass.prototype, klass.prototype[Gi.gobject_prototype_symbol], klass.$gtype);
+
+            gobjectInterfaces.forEach(iface =>
+                _checkInterface(iface, klass.prototype));
+
+            // Lang.Class parent classes don't support static inheritance
+            if (!('implements' in klass))
+                klass.implements = GObject.Object.implements;
+        },
+    });
+
+    GObject.Object._classInit = function (klass) {
+        _checkProperties(klass);
+
+        if (_registerType in klass)
+            klass[_registerType]();
+        else
+            _resolveLegacyClassFunction(klass, _registerType).call(klass);
 
-        return newClass;
+        return klass;
     };
 
-    GObject.Interface._classInit = function (klass) {
-        let gtypename = _createGTypeName(klass);
-        let gobjectInterfaces = klass.hasOwnProperty(requires) ? klass[requires] : [];
-        let props = _propertiesAsArray(klass);
-        let gobjectSignals = klass.hasOwnProperty(signals) ? klass[signals] : [];
+    function interfaceInstanceOf(instance) {
+        if (GObject.Interface.prototype.isPrototypeOf(this.prototype))
+            return GObject.type_is_a(instance, this);
 
-        let newInterface = Gi.register_interface(gtypename, gobjectInterfaces,
-            props);
 
-        _createSignals(newInterface.$gtype, gobjectSignals);
+        return false;
+    }
 
-        _copyAllDescriptors(newInterface, klass);
+    definePrivateProperties(GObject.Interface, {
+        [_registerType]() {
+            let klass = this;
 
-        Object.getOwnPropertyNames(klass.prototype)
-        .filter(key => key !== 'constructor')
-        .concat(Object.getOwnPropertySymbols(klass.prototype))
-        .forEach(key => {
-            let descr = Object.getOwnPropertyDescriptor(klass.prototype, key);
+            let gtypename = _createGTypeName(klass);
+            let gobjectInterfaces = klass.hasOwnProperty(requires) ? klass[requires] : [];
+            let props = _propertiesAsArray(klass);
+            let gobjectSignals = klass.hasOwnProperty(signals) ? klass[signals] : [];
 
-            // Create wrappers on the interface object so that generics work (e.g.
-            // SomeInterface.some_function(this, blah) instead of
-            // SomeInterface.prototype.some_function.call(this, blah)
-            if (typeof descr.value === 'function') {
-                let interfaceProto = klass.prototype;  // capture in closure
-                newInterface[key] = function (thisObj, ...args) {
-                    return interfaceProto[key].call(thisObj, ...args);
-                };
-            }
+            const [giPrototype, registeredType] = Gi.register_interface_with_class(klass, gtypename, 
gobjectInterfaces,
+                props);
 
-            Object.defineProperty(newInterface.prototype, key, descr);
-        });
+            _defineGType(klass, giPrototype, registeredType);
+            _createSignals(klass.$gtype, gobjectSignals);
+
+            Object.defineProperty(klass, Symbol.hasInstance, {
+                value: interfaceInstanceOf,
+            });
+
+            return klass;
+        },
+    });
+
+    GObject.Interface._classInit = function (klass) {
+        if (_registerType in klass)
+            klass[_registerType]();
+        else
+            _resolveLegacyClassFunction(klass, _registerType).call(klass);
+
+        _createInterfaceGenerics(klass);
 
-        return newInterface;
+        return klass;
     };
 
     /**
diff --git a/modules/core/overrides/Gtk.js b/modules/core/overrides/Gtk.js
index 77649a73..90deefb7 100644
--- a/modules/core/overrides/Gtk.js
+++ b/modules/core/overrides/Gtk.js
@@ -4,10 +4,25 @@
 
 const Legacy = imports._legacy;
 const {Gio, GjsPrivate, GObject} = imports.gi;
+const {_registerType, definePrivateProperties} = imports._common;
 
 let Gtk;
 let BuilderScope;
 
+function defineChildren(instance, constructor, target = instance) {
+    let children = constructor[Gtk.children] || [];
+    for (let child of children) {
+        target[child.replace(/-/g, '_')] =
+            instance.get_template_child(constructor, child);
+    }
+
+    let internalChildren = constructor[Gtk.internalChildren] || [];
+    for (let child of internalChildren) {
+        target[`_${child.replace(/-/g, '_')}`] =
+            instance.get_template_child(constructor, child);
+    }
+}
+
 function _init() {
     Gtk = this;
 
@@ -46,70 +61,65 @@ function _init() {
 
         wrapper = GObject.Object.prototype._init.call(wrapper, params) ?? wrapper;
 
-        if (wrapper.constructor[Gtk.template]) {
-            let children = wrapper.constructor[Gtk.children] || [];
-            for (let child of children) {
-                wrapper[child.replace(/-/g, '_')] =
-                    wrapper.get_template_child(wrapper.constructor, child);
-            }
-
-            let internalChildren = wrapper.constructor[Gtk.internalChildren] || [];
-            for (let child of internalChildren) {
-                wrapper[`_${child.replace(/-/g, '_')}`] =
-                    wrapper.get_template_child(wrapper.constructor, child);
-            }
-        }
+        if (wrapper.constructor[Gtk.template])
+            defineChildren(this, wrapper.constructor);
 
         return wrapper;
     };
 
     Gtk.Widget._classInit = function (klass) {
-        let template = klass[Gtk.template];
-        let cssName = klass[Gtk.cssName];
-        let children = klass[Gtk.children];
-        let internalChildren = klass[Gtk.internalChildren];
-
-        if (template) {
-            klass.prototype._instance_init = function () {
-                this.init_template();
-            };
-        }
+        return GObject.Object._classInit(klass);
+    };
 
-        klass = GObject.Object._classInit(klass);
+    definePrivateProperties(Gtk.Widget, {
+        [_registerType]() {
+            let klass = this;
 
-        if (cssName)
-            Gtk.Widget.set_css_name.call(klass, cssName);
+            let template = klass[Gtk.template];
+            let cssName = klass[Gtk.cssName];
+            let children = klass[Gtk.children];
+            let internalChildren = klass[Gtk.internalChildren];
 
-        if (template) {
-            if (typeof template === 'string') {
-                if (template.startsWith('resource:///')) {
-                    Gtk.Widget.set_template_from_resource.call(klass,
-                        template.slice(11));
-                } else if (template.startsWith('file:///')) {
-                    let file = Gio.File.new_for_uri(template);
-                    let [, contents] = file.load_contents(null);
-                    Gtk.Widget.set_template.call(klass, contents);
-                }
-            } else {
-                Gtk.Widget.set_template.call(klass, template);
+            if (template) {
+                klass.prototype._instance_init = function () {
+                    this.init_template();
+                };
             }
 
-            if (BuilderScope)
-                Gtk.Widget.set_template_scope.call(klass, new BuilderScope());
-        }
+            GObject.Object[_registerType].call(klass);
+
+            if (cssName)
+                Gtk.Widget.set_css_name.call(klass, cssName);
+
+            if (template) {
+                if (typeof template === 'string') {
+                    if (template.startsWith('resource:///')) {
+                        Gtk.Widget.set_template_from_resource.call(klass,
+                            template.slice(11));
+                    } else if (template.startsWith('file:///')) {
+                        let file = Gio.File.new_for_uri(template);
+                        let [, contents] = file.load_contents(null);
+                        Gtk.Widget.set_template.call(klass, contents);
+                    }
+                } else {
+                    Gtk.Widget.set_template.call(klass, template);
+                }
 
-        if (children) {
-            children.forEach(child =>
-                Gtk.Widget.bind_template_child_full.call(klass, child, false, 0));
-        }
+                if (BuilderScope)
+                    Gtk.Widget.set_template_scope.call(klass, new BuilderScope());
+            }
 
-        if (internalChildren) {
-            internalChildren.forEach(child =>
-                Gtk.Widget.bind_template_child_full.call(klass, child, true, 0));
-        }
+            if (children) {
+                children.forEach(child =>
+                    Gtk.Widget.bind_template_child_full.call(klass, child, false, 0));
+            }
 
-        return klass;
-    };
+            if (internalChildren) {
+                internalChildren.forEach(child =>
+                    Gtk.Widget.bind_template_child_full.call(klass, child, true, 0));
+            }
+        },
+    });
 
     if (Gtk.Widget.prototype.get_first_child) {
         Gtk.Widget.prototype[Symbol.iterator] = function* () {


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]