[gjs/ewlsh/register-type] Implement GObject.registerType




commit 5a86bfea023db03529911da59ff4af7b5c0fbe00
Author: Evan Welsh <contact evanwelsh com>
Date:   Wed Mar 31 19:41:19 2021 -0700

    Implement GObject.registerType

 .eslintignore                         |    1 +
 gi/cwrapper.cpp                       |    4 +-
 gi/gtype.cpp                          |    3 +
 gi/object.cpp                         |    4 +
 gi/private.cpp                        |   95 +-
 gi/wrapperutils.h                     |   82 +-
 gjs/atoms.h                           |    2 +
 installed-tests/js/meson.build        |    1 +
 installed-tests/js/testGObjectType.js | 1702 +++++++++++++++++++++++++++++++++
 modules/core/overrides/GObject.js     |  181 +++-
 10 files changed, 2026 insertions(+), 49 deletions(-)
---
diff --git a/.eslintignore b/.eslintignore
index 9ee950d3..7a580e14 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -3,4 +3,5 @@
 
 installed-tests/js/jasmine.js
 installed-tests/js/modules/badOverrides/WarnLib.js
+installed-tests/js/testGObjectType.js
 modules/script/jsUnit.js
diff --git a/gi/cwrapper.cpp b/gi/cwrapper.cpp
index f5fbff8a..a9fd6557 100644
--- a/gi/cwrapper.cpp
+++ b/gi/cwrapper.cpp
@@ -24,5 +24,7 @@ bool gjs_wrapper_define_gtype_prop(JSContext* cx, JS::HandleObject constructor,
 
     const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
     return JS_DefinePropertyById(cx, constructor, atoms.gtype(), gtype_obj,
-                                 JSPROP_PERMANENT);
+                                 JSPROP_PERMANENT) &&
+           JS_DefinePropertyById(cx, constructor, atoms.gobject_type(),
+                                 gtype_obj, JSPROP_PERMANENT);
 }
