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




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

    overrides: Support class fields and references in GObject.registerClass
    
    This commit allows classes passed to GObject.registerClass
    to reference themselves and use class fields.
    
    ```
    GObject.registerClass(class MyObject {
        specialField = 5;
    
        doSomething() {
            console.log(this.specialField);
        }
    
        static new_special() {
            // Class self-references now work
            return new MyObject({ ... });
        }
    
    });
    ```
    
    Instead of copying descriptors to a new, native class in
    registerClass, we can instead "wrap" the existing class
    constructor in the relevant GObject mechanics.
    
    Because constructors chain and can return arbitrary
    objects, we rely on the GObject.Object constructor to
    return a native ObjectInstance which the child class
    can extend.
    
    The native ObjectPrototype is stored in a private symbol
    on the JavaScript prototype and is used to resolve
    internally.
    
    Fixes #331

 gi/gobject.cpp                         |  23 ++
 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, 915 insertions(+), 208 deletions(-)
---
diff --git a/gi/gobject.cpp b/gi/gobject.cpp
index d6501624..4174aaa5 100644
--- a/gi/gobject.cpp
+++ b/gi/gobject.cpp
@@ -61,6 +61,7 @@ static bool jsobj_set_gproperty(JSContext* cx, JS::HandleObject object,
 
         if (g_param_spec_get_qdata(pspec, ObjectBase::custom_property_quark())) {
             JS::Rooted<JS::PropertyDescriptor> jsprop(cx);
+            JS::RootedObject getter(cx);
 
             // Ensure to call any associated setter method
             if (!g_str_equal(underscore_name.get(), pspec->name)) {
@@ -69,6 +70,9 @@ static bool jsobj_set_gproperty(JSContext* cx, JS::HandleObject object,
                 if (jsprop.setter() &&
                     !JS_SetProperty(cx, object, underscore_name, jsvalue))
                     return false;
+                if (jsprop.hasGetterObject()) {
+                    getter.set(jsprop.getterObject());
+                }
             }
 
             if (!g_str_equal(camel_name.get(), pspec->name)) {
@@ -77,6 +81,9 @@ static bool jsobj_set_gproperty(JSContext* cx, JS::HandleObject object,
                 if (jsprop.setter() &&
                     !JS_SetProperty(cx, object, camel_name, jsvalue))
                     return false;
+                if (!getter && jsprop.hasGetterObject()) {
+                    getter.set(jsprop.getterObject());
+                }
             }
 
             if (!JS_GetPropertyDescriptor(cx, object, pspec->name, &jsprop))
@@ -84,6 +91,22 @@ static bool jsobj_set_gproperty(JSContext* cx, JS::HandleObject object,
             if (jsprop.setter() &&
                 !JS_SetProperty(cx, object, pspec->name, jsvalue))
                 return false;
+            if (!getter && jsprop.hasGetterObject()) {
+                getter.set(jsprop.getterObject());
+            }
+
+            // If a getter is found, redefine the property with that getter
+            // and no setter.
+            if (getter) {
+                unsigned getter_flags = GJS_MODULE_PROP_FLAGS | JSPROP_GETTER;
+
+                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]