diff --git a/gi/gtype.cpp b/gi/gtype.cpp
index 7ccb8099..aa2d665a 100644
--- a/gi/gtype.cpp
+++ b/gi/gtype.cpp
@@ -129,6 +129,9 @@ class GTypeObj : public CWrapper<GTypeObj, void> {
         // property on that and hope it's a GType wrapper object
         if (!JS_GetPropertyById(cx, object, atoms.gtype(), &v_gtype))
             return false;
+        if (!v_gtype.isObject() &&
+            !JS_GetPropertyById(cx, object, atoms.gobject_type(), &v_gtype))
+            return false;
         if (!v_gtype.isObject()) {
             // OK, so we're not a class. But maybe we're an instance. Check for
             // "constructor" and recurse on that.
diff --git a/gi/object.cpp b/gi/object.cpp
index 3bfa42e8..6f52c0aa 100644
--- a/gi/object.cpp
+++ b/gi/object.cpp
@@ -1735,6 +1735,10 @@ bool ObjectInstance::constructor_impl(JSContext* context,
     if (!JS_HasOwnPropertyById(context, rooted_target, gjs->atoms().gtype(),
                                &has_gtype))
         return false;
+    if (!has_gtype &&
+        !JS_HasOwnPropertyById(context, rooted_target,
+                               gjs->atoms().gobject_type(), &has_gtype))
+        return false;
 
     if (!has_gtype) {
         gjs_throw(context,
diff --git a/gi/private.cpp b/gi/private.cpp
index a6ecc723..368d873e 100644
--- a/gi/private.cpp
+++ b/gi/private.cpp
@@ -17,6 +17,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"
@@ -240,18 +241,11 @@ 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* gtype) {
     if (!parent)
         return false;
 
@@ -273,8 +267,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;
     }
 
@@ -293,8 +287,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));
@@ -306,6 +300,28 @@ 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;
+    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;
+    if (!gjs_register_type_impl(cx, name.get(), type_flags, parent, interfaces,
+                                properties, &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 +337,47 @@ 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;
+    if (!gjs_register_type_impl(cx, name.get(), type_flags, parent, interfaces,
+                                properties, &instance_type))
+        return false;
+
+    /* create a custom JSClass */
+    JS::RootedObject module(cx, gjs_lookup_private_namespace(cx));
+    JS::RootedObject prototype(cx);
+
+    if (!ObjectPrototype::wrap_class(cx, module, nullptr, instance_type, klass,
+                                     &prototype))
+        return false;
+
+    auto* priv = ObjectPrototype::for_js(cx, prototype);
+    priv->set_type_qdata();
+
+    JS::RootedObject gtype_wrapper(
+        cx, gjs_gtype_create_gtype_wrapper(cx, instance_type));
+    JS::RootedValueArray<2> tuple(cx);
+    tuple[0].setObject(*prototype);
+    tuple[1].setObject(*gtype_wrapper);
+    JS::RootedObject array(cx, JS::NewArrayObject(cx, tuple));
+    argv.rval().setObject(*array);
+
+    return true;
+}
+
 GJS_JSAPI_RETURN_CONVENTION
 static bool gjs_signal_new(JSContext* cx, unsigned argc, JS::Value* vp) {
     JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
@@ -408,11 +465,17 @@ static JSFunctionSpec private_module_funcs[] = {
     JS_FN("register_interface", gjs_register_interface, 3,
           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("gobject_type_symbol", symbol_getter<&GjsAtoms::gobject_type>,
+           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 777fb72c..17d85768 100644
--- a/gi/wrapperutils.h
+++ b/gi/wrapperutils.h
@@ -429,8 +429,23 @@ class GIWrapperBase : public CWrapperPointerOps<Base> {
         JS::RootedObject proto(cx);
         if (!JS_GetPrototype(cx, obj, &proto))
             return false;
-        if (JS_GetClass(proto) != &Base::klass) {
-            gjs_throw(cx, "Tried to construct an object without a GType");
+
+        JS::RootedValue gproto(cx);
+
+        bool has_property = false;
+
+        auto gjs_cx = GjsContextPrivate::from_cx(cx);
+        auto atoms = gjs_cx->atoms();
+        if (!JS_HasOwnPropertyById(cx, proto, atoms.gobject_prototype(),
+                                   &has_property))
+            return false;
+
+        if (JS_GetClass(proto) != &Base::klass &&
+            (!has_property ||
+             !JS_GetPropertyById(cx, proto, atoms.gobject_prototype(),
+                                 &gproto) ||
+             !gproto.isObject())) {
+            gjs_throw(cx, "Tried to construct an object without a GType!");
             return false;
         }
 
@@ -873,6 +888,53 @@ 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);
+        g_assert(gtype != G_TYPE_INVALID);
+
+        // We have to keep the Prototype in an arcbox because some of its
+        // members are needed in some Instance destructors, e.g. m_gtype to
+        // figure out how to free the Instance's m_ptr, and m_info to figure out
+        // how many bytes to free if it is allocated directly. Storing a
+        // refcount on the prototype is cheaper than storing pointers to m_info
+        // and m_gtype on each instance.
+        GjsAutoPointer<Prototype, void, g_atomic_rc_box_release> priv =
+            g_atomic_rc_box_new0(Prototype);
+        new (priv) Prototype(info, gtype);
+        if (!priv->init(cx))
+            return nullptr;
+
+        JS::RootedObject parent_proto(cx);
+        if (!priv->get_parent_proto(cx, &parent_proto))
+            return nullptr;
+
+        prototype.set(
+            JS_NewObjectWithGivenProto(cx, &Base::klass, parent_proto));
+
+        if (!prototype)
+            return nullptr;
+
+        // Init the private variable of @private before we do anything else. If
+        // a garbage collection or error happens subsequently, then this object
+        // might be traced and we would end up dereferencing a null pointer.
+        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
 
     /*
@@ -896,6 +958,22 @@ class GIWrapperPrototype : public Base {
                                                      JS::HandleObject wrapper) {
         JS::RootedObject proto(cx);
         JS_GetPrototype(cx, wrapper, &proto);
+
+        if (JS_GetClass(proto) != &Base::klass) {
+            JS::RootedValue gproto(cx);
+
+            auto priv = GjsContextPrivate::from_cx(cx);
+            auto atoms = priv->atoms();
+
+            if (JS_GetPropertyById(cx, proto, atoms.gobject_prototype(),
+                                   &gproto) &&
+                gproto.isObject()) {
+                proto.set(&gproto.toObject());
+            }
+
+            // TODO(ewlsh): Handle assertions with errors instead.
+        }
+
         Base* retval = Base::for_js(cx, proto);
         g_assert(retval);
         return retval->to_prototype();
diff --git a/gjs/atoms.h b/gjs/atoms.h
index 4d3f0faf..79951c20 100644
--- a/gjs/atoms.h
+++ b/gjs/atoms.h
@@ -74,6 +74,8 @@ class JSTracer;
     macro(zone, "zone")
 
 #define FOR_EACH_SYMBOL_ATOM(macro) \
+    macro(gobject_type, "__GObject__type") \
+    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/meson.build b/installed-tests/js/meson.build
index e11f1418..5cf51204 100644
--- a/installed-tests/js/meson.build
+++ b/installed-tests/js/meson.build
@@ -103,6 +103,7 @@ jasmine_tests = [
     'GLib',
     'GObject',
     'GObjectClass',
+    'GObjectType',
     'GObjectInterface',
     'GObjectValue',
     'GTypeClass',
diff --git a/installed-tests/js/testGObjectType.js b/installed-tests/js/testGObjectType.js
new file mode 100644
index 00000000..d4107a6d
--- /dev/null
+++ b/installed-tests/js/testGObjectType.js
@@ -0,0 +1,1702 @@
+// -*- mode: js; indent-tabs-mode: nil -*-
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2011 Giovanni Campagna <gcampagna src gnome org>
+
+const System = imports.system;
+
+imports.gi.versions.Gtk = "3.0";
+
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Gtk = imports.gi.Gtk;
+
+const MyRegisteredObject = GObject.registerClass(class MyRegisteredObject extends GObject.Object {
+    static [GObject.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,
+            "default"
+        ),
+    };
+
+    static [GObject.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],
+        },
+    };
+
+    get readwrite() {
+        if (typeof this._readwrite === "undefined") return "foo";
+        return this._readwrite;
+    }
+
+    set readwrite(val) {
+        if (val === "ignore") return;
+
+        this._readwrite = val;
+    }
+
+    get readonly() {
+        if (typeof this._readonly === "undefined") return "bar";
+        return this._readonly;
+    }
+
+    set readonly(val) {
+        // this should never be called
+        void val;
+        this._readonly = "bogus";
+    }
+
+    get construct() {
+        if (typeof this._constructProp === "undefined") return null;
+        return this._constructProp;
+    }
+
+    set construct(val) {
+        this._constructProp = val;
+    }
+
+    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;
+    }
+});
+
+class MyObject extends GObject.Object {
+    static [GObject.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,
+            "default"
+        ),
+    };
+
+    static [GObject.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],
+        },
+    };
+
+    get readwrite() {
+        if (typeof this._readwrite === "undefined") return "foo";
+        return this._readwrite;
+    }
+
+    set readwrite(val) {
+        if (val === "ignore") return;
+
+        this._readwrite = val;
+    }
+
+    get readonly() {
+        if (typeof this._readonly === "undefined") return "bar";
+        return this._readonly;
+    }
+
+    set readonly(val) {
+        // this should never be called
+        void val;
+        this._readonly = "bogus";
+    }
+
+    get construct() {
+        if (typeof this._constructProp === "undefined") return null;
+        return this._constructProp;
+    }
+
+    set construct(val) {
+        this._constructProp = val;
+    }
+
+    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;
+    }
+}
+
+GObject.registerType(MyObject);
+
+class MyAbstractObject extends GObject.Object {
+    static [GObject.GTypeFlags] = GObject.TypeFlags.ABSTRACT;
+}
+
+class MyApplication extends Gio.Application {
+    static [GObject.signals] = {
+        custom: { param_types: [GObject.TYPE_INT] },
+    };
+
+    emitCustom(n) {
+        this.emit("custom", n);
+    }
+}
+
+GObject.registerType(MyApplication);
+
+class MyInitable extends GObject.Object {
+    static [GObject.interfaces] = [Gio.Initable];
+
+    vfunc_init(cancellable) {
+        if (!(cancellable instanceof Gio.Cancellable))
+            throw new Error("Bad argument");
+
+        this.inited = true;
+    }
+}
+
+GObject.registerType(MyInitable);
+
+class Derived extends MyObject {
+    constructor() {
+        super({ readwrite: "yes" });
+    }
+}
+
+GObject.registerType(Derived);
+
+class Cla$$ extends MyObject {}
+
+GObject.registerType(Cla$$);
+
+class MyCustomInit extends GObject.Object {
+    _instance_init() {
+        this.foo = true;
+    }
+}
+
+GObject.registerType(MyCustomInit);
+
+const NoName = class extends GObject.Object {};
+
+GObject.registerType(NoName);
+
+describe("GObject class with decorator", function () {
+    let myInstance;
+    beforeEach(function () {
+        myInstance = new MyObject();
+    });
+
+    it("throws an error when not used with a GObject-derived class", function () {
+        class Foo {}
+        class Bar extends Foo {}
+        expect(() => GObject.registerType(Bar)).toThrow();
+    });
+
+    it("throws an error when used with an abstract class", function () {
+        expect(() => new MyAbstractObject()).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 MyObject({ readwrite: "baz", construct: "asdf" });
+        expect(myInstance2.readwrite).toEqual("baz");
+        expect(myInstance2.readonly).toEqual("bar");
+        expect(myInstance2.construct).toEqual("asdf");
+    });
+
+    it("warns if more than one argument passed to the default constructor", function () {
+        GLib.test_expect_message(
+            "Gjs",
+            GLib.LogLevelFlags.LEVEL_MESSAGE,
+            "*Too many arguments*"
+        );
+
+        new MyObject({ readwrite: "baz" }, "this is ignored", 123);
+
+        GLib.test_assert_expected_messages_internal(
+            "Gjs",
+            "testGObjectClass.js",
+            0,
+            "testGObjectClassTooManyArguments"
+        );
+    });
+
+    it("throws an error if the first argument to the default constructor is not a property hash", function 
() {
+        expect(() => new MyObject("this is wrong")).toThrow();
+    });
+
+    it("accepts a property hash that is not a plain object", function () {
+        expect(() => new MyObject(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 instanceof MyObject).toBe(true);
+        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(MyObject.name).toEqual("MyObject");
+    });
+
+    // the following would (should) cause a CRITICAL:
+    // myInstance.readonly = 'val';
+
+    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 inherit from something that's not GObject.Object", function () {
+        // ...and still get all the goodies of GObject.Class
+        let instance = new MyApplication({
+            application_id: "org.gjs.Application",
+        });
+        let customSpy = jasmine.createSpy("customSpy");
+        instance.connect("custom", customSpy);
+
+        instance.emitCustom(73);
+        expect(customSpy).toHaveBeenCalledWith(instance, 73);
+    });
+
+    it("can implement an interface", function () {
+        let instance = new MyInitable();
+        expect(instance instanceof Gio.Initable).toBeTruthy();
+        expect(instance instanceof Gio.AsyncInitable).toBeFalsy();
+    });
+
+    it("can implement interface vfuncs", function () {
+        let instance = new MyInitable();
+        expect(instance.inited).toBeFalsy();
+
+        instance.init(new Gio.Cancellable());
+        expect(instance.inited).toBeTruthy();
+    });
+
+    it("can be a subclass", function () {
+        let derived = new Derived();
+
+        expect(derived instanceof Derived).toBeTruthy();
+        expect(derived instanceof MyObject).toBeTruthy();
+
+        expect(derived.readwrite).toEqual("yes");
+    });
+
+    it("can have any valid class name", function () {
+        let obj = new Cla$$();
+
+        expect(obj instanceof Cla$$).toBeTruthy();
+        expect(obj instanceof MyObject).toBeTruthy();
+    });
+
+    it("handles anonymous class expressions", function () {
+        const obj = new NoName();
+        expect(obj instanceof NoName).toBeTruthy();
+
+        const NoName2 = class extends GObject.Object {};
+        GObject.registerType(NoName2);
+        const obj2 = new NoName2();
+        expect(obj2 instanceof NoName2).toBeTruthy();
+    });
+
+    it("calls its _instance_init() function while chaining up in constructor", function () {
+        let instance = new MyCustomInit();
+        expect(instance.foo).toBeTruthy();
+    });
+
+    it("can have an interface-valued property", function () {
+        class InterfacePropObject extends GObject.Object {
+            static [GObject.properties] = {
+                file: GObject.ParamSpec.object(
+                    "file",
+                    "File",
+                    "File",
+                    GObject.ParamFlags.READWRITE |
+                        GObject.ParamFlags.CONSTRUCT_ONLY,
+                    Gio.File.$gtype
+                ),
+            };
+        }
+
+        GObject.registerType(InterfacePropObject);
+        let file = Gio.File.new_for_path("dummy");
+        expect(() => new InterfacePropObject({ file })).not.toThrow();
+    });
+
+    it("can override a property from the parent class", function () {
+        class OverrideObject extends MyObject {
+            static [GObject.properties] = {
+                readwrite: GObject.ParamSpec.override("readwrite", MyObject),
+            };
+
+            get readwrite() {
+                return this._subclass_readwrite;
+            }
+
+            set readwrite(val) {
+                this._subclass_readwrite = `subclass${val}`;
+            }
+        }
+        GObject.registerType(OverrideObject);
+        let obj = new OverrideObject();
+        obj.readwrite = "foo";
+        expect(obj.readwrite).toEqual("subclassfoo");
+    });
+
+    it("cannot override a non-existent property", function () {
+        expect(() => {
+            class BadOverride extends GObject.Object {
+                static [GObject.properties] = {
+                    nonexistent: GObject.ParamSpec.override(
+                        "nonexistent",
+                        GObject.Object
+                    ),
+                };
+            }
+        }).toThrow();
+    });
+
+    it("handles gracefully forgetting to override a C property", function () {
+        GLib.test_expect_message(
+            "GLib-GObject",
+            GLib.LogLevelFlags.LEVEL_CRITICAL,
+            "*Object class Gjs_ForgottenOverride doesn't implement property " +
+                "'anchors' from interface 'GTlsFileDatabase'*"
+        );
+
+        class ForgottenOverride extends Gio.TlsDatabase {
+            static [GObject.interfaces] = [Gio.TlsFileDatabase];
+        }
+        // This is a random interface in Gio with a read-write property
+        GObject.registerType(ForgottenOverride);
+        const obj = new ForgottenOverride();
+        expect(obj.anchors).not.toBeDefined();
+        expect(() => (obj.anchors = "foo")).not.toThrow();
+        expect(obj.anchors).toEqual("foo");
+
+        GLib.test_assert_expected_messages_internal(
+            "Gjs",
+            "testGObjectClass.js",
+            0,
+            "testGObjectClassForgottenOverride"
+        );
+    });
+
+    it("handles gracefully overriding a C property but forgetting the accessors", function () {
+        // This is a random interface in Gio with a read-write property
+        class ForgottenAccessors extends Gio.TlsDatabase {
+            static [GObject.interfaces] = [Gio.TlsFileDatabase];
+            static [GObject.properties] = {
+                anchors: GObject.ParamSpec.override(
+                    "anchors",
+                    Gio.TlsFileDatabase
+                ),
+            };
+        }
+        GObject.registerType(ForgottenAccessors);
+        const obj = new ForgottenAccessors();
+        expect(obj.anchors).toBeNull(); // the property's default value
+        obj.anchors = "foo";
+        expect(obj.anchors).toEqual("foo");
+
+        class ForgottenAccessors2 extends ForgottenAccessors {}
+        GObject.registerType(ForgottenAccessors2);
+        const obj2 = new ForgottenAccessors2();
+        expect(obj2.anchors).toBeNull();
+        obj2.anchors = "foo";
+        expect(obj2.anchors).toEqual("foo");
+    });
+
+    it("does not pollute the wrong prototype with GObject properties", function () {
+        class MyCustomCharset extends Gio.CharsetConverter {
+            constructor() {
+                super();
+                void this.from_charset;
+            }
+        }
+
+        GObject.registerType(MyCustomCharset);
+
+        class MySecondCustomCharset extends GObject.Object {
+            constructor() {
+                super();
+                this.from_charset = "another value";
+            }
+        }
+
+        GObject.registerType(MySecondCustomCharset);
+
+        expect(
+            () => new MyCustomCharset() && new MySecondCustomCharset()
+        ).not.toThrow();
+    });
+
+    it("resolves properties from interfaces", function () {
+        const mon = Gio.NetworkMonitor.get_default();
+        expect(mon.network_available).toBeDefined();
+        expect(mon.networkAvailable).toBeDefined();
+        expect(mon["network-available"]).toBeDefined();
+    });
+
+    it("has a toString() definition", function () {
+        expect(myInstance.toString()).toMatch(
+            /\[object instance wrapper GType:Gjs_MyObject jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/
+        );
+        expect(new Derived().toString()).toMatch(
+            /\[object instance wrapper GType:Gjs_Derived jsobj@0x[a-f0-9]+ native@0x[a-f0-9]+\]/
+        );
+    });
+});
+
+describe("GObject virtual function", function () {
+    it("can have its property read", function () {
+        expect(GObject.Object.prototype.vfunc_constructed).toBeTruthy();
+    });
+
+    it("can have its property overridden with an anonymous function", function () {
+        let callback;
+
+        let key = "vfunc_constructed";
+
+        class _SimpleTestClass1 extends GObject.Object {
+            static [GObject.GTypeName] = "SimpleTestClass1";
+        }
+
+        if (GObject.Object.prototype.vfunc_constructed) {
+            let parentFunc = GObject.Object.prototype.vfunc_constructed;
+            _SimpleTestClass1.prototype[key] = function (...args) {
+                parentFunc.call(this, ...args);
+                callback("123");
+            };
+        } else {
+            _SimpleTestClass1.prototype[key] = function () {
+                callback("abc");
+            };
+        }
+
+        callback = jasmine.createSpy("callback");
+
+        GObject.registerType(_SimpleTestClass1);
+        new _SimpleTestClass1();
+
+        expect(callback).toHaveBeenCalledWith("123");
+    });
+
+    it("can access the parent prototype with super()", function () {
+        let callback;
+
+        class _SimpleTestClass2 extends GObject.Object {
+            static [GObject.GTypeName] = "SimpleTestClass2";
+
+            vfunc_constructed() {
+                super.vfunc_constructed();
+                callback("vfunc_constructed");
+            }
+        }
+
+        callback = jasmine.createSpy("callback");
+
+        GObject.registerType(_SimpleTestClass2);
+        new _SimpleTestClass2();
+
+        expect(callback).toHaveBeenCalledWith("vfunc_constructed");
+    });
+
+    it("handles non-existing properties", function () {
+        class _SimpleTestClass3 extends GObject.Object {
+            static [GObject.GTypeName] = "SimpleTestClass3";
+        }
+
+        _SimpleTestClass3.prototype.vfunc_doesnt_exist = function () {};
+
+        if (GObject.Object.prototype.vfunc_doesnt_exist)
+            fail("Virtual function should not exist");
+
+        expect(() => GObject.registerType(_SimpleTestClass3)).toThrow();
+    });
+
+    it("gracefully bails out when overriding an unsupported vfunc type", function () {
+        class Foo extends GObject.Object {
+            static [GObject.interfaces] = [Gio.AsyncInitable];
+
+            vfunc_init_async() {}
+        }
+
+        expect(() => GObject.registerType(Foo)).toThrow();
+    });
+});
+
+describe("GObject creation using base classes without registered GType", function () {
+    it("fails when trying to instantiate a class that inherits from a GObject type", function () {
+        const BadInheritance = class extends GObject.Object {};
+        const BadDerivedInheritance = class extends Derived {};
+
+        expect(() => new BadInheritance()).toThrowError(
+            /Tried to construct an object without a GType/
+        );
+        expect(() => new BadDerivedInheritance()).toThrowError(
+            /Tried to construct an object without a GType/
+        );
+    });
+
+    it("fails when trying to register a GObject class that inherits from a non-GObject type", function () {
+        class BadInheritance extends GObject.Object {}
+        class BadInheritanceDerived extends BadInheritance {}
+        expect(() => GObject.registerType(BadInheritanceDerived)).toThrowError(
+            /Object 0x[a-f0-9]+ is not a subclass of GObject_Object, it's a Object/
+        );
+    });
+});
+
+describe("Register GType name", function () {
+    beforeAll(function () {
+        expect(GObject.gtypeNameBasedOnJSPath).toBeFalsy();
+    });
+
+    afterEach(function () {
+        GObject.gtypeNameBasedOnJSPath = false;
+    });
+
+    it("uses the class name", function () {
+        class GTypeTestAutoName extends GObject.Object {}
+        GObject.registerType(GTypeTestAutoName);
+
+        expect(GTypeTestAutoName.$gtype.name).toEqual("Gjs_GTypeTestAutoName");
+    });
+
+    it("uses the sanitized class name", function () {
+        class GTypeTestAutoCla$$Name extends GObject.Object {}
+        GObject.registerType(GTypeTestAutoCla$$Name);
+
+        expect(GTypeTestAutoCla$$Name.$gtype.name).toEqual(
+            "Gjs_GTypeTestAutoCla__Name"
+        );
+    });
+
+    it("use the file path and class name", function () {
+        GObject.gtypeNameBasedOnJSPath = true;
+        class GTypeTestAutoName extends GObject.Object {}
+        GObject.registerType(GTypeTestAutoName);
+
+        /* Update this test if the file is moved */
+        expect(GTypeTestAutoName.$gtype.name).toEqual(
+            "Gjs_js_testGObjectType_GTypeTestAutoName"
+        );
+    });
+
+    it("use the file path and sanitized class name", function () {
+        GObject.gtypeNameBasedOnJSPath = true;
+        class GTypeTestAutoCla$$Name extends GObject.Object {}
+        GObject.registerType(GTypeTestAutoCla$$Name);
+
+        /* Update this test if the file is moved */
+        expect(GTypeTestAutoCla$$Name.$gtype.name).toEqual(
+            "Gjs_js_testGObjectType_GTypeTestAutoCla__Name"
+        );
+    });
+
+    it("use provided class name", function () {
+        class GtypeClass extends GObject.Object {
+            static [GObject.GTypeName] = "GTypeTestManualName";
+        }
+        GObject.registerType(GtypeClass);
+
+        expect(GtypeClass.$gtype.name).toEqual("GTypeTestManualName");
+    });
+
+    it("sanitizes user provided class name", function () {
+        let gtypeName = "GType$Test/WithLòt's of*bad§chars!";
+        let expectedSanitized = "GType_Test_WithL_t_s_of_bad_chars_";
+
+        GLib.test_expect_message(
+            "Gjs",
+            GLib.LogLevelFlags.LEVEL_WARNING,
+            `*RangeError: Provided GType name '${gtypeName}' is not valid; ` +
+                `automatically sanitized to '${expectedSanitized}'*`
+        );
+
+        class GtypeClass extends GObject.Object {
+            static [GObject.GTypeName] = gtypeName;
+        }
+        GObject.registerType(GtypeClass);
+
+        GLib.test_assert_expected_messages_internal(
+            "Gjs",
+            "testGObjectClass.js",
+            0,
+            "testGObjectRegisterClassSanitize"
+        );
+
+        expect(GtypeClass.$gtype.name).toEqual(expectedSanitized);
+    });
+});
+
+describe("Signal handler matching", function () {
+    let o,
+        handleEmpty,
+        emptyId,
+        handleDetailed,
+        detailedId,
+        handleDetailedOne,
+        detailedOneId,
+        handleDetailedTwo,
+        detailedTwoId,
+        handleNotifyTwo,
+        notifyTwoId,
+        handleMinimalOrFull,
+        minimalId,
+        fullId;
+
+    beforeEach(function () {
+        o = new MyObject();
+        handleEmpty = jasmine.createSpy("handleEmpty");
+        emptyId = o.connect("empty", handleEmpty);
+        handleDetailed = jasmine.createSpy("handleDetailed");
+        detailedId = o.connect("detailed", handleDetailed);
+        handleDetailedOne = jasmine.createSpy("handleDetailedOne");
+        detailedOneId = o.connect("detailed::one", handleDetailedOne);
+        handleDetailedTwo = jasmine.createSpy("handleDetailedTwo");
+        detailedTwoId = o.connect("detailed::two", handleDetailedTwo);
+        handleNotifyTwo = jasmine.createSpy("handleNotifyTwo");
+        notifyTwoId = o.connect("notify::two", handleNotifyTwo);
+        handleMinimalOrFull = jasmine.createSpy("handleMinimalOrFull");
+        minimalId = o.connect("minimal", handleMinimalOrFull);
+        fullId = o.connect("full", handleMinimalOrFull);
+    });
+
+    it("finds handlers by signal ID", function () {
+        expect(GObject.signal_handler_find(o, { signalId: "empty" })).toEqual(
+            emptyId
+        );
+        // when more than one are connected, returns an arbitrary one
+        expect([detailedId, detailedOneId, detailedTwoId]).toContain(
+            GObject.signal_handler_find(o, { signalId: "detailed" })
+        );
+    });
+
+    it("finds handlers by signal detail", function () {
+        expect(GObject.signal_handler_find(o, { detail: "one" })).toEqual(
+            detailedOneId
+        );
+        // when more than one are connected, returns an arbitrary one
+        expect([detailedTwoId, notifyTwoId]).toContain(
+            GObject.signal_handler_find(o, { detail: "two" })
+        );
+    });
+
+    it("finds handlers by callback", function () {
+        expect(GObject.signal_handler_find(o, { func: handleEmpty })).toEqual(
+            emptyId
+        );
+        expect(
+            GObject.signal_handler_find(o, { func: handleDetailed })
+        ).toEqual(detailedId);
+        expect(
+            GObject.signal_handler_find(o, { func: handleDetailedOne })
+        ).toEqual(detailedOneId);
+        expect(
+            GObject.signal_handler_find(o, { func: handleDetailedTwo })
+        ).toEqual(detailedTwoId);
+        expect(
+            GObject.signal_handler_find(o, { func: handleNotifyTwo })
+        ).toEqual(notifyTwoId);
+        // when more than one are connected, returns an arbitrary one
+        expect([minimalId, fullId]).toContain(
+            GObject.signal_handler_find(o, { func: handleMinimalOrFull })
+        );
+    });
+
+    it("finds handlers by a combination of parameters", function () {
+        expect(
+            GObject.signal_handler_find(o, {
+                signalId: "detailed",
+                detail: "two",
+            })
+        ).toEqual(detailedTwoId);
+        expect(
+            GObject.signal_handler_find(o, {
+                signalId: "detailed",
+                func: handleDetailed,
+            })
+        ).toEqual(detailedId);
+    });
+
+    it("blocks a handler by callback", function () {
+        expect(
+            GObject.signal_handlers_block_matched(o, { func: handleEmpty })
+        ).toEqual(1);
+        o.emitEmpty();
+        expect(handleEmpty).not.toHaveBeenCalled();
+
+        expect(
+            GObject.signal_handlers_unblock_matched(o, { func: handleEmpty })
+        ).toEqual(1);
+        o.emitEmpty();
+        expect(handleEmpty).toHaveBeenCalled();
+    });
+
+    it("blocks multiple handlers by callback", function () {
+        expect(
+            GObject.signal_handlers_block_matched(o, {
+                func: handleMinimalOrFull,
+            })
+        ).toEqual(2);
+        o.emitMinimal();
+        o.emitFull();
+        expect(handleMinimalOrFull).not.toHaveBeenCalled();
+
+        expect(
+            GObject.signal_handlers_unblock_matched(o, {
+                func: handleMinimalOrFull,
+            })
+        ).toEqual(2);
+        o.emitMinimal();
+        o.emitFull();
+        expect(handleMinimalOrFull).toHaveBeenCalledTimes(2);
+    });
+
+    it("blocks handlers by a combination of parameters", function () {
+        expect(
+            GObject.signal_handlers_block_matched(o, {
+                signalId: "detailed",
+                func: handleDetailed,
+            })
+        ).toEqual(1);
+        o.emit("detailed", "");
+        o.emit("detailed::one", "");
+        expect(handleDetailed).not.toHaveBeenCalled();
+        expect(handleDetailedOne).toHaveBeenCalled();
+
+        expect(
+            GObject.signal_handlers_unblock_matched(o, {
+                signalId: "detailed",
+                func: handleDetailed,
+            })
+        ).toEqual(1);
+        o.emit("detailed", "");
+        o.emit("detailed::one", "");
+        expect(handleDetailed).toHaveBeenCalled();
+    });
+
+    it("disconnects a handler by callback", function () {
+        expect(
+            GObject.signal_handlers_disconnect_matched(o, { func: handleEmpty })
+        ).toEqual(1);
+        o.emitEmpty();
+        expect(handleEmpty).not.toHaveBeenCalled();
+    });
+
+    it("blocks multiple handlers by callback", function () {
+        expect(
+            GObject.signal_handlers_disconnect_matched(o, {
+                func: handleMinimalOrFull,
+            })
+        ).toEqual(2);
+        o.emitMinimal();
+        o.emitFull();
+        expect(handleMinimalOrFull).not.toHaveBeenCalled();
+    });
+
+    it("blocks handlers by a combination of parameters", function () {
+        expect(
+            GObject.signal_handlers_disconnect_matched(o, {
+                signalId: "detailed",
+                func: handleDetailed,
+            })
+        ).toEqual(1);
+        o.emit("detailed", "");
+        o.emit("detailed::one", "");
+        expect(handleDetailed).not.toHaveBeenCalled();
+        expect(handleDetailedOne).toHaveBeenCalled();
+    });
+
+    it("blocks a handler by callback, convenience method", function () {
+        expect(GObject.signal_handlers_block_by_func(o, handleEmpty)).toEqual(
+            1
+        );
+        o.emitEmpty();
+        expect(handleEmpty).not.toHaveBeenCalled();
+
+        expect(GObject.signal_handlers_unblock_by_func(o, handleEmpty)).toEqual(
+            1
+        );
+        o.emitEmpty();
+        expect(handleEmpty).toHaveBeenCalled();
+    });
+
+    it("disconnects a handler by callback, convenience method", function () {
+        expect(
+            GObject.signal_handlers_disconnect_by_func(o, handleEmpty)
+        ).toEqual(1);
+        o.emitEmpty();
+        expect(handleEmpty).not.toHaveBeenCalled();
+    });
+
+    it("does not support disconnecting a handler by callback data", function () {
+        expect(() =>
+            GObject.signal_handlers_disconnect_by_data(o, null)
+        ).toThrow();
+    });
+});
+
+describe("Auto accessor generation", function () {
+    class AutoAccessors extends GObject.Object {
+        constructor(props = {}) {
+            super(props);
+            this._snakeNameGetterCalled = 0;
+            this._snakeNameSetterCalled = 0;
+            this._camelNameGetterCalled = 0;
+            this._camelNameSetterCalled = 0;
+            this._kebabNameGetterCalled = 0;
+            this._kebabNameSetterCalled = 0;
+        }
+
+        static [GObject.properties] = {
+            simple: GObject.ParamSpec.int(
+                "simple",
+                "Simple",
+                "Short-named property",
+                GObject.ParamFlags.READWRITE,
+                0,
+                100,
+                24
+            ),
+            "long-long-name": GObject.ParamSpec.int(
+                "long-long-name",
+                "Long long name",
+                "Long-named property",
+                GObject.ParamFlags.READWRITE,
+                0,
+                100,
+                48
+            ),
+            construct: GObject.ParamSpec.int(
+                "construct",
+                "Construct",
+                "Construct",
+                GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
+                0,
+                100,
+                96
+            ),
+            "construct-only": GObject.ParamSpec.int(
+                "construct-only",
+                "Construct only",
+                "Construct-only property",
+                GObject.ParamFlags.READWRITE |
+                    GObject.ParamFlags.CONSTRUCT_ONLY,
+                0,
+                100,
+                80
+            ),
+            "snake-name": GObject.ParamSpec.int(
+                "snake-name",
+                "Snake name",
+                "Snake-cased property",
+                GObject.ParamFlags.READWRITE,
+                0,
+                100,
+                36
+            ),
+            "camel-name": GObject.ParamSpec.int(
+                "camel-name",
+                "Camel name",
+                "Camel-cased property",
+                GObject.ParamFlags.READWRITE,
+                0,
+                100,
+                72
+            ),
+            "kebab-name": GObject.ParamSpec.int(
+                "kebab-name",
+                "Kebab name",
+                "Kebab-cased property",
+                GObject.ParamFlags.READWRITE,
+                0,
+                100,
+                12
+            ),
+            readonly: GObject.ParamSpec.int(
+                "readonly",
+                "Readonly",
+                "Readonly property",
+                GObject.ParamFlags.READABLE,
+                0,
+                100,
+                54
+            ),
+            writeonly: GObject.ParamSpec.int(
+                "writeonly",
+                "Writeonly",
+                "Writeonly property",
+                GObject.ParamFlags.WRITABLE,
+                0,
+                100,
+                60
+            ),
+            "missing-getter": GObject.ParamSpec.int(
+                "missing-getter",
+                "Missing getter",
+                "Missing a getter",
+                GObject.ParamFlags.READWRITE,
+                0,
+                100,
+                18
+            ),
+            "missing-setter": GObject.ParamSpec.int(
+                "missing-setter",
+                "Missing setter",
+                "Missing a setter",
+                GObject.ParamFlags.READWRITE,
+                0,
+                100,
+                42
+            ),
+        };
+
+        get snake_name() {
+            this._snakeNameGetterCalled++;
+            return 42;
+        }
+
+        set snake_name(value) {
+            this._snakeNameSetterCalled++;
+        }
+
+        get camelName() {
+            this._camelNameGetterCalled++;
+            return 42;
+        }
+
+        set camelName(value) {
+            this._camelNameSetterCalled++;
+        }
+
+        get ["kebab-name"]() {
+            this._kebabNameGetterCalled++;
+            return 42;
+        }
+
+        set ["kebab-name"](value) {
+            this._kebabNameSetterCalled++;
+        }
+
+        set missing_getter(value) {
+            this._missingGetter = value;
+        }
+
+        get missing_setter() {
+            return 42;
+        }
+    }
+
+    GObject.registerType(AutoAccessors);
+
+    let a;
+    beforeEach(function () {
+        a = new AutoAccessors();
+    });
+
+    it("get and set the property", function () {
+        a.simple = 1;
+        expect(a.simple).toEqual(1);
+        a["long-long-name"] = 1;
+        expect(a["long-long-name"]).toEqual(1);
+        a.construct = 1;
+        expect(a.construct).toEqual(1);
+    });
+
+    it("initial value is the param spec's default value", function () {
+        expect(a.simple).toEqual(24);
+        expect(a.long_long_name).toEqual(48);
+        expect(a.longLongName).toEqual(48);
+        expect(a["long-long-name"]).toEqual(48);
+        expect(a.construct).toEqual(96);
+        expect(a.construct_only).toEqual(80);
+        expect(a.constructOnly).toEqual(80);
+        expect(a["construct-only"]).toEqual(80);
+    });
+
+    it("set properties at construct time", function () {
+        a = new AutoAccessors({
+            simple: 1,
+            longLongName: 1,
+            construct: 1,
+            "construct-only": 1,
+        });
+        expect(a.simple).toEqual(1);
+        expect(a.long_long_name).toEqual(1);
+        expect(a.longLongName).toEqual(1);
+        expect(a["long-long-name"]).toEqual(1);
+        expect(a.construct).toEqual(1);
+        expect(a.construct_only).toEqual(1);
+        expect(a.constructOnly).toEqual(1);
+        expect(a["construct-only"]).toEqual(1);
+    });
+
+    it("notify when the property changes", function () {
+        const notify = jasmine.createSpy("notify");
+        a.connect("notify::simple", notify);
+        a.simple = 1;
+        expect(notify).toHaveBeenCalledTimes(1);
+        notify.calls.reset();
+        a.simple = 1;
+        expect(notify).not.toHaveBeenCalled();
+    });
+
+    it("copies accessors for camel and kebab if snake accessors given", function () {
+        a.snakeName = 42;
+        expect(a.snakeName).toEqual(42);
+        a["snake-name"] = 42;
+        expect(a["snake-name"]).toEqual(42);
+        expect(a._snakeNameGetterCalled).toEqual(2);
+        expect(a._snakeNameSetterCalled).toEqual(2);
+    });
+
+    it("copies accessors for snake and kebab if camel accessors given", function () {
+        a.camel_name = 42;
+        expect(a.camel_name).toEqual(42);
+        a["camel-name"] = 42;
+        expect(a["camel-name"]).toEqual(42);
+        expect(a._camelNameGetterCalled).toEqual(2);
+        expect(a._camelNameSetterCalled).toEqual(2);
+    });
+
+    it("copies accessors for snake and camel if kebab accessors given", function () {
+        a.kebabName = 42;
+        expect(a.kebabName).toEqual(42);
+        a.kebab_name = 42;
+        expect(a.kebab_name).toEqual(42);
+        expect(a._kebabNameGetterCalled).toEqual(2);
+        expect(a._kebabNameSetterCalled).toEqual(2);
+    });
+
+    it("readonly getter throws", function () {
+        expect(() => a.readonly).toThrowError(/getter/);
+    });
+
+    it("writeonly setter throws", function () {
+        expect(() => (a.writeonly = 1)).toThrowError(/setter/);
+    });
+
+    it("getter throws when setter defined", function () {
+        expect(() => a.missingGetter).toThrowError(/getter/);
+    });
+
+    it("setter throws when getter defined", function () {
+        expect(() => (a.missingSetter = 1)).toThrowError(/setter/);
+    });
+});
+
+class MyObjectWithJSObjectProperty extends GObject.Object {
+    static [GObject.properties] = {
+        "jsobj-prop": GObject.ParamSpec.jsobject(
+            "jsobj-prop",
+            "jsobj-prop",
+            "jsobj-prop",
+            GObject.ParamFlags.CONSTRUCT | GObject.ParamFlags.READWRITE,
+            ""
+        ),
+    };
+}
+
+GObject.registerType(MyObjectWithJSObjectProperty);
+
+describe("GObject class with JSObject property", function () {
+    it("assigns a valid JSObject on construct", function () {
+        let date = new Date();
+        let obj = new MyObjectWithJSObjectProperty({ jsobj_prop: date });
+        expect(obj.jsobj_prop).toEqual(date);
+        expect(obj.jsobj_prop).not.toEqual(new Date(0));
+        expect(() => obj.jsobj_prop.setFullYear(1985)).not.toThrow();
+        expect(obj.jsobj_prop.getFullYear()).toEqual(1985);
+    });
+
+    it("Set null with an empty JSObject on construct", function () {
+        expect(new MyObjectWithJSObjectProperty().jsobj_prop).toBeNull();
+        expect(new MyObjectWithJSObjectProperty({}).jsobj_prop).toBeNull();
+    });
+
+    it("assigns a null JSObject on construct", function () {
+        expect(
+            new MyObjectWithJSObjectProperty({ jsobj_prop: null }).jsobj_prop
+        ).toBeNull();
+    });
+
+    it("assigns a JSObject Array on construct", function () {
+        expect(
+            () => new MyObjectWithJSObjectProperty({ jsobj_prop: [1, 2, 3] })
+        ).not.toThrow();
+    });
+
+    it("assigns a Function on construct", function () {
+        expect(
+            () =>
+                new MyObjectWithJSObjectProperty({
+                    jsobj_prop: () => {
+                        return true;
+                    },
+                })
+        ).not.toThrow();
+    });
+
+    it("throws an error when using a boolean value on construct", function () {
+        expect(
+            () => new MyObjectWithJSObjectProperty({ jsobj_prop: true })
+        ).toThrowError(/JSObject expected/);
+    });
+
+    it("throws an error when using an int value on construct", function () {
+        expect(
+            () => new MyObjectWithJSObjectProperty({ jsobj_prop: 1 })
+        ).toThrowError(/JSObject expected/);
+    });
+
+    it("throws an error when using a numeric value on construct", function () {
+        expect(
+            () => new MyObjectWithJSObjectProperty({ jsobj_prop: Math.PI })
+        ).toThrowError(/JSObject expected/);
+    });
+
+    it("throws an error when using a string value on construct", function () {
+        expect(
+            () => new MyObjectWithJSObjectProperty({ jsobj_prop: "string" })
+        ).toThrowError(/JSObject expected/);
+    });
+
+    it("throws an error when using an undefined value on construct", function () {
+        expect(
+            () => new MyObjectWithJSObjectProperty({ jsobj_prop: undefined })
+        ).toThrow();
+    });
+
+    it("property value survives when GObject wrapper is collected", function () {
+        class MyConverter extends GObject.Object {
+            static [GObject.properties] = {
+                testprop: GObject.ParamSpec.jsobject(
+                    "testprop",
+                    "testprop",
+                    "Test property",
+                    GObject.ParamFlags.CONSTRUCT | GObject.ParamFlags.READWRITE
+                ),
+            };
+
+            static [GObject.interfaces] = [Gio.Converter];
+        }
+
+        GObject.registerType(MyConverter);
+
+        function stashObject() {
+            const base = new Gio.MemoryInputStream();
+            const converter = new MyConverter({ testprop: [1, 2, 3] });
+            return Gio.ConverterInputStream.new(base, converter);
+        }
+
+        const stream = stashObject();
+        System.gc();
+        expect(stream.get_converter().testprop).toEqual([1, 2, 3]);
+    });
+});
+
+class MyObjectWithJSObjectSignals extends GObject.Object {
+    static [GObject.signals] = {
+        "send-object": {
+            param_types: [GObject.TYPE_JSOBJECT],
+        },
+        "send-many-objects": {
+            param_types: [
+                GObject.TYPE_JSOBJECT,
+                GObject.TYPE_JSOBJECT,
+                GObject.TYPE_JSOBJECT,
+            ],
+        },
+        "get-object": {
+            flags: GObject.SignalFlags.RUN_LAST,
+            accumulator: GObject.AccumulatorType.FIRST_WINS,
+            return_type: GObject.TYPE_JSOBJECT,
+            param_types: [GObject.TYPE_JSOBJECT],
+        },
+    };
+
+    emitObject(obj) {
+        this.emit("send-object", obj);
+    }
+}
+
+GObject.registerType(MyObjectWithJSObjectSignals);
+
+describe("GObject class with JSObject signals", function () {
+    let myInstance;
+    beforeEach(function () {
+        myInstance = new MyObjectWithJSObjectSignals();
+    });
+
+    it("emits signal with null JSObject parameter", function () {
+        let customSpy = jasmine.createSpy("sendObjectSpy");
+        myInstance.connect("send-object", customSpy);
+        myInstance.emitObject(null);
+        expect(customSpy).toHaveBeenCalledWith(myInstance, null);
+    });
+
+    it("emits signal with JSObject parameter", function () {
+        let customSpy = jasmine.createSpy("sendObjectSpy");
+        myInstance.connect("send-object", customSpy);
+
+        let obj = {
+            foo: [1, 2, 3],
+            sub: { a: {}, b: this },
+            desc: "test",
+            date: new Date(),
+        };
+        myInstance.emitObject(obj);
+        expect(customSpy).toHaveBeenCalledWith(myInstance, obj);
+    });
+
+    it("emits signal with multiple JSObject parameters", function () {
+        let customSpy = jasmine.createSpy("sendManyObjectsSpy");
+        myInstance.connect("send-many-objects", customSpy);
+
+        let obj = {
+            foo: [9, 8, 7, "a", "b", "c"],
+            sub: { a: {}, b: this },
+            desc: "test",
+            date: new RegExp("\\w+"),
+        };
+        myInstance.emit("send-many-objects", obj, obj.foo, obj.sub);
+        expect(customSpy).toHaveBeenCalledWith(
+            myInstance,
+            obj,
+            obj.foo,
+            obj.sub
+        );
+    });
+
+    it("re-emits signal with same JSObject parameter", function () {
+        let obj = {
+            foo: [9, 8, 7, "a", "b", "c"],
+            sub: { a: {}, b: this },
+            func: (arg) => {
+                return { ret: [arg] };
+            },
+        };
+
+        myInstance.connect("send-many-objects", (instance, func, args, foo) => {
+            expect(instance).toEqual(myInstance);
+            expect(System.addressOf(instance)).toEqual(
+                System.addressOf(myInstance)
+            );
+            expect(foo).toEqual(obj.foo);
+            expect(System.addressOf(foo)).toEqual(System.addressOf(obj.foo));
+            expect(func(args).ret[0]).toEqual(args);
+        });
+        myInstance.connect("send-object", (instance, param) => {
+            expect(instance).toEqual(myInstance);
+            expect(System.addressOf(instance)).toEqual(
+                System.addressOf(myInstance)
+            );
+            expect(param).toEqual(obj);
+            expect(System.addressOf(param)).toEqual(System.addressOf(obj));
+            expect(() =>
+                instance.emit("send-many-objects", param.func, param, param.foo)
+            ).not.toThrow();
+        });
+
+        myInstance.emit("send-object", obj);
+    });
+
+    it("throws an error when using a boolean value as parameter", function () {
+        expect(() => myInstance.emit("send-object", true)).toThrowError(
+            /JSObject expected/
+        );
+        expect(() =>
+            myInstance.emit("send-many-objects", ["a"], true, {})
+        ).toThrowError(/JSObject expected/);
+    });
+
+    it("throws an error when using an int value as parameter", function () {
+        expect(() => myInstance.emit("send-object", 1)).toThrowError(
+            /JSObject expected/
+        );
+        expect(() =>
+            myInstance.emit("send-many-objects", ["a"], 1, {})
+        ).toThrowError(/JSObject expected/);
+    });
+
+    it("throws an error when using a numeric value as parameter", function () {
+        expect(() => myInstance.emit("send-object", Math.PI)).toThrowError(
+            /JSObject expected/
+        );
+        expect(() =>
+            myInstance.emit("send-many-objects", ["a"], Math.PI, {})
+        ).toThrowError(/JSObject expected/);
+    });
+
+    it("throws an error when using a string value as parameter", function () {
+        expect(() => myInstance.emit("send-object", "string")).toThrowError(
+            /JSObject expected/
+        );
+        expect(() =>
+            myInstance.emit("send-many-objects", ["a"], "string", {})
+        ).toThrowError(/JSObject expected/);
+    });
+
+    it("throws an error when using an undefined value as parameter", function () {
+        expect(() => myInstance.emit("send-object", undefined)).toThrowError(
+            /JSObject expected/
+        );
+        expect(() =>
+            myInstance.emit("send-many-objects", ["a"], undefined, {})
+        ).toThrowError(/JSObject expected/);
+    });
+
+    it("returns a JSObject", function () {
+        let data = {
+            foo: [9, 8, 7, "a", "b", "c"],
+            sub: { a: {}, b: this },
+            func: (arg) => {
+                return { ret: [arg] };
+            },
+        };
+        let id = myInstance.connect("get-object", () => {
+            return data;
+        });
+        expect(myInstance.emit("get-object", {})).toBe(data);
+        myInstance.disconnect(id);
+
+        myInstance.connect("get-object", (instance, input) => {
+            if (input) {
+                if (typeof input === "function") input();
+                return input;
+            }
+
+            class SubObject {
+                constructor() {
+                    this.pi = Math.PI;
+                }
+
+                method() {}
+
+                gobject() {
+                    return GObject.Object;
+                }
+
+                get data() {
+                    return data;
+                }
+            }
+
+            return new SubObject();
+        });
+
+        expect(myInstance.emit("get-object", null).constructor.name).toBe(
+            "SubObject"
+        );
+        expect(myInstance.emit("get-object", null).data).toBe(data);
+        expect(myInstance.emit("get-object", null).pi).toBe(Math.PI);
+        expect(() =>
+            myInstance.emit("get-object", null).method()
+        ).not.toThrow();
+        expect(myInstance.emit("get-object", null).gobject()).toBe(
+            GObject.Object
+        );
+        expect(
+            new (myInstance.emit("get-object", null).gobject())() instanceof
+                GObject.Object
+        ).toBeTruthy();
+        expect(myInstance.emit("get-object", data)).toBe(data);
+        expect(
+            myInstance.emit("get-object", jasmine.createSpy("callMeSpy"))
+        ).toHaveBeenCalled();
+    });
+
+    it("returns null when returning undefined", function () {
+        myInstance.connect("get-object", () => {
+            return undefined;
+        });
+        expect(myInstance.emit("get-object", {})).toBeNull();
+    });
+
+    it("returns null when not returning", function () {
+        myInstance.connect("get-object", () => {});
+        expect(myInstance.emit("get-object", {})).toBeNull();
+    });
+
+    // These tests are intended to throw an error, but currently errors cannot
+    // be caught from signal handlers, so we check for logged messages instead
+
+    it("throws an error when returning a boolean value", function () {
+        GLib.test_expect_message(
+            "Gjs",
+            GLib.LogLevelFlags.LEVEL_WARNING,
+            "*JSObject expected*"
+        );
+        myInstance.connect("get-object", () => true);
+        myInstance.emit("get-object", {});
+        GLib.test_assert_expected_messages_internal(
+            "Gjs",
+            "testGObjectClass.js",
+            0,
+            "throws an error when returning a boolean value"
+        );
+    });
+
+    it("throws an error when returning an int value", function () {
+        GLib.test_expect_message(
+            "Gjs",
+            GLib.LogLevelFlags.LEVEL_WARNING,
+            "*JSObject expected*"
+        );
+        myInstance.connect("get-object", () => 1);
+        myInstance.emit("get-object", {});
+        GLib.test_assert_expected_messages_internal(
+            "Gjs",
+            "testGObjectClass.js",
+            0,
+            "throws an error when returning a boolean value"
+        );
+    });
+
+    it("throws an error when returning a numeric value", function () {
+        GLib.test_expect_message(
+            "Gjs",
+            GLib.LogLevelFlags.LEVEL_WARNING,
+            "*JSObject expected*"
+        );
+        myInstance.connect("get-object", () => Math.PI);
+        myInstance.emit("get-object", {});
+        GLib.test_assert_expected_messages_internal(
+            "Gjs",
+            "testGObjectClass.js",
+            0,
+            "throws an error when returning a boolean value"
+        );
+    });
+
+    it("throws an error when returning a string value", function () {
+        GLib.test_expect_message(
+            "Gjs",
+            GLib.LogLevelFlags.LEVEL_WARNING,
+            "*JSObject expected*"
+        );
+        myInstance.connect("get-object", () => "string");
+        myInstance.emit("get-object", {});
+        GLib.test_assert_expected_messages_internal(
+            "Gjs",
+            "testGObjectClass.js",
+            0,
+            "throws an error when returning a boolean value"
+        );
+    });
+});
+
+describe('GObject class registered with registerType', function() {
+    class SubObject extends MyRegisteredObject {
+    }
+    
+    GObject.registerType(SubObject);
+    
+    it('extends class registered with registerClass', function() {
+        expect(() => new SubObject()).not.toThrow();
+        
+        const instance = new SubObject();
+        
+        expect(instance instanceof SubObject).toBeTrue();
+        expect(instance instanceof GObject.Object).toBeTrue();
+        expect(instance instanceof MyRegisteredObject).toBeTrue();
+    });
+});
\ No newline at end of file
diff --git a/modules/core/overrides/GObject.js b/modules/core/overrides/GObject.js
index 8058a2ec..3cf98670 100644
--- a/modules/core/overrides/GObject.js
+++ b/modules/core/overrides/GObject.js
@@ -64,11 +64,7 @@ function registerClass(...args) {
             klass[_gtkInternalChildren] = metaInfo.InternalChildren;
     }
 
-    if (!(klass.prototype instanceof GObject.Object) &&
-        !(klass.prototype instanceof GObject.Interface)) {
-        throw new TypeError('GObject.registerClass() used with invalid base ' +
-            `class (is ${Object.getPrototypeOf(klass).name})`);
-    }
+    assertDerivesFromGObject(klass, 'registerClass');
 
     // Find the "least derived" class with a _classInit static function; there
     // definitely is one, since this class must inherit from GObject
@@ -78,6 +74,150 @@ function registerClass(...args) {
     return initclass._classInit(klass);
 }
 
+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 assertDerivesFromGObject(klass, functionName) {
+    if (!(klass.prototype instanceof GObject.Object) &&
+        !(klass.prototype instanceof GObject.Interface)) {
+        throw new TypeError(`GObject.${functionName}() used with invalid base ` +
+            `class (is ${Object.getPrototypeOf(klass).name})`);
+    }
+}
+
+function registerType(klass) {
+    // Ensure the class derives from GObject.Object or
+    // GObject.Interface
+    assertDerivesFromGObject(klass, 'registerType');
+
+    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] : [];
+
+    propertiesArray.forEach(pspec => _checkAccessors(klass.prototype, pspec, GObject));
+
+    // Default to the GObject-specific prototype, fallback on the JS prototype.
+    const parentPrototype = parent[Gi.gobject_prototype_symbol] ?? parent.prototype;
+    let [giPrototype, registeredType] = Gi.register_type_with_class(klass, parentPrototype, gtypename, 
gflags,
+        gobjectInterfaces, propertiesArray);
+
+    const config = {
+        enumerable: false,
+        writable: false,
+        configurable: false,
+    };
+
+    Object.defineProperties(klass, {
+        [Gi.gobject_prototype_symbol]: {
+            ...config,
+            value: giPrototype,
+        },
+        [Gi.gobject_type_symbol]: {
+            ...config,
+            value: registeredType,
+        },
+        /**
+         * $gtype is an enumerable property,
+         * so allow template classes to explicitely
+         * define it.
+         *
+         * class X extends GObject.Object {
+         *    static $gtype = GObject.registerType(X);
+         * }
+         *
+         * is identical to...
+         *
+         * class Y extends GObject.Object {
+         *
+         * }
+         *
+         * GObject.registerType(Y);
+         *
+         * Internally, GJS depends on Gi.gobject_type_symbol which is
+         * not enumerable or configurable.
+         */
+        $gtype: {
+            enumerable: true,
+            configurable: true,
+            set() {
+            },
+            get() {
+                return this[Gi.gobject_type_symbol];
+            },
+        },
+        /**
+         * To allow types registered with GObject.registerClass
+         * to extend types regisered with GObject.registerType,
+         * we add a _classInit function which overrides the default
+         * GObject.registerClass behavior.
+         *
+         * NOTE: This does slightly change the behavior of GObject.registerClass
+         * in the context of GObject.registerType, GObject.registerClass will return
+         * the same class object it was passed.
+         */
+        _classInit: {
+            ...config,
+            // eslint-disable-next-line func-name-matching
+            value: function _classInit(klassConstructor) {
+                GObject.registerType(klassConstructor);
+
+                return klassConstructor;
+            },
+        },
+    });
+
+    Object.defineProperty(klass.prototype, Gi.gobject_prototype_symbol, {
+        ...config,
+        value: giPrototype,
+    });
+
+    _createSignals(klass[Gi.gobject_type_symbol], gobjectSignals);
+
+    gobjectInterfaces.forEach(iface => _copyAllDescriptors(klass.prototype, iface.prototype, ['toString']));
+    _copyAllDescriptors(klass.prototype, klass[Gi.gobject_prototype_symbol]);
+
+    // TODO(ewlsh): toString workaround
+    klass.prototype.toString = function (...args) {
+        if (this === klass.prototype)
+            return `${klass[Gi.gobject_prototype_symbol].toString(...args)} {registerType}`;
+
+        return `${imports.gi.GObject.Object.prototype.toString.call(this, ...args)} {registerType}`;
+    };
+
+    _hookupVFuncs(klass.prototype, klass[Gi.gobject_prototype_symbol], klass[Gi.gobject_type_symbol]);
+
+    gobjectInterfaces.forEach(iface =>
+        _checkInterface(iface, klass.prototype));
+
+    return registeredType;
+}
+
 // Some common functions between GObject.Class and GObject.Interface
 
 function _createSignals(gtype, sigs) {
@@ -172,7 +312,9 @@ function _copyAllDescriptors(target, source, filter) {
     .concat(Object.getOwnPropertySymbols(source))
     .forEach(key => {
         let descriptor = Object.getOwnPropertyDescriptor(source, key);
-        Object.defineProperty(target, key, descriptor);
+
+        if (descriptor)
+            Object.defineProperty(target, key, descriptor);
     });
 }
 
@@ -430,6 +572,7 @@ function _init() {
     };
 
     GObject.registerClass = registerClass;
+    GObject.registerType = registerType;
 
     GObject.Object._classInit = function (klass) {
         let gtypename = _createGTypeName(klass);
@@ -441,7 +584,7 @@ function _init() {
 
         propertiesArray.forEach(pspec => _checkAccessors(klass.prototype, pspec, GObject));
 
-        let newClass = Gi.register_type(parent.prototype, gtypename, gflags,
+        let newClass = Gi.register_type(parent[Gi.gobject_prototype_symbol] ?? parent.prototype, gtypename, 
gflags,
             gobjectInterfaces, propertiesArray);
         Object.setPrototypeOf(newClass, parent);
 
@@ -453,29 +596,7 @@ function _init() {
                 ['toString']));
         _copyAllDescriptors(newClass.prototype, klass.prototype);
 
-        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];
-
-            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);
-                    });
-                }
-            }
-        });
+        _hookupVFuncs(newClass.prototype, newClass.prototype, newClass.$gtype);
 
         gobjectInterfaces.forEach(iface =>
             _checkInterface(iface, newClass.prototype));


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